using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using JetBrains.Annotations; using NugetForUnity.Configuration; using NugetForUnity.Helper; using NugetForUnity.Models; using UnityEngine; namespace NugetForUnity { /// /// Manages the content (files inside the .nupkg) of NuGet packages. /// internal static class PackageContentManager { /// /// Deletes all files and folders associated with a package. /// /// The package to remove all its content of. internal static void DeletePackageContentPackage([NotNull] INugetPackageIdentifier package) { var packageInstallDirectory = package.GetPackageInstallPath(); FileSystemHelper.DeleteDirectory(packageInstallDirectory, true); var metaFile = $"{packageInstallDirectory}.meta"; FileSystemHelper.DeleteFile(metaFile); var toolsInstallDirectory = GetPackageOutsideInstallDirectory(package); FileSystemHelper.DeleteDirectory(toolsInstallDirectory, true); } /// /// Cleans up a package after it has been installed. /// Since we are in Unity, we can make certain assumptions on which files will NOT be used, so we can delete them. /// /// The NugetPackage to clean. internal static void CleanInstallationDirectory([NotNull] INugetPackageIdentifier package) { var packageInstallDirectory = package.GetPackageInstallPath(); NugetLogger.LogVerbose("Cleaning {0}", packageInstallDirectory); FileSystemHelper.FixSpaces(packageInstallDirectory); var packageToolsDirectory = Path.Combine(packageInstallDirectory, "tools"); if (Directory.Exists(packageToolsDirectory)) { // Move the tools folder outside of the Unity Assets folder var toolsInstallDirectory = Path.Combine(GetPackageOutsideInstallDirectory(package), "tools"); NugetLogger.LogVerbose("Moving {0} to {1}", packageToolsDirectory, toolsInstallDirectory); // create the directory to create any of the missing folders in the path Directory.CreateDirectory(toolsInstallDirectory); // delete the final directory to prevent the Move operation from throwing exceptions. FileSystemHelper.DeleteDirectory(toolsInstallDirectory, false); Directory.Move(packageToolsDirectory, toolsInstallDirectory); } // if there are native DLLs, copy them to the Unity project root (1 up from Assets) var packageOutputDirectory = Path.Combine(packageInstallDirectory, "output"); if (Directory.Exists(packageOutputDirectory)) { var files = Directory.GetFiles(packageOutputDirectory); foreach (var file in files) { var newFilePath = Path.Combine(UnityPathHelper.AbsoluteProjectPath, Path.GetFileName(file)); FileSystemHelper.MoveFile(file, newFilePath, false); } FileSystemHelper.DeleteDirectory(packageOutputDirectory, true); } // if there are Unity plugin DLLs, copy them to the Unity Plugins folder (Assets/Plugins) var packageUnityPluginDirectory = Path.Combine(packageInstallDirectory, "unityplugin"); if (Directory.Exists(packageUnityPluginDirectory)) { var pluginsDirectory = Path.Combine(Application.dataPath, "Plugins"); FileSystemHelper.DirectoryMove(packageUnityPluginDirectory, pluginsDirectory); FileSystemHelper.DeleteDirectory(packageUnityPluginDirectory, true); FileSystemHelper.DeleteFile($"{packageUnityPluginDirectory}.meta"); } // if there are Unity StreamingAssets, copy them to the Unity StreamingAssets folder (Assets/StreamingAssets) var packageStreamingAssetsDirectory = Path.Combine(packageInstallDirectory, "StreamingAssets"); if (Directory.Exists(packageStreamingAssetsDirectory)) { var streamingAssetsDirectory = Path.Combine(Application.dataPath, "StreamingAssets"); FileSystemHelper.DirectoryMove(packageStreamingAssetsDirectory, streamingAssetsDirectory); // delete the package's StreamingAssets folder and .meta file FileSystemHelper.DeleteDirectory(packageStreamingAssetsDirectory, true); FileSystemHelper.DeleteFile($"{packageStreamingAssetsDirectory}.meta"); } } /// /// Specifies if a file should be extracted from a .nupkg, because NuGetForUnity needs it. /// /// /// The path of the file inside the .nupkg it is relative starting from the package route. It always uses '/' as a slash on all /// platforms. /// /// True if the file can be skipped, is not needed. internal static bool ShouldSkipUnpackingOnPath([NotNull] string path) { if (path.EndsWith("/")) { // We do not want to extract empty directory entries. If there are empty directories within the .nupkg, we // expect them to have a file named '_._' in them that indicates that it should be extracted, usually as a // compatibility indicator (https://stackoverflow.com/questions/36338052/what-do-files-mean-in-nuget-packages) return true; } // skip directories & files that NuGet normally deletes if (path.StartsWith("_rels/", StringComparison.Ordinal) || path.Contains("/_rels/")) { return true; } if (path.StartsWith("package/", StringComparison.Ordinal) || path.Contains("/package/")) { return true; } if (path.EndsWith("[Content_Types].xml", StringComparison.Ordinal)) { return true; } // Unity has no use for the build directory if (path.StartsWith("build/", StringComparison.Ordinal) || path.Contains("/build/")) { return true; } // For now, skip src. We may use it later... if (path.StartsWith("src/", StringComparison.Ordinal) || path.Contains("/src/")) { return true; } // Since we don't automatically fix up the runtime dll platforms, skip them until we improve support // for this newer feature of nuget packages. if (path.StartsWith("runtimes/", StringComparison.Ordinal) || path.Contains("/runtimes/")) { return true; } // Skip documentation folders since they sometimes have HTML docs with JavaScript, which Unity tried to parse as "UnityScript" if (path.StartsWith("docs/", StringComparison.Ordinal) || path.Contains("/docs/")) { return true; } // Skip ref folder, as it is just used for compile-time reference and does not contain implementations. // Leaving it results in "assembly loading" and "multiple pre-compiled assemblies with same name" errors if (path.StartsWith("ref/", StringComparison.Ordinal) || path.Contains("/ref/")) { return true; } // Skip all PDB files since Unity uses Mono and requires MDB files, which causes it to output "missing MDB" errors if (path.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase)) { return true; } // Skip all folders that contain localization resource file // Format of these entries is lib///... const string libDirectoryName = "lib/"; if (path.StartsWith(libDirectoryName, StringComparison.Ordinal) || path.Contains("/lib/")) { var libSlashIndex = path.IndexOf(libDirectoryName, StringComparison.Ordinal) + libDirectoryName.Length; var secondSlashIndex = path.IndexOf('/', libSlashIndex); if (secondSlashIndex == -1) { return false; } var thirdSlashIndex = path.IndexOf('/', secondSlashIndex + 1); if (thirdSlashIndex == -1) { return false; } var langLength = thirdSlashIndex - secondSlashIndex - 1; if (langLength == 2 || (langLength > 2 && path[secondSlashIndex + 3] == '-')) { return true; } } return false; } /// /// Extracts source files from a source code .nupkg into the /Sources. /// /// The source file entries from the .nupkg zip file. /// The path of the directory under which the 'Sources' sub-directory should be placed. internal static void ExtractPackageSources([NotNull] List entries, [NotNull] string baseDir) { if (entries.Count == 0) { return; } var lastCommonDir = entries[0].FullName; lastCommonDir = lastCommonDir.Substring(0, lastCommonDir.LastIndexOf('/') + 1); for (var i = 1; i < entries.Count; ++i) { var entryFullName = entries[i].FullName; for (var j = 0; j < entryFullName.Length && j < lastCommonDir.Length; j++) { if (entryFullName[j] != lastCommonDir[j]) { lastCommonDir = entryFullName.Substring(0, entryFullName.LastIndexOf('/', j) + 1); break; } } } var sourcesDirectory = Path.Combine(baseDir, "Sources"); if (!Directory.Exists(sourcesDirectory)) { Directory.CreateDirectory(sourcesDirectory); } foreach (var entry in entries) { ExtractPackageEntry(entry, sourcesDirectory, lastCommonDir.Length); } } /// /// Extracts a file from a .nupkg into the . /// /// The file entry from the .nupkg zip file. /// The path of the directory where the package output should be placed. /// Index to which we want to skip within path located in FullPath of 'entry' parameter. internal static void ExtractPackageEntry([NotNull] ZipArchiveEntry entry, [NotNull] string baseDir, int skipEntryLength = 0) { // Normalizes the path. baseDir = Path.GetFullPath(baseDir); // Ensures that the last character on the extraction path is the directory separator char. if (!baseDir.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)) { baseDir += Path.DirectorySeparatorChar; } // Gets the full path to ensure that relative segments are removed. var entryFullName = entry.FullName; if (skipEntryLength > 0) { entryFullName = entryFullName.Substring(skipEntryLength); } var filePath = Path.GetFullPath(Path.Combine(baseDir, entryFullName)); if (!filePath.StartsWith(baseDir, StringComparison.Ordinal)) { Debug.LogWarning($"Entry {entryFullName} is trying to leave the output directory. We skip it."); return; } var directory = Path.GetDirectoryName(filePath) ?? throw new InvalidOperationException($"Failed to get directory name of '{filePath}'"); Directory.CreateDirectory(directory); if (Directory.Exists(filePath)) { Debug.LogWarning($"The path {filePath} refers to an existing directory. Overwriting it may lead to data loss."); return; } entry.ExtractToFile(filePath, true); if (ConfigurationManager.NugetConfigFile.ReadOnlyPackageFiles) { var extractedFile = new FileInfo(filePath); extractedFile.Attributes |= FileAttributes.ReadOnly; } } /// /// Moves already installed packages from the old to a new path. /// /// Old package installation absolute path. /// New package installation absolute path. internal static void MoveInstalledPackages(string oldPath, string newPath) { if (!Directory.Exists(oldPath)) { return; } Directory.CreateDirectory(newPath); // We only move the package folders because users might have other things in that folder foreach (var package in InstalledPackagesManager.InstalledPackages) { var packageFolderName = $"{package.Id}.{package.Version}"; var oldPackagePath = Path.Combine(oldPath, packageFolderName); var newPackagePath = Path.Combine(newPath, packageFolderName); if (Directory.Exists(oldPackagePath)) { Directory.Move(oldPackagePath, newPackagePath); var oldMetaPath = $"{oldPackagePath}.meta"; if (File.Exists(oldMetaPath)) { File.Move(oldMetaPath, $"{newPackagePath}.meta"); } } } var oldPackageJsonPath = Path.Combine(oldPath, "package.json"); if (File.Exists(oldPackageJsonPath)) { File.Delete(oldPackageJsonPath); var oldPackageJsonMetaPath = $"{oldPackageJsonPath}.meta"; if (File.Exists(oldPackageJsonMetaPath)) { File.Delete(oldPackageJsonMetaPath); } } if (Directory.EnumerateFileSystemEntries(oldPath).Any()) { return; } // If there is nothing left in the oldPath we remove the whole directory Directory.Delete(oldPath); var metaPath = oldPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + ".meta"; if (File.Exists(metaPath)) { File.Delete(metaPath); } } [NotNull] private static string GetPackageOutsideInstallDirectory([NotNull] INugetPackageIdentifier package) { return Path.Combine(UnityPathHelper.AbsoluteProjectPath, "Packages", $"{package.Id}.{package.Version}"); } } }