diff --git a/README.md b/README.md index 0e7813e9..56ca7f8b 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,16 @@ Note: Depending on the size and number of packages you need to install, the `Res If you are interested in the process NuGetForUnity follows or you are trying to debug an issue, you can force NuGetForUnity to use verbose logging to output an increased amount of data to the Unity console. Either check the `Use Verbose Logging` checkbox in the `NuGet For Unity` settings window or add the line `` to the `` element in the _NuGet.config_ file. You can disable verbose logging by either setting the value to false or completely deleting the line. +## Ignored Dependencies + +If a dependency is already provided by another Unity plugin that is installed outside of NuGetForUnity, add its package ID to `Ignored Dependencies` in the `NuGet For Unity` settings window. The same setting can be configured manually in _NuGet.config_: + +```xml + +``` + +NuGetForUnity skips these package IDs only when they are pulled transitively as dependencies. Existing non-manually-installed entries for ignored dependencies are removed from _packages.config_ during restore or package cleanup. Explicitly installing the same package still works. + ## Caching The _.nupkg_ files downloaded from the NuGet server are cached locally in the current user's Application Data folder `%localappdata%\NuGet\Cache` (Windows: `C:\Users\[username]\AppData\Local\NuGet\Cache`, Mac/Linux: `~/.local/share/NuGet/Cache`). The cache location can be overwritten by setting the `NuGetCachePath` environment variable. Packages previously installed are installed via the cache folder instead of downloading it from the server again. diff --git a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs index 33680a7e..989fd7b2 100644 --- a/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs +++ b/src/NuGetForUnity.Tests/Assets/Tests/Editor/NuGetTests.cs @@ -338,6 +338,38 @@ public void InstallStyleCopTest([Values] InstallMode installMode) Assert.That(InstalledPackagesManager.InstalledPackages, Is.EquivalentTo(new[] { styleCopPlusId, styleCopId })); } + [Test] + public void InstallStyleCopWithIgnoredDependencyTest([Values] InstallMode installMode) + { + ConfigureNugetConfig(installMode); + + var oldIgnoredPackageDependencies = ConfigurationManager.NugetConfigFile.IgnoredPackageDependenciesText; + try + { + ConfigurationManager.NugetConfigFile.IgnoredPackageDependenciesText = "stylecop.msbuild"; + + var styleCopPlusId = new NugetPackageIdentifier("StyleCopPlus.MSBuild", "4.7.49.5") { IsManuallyInstalled = true }; + var styleCopId = new NugetPackageIdentifier("StyleCop.MSBuild", "4.7.49.0"); + + NugetPackageInstaller.InstallIdentifier(styleCopPlusId); + + Assert.That(InstalledPackagesManager.InstalledPackages, Does.Contain(styleCopPlusId)); + Assert.That(InstalledPackagesManager.InstalledPackages, Does.Not.Contain(styleCopId)); + Assert.That(InstalledPackagesManager.PackagesConfigFile.Packages, Does.Not.Contain(styleCopId)); + + InstalledPackagesManager.PackagesConfigFile.Packages.Add(new PackageConfig { Id = styleCopId.Id, Version = styleCopId.Version }); + PackageRestorer.Restore(false); + + Assert.That(InstalledPackagesManager.InstalledPackages, Does.Contain(styleCopPlusId)); + Assert.That(InstalledPackagesManager.InstalledPackages, Does.Not.Contain(styleCopId)); + Assert.That(InstalledPackagesManager.PackagesConfigFile.Packages, Does.Not.Contain(styleCopId)); + } + finally + { + ConfigurationManager.NugetConfigFile.IgnoredPackageDependenciesText = oldIgnoredPackageDependencies; + } + } + [Test] public void InstallStyleCopWithoutDependenciesTest() { @@ -722,6 +754,31 @@ public void PackageSourceCredentialsTest(string name) Assert.That(parsedSource.SavedPassword, Is.EqualTo(password)); } + [Test] + public void IgnoredPackageDependenciesConfigRoundTrips() + { + var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}-{NugetConfigFile.FileName}"); + try + { + var file = NugetConfigFile.CreateDefaultFile(path); + file.IgnoredPackageDependenciesText = "StyleCop.MSBuild; stylecop.msbuild, Newtonsoft.Json"; + file.Save(path); + + var loaded = NugetConfigFile.Load(path); + + Assert.That(loaded.IgnoredPackageDependencies, Is.EqualTo(new[] { "StyleCop.MSBuild", "Newtonsoft.Json" })); + Assert.That(loaded.IsPackageDependencyIgnored("stylecop.msbuild"), Is.True); + Assert.That(loaded.IsPackageDependencyIgnored("NUnit"), Is.False); + } + finally + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + [Test] [TestCase("2018.4.30f1", false, false, false)] [TestCase("2018.4.30f1", true, false, false)] diff --git a/src/NuGetForUnity/Editor/Configuration/NugetConfigFile.cs b/src/NuGetForUnity/Editor/Configuration/NugetConfigFile.cs index 7baa678f..011a7065 100644 --- a/src/NuGetForUnity/Editor/Configuration/NugetConfigFile.cs +++ b/src/NuGetForUnity/Editor/Configuration/NugetConfigFile.cs @@ -56,6 +56,8 @@ public class NugetConfigFile private const string KeepingPdbFilesConfigKey = "KeepingPdbFiles"; + private const string IgnoredPackageDependenciesConfigKey = "ignoredPackageDependencies"; + private const string ProtocolVersionAttributeName = "protocolVersion"; private const string EnableCredentialProviderAttributeName = "enableCredentialProvider"; @@ -66,6 +68,8 @@ public class NugetConfigFile private const string SupportsPackageIdSearchFilterAttributeName = "supportsPackageIdSearchFilter"; + private static readonly char[] IgnoredPackageDependencySeparators = { ';', ',', '\r', '\n' }; + [NotNull] private readonly string unityPackagesNugetInstallPath = Path.Combine(UnityPathHelper.AbsoluteUnityPackagesNugetPath, "InstalledPackages"); @@ -197,6 +201,12 @@ public string RelativePackagesConfigDirectoryPath /// public bool PreferNetStandardOverNetFramework { get; set; } + /// + /// Gets the package dependency IDs that should not be installed when pulled transitively by another package. + /// + [NotNull] + internal List IgnoredPackageDependencies { get; } = new List(); + /// /// Gets or sets a value indicating whether PDB files included in NuGet packages should not be deleted if they can be read by Unity. /// @@ -212,6 +222,17 @@ public string RelativePackagesConfigDirectoryPath /// internal List EnabledPlugins { get; } = new List(); + /// + /// Gets or sets the ignored package dependency IDs as editable text. + /// + [NotNull] + internal string IgnoredPackageDependenciesText + { + get => string.Join("; ", IgnoredPackageDependencies); + + set => SetIgnoredPackageDependencies(value); + } + /// /// Loads a NuGet.config file at the given file-path. /// @@ -382,6 +403,10 @@ public static NugetConfigFile Load([NotNull] string filePath) { configFile.KeepingPdbFiles = bool.Parse(value); } + else if (string.Equals(key, IgnoredPackageDependenciesConfigKey, StringComparison.OrdinalIgnoreCase)) + { + configFile.SetIgnoredPackageDependencies(value); + } } return configFile; @@ -426,6 +451,8 @@ public static NugetConfigFile CreateDefaultFile([NotNull] string filePath) [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "We intentionally use lower case.")] public void Save([NotNull] string filePath) { + NormalizeIgnoredPackageDependencies(); + var configFile = new XDocument(); var packageSources = new XElement("packageSources"); @@ -587,6 +614,14 @@ public void Save([NotNull] string filePath) config.Add(addElement); } + if (IgnoredPackageDependencies.Count > 0) + { + addElement = new XElement("add"); + addElement.Add(new XAttribute("key", IgnoredPackageDependenciesConfigKey)); + addElement.Add(new XAttribute("value", string.Join(";", IgnoredPackageDependencies))); + config.Add(addElement); + } + var configuration = new XElement("configuration"); configuration.Add(packageSources); configuration.Add(disabledPackageSources); @@ -627,6 +662,46 @@ public void Save([NotNull] string filePath) configFile.Save(filePath); } + /// + /// Checks if the given package ID is configured to be ignored when it is pulled as a dependency. + /// + /// The dependency package ID. + /// True if the dependency should be skipped. + internal bool IsPackageDependencyIgnored([NotNull] string packageId) + { + if (string.IsNullOrWhiteSpace(packageId)) + { + return false; + } + + var normalizedPackageId = packageId.Trim(); + return IgnoredPackageDependencies.Exists( + ignoredPackageId => string.Equals(ignoredPackageId, normalizedPackageId, StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Adds the given package ID to the dependency ignore list. + /// + /// The dependency package ID to ignore. + /// True if the package ID was added, false if it was already present or empty. + internal bool AddIgnoredPackageDependency([NotNull] string packageId) + { + if (string.IsNullOrWhiteSpace(packageId)) + { + return false; + } + + var normalizedPackageId = packageId.Trim(); + if (IgnoredPackageDependencies.Exists( + ignoredPackageId => string.Equals(ignoredPackageId, normalizedPackageId, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + + IgnoredPackageDependencies.Add(normalizedPackageId); + return true; + } + /// /// Changes the package install location config and also moves the packages.config to the new location. /// @@ -673,6 +748,28 @@ internal void FillMissingPackageCredentialsFromExternalFile([NotNull] string fil } } + [NotNull] + private static List ParseIgnoredPackageDependencies([CanBeNull] string value) + { + if (string.IsNullOrEmpty(value)) + { + return new List(); + } + + var packageIds = new List(); + foreach (var packageId in value.Split(IgnoredPackageDependencySeparators, StringSplitOptions.RemoveEmptyEntries)) + { + var trimmedPackageId = packageId.Trim(); + if (!string.IsNullOrEmpty(trimmedPackageId) && + !packageIds.Exists(existingPackageId => string.Equals(existingPackageId, trimmedPackageId, StringComparison.OrdinalIgnoreCase))) + { + packageIds.Add(trimmedPackageId); + } + } + + return packageIds; + } + private void FillPackageSourceCredentialsFromConfig(XDocument file, bool overwriteMissingFromExternal) { var packageSourceCredentials = file.Root?.Element("packageSourceCredentials"); @@ -715,5 +812,28 @@ private void FillPackageSourceCredentialsFromConfig(XDocument file, bool overwri } } } + + private void SetIgnoredPackageDependencies([CanBeNull] string value) + { + IgnoredPackageDependencies.Clear(); + IgnoredPackageDependencies.AddRange(ParseIgnoredPackageDependencies(value)); + } + + private void NormalizeIgnoredPackageDependencies() + { + var uniquePackageIds = new List(); + foreach (var packageId in IgnoredPackageDependencies) + { + var trimmedPackageId = packageId?.Trim(); + if (!string.IsNullOrEmpty(trimmedPackageId) && + !uniquePackageIds.Exists(existingPackageId => string.Equals(existingPackageId, trimmedPackageId, StringComparison.OrdinalIgnoreCase))) + { + uniquePackageIds.Add(trimmedPackageId); + } + } + + IgnoredPackageDependencies.Clear(); + IgnoredPackageDependencies.AddRange(uniquePackageIds); + } } } diff --git a/src/NuGetForUnity/Editor/InstalledPackagesManager.cs b/src/NuGetForUnity/Editor/InstalledPackagesManager.cs index 4bb1ef95..90171dfd 100644 --- a/src/NuGetForUnity/Editor/InstalledPackagesManager.cs +++ b/src/NuGetForUnity/Editor/InstalledPackagesManager.cs @@ -246,20 +246,25 @@ internal static void UpdateInstalledPackages() } /// - /// Finds and removes any installed packages detected that are not listed inside the packages.config. + /// Finds and removes any installed packages detected that are not listed inside the packages.config, and removes ignored implicit entries from + /// packages.config. /// - /// True if some packages are deleted. + /// True if some packages or packages.config entries are deleted. internal static bool RemoveUnnecessaryPackages() { + var somethingDeleted = RemoveIgnoredImplicitPackagesFromConfig(); + if (!Directory.Exists(ConfigurationManager.NugetConfigFile.RepositoryPath)) { - return false; + return somethingDeleted; } - var somethingDeleted = false; foreach (var installedPackage in InstalledPackages) { - var shouldBeInstalled = PackagesConfigFile.Packages.Exists(packageId => packageId.Equals(installedPackage)); + var packageConfig = PackagesConfigFile.Packages.Find(packageId => packageId.Equals(installedPackage)); + var shouldBeInstalled = packageConfig != null && + (packageConfig.IsManuallyInstalled || + !ConfigurationManager.NugetConfigFile.IsPackageDependencyIgnored(packageConfig.Id)); if (!shouldBeInstalled) { @@ -341,6 +346,27 @@ internal static List GetInstalledRootPackages() return roots; } + private static bool RemoveIgnoredImplicitPackagesFromConfig() + { + var ignoredImplicitPackages = PackagesConfigFile.Packages + .Where(package => !package.IsManuallyInstalled && ConfigurationManager.NugetConfigFile.IsPackageDependencyIgnored(package.Id)) + .ToList(); + + foreach (var ignoredImplicitPackage in ignoredImplicitPackages) + { + NugetLogger.LogVerbose("---REMOVE ignored dependency from packages.config {0}", ignoredImplicitPackage.Id); + PackagesConfigFile.RemovePackage(ignoredImplicitPackage); + } + + if (ignoredImplicitPackages.Count <= 0) + { + return false; + } + + PackagesConfigFile.Save(); + return true; + } + private static void AddPackageToInstalledInternal([NotNull] NugetPackageLocal package, ref int manuallyInstalledPackagesNumber) { var packages = InstalledPackagesDictionary; diff --git a/src/NuGetForUnity/Editor/NugetPackageInstaller.cs b/src/NuGetForUnity/Editor/NugetPackageInstaller.cs index 88267384..8c82c301 100644 --- a/src/NuGetForUnity/Editor/NugetPackageInstaller.cs +++ b/src/NuGetForUnity/Editor/NugetPackageInstaller.cs @@ -177,6 +177,12 @@ private static PackageInstallOperationResult Install( NugetLogger.LogVerbose("Installing dependencies for TargetFramework: {0}", frameworkGroup.TargetFramework); foreach (var dependency in frameworkGroup.Dependencies) { + if (ConfigurationManager.NugetConfigFile.IsPackageDependencyIgnored(dependency.Id)) + { + NugetLogger.LogVerbose("Skipping ignored dependency: {0} {1}", dependency.Id, dependency.Version); + continue; + } + NugetLogger.LogVerbose("Installing Dependency: {0} {1}", dependency.Id, dependency.Version); var dependencyResult = Install(dependency, false, false, false); result.Combine(dependencyResult); diff --git a/src/NuGetForUnity/Editor/PackageRestorer.cs b/src/NuGetForUnity/Editor/PackageRestorer.cs index 8961af9e..88bf3357 100644 --- a/src/NuGetForUnity/Editor/PackageRestorer.cs +++ b/src/NuGetForUnity/Editor/PackageRestorer.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using NugetForUnity.Configuration; using NugetForUnity.Helper; using UnityEditor; using Debug = UnityEngine.Debug; @@ -26,7 +27,7 @@ public static void Restore(bool slimRestore) { var packagesToInstall = InstalledPackagesManager.PackagesConfigFile.Packages.FindAll( - package => !InstalledPackagesManager.IsInstalled(package, !slimRestore)); + package => ShouldRestorePackage(package, !slimRestore)); if (packagesToInstall.Count > 0) { var progressStep = 1.0f / packagesToInstall.Count; @@ -69,5 +70,16 @@ public static void Restore(bool slimRestore) EditorUtility.ClearProgressBar(); } } + + private static bool ShouldRestorePackage(PackageConfig package, bool checkIsAlreadyImportedInEngine) + { + if (!package.IsManuallyInstalled && ConfigurationManager.NugetConfigFile.IsPackageDependencyIgnored(package.Id)) + { + NugetLogger.LogVerbose("Skipping ignored dependency from packages.config: {0} {1}", package.Id, package.Version); + return false; + } + + return !InstalledPackagesManager.IsInstalled(package, checkIsAlreadyImportedInEngine); + } } } diff --git a/src/NuGetForUnity/Editor/Ui/NugetPreferences.cs b/src/NuGetForUnity/Editor/Ui/NugetPreferences.cs index 6f971386..657222ea 100644 --- a/src/NuGetForUnity/Editor/Ui/NugetPreferences.cs +++ b/src/NuGetForUnity/Editor/Ui/NugetPreferences.cs @@ -65,6 +65,8 @@ public class NugetPreferences : SettingsProvider /// private Vector2 pluginsScrollPosition; + private string ignoredPackageDependenciesText; + private GUIStyle redToggleStyle; /// @@ -92,6 +94,7 @@ private NugetPreferences() !UnityPathHelper.IsPathInAssets(ConfigurationManager.NugetConfigFile.PackagesConfigDirectoryPath); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); var enabledPlugins = new HashSet(ConfigurationManager.NugetConfigFile.EnabledPlugins); + ignoredPackageDependenciesText = ConfigurationManager.NugetConfigFile.IgnoredPackageDependenciesText; plugins = assemblies.Where(assembly => assembly.FullName.IndexOf("NugetForUnityPlugin", StringComparison.OrdinalIgnoreCase) >= 0) .Select(assembly => new NugetPlugin(assembly, enabledPlugins.Remove(new NugetForUnityPluginId(assembly)))) .ToList(); @@ -175,6 +178,19 @@ public override void OnGUI([CanBeNull] string searchContext) ConfigurationManager.NugetConfigFile.SlimRestore = slimRestore; } + var ignoredPackageDependencies = EditorGUILayout.TextField( + new GUIContent( + "Ignored Dependencies", + "Package IDs that NuGetForUnity should skip when they are pulled as dependencies of another package. " + + "Separate IDs with semicolons or commas. Explicit installs are still allowed."), + ignoredPackageDependenciesText); + if (ignoredPackageDependencies != ignoredPackageDependenciesText) + { + preferencesChangedThisFrame = true; + ignoredPackageDependenciesText = ignoredPackageDependencies; + ConfigurationManager.NugetConfigFile.IgnoredPackageDependenciesText = ignoredPackageDependenciesText; + } + var newInstallLocation = (PackageInstallLocation)EditorGUILayout.EnumPopup( "Placement:", ConfigurationManager.NugetConfigFile.InstallLocation); @@ -606,6 +622,7 @@ public override void OnGUI([CanBeNull] string searchContext) { NugetConfigFile.CreateDefaultFile(ConfigurationManager.NugetConfigFilePath); ConfigurationManager.LoadNugetConfigFile(); + ignoredPackageDependenciesText = ConfigurationManager.NugetConfigFile.IgnoredPackageDependenciesText; preferencesChangedThisFrame = true; } diff --git a/src/NuGetForUnity/Editor/Ui/NugetWindow.cs b/src/NuGetForUnity/Editor/Ui/NugetWindow.cs index d118c320..8bb7ad87 100644 --- a/src/NuGetForUnity/Editor/Ui/NugetWindow.cs +++ b/src/NuGetForUnity/Editor/Ui/NugetWindow.cs @@ -1160,6 +1160,14 @@ private void DrawPackage(INugetPackage package, GUIStyle backgroundStyle, bool c InstalledPackagesManager.SetManuallyInstalledFlag(installed); ClearViewCache(); } + + if (GUILayout.Button( + new GUIContent( + "Uninstall and Ignore", + "Uninstall this implicit package and add its package ID to Ignored Dependencies."))) + { + UninstallAndIgnoreDependency(installed); + } } if (currentTab == NugetWindowTab.UpdatesTab) @@ -1451,6 +1459,18 @@ private void DrawPackage(INugetPackage package, GUIStyle backgroundStyle, bool c EditorGUI.DrawRect(EditorGUILayout.GetControlRect(false, 1f), Styles.LineColor); } + private void UninstallAndIgnoreDependency([NotNull] INugetPackage installed) + { + if (ConfigurationManager.NugetConfigFile.AddIgnoredPackageDependency(installed.Id)) + { + ConfigurationManager.NugetConfigFile.Save(ConfigurationManager.NugetConfigFilePath); + } + + NugetPackageUninstaller.Uninstall(installed, PackageUninstallReason.IndividualUninstall); + UpdateInstalledPackages(); + UpdateUpdatePackages(); + } + private sealed class VersionDropdownData { public int SelectedIndex { get; set; }