diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..b4fc925
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,14 @@
+
+name: Build
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone Repository
+ uses: actions/checkout@v4
+ - name: Build Solution
+ run: dotnet build
\ No newline at end of file
diff --git a/BuildContext.cs b/BuildContext.cs
new file mode 100644
index 0000000..4428225
--- /dev/null
+++ b/BuildContext.cs
@@ -0,0 +1,20 @@
+
+namespace BuildScripts;
+
+public class BuildContext : FrostingContext
+{
+ public string ArtifactsDir { get; }
+
+ public PackContext PackContext { get; }
+
+ public BuildContext(ICakeContext context) : base(context)
+ {
+ ArtifactsDir = context.Arguments("artifactsDir", "artifacts").FirstOrDefault()!;
+ PackContext = new PackContext(context);
+
+ if (context.BuildSystem().IsRunningOnGitHubActions)
+ {
+ context.BuildSystem().GitHubActions.Commands.SetSecret(context.EnvironmentVariable("GITHUB_TOKEN"));
+ }
+ }
+}
diff --git a/MonoGame.Library.BuildScripts.csproj b/MonoGame.Library.BuildScripts.csproj
new file mode 100644
index 0000000..9312078
--- /dev/null
+++ b/MonoGame.Library.BuildScripts.csproj
@@ -0,0 +1,43 @@
+
+
+
+ net7.0
+ $(MSBuildProjectDirectory)
+ enable
+ enable
+ true
+
+
+
+
+ PreserveNewest
+ Icon.png
+
+
+
+ PreserveNewest
+ MonoGame.Library.X.txt
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/MonoGame.Library.BuildScripts.sln b/MonoGame.Library.BuildScripts.sln
new file mode 100644
index 0000000..ada8fed
--- /dev/null
+++ b/MonoGame.Library.BuildScripts.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoGame.Library.BuildScripts", "MonoGame.Library.BuildScripts.csproj", "{D2A7576F-64AF-4AA2-B6AA-DB36F21A3BA4}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D2A7576F-64AF-4AA2-B6AA-DB36F21A3BA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D2A7576F-64AF-4AA2-B6AA-DB36F21A3BA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D2A7576F-64AF-4AA2-B6AA-DB36F21A3BA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D2A7576F-64AF-4AA2-B6AA-DB36F21A3BA4}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {45681AE0-4A1A-40AA-ABFE-97F7B8B2B351}
+ EndGlobalSection
+EndGlobal
diff --git a/PackContext.cs b/PackContext.cs
new file mode 100644
index 0000000..1f6254f
--- /dev/null
+++ b/PackContext.cs
@@ -0,0 +1,37 @@
+
+namespace BuildScripts;
+
+public class PackContext
+{
+ public string LibraryName { get; }
+
+ public string LicensePath { get; }
+
+ public string? RepositoryOwner { get; }
+
+ public string? RepositoryUrl { get; }
+
+ public string Version { get; }
+
+ public bool IsTag { get; }
+
+ public PackContext(ICakeContext context)
+ {
+ LibraryName = context.Arguments("libraryname", "X").FirstOrDefault()!;
+ LicensePath = context.Arguments("licensepath", "").FirstOrDefault()!;
+ Version = "1.0.0";
+ IsTag = false;
+
+ if (context.BuildSystem().IsRunningOnGitHubActions)
+ {
+ RepositoryOwner = context.EnvironmentVariable("GITHUB_REPOSITORY_OWNER");
+ RepositoryUrl = $"https://github.com/{context.EnvironmentVariable("GITHUB_REPOSITORY")}";
+ IsTag = context.EnvironmentVariable("GITHUB_REF_TYPE") == "tag";
+
+ if (IsTag)
+ {
+ Version = context.EnvironmentVariable("GITHUB_REF_NAME");
+ }
+ }
+ }
+}
diff --git a/Resources/Icon.png b/Resources/Icon.png
new file mode 100644
index 0000000..4a64b41
Binary files /dev/null and b/Resources/Icon.png differ
diff --git a/Resources/MonoGame.Library.X.txt b/Resources/MonoGame.Library.X.txt
new file mode 100644
index 0000000..fce1f65
--- /dev/null
+++ b/Resources/MonoGame.Library.X.txt
@@ -0,0 +1,26 @@
+
+
+
+ netstandard2.0
+ NU5128
+ MonoGame build of {X} Library
+ This package contains an {X} library built for distributing MonoGame games.
+ Icon.png
+ false
+ {LicenceName}
+ false
+
+
+
+
+
+
+
+
+
+
+
+ {LibrariesToInclude}
+
+
+
diff --git a/Tasks.cs b/Tasks.cs
new file mode 100644
index 0000000..d33eae7
--- /dev/null
+++ b/Tasks.cs
@@ -0,0 +1,17 @@
+
+namespace BuildScripts;
+
+[TaskName("BuildLibrary")]
+public class BuildLibraryTask : FrostingTask { }
+
+[TaskName("TestLibrary")]
+[IsDependentOn(typeof(TestWindowsTask))]
+[IsDependentOn(typeof(TestMacOSTask))]
+[IsDependentOn(typeof(TestLinuxTask))]
+public class TestLibraryTask : FrostingTask { }
+
+[TaskName("Default")]
+[IsDependentOn(typeof(BuildLibraryTask))]
+[IsDependentOn(typeof(PublishLibraryTask))]
+[IsDependentOn(typeof(TestLibraryTask))]
+public class DefaultTask : FrostingTask { }
diff --git a/Tasks/PrepTask.cs b/Tasks/PrepTask.cs
new file mode 100644
index 0000000..7b43a3f
--- /dev/null
+++ b/Tasks/PrepTask.cs
@@ -0,0 +1,12 @@
+
+namespace BuildScripts;
+
+[TaskName("Prepare Build")]
+public sealed class PrepTask : FrostingTask
+{
+ public override void Run(BuildContext context)
+ {
+ context.CleanDirectory(context.ArtifactsDir);
+ context.CreateDirectory(context.ArtifactsDir);
+ }
+}
diff --git a/Tasks/PublishLibraryTask.cs b/Tasks/PublishLibraryTask.cs
new file mode 100644
index 0000000..fd2acb0
--- /dev/null
+++ b/Tasks/PublishLibraryTask.cs
@@ -0,0 +1,28 @@
+using System.Runtime.InteropServices;
+
+namespace BuildScripts;
+
+[TaskName("Publish Library")]
+[IsDependentOn(typeof(PrepTask))]
+public sealed class PublishLibraryTask : AsyncFrostingTask
+{
+ public override bool ShouldRun(BuildContext context) => context.BuildSystem().IsRunningOnGitHubActions;
+
+ public override async Task RunAsync(BuildContext context)
+ {
+ var rid = "";
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ rid = "windows";
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ rid = "osx";
+ else
+ rid = "linux";
+ rid += RuntimeInformation.ProcessArchitecture switch
+ {
+ Architecture.Arm or Architecture.Arm64 => "-arm64",
+ _ => "-x64",
+ };
+
+ await context.BuildSystem().GitHubActions.Commands.UploadArtifact(DirectoryPath.FromString(context.ArtifactsDir), $"artifacts-{rid}");
+ }
+}
diff --git a/Tasks/PublishPackageTask.cs b/Tasks/PublishPackageTask.cs
new file mode 100644
index 0000000..0bce2cd
--- /dev/null
+++ b/Tasks/PublishPackageTask.cs
@@ -0,0 +1,97 @@
+
+namespace BuildScripts;
+
+[TaskName("Package")]
+public sealed class PublishPackageTask : AsyncFrostingTask
+{
+ private static async Task ReadEmbeddedResourceAsync(string resourceName)
+ {
+ await using var stream = typeof(PublishPackageTask).Assembly.GetManifestResourceStream(resourceName)!;
+ using var reader = new StreamReader(stream);
+ return await reader.ReadToEndAsync();
+ }
+
+ 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 writer = File.Create(outPath);
+ await stream.CopyToAsync(writer);
+ writer.Close();
+ }
+
+ public override async Task RunAsync(BuildContext context)
+ {
+ var requiredRids = new[] {
+ "windows-x64",
+ "osx-x64",
+ "osx-arm64",
+ "linux-x64"
+ };
+
+ // Download built artifacts
+ if (context.BuildSystem().IsRunningOnGitHubActions)
+ {
+ foreach (var rid in requiredRids)
+ {
+ var directoryPath = $"runtimes/{rid}/native";
+ if (context.DirectoryExists(directoryPath))
+ continue;
+
+ context.CreateDirectory(directoryPath);
+ await context.BuildSystem().GitHubActions.Commands.DownloadArtifact($"artifacts-{rid}", directoryPath);
+ }
+ }
+
+ // Generate Project
+ var projectData = await ReadEmbeddedResourceAsync("MonoGame.Library.X.txt");
+ projectData = projectData.Replace("{X}", context.PackContext.LibraryName);
+ projectData = projectData.Replace("{LicencePath}", context.PackContext.LicensePath);
+
+ if (context.PackContext.LicensePath.EndsWith(".txt"))
+ projectData = projectData.Replace("{LicenceName}", "LICENSE.txt");
+ else if (context.PackContext.LicensePath.EndsWith(".md"))
+ projectData = projectData.Replace("{LicenceName}", "LICENSE.md");
+ else
+ projectData = projectData.Replace("{LicenceName}", "LICENSE");
+
+ var librariesToInclude = from rid in requiredRids from filePath in Directory.GetFiles($"runtimes/{rid}/native")
+ select $"runtimes/{rid}/native";
+ projectData = projectData.Replace("{LibrariesToInclude}", string.Join(Environment.NewLine, librariesToInclude));
+
+ await File.WriteAllTextAsync($"MonoGame.Library.{context.PackContext.LibraryName}.csproj", projectData);
+ await SaveEmbeddedResourceAsync("Icon.png", "Icon.png");
+
+ // Build
+ var dnMsBuildSettings = new DotNetMSBuildSettings();
+ dnMsBuildSettings.WithProperty("Version", context.PackContext.Version);
+ dnMsBuildSettings.WithProperty("RepositoryUrl", context.PackContext.RepositoryUrl);
+
+ context.DotNetPack($"MonoGame.Library.{context.PackContext.LibraryName}.csproj", new DotNetPackSettings
+ {
+ MSBuildSettings = dnMsBuildSettings,
+ Verbosity = DotNetVerbosity.Minimal,
+ Configuration = "Release"
+ });
+
+ // Upload Artifacts
+ if (context.BuildSystem().IsRunningOnGitHubActions)
+ {
+ foreach (var nugetPath in context.GetFiles("bin/Release/*.nupkg"))
+ {
+ await context.BuildSystem().GitHubActions.Commands.UploadArtifact(nugetPath, nugetPath.GetFilename().ToString());
+
+ if (context.PackContext.IsTag)
+ {
+ context.DotNetNuGetPush(nugetPath, new()
+ {
+ ApiKey = context.EnvironmentVariable("GITHUB_TOKEN"),
+ Source = $"https://nuget.pkg.github.com/{context.PackContext.RepositoryOwner}/index.json"
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/Tasks/TestLinuxTask.cs b/Tasks/TestLinuxTask.cs
new file mode 100644
index 0000000..72817ae
--- /dev/null
+++ b/Tasks/TestLinuxTask.cs
@@ -0,0 +1,55 @@
+
+namespace BuildScripts;
+
+[TaskName("Test Linux")]
+public sealed class TestLinuxTask : FrostingTask
+{
+ private static readonly string[] ValidLibs = {
+ "linux-vdso.so",
+ "libstdc++.so",
+ "libgcc_s.so",
+ "libc.so",
+ "libm.so",
+ "/lib/ld-linux-",
+ "/lib64/ld-linux-"
+ };
+
+ public override bool ShouldRun(BuildContext context) => context.IsRunningOnLinux();
+
+ public override void Run(BuildContext context)
+ {
+ foreach (var filePath in context.GetFiles(context.ArtifactsDir))
+ {
+ context.Information($"Checking: {filePath}");
+ context.StartProcess(
+ "ldd",
+ new ProcessSettings
+ {
+ Arguments = $"{filePath}",
+ RedirectStandardOutput = true
+ },
+ out IEnumerable processOutput);
+
+ foreach (var line in processOutput)
+ {
+ var libPath = line.Trim().Split(' ')[0];
+ context.Information($"DEP: {libPath}");
+
+ var isValidLib = false;
+ foreach (var validLib in ValidLibs)
+ {
+ if (libPath.StartsWith(validLib))
+ {
+ isValidLib = true;
+ break;
+ }
+ }
+
+ if (!isValidLib)
+ throw new Exception($"Found a dynamic library ref: {libPath}");
+ }
+
+ context.Information("");
+ }
+ }
+}
diff --git a/Tasks/TestMacOSTask.cs b/Tasks/TestMacOSTask.cs
new file mode 100644
index 0000000..c3eccc5
--- /dev/null
+++ b/Tasks/TestMacOSTask.cs
@@ -0,0 +1,37 @@
+
+namespace BuildScripts;
+
+[TaskName("Test macOS")]
+public sealed class TestMacOSTask : FrostingTask
+{
+ public override bool ShouldRun(BuildContext context) => context.IsRunningOnMacOs();
+
+ public override void Run(BuildContext context)
+ {
+ foreach (var filePath in context.GetFiles(context.ArtifactsDir))
+ {
+ context.Information($"Checking: {filePath}");
+ context.StartProcess(
+ "dyld_info",
+ new ProcessSettings
+ {
+ Arguments = $"-dependents {filePath}",
+ RedirectStandardOutput = true
+ },
+ out IEnumerable processOutput);
+
+ var processOutputList = processOutput.ToList();
+ for (int i = 3; i < processOutputList.Count; i++)
+ {
+ var libPath = processOutputList[i].Trim();
+ context.Information($"DEP: {libPath}");
+ if (libPath.StartsWith("/usr/lib/"))
+ continue;
+
+ throw new Exception($"Found a dynamic library ref: {libPath}");
+ }
+
+ context.Information("");
+ }
+ }
+}
diff --git a/Tasks/TestWindowsTask.cs b/Tasks/TestWindowsTask.cs
new file mode 100644
index 0000000..f88b983
--- /dev/null
+++ b/Tasks/TestWindowsTask.cs
@@ -0,0 +1,46 @@
+using Cake.Common.Tools.VSWhere.Latest;
+
+namespace BuildScripts;
+
+[TaskName("Test Windows")]
+public sealed class TestWindowsTask : FrostingTask
+{
+ private static readonly string[] ValidLibs = {
+ "WS2_32.dll",
+ "KERNEL32.dll"
+ };
+
+ public override bool ShouldRun(BuildContext context) => context.IsRunningOnWindows();
+
+ public override void Run(BuildContext context)
+ {
+ var vswhere = new VSWhereLatest(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools);
+ var devcmdPath = vswhere.Latest(new VSWhereLatestSettings()).FullPath + @"\Common7\Tools\vsdevcmd.bat";
+
+ foreach (var filePath in context.GetFiles(context.ArtifactsDir))
+ {
+ context.Information($"Checking: {filePath}");
+ context.StartProcess(
+ devcmdPath,
+ new ProcessSettings()
+ {
+ Arguments = $"& dumpbin /dependents /nologo {filePath}",
+ RedirectStandardOutput = true
+ },
+ out IEnumerable processOutput
+ );
+
+ foreach (string output in processOutput)
+ {
+ var libPath = output.Trim();
+ if (!libPath.EndsWith(".dll"))
+ continue;
+ context.Information($"DEP: {libPath}");
+ if (ValidLibs.Contains(libPath))
+ continue;
+
+ throw new Exception($"Found a dynamic library ref: {libPath}");
+ }
+ }
+ }
+}