diff --git a/MonoGame.Tool.BuildScripts.csproj b/MonoGame.Tool.BuildScripts.csproj index 4c9ff77..8be078f 100644 --- a/MonoGame.Tool.BuildScripts.csproj +++ b/MonoGame.Tool.BuildScripts.csproj @@ -9,19 +9,9 @@ - + PreserveNewest - Icon.png - - - - PreserveNewest - MonoGame.Tool.X.txt - - - - PreserveNewest - Program.txt + %(Filename)%(Extension) diff --git a/PackContext.cs b/PackContext.cs index 0eb5383..633ba82 100644 --- a/PackContext.cs +++ b/PackContext.cs @@ -7,7 +7,7 @@ public class PackContext { public string ToolName { get; } - public string CommandName { get; } + public string Description { get; } public string ExecutableName { get; } @@ -24,10 +24,11 @@ public class PackContext public PackContext(ICakeContext context) { ToolName = context.Argument("toolname", "X"); - CommandName = context.Argument("commandname", "X"); + ToolName = char.ToUpper(ToolName[0]) + ToolName[1..]; ExecutableName = context.Argument("executablename", "X"); LicensePath = context.Argument("licensepath", ""); Version = context.Argument("version", "1.0.0"); + Description = $"This package contains executables for {ToolName} built for usage with MonoGame."; RepositoryUrl = "X"; IsTag = false; diff --git a/Resources/MonoGame.Tool.X.targets b/Resources/MonoGame.Tool.X.targets new file mode 100644 index 0000000..53baa61 --- /dev/null +++ b/Resources/MonoGame.Tool.X.targets @@ -0,0 +1,9 @@ + + + + + + + diff --git a/Resources/MonoGame.Tool.X.txt b/Resources/MonoGame.Tool.X.txt index 5c2c9cd..ac15a39 100644 --- a/Resources/MonoGame.Tool.X.txt +++ b/Resources/MonoGame.Tool.X.txt @@ -1,35 +1,27 @@ - Exe net8.0 true enable - Major - - - MonoGame build of {X} Tool {Description} - - - - true Icon.png - {CommandName} {LicenseName} - {ReadMeName} - {CommandName} - + - {ContentInclude} + + + + + diff --git a/Resources/Program.txt b/Resources/Program.txt index b1c0413..1954215 100644 --- a/Resources/Program.txt +++ b/Resources/Program.txt @@ -1,55 +1,116 @@ using System.Diagnostics; using System.Runtime.InteropServices; -for(int i = 0; i < args.Length; i++) +namespace MonoGame.Tool; + +public class {X} { - if(args[i].Contains(" ")) + static string FindCommand(string commandid) { - args[i] = $"\"{args[i]}\""; - } -} + var baseDir = Path.GetDirectoryName(typeof({X}).Assembly.Location) ?? ""; -string arguments = string.Join(" ", args); -string baseDirectory = AppContext.BaseDirectory; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Path.Combine(baseDir, "windows-x64", commandid); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + var osxPath = Path.Combine(baseDir, "osx", commandid); + if (!File.Exists(osxPath)) + { + osxPath = RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm or Architecture.Arm64 => Path.Combine(baseDir, "osx-arm64", commandid), + _ => Path.Combine(baseDir, "osx-x64", commandid) + }; + } + return osxPath; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return Path.Combine(baseDir, "linux-x64", commandid); + } -ProcessStartInfo startInfo = new ProcessStartInfo() -{ - Arguments = arguments, - UseShellExecute = false -}; + return commandid; + } -if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) -{ - startInfo.FileName = Path.Combine(baseDirectory, "binaries", "windows-x64", "{ExecutableName}"); -} -else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) -{ - startInfo.FileName = Path.Combine(baseDirectory, "binaries", "linux-x64", "{ExecutableName}"); -} -else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) -{ - var osxPath = Path.Combine(baseDirectory, "binaries", "osx", "{ExecutableName}"); - if(!File.Exists(osxPath)) + public static int Run(string arguments, out string stdout, out string stderr, string? stdin = null, string? workingDirectory = null) { - osxPath = RuntimeInformation.ProcessArchitecture switch + // This particular case is likely to be the most common and thus + // warrants its own specific error message rather than falling + // back to a general exception from Process.Start() + var fullPath = FindCommand("{ExecutableName}"); + + // We can't reference ref or out parameters from within + // lambdas (for the thread functions), so we have to store + // the data in a temporary variable and then assign these + // variables to the out parameters. + var stdoutTemp = string.Empty; + var stderrTemp = string.Empty; + + var processInfo = new ProcessStartInfo { - Architecture.Arm or Architecture.Arm64 => Path.Combine(baseDirectory, "binaries", "osx-arm64", "{ExecutableName}"), - _ => Path.Combine(baseDirectory, "binaries", "osx-x64", "{ExecutableName}") + Arguments = arguments, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + FileName = fullPath, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, }; - } - startInfo.FileName = osxPath; -} -using (Process? process = Process.Start(startInfo)) -{ - if (process is not null) - { - await process.WaitForExitAsync(); + if (!string.IsNullOrWhiteSpace(workingDirectory)) + { + processInfo.WorkingDirectory = workingDirectory; + } + + using var process = new Process(); + process.StartInfo = processInfo; + process.Start(); + + // We have to run these in threads, because using ReadToEnd + // on one stream can deadlock if the other stream's buffer is + // full. + var stdoutThread = new Thread(new ThreadStart(() => + { + var memory = new MemoryStream(); + process.StandardOutput.BaseStream.CopyTo(memory); + var bytes = new byte[memory.Position]; + memory.Seek(0, SeekOrigin.Begin); + memory.Read(bytes, 0, bytes.Length); + stdoutTemp = System.Text.Encoding.ASCII.GetString(bytes); + })); + var stderrThread = new Thread(new ThreadStart(() => + { + var memory = new MemoryStream(); + process.StandardError.BaseStream.CopyTo(memory); + var bytes = new byte[memory.Position]; + memory.Seek(0, SeekOrigin.Begin); + memory.Read(bytes, 0, bytes.Length); + stderrTemp = System.Text.Encoding.ASCII.GetString(bytes); + })); + + stdoutThread.Start(); + stderrThread.Start(); + + if (stdin != null) + { + process.StandardInput.Write(System.Text.Encoding.ASCII.GetBytes(stdin)); + } + + // Make sure interactive prompts don't block. + process.StandardInput.Close(); + + process.WaitForExit(); + + stdoutThread.Join(); + stderrThread.Join(); + + stdout = stdoutTemp; + stderr = stderrTemp; + return process.ExitCode; } - else - { - // unable to start process - return 1; - } } diff --git a/Tasks/PublishPackageTask.cs b/Tasks/PackageTask.cs similarity index 66% rename from Tasks/PublishPackageTask.cs rename to Tasks/PackageTask.cs index 4b4f0b2..b7cebb8 100644 --- a/Tasks/PublishPackageTask.cs +++ b/Tasks/PackageTask.cs @@ -1,12 +1,12 @@ - using System.Runtime.InteropServices; using Cake.Common.Tools.DotNet.NuGet.Push; namespace BuildScripts; [TaskName("Package")] -public sealed class PublishPackageTask : AsyncFrostingTask +public sealed class PackageTask : AsyncFrostingTask { + public override async Task RunAsync(BuildContext context) { // Create a temporary directory tha we can use to build the "project" in that we'll pack into a dotnet tool @@ -17,9 +17,9 @@ public override async Task RunAsync(BuildContext context) // local artifacts so we can test/run this locally as well if (context.BuildSystem().IsRunningOnGitHubActions) { - var requiredRids = context.IsUniversalBinary ? - new string[] { "windows-x64", "linux-x64", "osx" } : - new string[] { "windows-x64", "linux-x64", "osx-x64", "osx-arm64" }; + string[] requiredRids = context.IsUniversalBinary ? + ["windows-x64", "linux-x64", "osx"] : + ["windows-x64", "linux-x64", "osx-x64", "osx-arm64"]; foreach (var rid in requiredRids) { @@ -57,38 +57,14 @@ public override async Task RunAsync(BuildContext context) } // Create the temporary project that we'll use to pack into the dotnet tool - var licensePath = context.PackContext.LicensePath; - var licenseName = "LICENSE"; - - if (licensePath.EndsWith(".txt")) licenseName += ".txt"; - else if (licensePath.EndsWith(".md")) licenseName += ".md"; + var projectPath = $"{projectDir}/MonoGame.Tool.{context.PackContext.ToolName}.csproj"; + await WriteEmbeddedResource(context, "MonoGame.Tool.X.txt", projectPath); + await WriteEmbeddedResource(context, "MonoGame.Tool.X.targets", $"{projectDir}/MonoGame.Tool.{context.PackContext.ToolName}.targets"); + await WriteEmbeddedResource(context, "Program.txt", $"{projectDir}/Program.cs"); var readMeName = "README.md"; var readMePath = $"{projectDir}/{readMeName}"; - - var description = $"This package contains executables for {context.PackContext.ToolName} built for usage with MonoGame."; - - var contentInclude = $""; - - var projectData = await ReadEmbeddedResourceAsync("MonoGame.Tool.X.txt"); - projectData = projectData.Replace("{X}", context.PackContext.ToolName) - .Replace("{Description}", description) - .Replace("{CommandName}", context.PackContext.CommandName) - .Replace("{LicensePath}", context.PackContext.LicensePath) - .Replace("{ReadMePath}", readMeName) - .Replace("{LicenseName}", licenseName) - .Replace("{ReadMeName}", readMeName) - .Replace("{ContentInclude}", contentInclude); - - string projectPath = $"{projectDir}/MonoGame.Tool.{context.PackContext.ToolName}.csproj"; - await File.WriteAllTextAsync(projectPath, projectData); - - var programData = await ReadEmbeddedResourceAsync("Program.txt"); - programData = programData.Replace("{ExecutableName}", context.PackContext.ExecutableName); - var programPath = $"{projectDir}/Program.cs"; - await File.WriteAllTextAsync(programPath, programData); - - await File.WriteAllTextAsync(readMePath, description); + await File.WriteAllTextAsync(readMePath, context.PackContext.Description); await SaveEmbeddedResourceAsync("Icon.png", $"{projectDir}/Icon.png"); @@ -131,9 +107,9 @@ public override async Task RunAsync(BuildContext context) private static async Task RunOnGithubAsync(BuildContext context, string projectDir) { // Download remote artifacts from github - var requiredRids = context.IsUniversalBinary ? - new string[] { "windows-x64", "linux-x64", "osx" } : - new string[] { "windows-x64", "linux-x64", "osx-x64", "osx-arm64" }; + string[] requiredRids = context.IsUniversalBinary ? + ["windows-x64", "linux-x64", "osx" ]: + ["windows-x64", "linux-x64", "osx-x64", "osx-arm64"]; foreach (var rid in requiredRids) { @@ -146,11 +122,26 @@ private static async Task RunOnGithubAsync(BuildContext context, string projectD } } - private static async Task ReadEmbeddedResourceAsync(string resourceName) + private static async Task WriteEmbeddedResource(BuildContext context, string resource, string outputPath) { - await using var stream = typeof(PublishPackageTask).Assembly.GetManifestResourceStream(resourceName)!; + var licenseName = System.IO.Path.GetExtension(context.PackContext.LicensePath) switch + { + ".txt" => "LICENSE.txt", + ".md" => "LICENSE.md", + _ => "LICENSE" + }; + var contentInclude = $""; + await using var stream = typeof(PackageTask).Assembly.GetManifestResourceStream(resource)!; using var reader = new StreamReader(stream); - return await reader.ReadToEndAsync(); + var outputData = (await reader.ReadToEndAsync()) + .Replace("{X}", context.PackContext.ToolName) + .Replace("{x}", context.PackContext.ToolName.ToLower()) + .Replace("{ExecutableName}", context.PackContext.ExecutableName) + .Replace("{Description}", context.PackContext.Description) + .Replace("{LicensePath}", context.PackContext.LicensePath) + .Replace("{LicenseName}", licenseName) + .Replace("{ContentInclude}", contentInclude); + await File.WriteAllTextAsync(outputPath, outputData); } private static async Task SaveEmbeddedResourceAsync(string resourceName, string outPath) @@ -158,7 +149,7 @@ private static async Task SaveEmbeddedResourceAsync(string resourceName, string if (File.Exists(outPath)) File.Delete(outPath); - await using var stream = typeof(PublishPackageTask).Assembly.GetManifestResourceStream(resourceName)!; + await using var stream = typeof(PackageTask).Assembly.GetManifestResourceStream(resourceName)!; await using var writer = File.Create(outPath); await stream.CopyToAsync(writer); writer.Close();