using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using JetBrains.Annotations; using NugetForUnity.Configuration; using NugetForUnity.Models; using NugetForUnity.PackageSource; using NugetForUnity.PluginSupport; using Debug = UnityEngine.Debug; namespace NugetForUnity { /// /// Manages the packages currently installed in the unity project. /// public static class InstalledPackagesManager { /// /// The dictionary of currently installed s keyed off of their ID string. /// [CanBeNull] private static Dictionary installedPackages; /// /// Backing field for the packages.config file. /// [CanBeNull] private static PackagesConfigFile packagesConfigFile; /// /// Gets the loaded packages.config file that hold the dependencies for the project. /// [NotNull] public static PackagesConfigFile PackagesConfigFile { get { if (packagesConfigFile is null) { packagesConfigFile = PackagesConfigFile.Load(); } return packagesConfigFile; } } /// /// Gets the packages that are actually installed in the project. /// [NotNull] public static IEnumerable InstalledPackages => InstalledPackagesDictionary.Values; /// /// Gets the dictionary of packages that are actually installed in the project, keyed off of the ID. /// [NotNull] private static Dictionary InstalledPackagesDictionary { get { if (installedPackages == null) { UpdateInstalledPackages(); } return installedPackages; } } /// /// Gets the configuration of the given package from the packages.config file. /// /// The package identifier used to search the package in the packages.config (). /// The found configuration or null if there is no matching package inside the packages.config. [CanBeNull] internal static PackageConfig GetPackageConfigurationById([NotNull] string packageIdentifier) { return PackagesConfigFile.GetPackageConfigurationById(packageIdentifier); } /// /// Returns whether or not the specified package has been marked as manually installed inside the packages.config file. /// /// The package identifier used to search the package in the packages.config (). /// True if the package has been manually installed or no configuration was found, false otherwise. internal static bool GetManuallyInstalledFlagFromConfiguration([NotNull] string packageIdentifier) { var packageConfiguration = GetPackageConfigurationById(packageIdentifier); return packageConfiguration == null || packageConfiguration.IsManuallyInstalled; } /// /// Sets the manually installed flag for the given package and stores the change in the . /// /// The package to mark as manually installed. internal static void SetManuallyInstalledFlag([NotNull] INugetPackageIdentifier package) { PackagesConfigFile.SetManuallyInstalledFlag(package); PackagesConfigFile.Save(); } /// /// Invalidates the currently loaded 'packages.config' so it is reloaded when it is accessed the next time. /// internal static void ReloadPackagesConfig() { packagesConfigFile = null; } /// /// Removes the package from the 'packages.config' and from the installed packages dictionary (). /// /// The package to remove. internal static void RemovePackage([NotNull] INugetPackage package) { // update the package.config file if (PackagesConfigFile.RemovePackage(package)) { PackagesConfigFile.Save(); } InstalledPackagesDictionary.Remove(package.Id); } /// /// Adds the package to the 'packages.config' file. /// /// The package to add. /// The newly added or allready existing config entry from the packages.config file. [NotNull] internal static PackageConfig AddPackageToConfig([NotNull] INugetPackage package) { var packageConfig = PackagesConfigFile.AddPackage(package); PackagesConfigFile.Save(); return packageConfig; } /// /// Adds the package to the installed packages dictionary (). /// /// The package to add. internal static void AddPackageToInstalled([NotNull] INugetPackage package) { InstalledPackagesDictionary.Add(package.Id, package); } /// /// Gets the package from the installed packages dictionary () by its ID. /// /// The id of the package to search in the currently installed packages. /// The found installed package. /// True if a package was found. internal static bool TryGetById([NotNull] string id, out INugetPackage package) { return InstalledPackagesDictionary.TryGetValue(id, out package); } /// /// Updates the dictionary of packages that are actually installed in the project based on the files that are currently installed. /// internal static void UpdateInstalledPackages() { if (installedPackages == null) { installedPackages = new Dictionary(StringComparer.OrdinalIgnoreCase); } else { installedPackages.Clear(); } // loops through the packages that are actually installed in the project if (!Directory.Exists(ConfigurationManager.NugetConfigFile.RepositoryPath)) { return; } var stopwatch = Stopwatch.StartNew(); var manuallyInstalledPackagesNumber = 0; // a package that was installed via a old version of NuGetForUnity will have the .nupkg it came from inside the folder, // as we updated the behavior to only keep the .nuspec. So we need to extract the .nuspec from the .nupkg var nupkgFiles = Directory.GetFiles(ConfigurationManager.NugetConfigFile.RepositoryPath, "*.nupkg", SearchOption.AllDirectories); foreach (var nupkgFile in nupkgFiles) { using (var zip = ZipFile.OpenRead(nupkgFile)) { foreach (var entry in zip.Entries) { var entryFullName = entry.FullName; // extract only .nuspec and .nuspec.meta files if (entryFullName.EndsWith(".nuspec.meta", StringComparison.Ordinal) || entryFullName.EndsWith(".nuspec", StringComparison.Ordinal)) { PackageContentManager.ExtractPackageEntry( entry, Path.GetDirectoryName(nupkgFile) ?? throw new InvalidOperationException($"Failed to get directory from '{nupkgFile}'")); } } } // delete .nupkg and its .meta file to sync compatibility File.Delete(nupkgFile); var metaFile = $"{nupkgFile}.meta"; if (File.Exists(metaFile)) { File.Delete($"{nupkgFile}.meta"); } } var nuspecFiles = Directory.GetFiles(ConfigurationManager.NugetConfigFile.RepositoryPath, "*.nuspec", SearchOption.AllDirectories); foreach (var nuspecFile in nuspecFiles) { var directoryName = Path.GetFileName(Path.GetDirectoryName(nuspecFile)); if (!string.IsNullOrEmpty(directoryName) && directoryName[0] == '.') { // Skip nuspec files that are in directories starting with '.' since those are considered hidden and should be ignored. continue; } var package = NugetPackageLocal.FromNuspec( NuspecFile.Load(nuspecFile), new NugetPackageSourceLocal( "Nuspec file from Project", Path.GetDirectoryName(nuspecFile) ?? throw new InvalidOperationException($"Failed to get directory from '{nuspecFile}'"))); AddPackageToInstalledInternal(package, ref manuallyInstalledPackagesNumber); } if (manuallyInstalledPackagesNumber == 0) { // set root packages as manually installed if none are marked as such foreach (var rootPackage in GetInstalledRootPackages()) { PackagesConfigFile.SetManuallyInstalledFlag(rootPackage); } PackagesConfigFile.Save(); } NugetLogger.LogVerbose("Getting installed packages took {0} ms", stopwatch.ElapsedMilliseconds); } /// /// Checks if there are any packages inside the package install directory that are not listed inside the packages.config. /// /// True if some packages are deleted. internal static bool RemoveUnnecessaryPackages() { if (!Directory.Exists(ConfigurationManager.NugetConfigFile.RepositoryPath)) { return false; } var directories = Directory.GetDirectories(ConfigurationManager.NugetConfigFile.RepositoryPath, "*", SearchOption.TopDirectoryOnly); var somethingDeleted = false; foreach (var folder in directories) { var folderName = Path.GetFileName(folder); if (folderName.StartsWith(".", StringComparison.Ordinal)) { // ignore folders whose name starts with a dot because they are considered hidden continue; } var nuspecPath = Directory.GetFiles(folder, "*.nuspec").FirstOrDefault(); if (!File.Exists(nuspecPath)) { // ignore folder not containing a nuspec file continue; } var package = NugetPackageLocal.FromNuspecFile( nuspecPath, new NugetPackageSourceLocal( "Nuspec file already installed", Path.GetDirectoryName(nuspecPath) ?? throw new InvalidOperationException($"Failed to get directory from '{nuspecPath}'"))); var installed = PackagesConfigFile.Packages.Exists(packageId => packageId.Equals(package)); if (!installed) { somethingDeleted = true; NugetLogger.LogVerbose("---DELETE unnecessary package {0}", folder); PackageContentManager.DeletePackageContentPackage(package); } } if (somethingDeleted) { UpdateInstalledPackages(); } return somethingDeleted; } /// /// Checks if a given package is installed. /// /// The package to check if is installed. /// Determine if it should check if the package is already imported by unity itself. /// True if the given package is installed. False if it is not. internal static bool IsInstalled([NotNull] INugetPackageIdentifier package, bool checkIsAlreadyImportedInEngine) { if (checkIsAlreadyImportedInEngine && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id)) { return true; } var isInstalled = false; if (InstalledPackagesDictionary.TryGetValue(package.Id, out var installedPackage)) { isInstalled = package.Equals(installedPackage); } return isInstalled; } /// /// Checks if any version of the given package Id is installed. /// /// The package to check if is installed. /// Determine if it should check if the package is already imported by unity itself. /// True if the given package is installed. False if it is not. internal static bool IsInstalled([NotNull] string packageId, bool checkIsAlreadyImportedInEngine) { if (checkIsAlreadyImportedInEngine && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(packageId)) { return true; } return InstalledPackagesDictionary.ContainsKey(packageId); } /// /// Gets a list of all root packages that are installed in the project. /// Root packages are packages that are not depended on by any other package. /// /// The root packages. [NotNull] [ItemNotNull] internal static List GetInstalledRootPackages() { // default all packages to being roots var roots = new List(InstalledPackages); // remove a package as a root if another package is dependent on it foreach (var package in InstalledPackages) { var frameworkDependencies = package.CurrentFrameworkDependencies; foreach (var dependency in frameworkDependencies) { roots.RemoveAll(p => p.Id == dependency.Id); } } return roots; } private static void AddPackageToInstalledInternal([NotNull] INugetPackage package, ref int manuallyInstalledPackagesNumber) { var packages = InstalledPackagesDictionary; if (!packages.ContainsKey(package.Id)) { PluginRegistry.Instance.ProcessInstalledPackage(package); package.IsManuallyInstalled = GetPackageConfigurationById(package.Id)?.IsManuallyInstalled ?? false; if (package.IsManuallyInstalled) { manuallyInstalledPackagesNumber++; } packages.Add(package.Id, package); } else { Debug.LogErrorFormat("Package is already in installed list: {0}", package.Id); } } } }