diff --git a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs index 14308261..5329ade0 100644 --- a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs +++ b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs @@ -568,4 +568,70 @@ public void TestSerializeNugetPackageIdentifier(string version) Assert.That(deserialized.InRange(identifier), Is.True); } } + + [Test] + [TestCase("jQuery", "3.7.0")] + public void TestPostprocessInstall(string packageId, string packageVersion) + { + var package = new NugetPackageIdentifier(packageId, packageVersion) { IsManuallyInstalled = true }; + var filepath = NugetHelper.PackagesConfigFilePath; + + var packagesConfigFile = new PackagesConfigFile(); + packagesConfigFile.AddPackage(package); + packagesConfigFile.Save(filepath); + + Assert.IsFalse(NugetHelper.IsInstalled(package), "The package IS installed: {0} {1}", package.Id, package.Version); + + var assetsIndex = filepath.LastIndexOf("Assets", StringComparison.Ordinal); + filepath = filepath.Substring(assetsIndex); + NugetPackageAssetPostprocessor.OnPostprocessAllAssets(new[] { filepath }, null, null, null); + + Assert.IsTrue(NugetHelper.IsInstalled(package), "The package was NOT installed: {0} {1}", package.Id, package.Version); + } + + [Test] + [TestCase("jQuery", "3.7.0")] + public void TestPostprocessUninstall(string packageId, string packageVersion) + { + var package = new NugetPackageIdentifier(packageId, packageVersion) { IsManuallyInstalled = true }; + var filepath = NugetHelper.PackagesConfigFilePath; + + NugetHelper.InstallIdentifier(package); + Assert.IsTrue(NugetHelper.IsInstalled(package), "The package was NOT installed: {0} {1}", package.Id, package.Version); + + var packagesConfigFile = new PackagesConfigFile(); + packagesConfigFile.Save(filepath); + + var assetsIndex = filepath.LastIndexOf("Assets", StringComparison.Ordinal); + filepath = filepath.Substring(assetsIndex); + NugetPackageAssetPostprocessor.OnPostprocessAllAssets(new[]{filepath}, + null, null, null); + + Assert.IsFalse(NugetHelper.IsInstalled(package), "The package is STILL installed: {0} {1}", package.Id, package.Version); + } + + [Test] + [TestCase("jQuery", "3.6.4", "3.7.0")] + [TestCase("jQuery", "3.7.0", "3.6.4")] + public void TestPostprocessDifferentVersion(string packageId, string packageVersionOld, string packageVersionNew) + { + var packageOld = new NugetPackageIdentifier(packageId, packageVersionOld) { IsManuallyInstalled = true }; + var packageNew = new NugetPackageIdentifier(packageId, packageVersionNew) { IsManuallyInstalled = true }; + var filepath = NugetHelper.PackagesConfigFilePath; + + NugetHelper.InstallIdentifier(packageOld); + Assert.IsTrue(NugetHelper.IsInstalled(packageOld), "The package was NOT installed: {0} {1}", packageOld.Id, packageOld.Version); + + var packagesConfigFile = new PackagesConfigFile(); + packagesConfigFile.AddPackage(packageNew); + packagesConfigFile.Save(filepath); + + var assetsIndex = filepath.LastIndexOf("Assets", StringComparison.Ordinal); + filepath = filepath.Substring(assetsIndex); + NugetPackageAssetPostprocessor.OnPostprocessAllAssets(new[] { filepath }, null, null, null); + + Assert.IsFalse(NugetHelper.IsInstalled(packageOld), "The old package version IS STILL installed: {0} {1}", packageOld.Id, packageOld.Version); + + Assert.IsTrue(NugetHelper.IsInstalled(packageNew), "The new package version was NOT installed: {0} {1}", packageNew.Id, packageNew.Version); + } } diff --git a/src/NuGetForUnity/Editor/NugetHelper.cs b/src/NuGetForUnity/Editor/NugetHelper.cs index 6601f876..32c32a49 100644 --- a/src/NuGetForUnity/Editor/NugetHelper.cs +++ b/src/NuGetForUnity/Editor/NugetHelper.cs @@ -43,7 +43,7 @@ public static class NugetHelper /// /// /// - private static readonly string PackagesConfigFilePath = Path.GetFullPath(Path.Combine(Application.dataPath, PackagesConfigFile.FileName)); + internal static readonly string PackagesConfigFilePath = Path.GetFullPath(Path.Combine(Application.dataPath, PackagesConfigFile.FileName)); /// /// Gets the absolute path to the Unity-Project root directory. @@ -131,6 +131,14 @@ public static PackagesConfigFile PackagesConfigFile } } + /// + /// Invalidates the currently loaded 'packages.config' so it is reloaded when it is accessed the next time. + /// + internal static void ReloadPackagesConfig() + { + packagesConfigFile = null; + } + /// /// Gets the packages that are actually installed in the project. /// @@ -369,45 +377,41 @@ private static void CleanInstallationDirectory(NugetPackageIdentifier package) // go through the library folders in descending order (highest to lowest version) var libDirectories = new DirectoryInfo(packageLibsDirectory).GetDirectories(); - var isAlreadyImported = IsAlreadyImportedInEngine(package); - if (!isAlreadyImported) + var bestLibDirectory = TargetFrameworkResolver.TryGetBestTargetFramework(libDirectories, directory => directory.Name); + if (bestLibDirectory == null) { - var bestLibDirectory = TargetFrameworkResolver.TryGetBestTargetFramework(libDirectories, directory => directory.Name); - if (bestLibDirectory == null) - { - Debug.LogWarningFormat("Couldn't find a library folder with a supported target-framework for the package {0}", package); - } - else - { - LogVerbose( - "Selecting directory '{0}' with the best target framework {1} for current settings", - bestLibDirectory, - bestLibDirectory.Name); - } + Debug.LogWarningFormat("Couldn't find a library folder with a supported target-framework for the package {0}", package); + } + else + { + LogVerbose( + "Selecting directory '{0}' with the best target framework {1} for current settings", + bestLibDirectory, + bestLibDirectory.Name); + } - // delete all of the libraries except for the selected one - foreach (var directory in libDirectories) + // delete all of the libraries except for the selected one + foreach (var directory in libDirectories) + { + // we use reference equality as the TargetFrameworkResolver returns the input reference. + if (directory != bestLibDirectory) { - // we use reference equality as the TargetFrameworkResolver returns the input reference. - if (directory != bestLibDirectory) - { - DeleteDirectory(directory.FullName); - } + DeleteDirectory(directory.FullName); } + } - if (bestLibDirectory != null) + if (bestLibDirectory != null) + { + // some older packages e.g. Microsoft.CodeAnalysis.Common 2.10.0 have multiple localization resource files + // e.g. Microsoft.CodeAnalysis.resources.dll each inside a folder with the language name as a folder name e.g. zh-Hant or fr + // unity doesn't support importing multiple assemblies with the same file name. + // for now we just delete all folders so the language neutral version is used and Unity is happy. + var languageSupFolders = bestLibDirectory.GetDirectories(); + if (languageSupFolders.All(languageSupFolder => languageSupFolder.Name.Split('-').FirstOrDefault()?.Length == 2)) { - // some older packages e.g. Microsoft.CodeAnalysis.Common 2.10.0 have multiple localization resource files - // e.g. Microsoft.CodeAnalysis.resources.dll each inside a folder with the language name as a folder name e.g. zh-Hant or fr - // unity doesn't support importing multiple assemblies with the same file name. - // for now we just delete all folders so the language neutral version is used and Unity is happy. - var languageSupFolders = bestLibDirectory.GetDirectories(); - if (languageSupFolders.All(languageSupFolder => languageSupFolder.Name.Split('-').FirstOrDefault()?.Length == 2)) + foreach (var languageSupFolder in languageSupFolders) { - foreach (var languageSupFolder in languageSupFolders) - { - languageSupFolder.Delete(true); - } + languageSupFolder.Delete(true); } } } @@ -740,13 +744,11 @@ public static void Uninstall(NugetPackageIdentifier package, bool refreshAssets var foundPackage = package as NugetPackage ?? GetSpecificPackage(package); // update the package.config file - if (!PackagesConfigFile.RemovePackage(foundPackage)) + if (PackagesConfigFile.RemovePackage(foundPackage)) { - return; + PackagesConfigFile.Save(PackagesConfigFilePath); } - PackagesConfigFile.Save(PackagesConfigFilePath); - var packageInstallDirectory = Path.Combine(NugetConfigFile.RepositoryPath, $"{foundPackage.Id}.{foundPackage.Version}"); DeleteDirectory(packageInstallDirectory); @@ -797,7 +799,7 @@ public static bool Update(NugetPackageIdentifier currentVersion, NugetPackage ne LogVerbose("Updating {0} {1} to {2}", currentVersion.Id, currentVersion.Version, newVersion.Version); Uninstall(currentVersion, false); newVersion.IsManuallyInstalled = newVersion.IsManuallyInstalled || currentVersion.IsManuallyInstalled; - return InstallIdentifier(newVersion, refreshAssets); + return InstallIdentifier(newVersion, refreshAssets, true); } /// @@ -903,13 +905,13 @@ void AddPackageToInstalled(NugetPackage package) { // set root packages as manually installed if none are marked as such foreach (var rootPackage in GetInstalledRootPackages()) - { - PackagesConfigFile.SetManuallyInstalledFlag(rootPackage); - } + { + PackagesConfigFile.SetManuallyInstalledFlag(rootPackage); + } - PackagesConfigFile.Save(PackagesConfigFilePath); + PackagesConfigFile.Save(PackagesConfigFilePath); + } } - } stopwatch.Stop(); LogVerbose("Getting installed packages took {0} ms", stopwatch.ElapsedMilliseconds); @@ -1029,11 +1031,24 @@ private static NugetPackage GetInstalledPackage(NugetPackageIdentifier packageId { if (packageId.InRange(installedPackage)) { - LogVerbose( - "Requested {0} {1}, but {2} is already installed, so using that.", - packageId.Id, - packageId.Version, - installedPackage.Version); + var configPackage = PackagesConfigFile.Packages.Find(p => p.Id == packageId.Id); + if (configPackage != null && configPackage < installedPackage) + { + LogVerbose( + "Requested {0} {1}. {2} is already installed, but config demands lower version.", + packageId.Id, + packageId.Version, + installedPackage.Version); + installedPackage = null; + } + else + { + LogVerbose( + "Requested {0} {1}, but {2} is already installed, so using that.", + packageId.Id, + packageId.Version, + installedPackage.Version); + } } else { @@ -1147,9 +1162,10 @@ private static void CopyStream(Stream input, Stream output) /// /// The identifier of the package to install. /// True to refresh the Unity asset database. False to ignore the changes (temporarily). - internal static bool InstallIdentifier(NugetPackageIdentifier package, bool refreshAssets = true) + /// True to indicate we're calling method as result of Update and don't want to go through IsAlreadyImportedInEngine + internal static bool InstallIdentifier(NugetPackageIdentifier package, bool refreshAssets = true, bool isUpdate = false) { - if (IsAlreadyImportedInEngine(package, false)) + if (!isUpdate && IsAlreadyImportedInEngine(package, false)) { LogVerbose("Package {0} is already imported in engine, skipping install.", package); return true; @@ -1160,7 +1176,7 @@ internal static bool InstallIdentifier(NugetPackageIdentifier package, bool refr if (foundPackage != null) { foundPackage.IsManuallyInstalled = package.IsManuallyInstalled; - return Install(foundPackage, refreshAssets); + return Install(foundPackage, refreshAssets, isUpdate); } Debug.LogErrorFormat("Could not find {0} {1} or greater.", package.Id, package.Version); @@ -1188,9 +1204,10 @@ public static void LogVerbose(string format, params object[] args) /// /// The package to install. /// True to refresh the Unity asset database. False to ignore the changes (temporarily). - public static bool Install(NugetPackage package, bool refreshAssets = true) + /// True to indicate we're calling method as result of Update and don't want to go through IsAlreadyImportedInEngine + public static bool Install(NugetPackage package, bool refreshAssets = true, bool isUpdate = false) { - if (IsAlreadyImportedInEngine(package, false)) + if (!isUpdate && IsAlreadyImportedInEngine(package, false)) { LogVerbose("Package {0} is already imported in engine, skipping install.", package); return true; @@ -1211,6 +1228,16 @@ public static bool Install(NugetPackage package, bool refreshAssets = true) if (installedPackage > package) { + var configPackage = PackagesConfigFile.Packages.Find(identifier => identifier.Id == package.Id); + if (configPackage != null && configPackage < installedPackage) + { + LogVerbose( + "{0} {1} is installed but config needs {2} so downgrading.", + installedPackage.Id, + installedPackage.Version, + package.Version); + return Update(installedPackage, package, false); + } LogVerbose( "{0} {1} is installed. {2} or greater is needed, so using installed version.", installedPackage.Id, @@ -1225,7 +1252,6 @@ public static bool Install(NugetPackage package, bool refreshAssets = true) return true; } - var installSuccess = false; try { LogVerbose("Installing: {0} {1}", package.Id, package.Version); @@ -1360,13 +1386,13 @@ public static bool Install(NugetPackage package, bool refreshAssets = true) // update the installed packages list InstalledPackagesDictionary.Add(package.Id, package); - installSuccess = true; + return true; } catch (Exception e) { WarnIfDotNetAuthenticationIssue(e); Debug.LogErrorFormat("Unable to install package {0} {1}\n{2}", package.Id, package.Version, e); - installSuccess = false; + return false; } finally { @@ -1377,8 +1403,6 @@ public static bool Install(NugetPackage package, bool refreshAssets = true) EditorUtility.ClearProgressBar(); } } - - return installSuccess; } private static void WarnIfDotNetAuthenticationIssue(Exception e) @@ -1454,40 +1478,39 @@ public static void Restore() var stopwatch = new Stopwatch(); stopwatch.Start(); + var somethingChanged = false; try { - var progressStep = 1.0f / PackagesConfigFile.Packages.Count; - float currentProgress = 0; - - // copy the list since the InstallIdentifier operation below changes the actual installed packages list - var packagesToInstall = new List(PackagesConfigFile.Packages); + var packagesToInstall = PackagesConfigFile.Packages.FindAll(package => !IsInstalled(package)); + if (packagesToInstall.Count > 0) + { + var progressStep = 1.0f / packagesToInstall.Count; + float currentProgress = 0; - LogVerbose("Restoring {0} packages.", packagesToInstall.Count); + LogVerbose("Restoring {0} packages.", packagesToInstall.Count); - foreach (var package in packagesToInstall) - { - if (package != null) + foreach (var package in packagesToInstall) { - EditorUtility.DisplayProgressBar( - "Restoring NuGet Packages", - string.Format("Restoring {0} {1}", package.Id, package.Version), - currentProgress); - - if (!IsInstalled(package)) + if (package != null) { + EditorUtility.DisplayProgressBar( + "Restoring NuGet Packages", + string.Format("Restoring {0} {1}", package.Id, package.Version), + currentProgress); LogVerbose("---Restoring {0} {1}", package.Id, package.Version); InstallIdentifier(package); + somethingChanged = true; } - else - { - LogVerbose("---Already installed: {0} {1}", package.Id, package.Version); - } - } - currentProgress += progressStep; + currentProgress += progressStep; + } + } + else + { + LogVerbose("No packages need restoring."); } - CheckForUnnecessaryPackages(); + somethingChanged = somethingChanged || CheckForUnnecessaryPackages(); } catch (Exception e) { @@ -1498,19 +1521,24 @@ public static void Restore() stopwatch.Stop(); LogVerbose("Restoring packages took {0} ms", stopwatch.ElapsedMilliseconds); - AssetDatabase.Refresh(); + if (somethingChanged) + { + AssetDatabase.Refresh(); + } + EditorUtility.ClearProgressBar(); } } - internal static void CheckForUnnecessaryPackages() + internal static bool CheckForUnnecessaryPackages() { if (!Directory.Exists(NugetConfigFile.RepositoryPath)) { - return; + return false; } var directories = Directory.GetDirectories(NugetConfigFile.RepositoryPath, "*", SearchOption.TopDirectoryOnly); + var somethingDeleted = false; foreach (var folder in directories) { var folderName = Path.GetFileName(folder); @@ -1541,12 +1569,20 @@ internal static void CheckForUnnecessaryPackages() if (!installed) { + somethingDeleted = true; LogVerbose("---DELETE unnecessary package {0}", folder); DeleteDirectory(folder); DeleteFile(folder + ".meta"); } } + + if (somethingDeleted) + { + UpdateInstalledPackages(); + } + + return somethingDeleted; } /// diff --git a/src/NuGetForUnity/Editor/NugetPackageAssetPostprocessor.cs b/src/NuGetForUnity/Editor/NugetPackageAssetPostprocessor.cs index 0675fc26..974e595a 100644 --- a/src/NuGetForUnity/Editor/NugetPackageAssetPostprocessor.cs +++ b/src/NuGetForUnity/Editor/NugetPackageAssetPostprocessor.cs @@ -67,6 +67,28 @@ private void OnPreprocessAsset() LogResults(results); } + /// + /// Called when the asset database finishes importing assets. + /// We use it to check if packages.config has been changed and if so, we want to restore packages. + /// + internal static void OnPostprocessAllAssets(string[] importedAssets, + string[] deletedAssets, + string[] movedAssets, + string[] movedFromAssetPaths) + { + var packagesConfigFilePath = Path.GetFullPath(NugetHelper.PackagesConfigFilePath); + var foundPackagesConfigAsset = importedAssets.Any( + importedAsset => Path.GetFullPath(importedAsset).Equals(packagesConfigFilePath, StringComparison.Ordinal)); + + if (!foundPackagesConfigAsset) + { + return; + } + + NugetHelper.ReloadPackagesConfig(); + NugetHelper.Restore(); + } + private static IEnumerable<(string AssetType, string AssetPath, ResultStatus Status)> HandleAsset(string projectRelativeAssetPath, string absoluteRepositoryPath, bool reimport) diff --git a/src/NuGetForUnity/Editor/PackagesConfigFile.cs b/src/NuGetForUnity/Editor/PackagesConfigFile.cs index 3bf2aba8..8e7fa640 100644 --- a/src/NuGetForUnity/Editor/PackagesConfigFile.cs +++ b/src/NuGetForUnity/Editor/PackagesConfigFile.cs @@ -24,7 +24,7 @@ public class PackagesConfigFile /// /// Gets the s contained in the package.config file. /// - public List Packages { get; private set; } + public List Packages { get; private set; } = new List(); /// /// Adds a package to the packages.config file. diff --git a/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs b/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs index bbbb3120..297909ba 100644 --- a/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs +++ b/src/NuGetForUnity/Editor/UnityPreImportedLibraryResolver.cs @@ -39,17 +39,17 @@ internal static HashSet GetAlreadyImportedLibs() // Search the all project assemblies that are not editor only. // We only use player assemblies as we don't need to collect UnityEditor assemblies, we don't support installing NuGet packages with reference to UnityEditor. #if UNITY_2019_3_OR_NEWER - const AssembliesType assemblieType = AssembliesType.PlayerWithoutTestAssemblies; + const AssembliesType assemblyType = AssembliesType.PlayerWithoutTestAssemblies; #else - const AssembliesType assemblieType = AssembliesType.Player; + const AssembliesType assemblyType = AssembliesType.Player; #endif - var projectAssemblies = CompilationPipeline.GetAssemblies(assemblieType) + var projectAssemblies = CompilationPipeline.GetAssemblies(assemblyType) .Where(playerAssembly => playerAssembly.flags != AssemblyFlags.EditorAssembly); // Collect all referenced assemblies but exclude all assemblies installed by NuGetForUnity. - var porojectReferences = projectAssemblies.SelectMany(playerAssembly => playerAssembly.allReferences); + var projectReferences = projectAssemblies.SelectMany(playerAssembly => playerAssembly.allReferences); alreadyImportedLibs = new HashSet( - porojectReferences.Select(compiledAssemblyReference => Path.GetFileNameWithoutExtension(compiledAssemblyReference)) + projectReferences.Select(Path.GetFileNameWithoutExtension) .Where(assemblyName => !alreadyInstalledDllFileNames.Contains(assemblyName))); if (PlayerSettings.GetApiCompatibilityLevel(EditorUserBuildSettings.selectedBuildTargetGroup) == ApiCompatibilityLevel.NET_Standard_2_0)