319 lines
11 KiB
C#
319 lines
11 KiB
C#
using Newtonsoft.Json;
|
|
|
|
namespace AlicizaX.PackageManager.Editor
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using UnityEditor;
|
|
using UnityEditor.PackageManager;
|
|
using UnityEditor.PackageManager.Requests;
|
|
|
|
internal enum InstalledSource
|
|
{
|
|
None,
|
|
Git,
|
|
Embedded,
|
|
LocalPath,
|
|
Registry,
|
|
BuiltIn,
|
|
Unknown
|
|
}
|
|
|
|
internal sealed class LocalPackageInfo
|
|
{
|
|
public string Name;
|
|
public string ManifestVersion;
|
|
public string LockVersion;
|
|
public string ResolvedHash;
|
|
public string InstalledVersion;
|
|
public InstalledSource Source;
|
|
public bool IsInstalled;
|
|
}
|
|
|
|
public static class PackageManagerCheckTool
|
|
{
|
|
private const string ManifestPath = "Packages/manifest.json";
|
|
private const string PackagesLockPath = "Packages/packages-lock.json";
|
|
|
|
private static readonly Dictionary<string, string> InstalledPackageVersions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private static Dictionary<string, string> s_manifestDependencies;
|
|
private static Dictionary<string, LockDependency> s_lockDependencies;
|
|
private static ListRequest s_listRequest;
|
|
private static Action<IReadOnlyDictionary<string, string>> s_listCallback;
|
|
|
|
[Serializable]
|
|
private sealed class ManifestFile
|
|
{
|
|
public Dictionary<string, string> dependencies = new Dictionary<string, string>();
|
|
}
|
|
|
|
[Serializable]
|
|
private sealed class PackagesLockFile
|
|
{
|
|
public Dictionary<string, LockDependency> dependencies = new Dictionary<string, LockDependency>();
|
|
}
|
|
|
|
[Serializable]
|
|
private sealed class LockDependency
|
|
{
|
|
public string version;
|
|
public string source;
|
|
public string hash;
|
|
}
|
|
|
|
public static void ValidatePackageStates(List<RepositoryPackageData> packages)
|
|
{
|
|
if (packages == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RefreshLocalCache();
|
|
|
|
foreach (var package in packages)
|
|
{
|
|
var localPackage = GetLocalPackageInfo(package.name);
|
|
ApplyLocalState(package, localPackage);
|
|
}
|
|
}
|
|
|
|
public static void RefreshInstalledPackages(Action<IReadOnlyDictionary<string, string>> callback)
|
|
{
|
|
if (s_listRequest != null && !s_listRequest.IsCompleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
s_listCallback = callback;
|
|
s_listRequest = Client.List(true, false);
|
|
EditorApplication.update -= OnListRequestProgress;
|
|
EditorApplication.update += OnListRequestProgress;
|
|
}
|
|
|
|
public static void InstallPackage(string packageIdentifier, Action<bool, string> callback = null)
|
|
{
|
|
PackageOperationRunner.RunAdd(packageIdentifier, callback);
|
|
}
|
|
|
|
public static void UpdatePackage(RepositoryPackageData package, Action<bool, string> callback = null)
|
|
{
|
|
if (package == null)
|
|
{
|
|
callback?.Invoke(false, "Package data is null.");
|
|
return;
|
|
}
|
|
|
|
PackageOperationRunner.RunAdd(package.cloneUrl, callback);
|
|
}
|
|
|
|
public static void UninstallPackage(string packageName, Action<bool, string> callback = null)
|
|
{
|
|
PackageOperationRunner.RunRemove(packageName, callback);
|
|
}
|
|
|
|
internal static LocalPackageInfo GetLocalPackageInfo(string packageName)
|
|
{
|
|
RefreshLocalCache();
|
|
|
|
var info = new LocalPackageInfo
|
|
{
|
|
Name = packageName,
|
|
Source = InstalledSource.None
|
|
};
|
|
|
|
if (string.IsNullOrEmpty(packageName))
|
|
{
|
|
return info;
|
|
}
|
|
|
|
if (s_manifestDependencies != null && s_manifestDependencies.TryGetValue(packageName, out var manifestVersion))
|
|
{
|
|
info.ManifestVersion = manifestVersion;
|
|
}
|
|
|
|
if (s_lockDependencies != null && s_lockDependencies.TryGetValue(packageName, out var lockDependency))
|
|
{
|
|
info.LockVersion = lockDependency.version;
|
|
info.ResolvedHash = lockDependency.hash;
|
|
info.Source = ParseSource(lockDependency.source);
|
|
info.IsInstalled = info.Source != InstalledSource.None;
|
|
}
|
|
|
|
if (InstalledPackageVersions.TryGetValue(packageName, out var installedVersion))
|
|
{
|
|
info.InstalledVersion = installedVersion;
|
|
info.IsInstalled = true;
|
|
}
|
|
|
|
if (info.Source == InstalledSource.None && !string.IsNullOrEmpty(info.ManifestVersion))
|
|
{
|
|
info.Source = GuessSourceFromManifest(info.ManifestVersion);
|
|
info.IsInstalled = info.Source == InstalledSource.Embedded || info.Source == InstalledSource.LocalPath;
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
public static bool HasInstalledPackageVersion(string packageName, out string version)
|
|
{
|
|
return InstalledPackageVersions.TryGetValue(packageName, out version);
|
|
}
|
|
|
|
private static void OnListRequestProgress()
|
|
{
|
|
if (s_listRequest == null || !s_listRequest.IsCompleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EditorApplication.update -= OnListRequestProgress;
|
|
|
|
InstalledPackageVersions.Clear();
|
|
if (s_listRequest.Status == StatusCode.Success)
|
|
{
|
|
foreach (var package in s_listRequest.Result)
|
|
{
|
|
InstalledPackageVersions[package.name] = package.version;
|
|
}
|
|
}
|
|
|
|
var callback = s_listCallback;
|
|
s_listCallback = null;
|
|
s_listRequest = null;
|
|
callback?.Invoke(InstalledPackageVersions);
|
|
}
|
|
|
|
private static void RefreshLocalCache()
|
|
{
|
|
s_manifestDependencies = ReadManifestDependencies();
|
|
s_lockDependencies = ReadPackagesLockDependencies();
|
|
}
|
|
|
|
private static void ApplyLocalState(RepositoryPackageData package, LocalPackageInfo localPackage)
|
|
{
|
|
package.ManifestVersion = localPackage.ManifestVersion;
|
|
package.InstalledVersion = !string.IsNullOrEmpty(localPackage.InstalledVersion)
|
|
? localPackage.InstalledVersion
|
|
: localPackage.LockVersion;
|
|
package.InstalledHash = localPackage.ResolvedHash;
|
|
package.InstalledSource = localPackage.Source.ToString();
|
|
|
|
switch (localPackage.Source)
|
|
{
|
|
case InstalledSource.Embedded:
|
|
case InstalledSource.LocalPath:
|
|
package.PackageState = PackageState.InstallLocal;
|
|
break;
|
|
case InstalledSource.Git:
|
|
package.PackageState = HasRemoteUpdate(package, localPackage)
|
|
? PackageState.Update
|
|
: PackageState.Install;
|
|
break;
|
|
case InstalledSource.Registry:
|
|
case InstalledSource.BuiltIn:
|
|
case InstalledSource.Unknown:
|
|
package.PackageState = localPackage.IsInstalled ? PackageState.Install : PackageState.UnInstall;
|
|
break;
|
|
default:
|
|
package.PackageState = localPackage.IsInstalled ? PackageState.Install : PackageState.UnInstall;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static bool HasRemoteUpdate(RepositoryPackageData package, LocalPackageInfo localPackage)
|
|
{
|
|
if (string.IsNullOrEmpty(localPackage.ResolvedHash) || string.IsNullOrEmpty(package.remoteHead))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return !string.Equals(localPackage.ResolvedHash, package.remoteHead, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static Dictionary<string, string> ReadManifestDependencies()
|
|
{
|
|
var path = Path.GetFullPath(ManifestPath);
|
|
if (!File.Exists(path))
|
|
{
|
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
try
|
|
{
|
|
var json = File.ReadAllText(path);
|
|
var manifest = JsonConvert.DeserializeObject<ManifestFile>(json);
|
|
return manifest?.dependencies != null
|
|
? new Dictionary<string, string>(manifest.dependencies, StringComparer.OrdinalIgnoreCase)
|
|
: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
UnityEngine.Debug.LogError($"Read manifest failed: {e.Message}");
|
|
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
private static Dictionary<string, LockDependency> ReadPackagesLockDependencies()
|
|
{
|
|
var path = Path.GetFullPath(PackagesLockPath);
|
|
if (!File.Exists(path))
|
|
{
|
|
return new Dictionary<string, LockDependency>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
try
|
|
{
|
|
var json = File.ReadAllText(path);
|
|
var packagesLock = JsonConvert.DeserializeObject<PackagesLockFile>(json);
|
|
return packagesLock?.dependencies != null
|
|
? new Dictionary<string, LockDependency>(packagesLock.dependencies, StringComparer.OrdinalIgnoreCase)
|
|
: new Dictionary<string, LockDependency>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
UnityEngine.Debug.LogError($"Read packages-lock failed: {e.Message}");
|
|
return new Dictionary<string, LockDependency>(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
private static InstalledSource ParseSource(string source)
|
|
{
|
|
return source switch
|
|
{
|
|
"git" => InstalledSource.Git,
|
|
"embedded" => InstalledSource.Embedded,
|
|
"local" => InstalledSource.LocalPath,
|
|
"registry" => InstalledSource.Registry,
|
|
"builtin" => InstalledSource.BuiltIn,
|
|
_ => string.IsNullOrEmpty(source) ? InstalledSource.None : InstalledSource.Unknown
|
|
};
|
|
}
|
|
|
|
private static InstalledSource GuessSourceFromManifest(string manifestVersion)
|
|
{
|
|
if (string.IsNullOrEmpty(manifestVersion))
|
|
{
|
|
return InstalledSource.None;
|
|
}
|
|
|
|
if (manifestVersion.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return InstalledSource.LocalPath;
|
|
}
|
|
|
|
if (manifestVersion.EndsWith(".git", StringComparison.OrdinalIgnoreCase) ||
|
|
manifestVersion.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
|
manifestVersion.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
|
|
manifestVersion.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase) ||
|
|
manifestVersion.StartsWith("git@", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return InstalledSource.Git;
|
|
}
|
|
|
|
return InstalledSource.Registry;
|
|
}
|
|
}
|
|
}
|