using Build.Tools; using SharedTools; // ReSharper disable ClassNeverInstantiated.Global // ReSharper disable UnusedType.Global // ReSharper disable UnusedMember.Global namespace Build.Commands; /// /// The quality gate: one command, the same one locally and in the pipeline. /// /// /// Every check runs, even after one of them has failed. A gate which stops at the first failure /// tells you one thing per run, and the next run costs the same minutes again -- while the point of /// running the whole thing is to learn everything which is wrong in one go. /// public sealed class VerifyCommand { /// /// How the .NET app is named once it lies where Tauri expects it. /// private const string SIDECAR_PREFIX = "mindworkAIStudioServer-"; [Command("verify", Description = "Run the quality gate: .NET tests, the trim smoke test, Rust tests, Clippy, and the model sources")] public async Task Verify() { if(!Environment.IsWorkingDirectoryValid()) return 1; Console.WriteLine("=============================="); Console.WriteLine("- Quality gate: every check runs, so that the first failure does not hide the next ..."); var results = new List<(string What, int ExitCode)> { (".NET tests", await CommandRunner.RunAsync(Environment.GetTestsDirectory(), "dotnet", "test --nologo")), ("Trim smoke test", await RunTrimSmokeTestAsync()), }; var runtimeDirectory = Environment.GetRustRuntimeDirectory(); if (WhatTauriExpectsIsThere()) { results.Add(("Rust tests", await CommandRunner.RunAsync(runtimeDirectory, "cargo", "test"))); results.Add(("Clippy", await CommandRunner.RunAsync(runtimeDirectory, "cargo", "clippy --all-targets -- -D warnings"))); } else { // // Tauri's build script insists that everything the configuration lists is already // there and refuses to run otherwise, so nothing Rust compiles until a build has // produced those files once. Failing here would be a trap rather than a gate: the way // to produce them is `dotnet run build`, and that command runs this gate first -- a // fresh clone would never get past it. // Console.WriteLine("- Skipping the Rust tests and Clippy: the .NET sidecar or the downloaded libraries are missing, and Tauri's build script needs both before anything Rust compiles."); Console.WriteLine(" Run 'dotnet run build --skip-verify' once. From then on, this part of the gate runs with the rest."); } results.Add(("Model sources", new VerifyModelsCommand().VerifyModels())); Console.WriteLine("=============================="); Console.WriteLine("- Quality gate:"); foreach (var (what, exitCode) in results) Console.WriteLine($" - {what}: {(exitCode is 0 ? "passed" : $"failed, exit code {exitCode}")}"); var failed = results.Count(result => result.ExitCode is not 0); if (failed is 0) { Console.WriteLine($"- All {results.Count} checks passed."); return 0; } Console.WriteLine($"- {failed} of {results.Count} checks failed."); return 1; } /// /// Publishes the trim smoke test for this machine, trimmed like the app, and runs it. /// /// /// Debug builds and the tests are not trimmed, so only a published build shows that the trimmer /// removed code which System.Text.Json reaches by reflection. Then AI Studio can neither load nor /// save, e.g., its chats, while everything looks fine during development. /// /// The exit code of the publishing, when that failed; otherwise the one of the smoke test. private static async Task RunTrimSmokeTestAsync() { var rid = Environment.GetCurrentRid(); if (rid is RID.NONE) { Console.WriteLine("- Error: The trim smoke test needs a platform AI Studio is built for."); return 1; } var directory = Environment.GetTrimSmokeTestDirectory(); var publishDirectory = Path.Combine(directory, "bin", "verify"); // // Publishing leaves the files of an earlier run in place, among them copies of assemblies which the trimmer // now removes completely. The dependency manifest still lists such an assembly, so a leftover copy can be // loaded in its place and hide exactly what the smoke test is there to find: // if (Directory.Exists(publishDirectory)) Directory.Delete(publishDirectory, recursive: true); var publishExitCode = await CommandRunner.RunAsync(directory, "dotnet", $"publish --configuration release --runtime {rid.AsMicrosoftRid()} --output \"{publishDirectory}\" --nologo"); if (publishExitCode is not 0) return publishExitCode; var program = Path.Combine(publishDirectory, OperatingSystem.IsWindows() ? "TrimSmokeTest.exe" : "TrimSmokeTest"); return await CommandRunner.RunAsync(directory, program, string.Empty); } /// /// Whether a build has already produced the files Tauri's build script reads. /// /// /// Both are products of a build rather than of the repository: the .NET app arrives as a /// sidecar, and the PDF library is downloaded into the resources. The other resource /// directories the configuration names are in the repository and are always there. /// /// True, when cargo can get past the build script. private static bool WhatTauriExpectsIsThere() { var distributionDirectory = Path.Combine(Environment.GetAIStudioDirectory(), "bin", "dist"); if (!Directory.Exists(distributionDirectory) || !Directory.EnumerateFiles(distributionDirectory, $"{SIDECAR_PREFIX}*").Any()) return false; var librariesDirectory = Path.Combine(Environment.GetRustRuntimeDirectory(), "resources", "libraries"); return Directory.Exists(librariesDirectory) && Directory.EnumerateFiles(librariesDirectory).Any(); } }