Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions MonoGame.Tool.BuildScripts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,9 @@
</PropertyGroup>

<ItemGroup>
<EmbeddedResource Include="Resources/Icon.png">
<EmbeddedResource Include="Resources/*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<LogicalName>Icon.png</LogicalName>
</EmbeddedResource>

<EmbeddedResource Include="Resources/MonoGame.Tool.X.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<LogicalName>MonoGame.Tool.X.txt</LogicalName>
</EmbeddedResource>

<EmbeddedResource Include="Resources/Program.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<LogicalName>Program.txt</LogicalName>
<LogicalName>%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>

Expand Down
5 changes: 3 additions & 2 deletions PackContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class PackContext
{
public string ToolName { get; }

public string CommandName { get; }
public string Description { get; }

public string ExecutableName { get; }

Expand All @@ -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;

Expand Down
9 changes: 9 additions & 0 deletions Resources/MonoGame.Tool.X.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>

<ItemGroup>
<Content
Include="$(MSBuildThisFileDirectory)..\binaries\**\*"
CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

</Project>
20 changes: 6 additions & 14 deletions Resources/MonoGame.Tool.X.txt
Original file line number Diff line number Diff line change
@@ -1,35 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
<RollForward>Major</RollForward>
</PropertyGroup>

<PropertyGroup>
<Title>MonoGame build of {X} Tool</Title>
<Description>{Description}</Description>
</PropertyGroup>

<PropertyGroup>
<PackAsTool>true</PackAsTool>
<PackageIcon>Icon.png</PackageIcon>
<PackageId>{CommandName}</PackageId>
<PackageLicenseFile>{LicenseName}</PackageLicenseFile>
<PackageReadmeFile>{ReadMeName}</PackageReadmeFile>
<ToolCommandName>{CommandName}</ToolCommandName>
</PropertyGroup>

<ItemGroup>
<None Include="../{LicensePath}" Pack="true" PackagePath="" />
<None Include="../{ReadMePath}" Pack="true" PackagePath="" />
<None Include="../README.md" Pack="true" PackagePath="" />
<None Include="Icon.png" Pack="true" PackagePath="" />
</ItemGroup>

<ItemGroup>
{ContentInclude}
<Content Include="binaries/**/*" PackagePath="binaries" />
</ItemGroup>

<ItemGroup>
<Content Include="MonoGame.Tool.{X}.targets" PackagePath="build" />
</ItemGroup>

</Project>
141 changes: 101 additions & 40 deletions Resources/Program.txt
Original file line number Diff line number Diff line change
@@ -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;
}
}
73 changes: 32 additions & 41 deletions Tasks/PublishPackageTask.cs → Tasks/PackageTask.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@

using System.Runtime.InteropServices;
using Cake.Common.Tools.DotNet.NuGet.Push;

namespace BuildScripts;

[TaskName("Package")]
public sealed class PublishPackageTask : AsyncFrostingTask<BuildContext>
public sealed class PackageTask : AsyncFrostingTask<BuildContext>
{

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
Expand All @@ -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)
{
Expand Down Expand Up @@ -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 = $"<None Include=\"binaries\\**\\*\" CopyToOutputDirectory=\"PreserveNewest\" />";

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");

Expand Down Expand Up @@ -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)
{
Expand All @@ -146,19 +122,34 @@ private static async Task RunOnGithubAsync(BuildContext context, string projectD
}
}

private static async Task<string> 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 = $"<None Include=\"binaries\\**\\*\" CopyToOutputDirectory=\"PreserveNewest\" />";
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)
{
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();
Expand Down