using System; using System.Diagnostics; using System.IO; using System.Text; using JetBrains.Annotations; using NugetForUnity.Configuration; using NugetForUnity.Models; using UnityEngine; using Debug = UnityEngine.Debug; namespace NugetForUnity.Helper { /// /// Helper class for calling the nuget.exe command line tool. /// internal static class NugetCliHelper { /// /// The amount of time, in milliseconds, before the nuget.exe process times out and is killed. /// private const int TimeOut = 60000; private static string dotNetExecutablePath; /// /// Calls "nuget.exe pack" to create a .nupkg file based on the given .nuspec file. /// /// The full file-path to the .nuspec file to use. public static void Pack([NotNull] string nuspecFilePath) { Directory.CreateDirectory(PackageCacheManager.CacheOutputDirectory); // Use -NoDefaultExcludes to allow files and folders that start with a . to be packed into the package // This is done because if you want a file/folder in a Unity project, but you want Unity to ignore it, it must start with a . // This is especially useful for .cs and .js files that you don't want Unity to compile as game scripts var arguments = $"pack \"{nuspecFilePath}\" -OutputDirectory \"{PackageCacheManager.CacheOutputDirectory}\" -NoDefaultExcludes"; RunNugetProcess(arguments); } /// /// Calls "nuget.exe push" to push a .nupkg file to the server location defined in the NuGet.config file. /// Note: This differs slightly from NuGet's Push command by automatically calling Pack if the .nupkg doesn't already exist. /// /// The NuspecFile which defines the package to push. Only the ID and Version are used. /// The full file-path to the .nuspec file to use. This is required by NuGet's Push command. /// /// /// The API key to use when pushing a package to the server. This is optional. public static void Push([NotNull] NuspecFile nuspec, [NotNull] string nuspecFilePath, [NotNull] string apiKey = "") { var packagePath = nuspec.GetLocalPackageFilePath(PackageCacheManager.CacheOutputDirectory); if (!File.Exists(packagePath)) { NugetLogger.LogVerbose("Attempting to Pack."); Pack(nuspecFilePath); if (!File.Exists(packagePath)) { Debug.LogErrorFormat("NuGet package not found: {0}", packagePath); return; } } var arguments = $"push \"{packagePath}\" {apiKey} -configfile \"{ConfigurationManager.NugetConfigFilePath}\""; RunNugetProcess(arguments); } /// /// Creates a configured to call a .Net '.exe' on the current operating system. This function ensures the correct /// output encoding is used. On Unix systems (Linux / MacOS) it also uses the 'dotnet' / 'mono' executable to start the /// . /// /// The absolute file path to the .Net '.exe' file that should be started. /// The arguments that should be passed to the executable . /// The start info with the configuration. internal static ProcessStartInfo CreateStartInfoForDotNetExecutable(string executableFilePath, string arguments) { var outputEncoding = Encoding.UTF8; string executableToStart; string executableArguments; if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows -> just call the .exe but use a special encoding try { // http://stackoverflow.com/questions/16803748/how-to-decode-cmd-output-correctly // Default = 65533, ASCII = ?, Unicode = nothing works at all, UTF-8 = 65533, UTF-7 = 242 = WORKS!, UTF-32 = nothing works at all outputEncoding = Encoding.GetEncoding(850); } catch (Exception e) when (e is ArgumentException || e is NotSupportedException) { Debug.LogWarningFormat("Failed to get console output encoding 850 -> using UTF8 instead. Error: {0}", e); } executableToStart = executableFilePath; executableArguments = arguments; } else { // Linux or MacOS -> we need 'mono' / 'dotnet' to run the executable if (dotNetExecutablePath == null) { dotNetExecutablePath = GetDotnetExecutablePath(outputEncoding); } executableToStart = dotNetExecutablePath; executableArguments = $" {executableFilePath} {arguments}"; NugetLogger.LogVerbose("Changed command to call executable '{0}' with arguments '{1}'.", executableToStart, executableArguments); } return new ProcessStartInfo(executableToStart, executableArguments) { StandardOutputEncoding = outputEncoding, StandardErrorEncoding = outputEncoding, }; } /// /// Runs nuget.exe using the given arguments. /// /// The arguments to run nuget.exe with. /// True to output debug information to the Unity console. Defaults to true. private static void RunNugetProcess([NotNull] string arguments, bool logOutput = true) { // Try to find any nuget.exe in the package tools installation location var toolsPackagesFolder = Path.Combine(UnityPathHelper.AbsoluteProjectPath, "Packages"); // create the folder to prevent an exception when getting the files Directory.CreateDirectory(toolsPackagesFolder); var files = Directory.GetFiles(toolsPackagesFolder, "nuget.exe", SearchOption.AllDirectories); if (files.Length > 1) { Debug.LogWarningFormat("More than one nuget.exe found. Using first one."); } else if (files.Length < 1) { Debug.LogWarningFormat("No nuget.exe found! Attempting to install the latest version of the NuGet.CommandLine package."); NugetPackageInstaller.InstallIdentifier(new NugetPackageIdentifier("NuGet.CommandLine", null)); files = Directory.GetFiles(toolsPackagesFolder, "nuget.exe", SearchOption.AllDirectories); if (files.Length < 1) { Debug.LogErrorFormat("nuget.exe still not found. Quitting..."); return; } } NugetLogger.LogVerbose("Running: {0} \nArgs: {1}", files[0], arguments); var startInfo = CreateStartInfoForDotNetExecutable(files[0], arguments); startInfo.RedirectStandardError = true; startInfo.RedirectStandardOutput = true; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; using (var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start process.")) { if (!process.WaitForExit(TimeOut)) { Debug.LogWarning("NuGet took too long to finish. Killing operation."); process.Kill(); } var error = process.StandardError.ReadToEnd(); if (!string.IsNullOrEmpty(error)) { Debug.LogError(error); } var output = process.StandardOutput.ReadToEnd(); if (logOutput && !string.IsNullOrEmpty(output)) { Debug.Log(output); } } } private static string GetDotnetExecutablePath(Encoding outputEncoding) { // search for full path to 'dotnet' or 'mono' executable foreach (var commandLineToolName in new[] { "dotnet", "mono" }) { try { using (var process = Process.Start( new ProcessStartInfo("which", commandLineToolName) { RedirectStandardOutput = true, StandardOutputEncoding = outputEncoding, UseShellExecute = false, }) ?? throw new InvalidOperationException("Failed to start process.")) { process.WaitForExit(); // which either returns nothing (not found) or the full path to the executable. var commandPath = process.StandardOutput.ReadLine(); if (!string.IsNullOrEmpty(commandPath)) { return commandPath; } } } catch (Exception e) { Debug.LogWarningFormat( "Failed to get path of command {0} using 'which' command -> using '{0}' as a command path instead. Error: {1}", commandLineToolName, e); return commandLineToolName; } } Debug.LogWarning( "Can't find 'dotnet' or 'mono' executable. Is it installed on your system? It is needed to run dotnet .exe files. Try to use 'dotnet' as a command even if 'which dotnet' didn't find it."); return "dotnet"; } } }