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 { private static readonly string[] RuntimePluginFileExtentions = { ".so" /* Linux */, ".dylib" /* OSX */, ".dll" /* Windows */, ".lib", /* Windows */ }; /// /// 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. /// False to prevent updating of packages that are explicitly installed. /// True if the package was installed successfully, otherwise false. public static bool InstallIdentifier( [NotNull] INugetPackageIdentifier package, bool refreshAssets = true, bool isSlimRestoreInstall = false, bool allowUpdateForExplicitlyInstalled = true) { var result = Install(package, refreshAssets, isSlimRestoreInstall, allowUpdateForExplicitlyInstalled); return result.Successful; } /// /// 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. /// False to prevent updating of packages that are explicitly installed. /// True if the package was installed successfully, otherwise false. internal static PackageInstallOperationResult Install( [NotNull] INugetPackageIdentifier package, bool refreshAssets = true, bool isSlimRestoreInstall = false, bool allowUpdateForExplicitlyInstalled = true) { if (!isSlimRestoreInstall && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false)) { NugetLogger.LogVerbose("Package {0} is already imported in engine, skipping install.", package); return new PackageInstallOperationResult { Successful = true }; } var foundPackage = PackageCacheManager.GetPackageFromCacheOrSource(package); if (foundPackage == null) { Debug.LogErrorFormat("Could not find {0} {1} or greater.", package.Id, package.Version); return new PackageInstallOperationResult { Successful = false }; } foundPackage.IsManuallyInstalled = package.IsManuallyInstalled; return Install(foundPackage, refreshAssets, isSlimRestoreInstall, allowUpdateForExplicitlyInstalled); } /// /// 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. /// False to prevent updating of packages that are explicitly installed. /// True if the package was installed successfully, otherwise false. private static PackageInstallOperationResult Install( [NotNull] INugetPackage package, bool refreshAssets, bool isSlimRestoreInstall, bool allowUpdateForExplicitlyInstalled) { if (!isSlimRestoreInstall && UnityPreImportedLibraryResolver.IsAlreadyImportedInEngine(package.Id, false)) { NugetLogger.LogVerbose("Package {0} is already imported in engine, skipping install.", package); return new PackageInstallOperationResult { Successful = true }; } // check if the package (any version) is already installed if (InstalledPackagesManager.TryGetById(package.Id, out var installedPackage)) { var comparisonResult = installedPackage.CompareTo(package); if (!allowUpdateForExplicitlyInstalled && comparisonResult != 0 && installedPackage.IsManuallyInstalled) { NugetLogger.LogVerbose( "{0} {1} is installed explicitly so it will not be replaced with {3}", installedPackage.Id, installedPackage.Version, package.Version, package.Version); return new PackageInstallOperationResult { Successful = true }; } 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.UpdateWithInformation(installedPackage, package, refreshAssets, isSlimRestoreInstall); } 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.UpdateWithInformation(installedPackage, package, refreshAssets, isSlimRestoreInstall); } 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 new PackageInstallOperationResult { Successful = true }; } try { var result = new PackageInstallOperationResult { Successful = true }; 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 dependencyResult = Install(dependency, false, false, false); result.Combine(dependencyResult); if (!dependencyResult.Successful) { 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 resultEntry = new PackageInstallOperationResultEntry(package); result.Packages.Add(resultEntry); 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, resultEntry)) { 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; } var extractedFilePath = PackageContentManager.ExtractPackageEntry(entry, baseDirectory); if (extractedFilePath != null && (entryFullName.StartsWith("runtimes/", StringComparison.Ordinal) || entryFullName.Contains("/runtimes/")) && RuntimePluginFileExtentions.Any(extension => entryFullName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))) { // write a temporary plug-in importer setting that is replaced with the correct configuration by 'NugetAssetPostprocessor' WriteInitialExcludeAllPluginImporterConfig(extractedFilePath); } } // 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 result; } catch (Exception e) { Debug.LogErrorFormat("Unable to install package {0} {1}\n{2}", package.Id, package.Version, e); return new PackageInstallOperationResult { Successful = false }; } finally { if (refreshAssets) { EditorUtility.DisplayProgressBar($"Installing {package.Id} {package.Version}", "Importing Package", 0.95f); AssetDatabase.Refresh(); } EditorUtility.ClearProgressBar(); } } private static void WriteInitialExcludeAllPluginImporterConfig([NotNull] string extractedFilePath) { File.WriteAllText( $"{extractedFilePath}.meta", $@" fileFormatVersion: 2 guid: {Guid.NewGuid():N} PluginImporter: serializedVersion: 2 defineConstraints: [] isPreloaded: 0 isOverridable: 0 isExplicitlyReferenced: 0 validateReferences: 1 platformData: - first: '': Any second: enabled: 0 settings: 'Exclude ': 1 Exclude Android: 1 Exclude Bratwurst: 1 Exclude CloudRendering: 1 Exclude Editor: 1 Exclude EmbeddedLinux: 1 Exclude GameCoreScarlett: 1 Exclude GameCoreXboxOne: 1 Exclude Linux64: 1 Exclude OSXUniversal: 1 Exclude PS4: 1 Exclude PS5: 1 Exclude QNX: 1 Exclude Switch: 1 Exclude WebGL: 1 Exclude Win: 1 Exclude Win64: 1 Exclude WindowsStoreApps: 1 Exclude XboxOne: 1 Exclude iOS: 1 Exclude tvOS: 1 - first: Any: second: enabled: 0 - first: Editor: Editor second: enabled: 0 - first: Standalone: Linux64 second: enabled: 0 - first: Standalone: OSXUniversal second: enabled: 0 - first: Standalone: Win second: enabled: 0 - first: Standalone: Win64 second: enabled: 0 - first: Windows Store Apps: WindowsStoreApps second: enabled: 0 - first: iPhone: iOS second: enabled: 0 userData: assetBundleName: assetBundleVariant: "); } private static void TryExtractBestFrameworkSources( [NotNull] [ItemNotNull] Dictionary> 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}", frameworkKey, 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( Dictionary> frameworkZipEntries, string framework, ZipArchiveEntry entry) { if (!frameworkZipEntries.TryGetValue(framework, out var entryList)) { entryList = new List(); frameworkZipEntries.Add(framework, entryList); } entryList.Add(entry); } } }