From 21f50cca861d23a91c6838dd4dc6cd275e0a5d3b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 8 Dec 2025 14:16:50 +1300 Subject: [PATCH 1/6] Added a simple dev script to replicate my zshrc file --- dev.cs | 231 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100755 dev.cs diff --git a/dev.cs b/dev.cs new file mode 100755 index 0000000000..b32ef6092d --- /dev/null +++ b/dev.cs @@ -0,0 +1,231 @@ +#!/usr/bin/env dotnet +#:package Cocona@2.2.0 +#:package Microsoft.Extensions.DependencyInjection@8.0.0 + +#nullable enable + +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; +using Cocona; + +// Simple dev helper CLI implemented as a .NET file-based app using Cocona. +// Usage examples (run from repo root): +// ./dev.cs --help +// ./dev.cs cleanslate +// ./dev.cs cleanslate Sentry-CI-Build-macOS.slnf +// ./dev.cs subup +// ./dev.cs wrest +// ./dev.cs nrest +// ./dev.cs --dry-run cleanslate +// +// This is intended to be run from the repo root so that git/dotnet +// commands operate on this repository. + +// Central configuration/constants for the dev script. +public static class DevConfig +{ + public const string DefaultSolution = "Sentry-CI-Build-macOS.slnf"; +} + +public static class Program +{ + public static async Task Main(string[] args) + { + var builder = CoconaApp.CreateBuilder(); + + builder.Services.AddSingleton(sp => + { + var options = sp.GetRequiredService(); + return new DevProcessRunner(options.DryRun); + }); + + builder.Services.AddSingleton(); + + var app = builder.Build(); + app.AddCommands(); + + await app.RunAsync(); + } +} + +// ----------------- Options & Commands ----------------- + +public class GlobalOptions +{ + [Option("dry-run", new[] { 'n' }, Description = "Print commands instead of executing them.")] + public bool DryRun { get; set; } +} + +public class DevCommands +{ + private readonly IDevProcessRunner _runner; + + public DevCommands(IDevProcessRunner runner) + { + _runner = runner; + } + + [Command("cleanslate", Description = "Clean repo, update submodules, and restore the solution.")] + public async Task CleanSlateAsync( + [Argument("solution", Description = "Solution file to restore. Defaults to Sentry-CI-Build-macOS.slnf if omitted.")] string? solution = null) + { + solution ??= DevConfig.DefaultSolution; + + Console.WriteLine($"[dev] cleanslate for solution: {solution}"); + + var steps = new (string Description, string FileName, string Arguments)[] + { + ("git clean", "git", "clean -dfx"), + ("git submodule update", "git", "submodule update --recursive"), + ("dotnet restore", "dotnet", $"restore \"{solution}\"") + }; + + foreach (var (description, fileName, arguments) in steps) + { + int code = await _runner.RunStepAsync(description, fileName, arguments); + if (code != 0) + { + Console.Error.WriteLine($"[dev] Step '{description}' failed with exit code {code}."); + return code; + } + } + + return 0; + } + + [Command("subup", Description = "Update git submodules recursively.")] + public Task SubmoduleUpdateAsync() + { + Console.WriteLine("[dev] Updating git submodules (recursive)"); + return _runner.RunStepAsync("git submodule update", "git", "submodule update --recursive"); + } + + [Command("wrest", Description = "Run 'dotnet workload restore' (with sudo on Unix if available).")] + public async Task WorkloadRestoreAsync() + { + Console.WriteLine("[dev] Restoring dotnet workloads"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // No sudo on Windows + return await _runner.RunStepAsync("dotnet workload restore", "dotnet", "workload restore"); + } + + bool sudoAvailable = await _runner.IsCommandAvailableAsync("sudo"); + + if (sudoAvailable) + { + return await _runner.RunStepAsync("sudo dotnet workload restore", "sudo", "dotnet workload restore"); + } + + Console.WriteLine("[dev] 'sudo' not found; running 'dotnet workload restore' without sudo."); + return await _runner.RunStepAsync("dotnet workload restore", "dotnet", "workload restore"); + } + + [Command("nrest", Description = "Restore the default CI solution.")] + public Task SolutionRestoreAsync( + [Argument("solution", Description = "Solution file to restore. Defaults to Sentry-CI-Build-macOS.slnf if omitted.")] string? solution = null) + { + solution ??= DevConfig.DefaultSolution; + Console.WriteLine($"[dev] Restoring solution: {solution}"); + return _runner.RunStepAsync("dotnet restore", "dotnet", $"restore \"{solution}\""); + } +} + +// ----------------- Process helpers ----------------- + +public interface IDevProcessRunner +{ + Task RunStepAsync(string description, string fileName, string arguments); + Task IsCommandAvailableAsync(string command); +} + +public class DevProcessRunner : IDevProcessRunner +{ + private readonly bool _dryRun; + + public DevProcessRunner(bool dryRun) + { + _dryRun = dryRun; + } + + public Task RunStepAsync(string description, string fileName, string arguments) + { + Console.WriteLine($"==> {description}: {fileName} {arguments}"); + return RunProcessAsync(fileName, arguments); + } + + private async Task RunProcessAsync(string fileName, string arguments) + { + if (_dryRun) + { + Console.WriteLine($"[DRY RUN] {fileName} {arguments}"); + return 0; + } + + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = false, + RedirectStandardError = false, + CreateNoWindow = false, + }; + + using var process = new Process { StartInfo = startInfo }; + + try + { + if (!process.Start()) + { + Console.Error.WriteLine($"[dev] Failed to start process: {fileName}"); + return 1; + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"[dev] Exception starting process '{fileName}': {ex.Message}"); + return 1; + } + + await process.WaitForExitAsync(); + return process.ExitCode; + } + + public async Task IsCommandAvailableAsync(string command) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return await Task.FromResult(true); + } + + var startInfo = new ProcessStartInfo + { + FileName = "which", + Arguments = command, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + + using var process = new Process { StartInfo = startInfo }; + + try + { + if (!process.Start()) + { + return false; + } + } + catch + { + return false; + } + + await process.WaitForExitAsync(); + return process.ExitCode == 0; + } +} From 4f48ec5c47ea1764910bc3311cf3f6df7738c2a3 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 8 Dec 2025 15:10:46 +1300 Subject: [PATCH 2/6] Made default solution platform dependent --- dev.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/dev.cs b/dev.cs index b32ef6092d..705d0a7664 100755 --- a/dev.cs +++ b/dev.cs @@ -25,7 +25,24 @@ // Central configuration/constants for the dev script. public static class DevConfig { - public const string DefaultSolution = "Sentry-CI-Build-macOS.slnf"; + public static string DefaultSolution + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "Sentry-CI-Build-Windows.slnf"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "Sentry-CI-Build-Linux.slnf"; + } + + // Fallback: macOS solution + return "Sentry-CI-Build-macOS.slnf"; + } + } } public static class Program From c060c651023ac8ac16336d3f04a85f9f68e25950 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 8 Dec 2025 15:54:49 +1300 Subject: [PATCH 3/6] Changed to top level statements --- dev.cs | 73 ++++++++++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/dev.cs b/dev.cs index 705d0a7664..8740e65876 100755 --- a/dev.cs +++ b/dev.cs @@ -22,49 +22,18 @@ // This is intended to be run from the repo root so that git/dotnet // commands operate on this repository. -// Central configuration/constants for the dev script. -public static class DevConfig +var builder = CoconaApp.CreateBuilder(); +builder.Services.AddSingleton(sp => { - public static string DefaultSolution - { - get - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "Sentry-CI-Build-Windows.slnf"; - } + var options = sp.GetRequiredService(); + return new DevProcessRunner(options.DryRun); +}); +builder.Services.AddSingleton(); +var app = builder.Build(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "Sentry-CI-Build-Linux.slnf"; - } +app.AddCommands(); - // Fallback: macOS solution - return "Sentry-CI-Build-macOS.slnf"; - } - } -} - -public static class Program -{ - public static async Task Main(string[] args) - { - var builder = CoconaApp.CreateBuilder(); - - builder.Services.AddSingleton(sp => - { - var options = sp.GetRequiredService(); - return new DevProcessRunner(options.DryRun); - }); - - builder.Services.AddSingleton(); - - var app = builder.Build(); - app.AddCommands(); - - await app.RunAsync(); - } -} +await app.RunAsync(); // ----------------- Options & Commands ----------------- @@ -246,3 +215,27 @@ public async Task IsCommandAvailableAsync(string command) return process.ExitCode == 0; } } + + +// Central configuration/constants for the dev script. +public static class DevConfig +{ + public static string DefaultSolution + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "Sentry-CI-Build-Windows.slnf"; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "Sentry-CI-Build-Linux.slnf"; + } + + // Fallback: macOS solution + return "Sentry-CI-Build-macOS.slnf"; + } + } +} From 13373eb6c077a510f9f0b4ba128fe4943e17c10e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 8 Dec 2025 16:05:47 +1300 Subject: [PATCH 4/6] Fixed dry-run --- dev.cs | 74 +++++++++++++++++----------------------------------------- 1 file changed, 21 insertions(+), 53 deletions(-) diff --git a/dev.cs b/dev.cs index 8740e65876..a9291894ed 100755 --- a/dev.cs +++ b/dev.cs @@ -1,12 +1,10 @@ #!/usr/bin/env dotnet #:package Cocona@2.2.0 -#:package Microsoft.Extensions.DependencyInjection@8.0.0 #nullable enable using System.Diagnostics; using System.Runtime.InteropServices; -using Microsoft.Extensions.DependencyInjection; using Cocona; // Simple dev helper CLI implemented as a .NET file-based app using Cocona. @@ -22,22 +20,15 @@ // This is intended to be run from the repo root so that git/dotnet // commands operate on this repository. -var builder = CoconaApp.CreateBuilder(); -builder.Services.AddSingleton(sp => -{ - var options = sp.GetRequiredService(); - return new DevProcessRunner(options.DryRun); -}); -builder.Services.AddSingleton(); -var app = builder.Build(); +var app = CoconaApp.Create(); app.AddCommands(); -await app.RunAsync(); +app.Run(); // ----------------- Options & Commands ----------------- -public class GlobalOptions +public class GlobalOptions : ICommandParameterSet { [Option("dry-run", new[] { 'n' }, Description = "Print commands instead of executing them.")] public bool DryRun { get; set; } @@ -45,16 +36,10 @@ public class GlobalOptions public class DevCommands { - private readonly IDevProcessRunner _runner; - - public DevCommands(IDevProcessRunner runner) - { - _runner = runner; - } - [Command("cleanslate", Description = "Clean repo, update submodules, and restore the solution.")] public async Task CleanSlateAsync( - [Argument("solution", Description = "Solution file to restore. Defaults to Sentry-CI-Build-macOS.slnf if omitted.")] string? solution = null) + [Argument("solution", Description = "Solution file to restore. Defaults to platform-specific CI solution if omitted.")] string? solution = null, + GlobalOptions options = default!) { solution ??= DevConfig.DefaultSolution; @@ -69,7 +54,7 @@ public async Task CleanSlateAsync( foreach (var (description, fileName, arguments) in steps) { - int code = await _runner.RunStepAsync(description, fileName, arguments); + int code = await RunStepAsync(description, fileName, arguments, options.DryRun); if (code != 0) { Console.Error.WriteLine($"[dev] Step '{description}' failed with exit code {code}."); @@ -81,70 +66,53 @@ public async Task CleanSlateAsync( } [Command("subup", Description = "Update git submodules recursively.")] - public Task SubmoduleUpdateAsync() + public Task SubmoduleUpdateAsync(GlobalOptions options = default!) { Console.WriteLine("[dev] Updating git submodules (recursive)"); - return _runner.RunStepAsync("git submodule update", "git", "submodule update --recursive"); + return RunStepAsync("git submodule update", "git", "submodule update --recursive", options.DryRun); } [Command("wrest", Description = "Run 'dotnet workload restore' (with sudo on Unix if available).")] - public async Task WorkloadRestoreAsync() + public async Task WorkloadRestoreAsync(GlobalOptions options = default!) { Console.WriteLine("[dev] Restoring dotnet workloads"); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // No sudo on Windows - return await _runner.RunStepAsync("dotnet workload restore", "dotnet", "workload restore"); + return await RunStepAsync("dotnet workload restore", "dotnet", "workload restore", options.DryRun); } - bool sudoAvailable = await _runner.IsCommandAvailableAsync("sudo"); + bool sudoAvailable = await IsCommandAvailableAsync("sudo"); if (sudoAvailable) { - return await _runner.RunStepAsync("sudo dotnet workload restore", "sudo", "dotnet workload restore"); + return await RunStepAsync("sudo dotnet workload restore", "sudo", "dotnet workload restore", options.DryRun); } Console.WriteLine("[dev] 'sudo' not found; running 'dotnet workload restore' without sudo."); - return await _runner.RunStepAsync("dotnet workload restore", "dotnet", "workload restore"); + return await RunStepAsync("dotnet workload restore", "dotnet", "workload restore", options.DryRun); } [Command("nrest", Description = "Restore the default CI solution.")] public Task SolutionRestoreAsync( - [Argument("solution", Description = "Solution file to restore. Defaults to Sentry-CI-Build-macOS.slnf if omitted.")] string? solution = null) + [Argument("solution", Description = "Solution file to restore. Defaults to platform-specific CI solution if omitted.")] string? solution = null, + GlobalOptions options = default!) { solution ??= DevConfig.DefaultSolution; Console.WriteLine($"[dev] Restoring solution: {solution}"); - return _runner.RunStepAsync("dotnet restore", "dotnet", $"restore \"{solution}\""); - } -} - -// ----------------- Process helpers ----------------- - -public interface IDevProcessRunner -{ - Task RunStepAsync(string description, string fileName, string arguments); - Task IsCommandAvailableAsync(string command); -} - -public class DevProcessRunner : IDevProcessRunner -{ - private readonly bool _dryRun; - - public DevProcessRunner(bool dryRun) - { - _dryRun = dryRun; + return RunStepAsync("dotnet restore", "dotnet", $"restore \"{solution}\"", options.DryRun); } - public Task RunStepAsync(string description, string fileName, string arguments) + private static async Task RunStepAsync(string description, string fileName, string arguments, bool dryRun) { Console.WriteLine($"==> {description}: {fileName} {arguments}"); - return RunProcessAsync(fileName, arguments); + return await RunProcessAsync(fileName, arguments, dryRun); } - private async Task RunProcessAsync(string fileName, string arguments) + private static async Task RunProcessAsync(string fileName, string arguments, bool dryRun) { - if (_dryRun) + if (dryRun) { Console.WriteLine($"[DRY RUN] {fileName} {arguments}"); return 0; @@ -180,7 +148,7 @@ private async Task RunProcessAsync(string fileName, string arguments) return process.ExitCode; } - public async Task IsCommandAvailableAsync(string command) + private static async Task IsCommandAvailableAsync(string command) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { From 299beff84ba371a44e5321d78604248554cab507 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 8 Dec 2025 16:08:49 +1300 Subject: [PATCH 5/6] Fixed comment --- dev.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev.cs b/dev.cs index a9291894ed..440f999f71 100755 --- a/dev.cs +++ b/dev.cs @@ -15,7 +15,7 @@ // ./dev.cs subup // ./dev.cs wrest // ./dev.cs nrest -// ./dev.cs --dry-run cleanslate +// ./dev.cs cleanslate --dry-run // // This is intended to be run from the repo root so that git/dotnet // commands operate on this repository. From 28bfdbb0270e3269a4eb022580a2d12eeb96f803 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 9 Dec 2025 14:07:17 +1300 Subject: [PATCH 6/6] Add --init to submodule update commands --- dev.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev.cs b/dev.cs index 440f999f71..06ef704b8f 100755 --- a/dev.cs +++ b/dev.cs @@ -48,7 +48,7 @@ public async Task CleanSlateAsync( var steps = new (string Description, string FileName, string Arguments)[] { ("git clean", "git", "clean -dfx"), - ("git submodule update", "git", "submodule update --recursive"), + ("git submodule update", "git", "submodule update --init --recursive"), ("dotnet restore", "dotnet", $"restore \"{solution}\"") }; @@ -69,7 +69,7 @@ public async Task CleanSlateAsync( public Task SubmoduleUpdateAsync(GlobalOptions options = default!) { Console.WriteLine("[dev] Updating git submodules (recursive)"); - return RunStepAsync("git submodule update", "git", "submodule update --recursive", options.DryRun); + return RunStepAsync("git submodule update", "git", "submodule update --init --recursive", options.DryRun); } [Command("wrest", Description = "Run 'dotnet workload restore' (with sudo on Unix if available).")]