|
@@ -0,0 +1,296 @@
|
|
|
+using System;
|
|
|
+using System.IO;
|
|
|
+using System.IO.Compression;
|
|
|
+using System.Linq;
|
|
|
+using System.Security.Cryptography;
|
|
|
+using System.Text;
|
|
|
+using System.Xml;
|
|
|
+using Godot;
|
|
|
+using GodotTools.Internals;
|
|
|
+using GodotTools.Shared;
|
|
|
+using Directory = GodotTools.Utils.Directory;
|
|
|
+using Environment = System.Environment;
|
|
|
+using File = GodotTools.Utils.File;
|
|
|
+
|
|
|
+namespace GodotTools.Build
|
|
|
+{
|
|
|
+ public static class NuGetUtils
|
|
|
+ {
|
|
|
+ public const string GodotFallbackFolderName = "Godot Offline Packages";
|
|
|
+
|
|
|
+ public static string GodotFallbackFolderPath
|
|
|
+ => Path.Combine(GodotSharpDirs.MonoUserDir, "GodotNuGetFallbackFolder");
|
|
|
+
|
|
|
+ private static void AddFallbackFolderToNuGetConfig(string nuGetConfigPath, string name, string path)
|
|
|
+ {
|
|
|
+ var xmlDoc = new XmlDocument();
|
|
|
+ xmlDoc.Load(nuGetConfigPath);
|
|
|
+
|
|
|
+ const string nuGetConfigRootName = "configuration";
|
|
|
+
|
|
|
+ var rootNode = xmlDoc.DocumentElement;
|
|
|
+
|
|
|
+ if (rootNode == null)
|
|
|
+ {
|
|
|
+ // No root node, create it
|
|
|
+ rootNode = xmlDoc.CreateElement(nuGetConfigRootName);
|
|
|
+ xmlDoc.AppendChild(rootNode);
|
|
|
+
|
|
|
+ // Since this can be considered pretty much a new NuGet.Config, add the default nuget.org source as well
|
|
|
+ XmlElement nugetOrgSourceEntry = xmlDoc.CreateElement("add");
|
|
|
+ nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("key")).Value = "nuget.org";
|
|
|
+ nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = "https://api.nuget.org/v3/index.json";
|
|
|
+ nugetOrgSourceEntry.Attributes.Append(xmlDoc.CreateAttribute("protocolVersion")).Value = "3";
|
|
|
+ rootNode.AppendChild(xmlDoc.CreateElement("packageSources")).AppendChild(nugetOrgSourceEntry);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // Check that the root node is the expected one
|
|
|
+ if (rootNode.Name != nuGetConfigRootName)
|
|
|
+ throw new Exception("Invalid root Xml node for NuGet.Config. " +
|
|
|
+ $"Expected '{nuGetConfigRootName}' got '{rootNode.Name}'.");
|
|
|
+ }
|
|
|
+
|
|
|
+ var fallbackFoldersNode = rootNode["fallbackPackageFolders"] ??
|
|
|
+ rootNode.AppendChild(xmlDoc.CreateElement("fallbackPackageFolders"));
|
|
|
+
|
|
|
+ // Check if it already has our fallback package folder
|
|
|
+ for (var xmlNode = fallbackFoldersNode.FirstChild; xmlNode != null; xmlNode = xmlNode.NextSibling)
|
|
|
+ {
|
|
|
+ if (xmlNode.NodeType != XmlNodeType.Element)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ var xmlElement = (XmlElement)xmlNode;
|
|
|
+ if (xmlElement.Name == "add" &&
|
|
|
+ xmlElement.Attributes["key"]?.Value == name &&
|
|
|
+ xmlElement.Attributes["value"]?.Value == path)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ XmlElement newEntry = xmlDoc.CreateElement("add");
|
|
|
+ newEntry.Attributes.Append(xmlDoc.CreateAttribute("key")).Value = name;
|
|
|
+ newEntry.Attributes.Append(xmlDoc.CreateAttribute("value")).Value = path;
|
|
|
+
|
|
|
+ fallbackFoldersNode.AppendChild(newEntry);
|
|
|
+
|
|
|
+ xmlDoc.Save(nuGetConfigPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Returns all the paths where the user NuGet.Config files can be found.
|
|
|
+ /// Does not determine whether the returned files exist or not.
|
|
|
+ /// </summary>
|
|
|
+ private static string[] GetAllUserNuGetConfigFilePaths()
|
|
|
+ {
|
|
|
+ // Where to find 'NuGet/NuGet.Config':
|
|
|
+ //
|
|
|
+ // - Mono/.NETFramework (standalone NuGet):
|
|
|
+ // Uses Environment.SpecialFolder.ApplicationData
|
|
|
+ // - Windows: '%APPDATA%'
|
|
|
+ // - Linux/macOS: '$HOME/.config'
|
|
|
+ // - CoreCLR (dotnet CLI NuGet):
|
|
|
+ // - Windows: '%APPDATA%'
|
|
|
+ // - Linux/macOS: '$DOTNET_CLI_HOME/.nuget' otherwise '$HOME/.nuget'
|
|
|
+
|
|
|
+ string applicationData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
|
|
+
|
|
|
+ if (Utils.OS.IsWindows)
|
|
|
+ {
|
|
|
+ // %APPDATA% for both
|
|
|
+ return new[] {Path.Combine(applicationData, "NuGet", "NuGet.Config")};
|
|
|
+ }
|
|
|
+
|
|
|
+ var paths = new string[2];
|
|
|
+
|
|
|
+ // CoreCLR (dotnet CLI NuGet)
|
|
|
+
|
|
|
+ string dotnetCliHome = Environment.GetEnvironmentVariable("DOTNET_CLI_HOME");
|
|
|
+ if (!string.IsNullOrEmpty(dotnetCliHome))
|
|
|
+ {
|
|
|
+ paths[0] = Path.Combine(dotnetCliHome, ".nuget", "NuGet", "NuGet.Config");
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ string home = Environment.GetEnvironmentVariable("HOME");
|
|
|
+ if (string.IsNullOrEmpty(home))
|
|
|
+ throw new InvalidOperationException("Required environment variable 'HOME' is not set.");
|
|
|
+ paths[0] = Path.Combine(home, ".nuget", "NuGet", "NuGet.Config");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Mono/.NETFramework (standalone NuGet)
|
|
|
+
|
|
|
+ // ApplicationData is $HOME/.config on Linux/macOS
|
|
|
+ paths[1] = Path.Combine(applicationData, "NuGet", "NuGet.Config");
|
|
|
+
|
|
|
+ return paths;
|
|
|
+ }
|
|
|
+
|
|
|
+ // nupkg extraction
|
|
|
+ //
|
|
|
+ // Exclude: (NuGet.Client -> NuGet.Packaging.PackageHelper.ExcludePaths)
|
|
|
+ // package/
|
|
|
+ // _rels/
|
|
|
+ // [Content_Types].xml
|
|
|
+ //
|
|
|
+ // Don't ignore files that begin with a dot (.)
|
|
|
+ //
|
|
|
+ // The nuspec is not lower case inside the nupkg but must be made lower case when extracted.
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Adds the specified fallback folder to the user NuGet.Config files,
|
|
|
+ /// for both standalone NuGet (Mono/.NETFramework) and dotnet CLI NuGet.
|
|
|
+ /// </summary>
|
|
|
+ public static void AddFallbackFolderToUserNuGetConfigs(string name, string path)
|
|
|
+ {
|
|
|
+ foreach (string nuGetConfigPath in GetAllUserNuGetConfigFilePaths())
|
|
|
+ {
|
|
|
+ if (!System.IO.File.Exists(nuGetConfigPath))
|
|
|
+ {
|
|
|
+ // It doesn't exist, so we create a default one
|
|
|
+ const string defaultConfig = @"<?xml version=""1.0"" encoding=""utf-8""?>
|
|
|
+<configuration>
|
|
|
+ <packageSources>
|
|
|
+ <add key=""nuget.org"" value=""https://api.nuget.org/v3/index.json"" protocolVersion=""3"" />
|
|
|
+ </packageSources>
|
|
|
+</configuration>
|
|
|
+";
|
|
|
+ System.IO.File.WriteAllText(nuGetConfigPath, defaultConfig, Encoding.UTF8); // UTF-8 with BOM
|
|
|
+ }
|
|
|
+
|
|
|
+ AddFallbackFolderToNuGetConfig(nuGetConfigPath, name, path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void AddPackageToFallbackFolder(string fallbackFolder,
|
|
|
+ string nupkgPath, string packageId, string packageVersion)
|
|
|
+ {
|
|
|
+ // dotnet CLI provides no command for this, but we can do it manually.
|
|
|
+ //
|
|
|
+ // - The expected structure is as follows:
|
|
|
+ // fallback_folder/
|
|
|
+ // <package.name>/<version>/
|
|
|
+ // <package.name>.<version>.nupkg
|
|
|
+ // <package.name>.<version>.nupkg.sha512
|
|
|
+ // <package.name>.nuspec
|
|
|
+ // ... extracted nupkg files (check code for excluded files) ...
|
|
|
+ //
|
|
|
+ // - <package.name> and <version> must be in lower case.
|
|
|
+ // - The sha512 of the nupkg is base64 encoded.
|
|
|
+ // - We can get the nuspec from the nupkg which is a Zip file.
|
|
|
+
|
|
|
+ string packageIdLower = packageId.ToLower();
|
|
|
+ string packageVersionLower = packageVersion.ToLower();
|
|
|
+
|
|
|
+ string destDir = Path.Combine(fallbackFolder, packageIdLower, packageVersionLower);
|
|
|
+ string nupkgDestPath = Path.Combine(destDir, $"{packageIdLower}.{packageVersionLower}.nupkg");
|
|
|
+ string nupkgSha512DestPath = Path.Combine(destDir, $"{packageIdLower}.{packageVersionLower}.nupkg.sha512");
|
|
|
+
|
|
|
+ if (File.Exists(nupkgDestPath) && File.Exists(nupkgSha512DestPath))
|
|
|
+ return; // Already added (for speed we don't check if every file is properly extracted)
|
|
|
+
|
|
|
+ Directory.CreateDirectory(destDir);
|
|
|
+
|
|
|
+ // Generate .nupkg.sha512 file
|
|
|
+
|
|
|
+ using (var alg = SHA512.Create())
|
|
|
+ {
|
|
|
+ alg.ComputeHash(File.ReadAllBytes(nupkgPath));
|
|
|
+ string base64Hash = Convert.ToBase64String(alg.Hash);
|
|
|
+ File.WriteAllText(nupkgSha512DestPath, base64Hash);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Extract nupkg
|
|
|
+ ExtractNupkg(destDir, nupkgPath, packageId, packageVersion);
|
|
|
+
|
|
|
+ // Copy .nupkg
|
|
|
+ File.Copy(nupkgPath, nupkgDestPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static readonly string[] NupkgExcludePaths =
|
|
|
+ {
|
|
|
+ "_rels/",
|
|
|
+ "package/",
|
|
|
+ "[Content_Types].xml"
|
|
|
+ };
|
|
|
+
|
|
|
+ private static void ExtractNupkg(string destDir, string nupkgPath, string packageId, string packageVersion)
|
|
|
+ {
|
|
|
+ // NOTE: Must use SimplifyGodotPath to make sure we don't extract files outside the destination directory.
|
|
|
+
|
|
|
+ using (var archive = ZipFile.OpenRead(nupkgPath))
|
|
|
+ {
|
|
|
+ // Extract .nuspec manually as it needs to be in lower case
|
|
|
+
|
|
|
+ var nuspecEntry = archive.GetEntry(packageId + ".nuspec");
|
|
|
+
|
|
|
+ if (nuspecEntry == null)
|
|
|
+ throw new InvalidOperationException($"Failed to extract package {packageId}.{packageVersion}. Could not find the nuspec file.");
|
|
|
+
|
|
|
+ nuspecEntry.ExtractToFile(Path.Combine(destDir, nuspecEntry.Name.ToLower().SimplifyGodotPath()));
|
|
|
+
|
|
|
+ // Extract the other package files
|
|
|
+
|
|
|
+ foreach (var entry in archive.Entries)
|
|
|
+ {
|
|
|
+ // NOTE: SimplifyGodotPath() removes trailing slash and backslash,
|
|
|
+ // so we can't use the result to check if the entry is a directory.
|
|
|
+
|
|
|
+ string entryFullName = entry.FullName.Replace('\\', '/');
|
|
|
+
|
|
|
+ // Check if the file must be ignored
|
|
|
+ if ( // Excluded files.
|
|
|
+ NupkgExcludePaths.Any(e => entryFullName.StartsWith(e, StringComparison.OrdinalIgnoreCase)) ||
|
|
|
+ // Nupkg hash files and nupkg metadata files on all directory.
|
|
|
+ entryFullName.EndsWith(".nupkg.sha512", StringComparison.OrdinalIgnoreCase) ||
|
|
|
+ entryFullName.EndsWith(".nupkg.metadata", StringComparison.OrdinalIgnoreCase) ||
|
|
|
+ // Nuspec at root level. We already extracted it previously but in lower case.
|
|
|
+ entryFullName.IndexOf('/') == -1 && entryFullName.EndsWith(".nuspec"))
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ string entryFullNameSimplified = entryFullName.SimplifyGodotPath();
|
|
|
+ string destFilePath = Path.Combine(destDir, entryFullNameSimplified);
|
|
|
+ bool isDir = entryFullName.EndsWith("/");
|
|
|
+
|
|
|
+ if (isDir)
|
|
|
+ {
|
|
|
+ Directory.CreateDirectory(destFilePath);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Directory.CreateDirectory(Path.GetDirectoryName(destFilePath));
|
|
|
+ entry.ExtractToFile(destFilePath, overwrite: true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Copies and extracts all the Godot bundled packages to the Godot NuGet fallback folder.
|
|
|
+ /// Does nothing if the packages were already copied.
|
|
|
+ /// </summary>
|
|
|
+ public static void AddBundledPackagesToFallbackFolder(string fallbackFolder)
|
|
|
+ {
|
|
|
+ GD.Print("Copying Godot Offline Packages...");
|
|
|
+
|
|
|
+ string nupkgsLocation = Path.Combine(GodotSharpDirs.DataEditorToolsDir, "nupkgs");
|
|
|
+
|
|
|
+ void AddPackage(string packageId, string packageVersion)
|
|
|
+ {
|
|
|
+ string nupkgPath = Path.Combine(nupkgsLocation, $"{packageId}.{packageVersion}.nupkg");
|
|
|
+ AddPackageToFallbackFolder(fallbackFolder, nupkgPath, packageId, packageVersion);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var (packageId, packageVersion) in PackagesToAdd)
|
|
|
+ AddPackage(packageId, packageVersion);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static readonly (string packageId, string packageVersion)[] PackagesToAdd =
|
|
|
+ {
|
|
|
+ ("Godot.NET.Sdk", GeneratedGodotNupkgsVersions.GodotNETSdk)
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|