using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using JetBrains.Annotations; using NugetForUnity.Configuration; using NugetForUnity.Models; using NugetForUnity.PluginSupport; using UnityEditor; using UnityEngine; namespace NugetForUnity { /// /// Installs NuGet packages into the Unity project. Opposite of . /// public static class NugetPackageInstaller { /// /// Installs the package given by the identifier. It fetches the appropriate full package from the installed packages, package cache, or package /// sources and installs it. /// /// The identifier of the package to install. /// True to refresh the Unity asset database. False to ignore the changes (temporarily). /// True to skip checking if lib is imported in Unity and skip installing dependencies. /// True if the package was installed successfully, otherwise false. public static bool InstallIdentifier([NotNull] INugetPackageIdentifier package, bool refreshAssets = true, bool isSlimRestoreInstall = false) { if (!isSlimRestoreInstall && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false)) { NugetLogger.LogVerbose("Package {0} is already imported in engine, skipping install.", package); return true; } var foundPackage = PackageCacheManager.GetPackageFromCacheOrSource(package); if (foundPackage == null) { Debug.LogErrorFormat("Could not find {0} {1} or greater.", package.Id, package.Version); return false; } foundPackage.IsManuallyInstalled = package.IsManuallyInstalled; return Install(foundPackage, refreshAssets, isSlimRestoreInstall); } /// /// Installs the given package. /// /// The package to install. /// True to refresh the Unity asset database. False to ignore the changes (temporarily). /// True to skip checking if lib is imported in Unity and skip installing dependencies. /// True if the package was installed successfully, otherwise false. private static bool Install([NotNull] INugetPackage package, bool refreshAssets, bool isSlimRestoreInstall) { if (!isSlimRestoreInstall && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false)) { NugetLogger.LogVerbose("Package {0} is already imported in engine, skipping install.", package); return true; } // check if the package (any version) is already installed if (InstalledPackagesManager.TryGetById(package.Id, out var installedPackage)) { var comparisonResult = installedPackage.CompareTo(package); if (comparisonResult < 0) { NugetLogger.LogVerbose( "{0} {1} is installed, but need {2} or greater. Updating to {3}", installedPackage.Id, installedPackage.Version, package.Version, package.Version); return NugetPackageUpdater.Update(installedPackage, package, refreshAssets); } if (comparisonResult > 0) { var configPackage = InstalledPackagesManager.GetPackageConfigurationById(package.Id); if (configPackage != null && configPackage.PackageVersion < installedPackage.PackageVersion) { NugetLogger.LogVerbose( "{0} {1} is installed but config needs {2} so downgrading.", installedPackage.Id, installedPackage.Version, package.Version); return NugetPackageUpdater.Update(installedPackage, package, refreshAssets); } NugetLogger.LogVerbose( "{0} {1} is installed. {2} or greater is needed, so using installed version.", installedPackage.Id, installedPackage.Version, package.Version); } else { NugetLogger.LogVerbose("Already installed: {0} {1}", package.Id, package.Version); } return true; } try { NugetLogger.LogVerbose("Installing: {0} {1}", package.Id, package.Version); EditorUtility.DisplayProgressBar($"Installing {package.Id} {package.Version}", "Installing Dependencies", 0.1f); if (!isSlimRestoreInstall) { var dependencyGroups = package.Dependencies; // install all dependencies for target framework var frameworkGroup = TargetFrameworkResolver.GetNullableBestDependencyFrameworkGroupForCurrentSettings( dependencyGroups, InstalledPackagesManager.GetPackageConfigurationById(package.Id)?.TargetFramework); if (frameworkGroup == null && dependencyGroups.Count != 0) { Debug.LogWarningFormat( "Can't find a matching dependency group for the NuGet Package {0} {1} that has a TargetFramework supported by the current Unity Scripting Backend. The NuGet Package supports the following TargetFramework's: {2}", package.Id, package.Version, string.Join(", ", dependencyGroups.Select(dependency => dependency.TargetFramework))); } else if (frameworkGroup != null) { NugetLogger.LogVerbose("Installing dependencies for TargetFramework: {0}", frameworkGroup.TargetFramework); foreach (var dependency in frameworkGroup.Dependencies) { NugetLogger.LogVerbose("Installing Dependency: {0} {1}", dependency.Id, dependency.Version); var installed = InstallIdentifier(dependency, false); if (!installed) { throw new InvalidOperationException($"Failed to install dependency: {dependency.Id} {dependency.Version}."); } } } } // update packages.config var packageConfig = InstalledPackagesManager.AddPackageToConfig(package); var cachedPackagePath = Path.Combine(PackageCacheManager.CacheOutputDirectory, package.PackageFileName); if (ConfigurationManager.NugetConfigFile.InstallFromCache && File.Exists(cachedPackagePath)) { NugetLogger.LogVerbose("Cached package found for {0} {1}", package.Id, package.Version); } else { NugetLogger.LogVerbose("Downloading package {0} {1}", package.Id, package.Version); EditorUtility.DisplayProgressBar($"Installing {package.Id} {package.Version}", "Downloading Package", 0.3f); package.DownloadNupkgToFile(cachedPackagePath); } EditorUtility.DisplayProgressBar($"Installing {package.Id} {package.Version}", "Extracting Package", 0.6f); if (File.Exists(cachedPackagePath)) { var baseDirectory = package.GetPackageInstallPath(); // unzip the package using (var zip = ZipFile.OpenRead(cachedPackagePath)) { var libs = new Dictionary>(); var csFiles = new Dictionary>(); var anyFiles = new Dictionary>(); foreach (var entry in zip.Entries) { var entryFullName = entry.FullName; if (PluginRegistry.Instance.HandleFileExtraction(package, entry, baseDirectory)) { continue; } if (PackageContentManager.ShouldSkipUnpackingOnPath(entryFullName)) { continue; } // we don't want to unpack all lib folders and then delete all but one; rather we will decide the best // target framework before unpacking, but first we need to collect all lib entries from zip const string libDirectoryName = "lib/"; if (entryFullName.StartsWith(libDirectoryName, StringComparison.Ordinal)) { var frameworkStartIndex = libDirectoryName.Length; var secondSlashIndex = entryFullName.IndexOf('/', frameworkStartIndex); if (secondSlashIndex == -1) { // a file inside lib folder -> we keep it. This is to support packages that have no framework dependent sub directories. PackageContentManager.ExtractPackageEntry(entry, baseDirectory); continue; } var framework = entryFullName.Substring(libDirectoryName.Length, secondSlashIndex - frameworkStartIndex); FillFrameworkZipEntries(libs, framework, entry); continue; } // in case this is a source code package, we want to collect all its entries that have 'cs' or 'any' set as language // and their frameworks so we can get the best framework later const string contentFilesDirectoryName = "contentFiles/"; if (entryFullName.StartsWith(contentFilesDirectoryName, StringComparison.Ordinal)) { // Folder structure for source code packages: // └─. (not counted here since entries start with next subfolder) // └─ // └─ (language) // └─ (framework) // └─ // In order to create shorter paths, we aim to make a structure like this: // └─. // └─Sources // └─ var directoriesSplit = entryFullName.Split('/'); if (directoriesSplit.Length >= 4) { var language = directoriesSplit[1]; var framework = directoriesSplit[2]; if (language.Equals("cs", StringComparison.Ordinal)) { FillFrameworkZipEntries(csFiles, framework, entry); } if (language.Equals("any", StringComparison.Ordinal)) { FillFrameworkZipEntries(anyFiles, framework, entry); } } // if the entry is in content files we want to skip unpacking it right now anyway continue; } PackageContentManager.ExtractPackageEntry(entry, baseDirectory); } // go through all lib zip entries and find the best target framework, then unpack it if (libs.Count > 0) { var bestFrameworkMatch = TargetFrameworkResolver.TryGetBestTargetFramework( libs, packageConfig.TargetFramework, framework => framework.Key); if (bestFrameworkMatch.Value != null) { NugetLogger.LogVerbose( "Selecting target framework directory '{0}' as best match for the package {1}", bestFrameworkMatch.Key, package); foreach (var entry in bestFrameworkMatch.Value) { PackageContentManager.ExtractPackageEntry(entry, baseDirectory); } } else { Debug.LogWarningFormat( "Couldn't find a library folder with a supported target-framework for the package {0}", package); } } // go through all content files' frameworks and figure the best target network, prioritizing 'cs' over 'any' language if (csFiles.Count > 0) { TryExtractBestFrameworkSources(csFiles, baseDirectory, package, packageConfig); } else if (anyFiles.Count > 0) { TryExtractBestFrameworkSources(anyFiles, baseDirectory, package, packageConfig); } } } else { Debug.LogErrorFormat("File not found: {0}", cachedPackagePath); } EditorUtility.DisplayProgressBar($"Installing {package.Id} {package.Version}", "Cleaning Package", 0.9f); // clean PackageContentManager.CleanInstallationDirectory(package); // update the installed packages list InstalledPackagesManager.AddPackageToInstalled(package); return true; } catch (Exception e) { Debug.LogErrorFormat("Unable to install package {0} {1}\n{2}", package.Id, package.Version, e); return false; } finally { if (refreshAssets) { EditorUtility.DisplayProgressBar($"Installing {package.Id} {package.Version}", "Importing Package", 0.95f); AssetDatabase.Refresh(); } EditorUtility.ClearProgressBar(); } } private static void TryExtractBestFrameworkSources( [NotNull] [ItemNotNull] IReadOnlyDictionary> frameworks, [NotNull] string sourceDirName, [NotNull] INugetPackage package, [NotNull] PackageConfig packageConfig) { var bestFrameworkMatch = TargetFrameworkResolver.TryGetBestTargetFramework( frameworks, packageConfig.TargetFramework, framework => framework.Key); var frameworkKey = bestFrameworkMatch.Key ?? "any"; if (frameworks.TryGetValue(frameworkKey, out var bestFramework)) { NugetLogger.LogVerbose( "Selecting target framework directory '{0}' and language '{1}' as best match for the package {2}", bestFramework, sourceDirName, package); PackageContentManager.ExtractPackageSources(bestFramework, sourceDirName); } else { Debug.LogWarningFormat("Couldn't find a source code folder with a supported target-framework for the package {0}", package); } } private static void FillFrameworkZipEntries( IDictionary> frameworkZipEntries, string framework, ZipArchiveEntry entry) { if (!frameworkZipEntries.TryGetValue(framework, out var entryList)) { entryList = new List(); frameworkZipEntries.Add(framework, entryList); } entryList.Add(entry); } } }