Skip to content

Instantly share code, notes, and snippets.

@garettbass
Last active September 20, 2023 15:00
Show Gist options
  • Save garettbass/0a801137d1376b86e7cbb2ae7ed736b6 to your computer and use it in GitHub Desktop.
Save garettbass/0a801137d1376b86e7cbb2ae7ed736b6 to your computer and use it in GitHub Desktop.
Convert Unity packages from Library/PackageCache into local packages. (more info in comments below)
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
public static class LocalPackageConverter
{
[MenuItem("Assets/Convert to Local Package", isValidateFunction:true)]
private static bool CanConvertSelectionToLocalPackages()
{
foreach (var assetPath in SelectedAssetPaths)
{
if (CanConvertToLocalPackage(assetPath))
{
return true;
}
}
return false;
}
[MenuItem("Assets/Convert to Local Package", isValidateFunction:false, priority:20)]
private static void ConvertSelectionToLocalPackages()
{
var packageVersions = LoadPackageVersions();
foreach (var assetPath in SelectedAssetPaths)
{
if (CanConvertToLocalPackage(assetPath))
{
ConvertToLocalPackage(assetPath, packageVersions);
}
}
}
private static bool ConvertToLocalPackage(string assetPath, Dictionary<string, string> packageVersions)
{
var packageName = Path.GetFileName(assetPath);
if (!packageVersions.TryGetValue(packageName, out var packageVersion))
{
Debug.LogError($"\"{packageName}\" not found in package manifest");
return false;
}
if (packageVersion.StartsWith("file:"))
{
var localPackagePath = packageVersion.Substring("file:".Length);
ResolvePath(ref localPackagePath);
Debug.Log($"\"{packageName}\" is already a local package: {localPackagePath}");
return true;
}
var sourcePackagePath = JoinPath(PackageCachePath, $"{packageName}@{packageVersion}");
var sourcePackageDir = new DirectoryInfo(sourcePackagePath);
if (!sourcePackageDir.Exists)
{
Debug.LogError($"\"{packageName}\" content not found: {sourcePackagePath}");
return false;
}
var targetPackagePath = JoinPath(ProjectPath, $"Packages.local/{packageName}@{packageVersion}");
if (Directory.Exists(targetPackagePath))
{
Debug.LogError($"Local package directory already exists: {targetPackagePath}");
return false;
}
var targetPackageDir = Directory.CreateDirectory(targetPackagePath);
if (!targetPackageDir.Exists)
{
Debug.LogError($"Failed to create local package directory: {targetPackagePath}");
return false;
}
if (!CopyDirectory(in sourcePackageDir, in targetPackageDir))
{
return false;
}
if (targetPackagePath.StartsWith(ProjectPath))
{
// convert to relative path
targetPackagePath = $"..{targetPackagePath.Substring(ProjectPath.Length)}";
}
if (!UpdatePackageManifest(packageName, $"file:{targetPackagePath}"))
{
return false;
}
Debug.Log($"Converted \"{packageName}\" to local package: {targetPackagePath}");
return true;
}
private static readonly string AssetsPath = Application.dataPath.Replace('\\','/');
private static readonly string ProjectPath = Path.GetDirectoryName(AssetsPath).Replace('\\','/');
private static readonly string LibraryPath = JoinPath(ProjectPath, "Library");
private static readonly string PackageCachePath = JoinPath(LibraryPath, "PackageCache");
private static readonly string PackagesPath = JoinPath(ProjectPath, "Packages");
private static readonly string ManifestPath = JoinPath(PackagesPath, "manifest.json");
private static readonly Type Json =
typeof(UnityEditor.Editor)
.Assembly
.GetType("UnityEditor.Json");
private delegate object JsonDeserializeDelegate(string json);
private static readonly JsonDeserializeDelegate JsonDeserialize =
(JsonDeserializeDelegate)
Delegate.CreateDelegate(
type:typeof(JsonDeserializeDelegate),
firstArgument:null,
method:Json.GetMethod("Deserialize", BindingFlags.Public|BindingFlags.Static)
);
private delegate string JsonSerializeDelegate(object obj, bool pretty, string indentText);
private static readonly JsonSerializeDelegate JsonSerialize =
(JsonSerializeDelegate)
Delegate.CreateDelegate(
type:typeof(JsonSerializeDelegate),
firstArgument:null,
method:Json.GetMethod("Serialize", BindingFlags.Public|BindingFlags.Static)
);
private static IEnumerable<string> SelectedAssetPaths =>
Selection.objects
.OfType<DefaultAsset>()
.Select(AssetDatabase.GetAssetPath);
private static bool UpdatePackageManifest(string packageName, string packageVersion)
{
var manifestPath = ManifestPath;
var manifest = LoadPackageManifest(manifestPath);
if (manifest == null || manifest.Count == 0)
{
Debug.LogError($"Package manifest failed to deserialize ({manifestPath})");
return false;
}
var dependencies = manifest["dependencies"] as Dictionary<string, object>;
if (dependencies == null)
{
Debug.LogError($"Package manifest has no \"dependencies\" key ({manifestPath})");
return false;
}
dependencies[packageName] = packageVersion;
File.WriteAllText(manifestPath, JsonSerialize(manifest, pretty:true, indentText:" "));
AssetDatabase.Refresh();
return true;
}
private static Dictionary<string, object> LoadPackageManifest(string manifestPath)
{
if (!File.Exists(manifestPath))
{
Debug.LogError($"Package manifest not found ({manifestPath})");
return null;
}
var manifestJson = File.ReadAllText(manifestPath);
if (string.IsNullOrEmpty(manifestJson))
{
Debug.LogError($"Package manifest was empty ({manifestPath})");
return null;
}
return JsonDeserialize(manifestJson) as Dictionary<string, object>;
}
private static Dictionary<string, string> LoadPackageVersions()
{
var manifestPath = ManifestPath;
var manifest = LoadPackageManifest(manifestPath);
if (manifest == null || manifest.Count == 0)
{
Debug.LogError($"Package manifest failed to deserialize ({manifestPath})");
return null;
}
var dependencies = manifest["dependencies"] as Dictionary<string, object>;
if (dependencies == null)
{
Debug.LogError($"Package manifest has no \"dependencies\" key ({manifestPath})");
return null;
}
var packageVersions = new Dictionary<string, string>();
foreach (var package in dependencies)
{
packageVersions.Add(package.Key, package.Value.ToString());
}
/** /
// We could get package versions from Library/PackageCache
// but it is unclear how to override these packages in manifest.json.
var packageCachePath = PackageCachePath;
var packageCacheDir = new DirectoryInfo(packageCachePath);
if (!packageCacheDir.Exists)
{
Debug.LogError($"PackageCache not found ({packageCachePath})");
return null;
}
foreach (var subdir in packageCacheDir.EnumerateDirectories())
{
var packageParts = subdir.Name.Split('@');
var packageName = packageParts[0];
var packageVersion = packageParts[1];
if (!packageVersions.ContainsKey(packageName))
{
packageVersions.Add(packageName, packageVersion);
}
}
/**/
return packageVersions;
}
private static bool CopyDirectory(in DirectoryInfo source, in DirectoryInfo target)
{
foreach (var sourceSubDir in source.EnumerateDirectories())
{
var targetSubPath = JoinPath(target.FullName, sourceSubDir.Name);
var targetSubDir = Directory.CreateDirectory(targetSubPath);
if (!targetSubDir.Exists)
{
Debug.LogError($"Failed to create local package subdirectory: {targetSubPath}");
return false;
}
CopyDirectory(sourceSubDir, targetSubDir);
}
foreach (var sourceFile in source.EnumerateFiles())
{
var targetFilePath = JoinPath(target.FullName, sourceFile.Name);
Debug.Log($"Copying {targetFilePath}...");
if (!sourceFile.CopyTo(targetFilePath).Exists)
{
Debug.LogError($"Failed to create local package file: {targetFilePath}");
return false;
}
}
return true;
}
public static bool CanConvertToLocalPackage(string assetPath)
{
return assetPath.StartsWith("Packages/");
}
private static string JoinPath(string a, string b) => Path.Combine(a, b).Replace('\\','/');
private static string[] SplitPath(string path) => path.Split('\\','/');
private static bool ResolvePath(ref string path)
{
var components = SplitPath(path).ToList();
if (components.Count == 0)
{
return false;
}
if (components[0] == ".")
{
return false;
}
else if (components[0] == "..")
{
components.RemoveAt(0);
components.InsertRange(0, SplitPath(ProjectPath));
}
path = string.Join("/", components);
return Directory.Exists(path);
}
}
@garettbass
Copy link
Author

Place this file in an Editor folder anywhere in your project's Assets folder. Select one or more packages, and choose "Convert to Local Package" from the Project tab's context menu.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment