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