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 InstalledPackageVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); private static Dictionary s_manifestDependencies; private static Dictionary s_lockDependencies; private static ListRequest s_listRequest; private static Action> s_listCallback; [Serializable] private sealed class ManifestFile { public Dictionary dependencies = new Dictionary(); } [Serializable] private sealed class PackagesLockFile { public Dictionary dependencies = new Dictionary(); } [Serializable] private sealed class LockDependency { public string version; public string source; public string hash; } public static void ValidatePackageStates(List packages) { if (packages == null) { return; } RefreshLocalCache(); foreach (var package in packages) { var localPackage = GetLocalPackageInfo(package.name); ApplyLocalState(package, localPackage); } } public static void RefreshInstalledPackages(Action> 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 callback = null) { PackageOperationRunner.RunAdd(packageIdentifier, callback); } public static void UpdatePackage(RepositoryPackageData package, Action 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 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 ReadManifestDependencies() { var path = Path.GetFullPath(ManifestPath); if (!File.Exists(path)) { return new Dictionary(StringComparer.OrdinalIgnoreCase); } try { var json = File.ReadAllText(path); var manifest = JsonConvert.DeserializeObject(json); return manifest?.dependencies != null ? new Dictionary(manifest.dependencies, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); } catch (Exception e) { UnityEngine.Debug.LogError($"Read manifest failed: {e.Message}"); return new Dictionary(StringComparer.OrdinalIgnoreCase); } } private static Dictionary ReadPackagesLockDependencies() { var path = Path.GetFullPath(PackagesLockPath); if (!File.Exists(path)) { return new Dictionary(StringComparer.OrdinalIgnoreCase); } try { var json = File.ReadAllText(path); var packagesLock = JsonConvert.DeserializeObject(json); return packagesLock?.dependencies != null ? new Dictionary(packagesLock.dependencies, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); } catch (Exception e) { UnityEngine.Debug.LogError($"Read packages-lock failed: {e.Message}"); return new Dictionary(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; } } }