Made the build script report failed builds through its exit code (#1067)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-15 19:53:06 +02:00
1 parent da0c4e6bd9
commit b1e5199f62
2 files changed
+98 -53

No files matched your search

+86 -53
View File
@@ -2,6 +2,8 @@ using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using Build.Tools;
using SharedTools;
namespace Build.Commands;
@@ -20,26 +22,27 @@ public sealed partial class UpdateMetadataCommands
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
{
if(!Environment.IsWorkingDirectoryValid())
return;
throw new CommandExitedException(1);
// Validate parameters: either action or version must be specified, but not both:
if (action == PrepareAction.NONE && string.IsNullOrWhiteSpace(version))
{
Console.WriteLine("- Error: You must specify either --action (-a) or --version (-v).");
return;
throw new CommandExitedException(1);
}
if (action != PrepareAction.NONE && !string.IsNullOrWhiteSpace(version))
{
Console.WriteLine("- Error: You cannot specify both --action and --version. Please use only one.");
return;
throw new CommandExitedException(1);
}
// If version is specified, use SET action:
if (!string.IsNullOrWhiteSpace(version))
action = PrepareAction.SET;
// Prepare the metadata for the next release:
// Prepare the metadata for the next release. When this fails, it ends the
// whole command, so that nothing is built from half-prepared metadata:
await this.PerformPrepare(action, true, version);
await this.BuildPreparedRelease(offline);
@@ -50,7 +53,7 @@ public sealed partial class UpdateMetadataCommands
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
{
if(!Environment.IsWorkingDirectoryValid())
return;
throw new CommandExitedException(1);
Console.WriteLine("==============================");
Console.WriteLine("- Prepare a new build of the current release ...");
@@ -63,7 +66,7 @@ public sealed partial class UpdateMetadataCommands
catch (InvalidOperationException exception)
{
Console.WriteLine($"- Error: {exception.Message}");
return;
throw new CommandExitedException(1);
}
await this.ApplyRebuildReleaseState(releaseState, DateTime.UtcNow);
@@ -76,7 +79,8 @@ public sealed partial class UpdateMetadataCommands
private async Task BuildPreparedRelease(bool offline)
{
// Build once to allow the Rust compiler to read the changed metadata
// and to update all .NET artifacts:
// and to update all .NET artifacts. When this build fails, it ends the
// whole command; nothing below runs on top of its broken artifacts:
await this.Build(offline);
// Now, we update the web assets (which may were updated by the first build):
@@ -187,18 +191,20 @@ public sealed partial class UpdateMetadataCommands
Console.WriteLine("- Prepare the metadata for the next release ...");
var appVersion = await this.UpdateAppVersion(action, version);
var appVersion = await this.DetermineNextAppVersion(action, version);
if (!string.IsNullOrWhiteSpace(appVersion.VersionText))
{
// The changelog is the source for the AppStream description. Check it before we write
// any further metadata, so that a missing changelog cannot leave a half-prepared release:
// any metadata, the version included, so that a missing changelog cannot leave a
// half-prepared release:
var changelogPath = GetChangelogPath(appVersion.VersionText);
if (!File.Exists(changelogPath))
{
Console.WriteLine($"- Error: The changelog file '{Path.GetFileName(changelogPath)}' does not exist.");
return;
throw new CommandExitedException(1);
}
await this.UpdateAppVersion(appVersion);
var buildNumber = await this.IncreaseBuildNumber();
var buildTime = await this.UpdateBuildTime();
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTime);
@@ -227,7 +233,7 @@ public sealed partial class UpdateMetadataCommands
[Option("skip-verify", Description = "Skip the quality gate which otherwise runs before anything is built")] bool skipVerify = false)
{
if(!Environment.IsWorkingDirectoryValid())
return;
throw new CommandExitedException(1);
//
// The gate runs before anything is built, and the build stops when it does not pass. That
@@ -255,7 +261,7 @@ public sealed partial class UpdateMetadataCommands
Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ...");
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}");
var dotnetBuildOutput = await this.ReadCommandOutput(pathApp, "dotnet", $"publish --configuration release --runtime {rid.AsMicrosoftRid()} --disable-build-servers --force");
var dotnetBuildOutputLines = dotnetBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var dotnetBuildOutputLines = dotnetBuildOutput.Text.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var foundIssue = false;
foreach (var buildOutputLine in dotnetBuildOutputLines)
{
@@ -275,11 +281,24 @@ public sealed partial class UpdateMetadataCommands
if(foundIssue)
Console.WriteLine();
else
else if(dotnetBuildOutput.ExitCode is 0)
{
Console.WriteLine(" completed successfully.");
}
else
{
// Unlike the Rust build, this one ran silently. When none of its lines looks like a
// diagnostic, its whole output is the only thing left to go on:
Console.WriteLine(" failed:");
Console.WriteLine(dotnetBuildOutput.Text);
}
if(dotnetBuildOutput.ExitCode is not 0)
{
Console.WriteLine($"- Error: The .NET build failed with exit code {dotnetBuildOutput.ExitCode}.");
throw new CommandExitedException(1);
}
//
// Prepare the .NET artifact to be used by Tauri as sidecar:
//
@@ -301,7 +320,7 @@ public sealed partial class UpdateMetadataCommands
if (string.IsNullOrWhiteSpace(tauriSidecarArtifactName))
{
Console.WriteLine($"- Error: Unsupported rid '{rid.AsMicrosoftRid()}'.");
return;
throw new CommandExitedException(1);
}
var dotnetArtifactPath = Path.Combine(pathApp, "bin", "dist");
@@ -322,7 +341,7 @@ public sealed partial class UpdateMetadataCommands
else
{
Console.WriteLine($"- Error: Published .NET artifact not found: '{dotnetPublishedPath}'.");
return;
throw new CommandExitedException(1);
}
Console.Write($"- Move the .NET artifact to the Tauri sidecar destination ...");
@@ -333,8 +352,11 @@ public sealed partial class UpdateMetadataCommands
}
catch (Exception e)
{
// Building on would not fail: Tauri would quietly take whichever sidecar an earlier build
// left in its place, and the result would contain an outdated .NET app:
Console.WriteLine(" failed.");
Console.WriteLine($" - Error: {e.Message}");
throw new CommandExitedException(1);
}
//
@@ -344,7 +366,7 @@ public sealed partial class UpdateMetadataCommands
var pathRuntime = Environment.GetRustRuntimeDirectory();
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --no-bundle", true);
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var rustBuildOutputLines = rustBuildOutput.Text.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var foundRustIssue = false;
foreach (var buildOutputLine in rustBuildOutputLines)
{
@@ -362,11 +384,15 @@ public sealed partial class UpdateMetadataCommands
}
}
if(foundRustIssue)
Console.WriteLine();
else
Console.WriteLine();
if(rustBuildOutput.ExitCode is not 0)
{
Console.WriteLine($"- Error: The Rust build failed with exit code {rustBuildOutput.ExitCode}.");
throw new CommandExitedException(1);
}
if(!foundRustIssue)
{
Console.WriteLine();
Console.WriteLine("- Compilation completed successfully.");
Console.WriteLine();
}
@@ -555,7 +581,7 @@ public sealed partial class UpdateMetadataCommands
if (!ReleasesStartRegex().IsMatch(await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8)))
throw new InvalidOperationException("The AppStream metainfo does not contain a <releases> element.");
var headCommitHash = (await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD")).Trim();
var headCommitHash = (await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD")).Text.Trim();
if (!GitCommitHashRegex().IsMatch(headCommitHash))
throw new InvalidOperationException("The current Git commit hash could not be determined.");
@@ -850,7 +876,7 @@ public sealed partial class UpdateMetadataCommands
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentCommitHash = lines[COMMIT_HASH_INDEX].Trim();
var headCommitHash = await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD");
var headCommitHash = (await this.ReadCommandOutput(Environment.GetAIStudioDirectory(), "git", "rev-parse HEAD")).Text;
var first10Chars = headCommitHash[..11];
var updatedCommitHash = $"{first10Chars}, release";
@@ -860,7 +886,7 @@ public sealed partial class UpdateMetadataCommands
await WriteMetadataLinesAsync(pathMetadata, lines);
}
private async Task<AppVersion> UpdateAppVersion(PrepareAction action, string? version = null)
private async Task<AppVersion> DetermineNextAppVersion(PrepareAction action, string? version = null)
{
const int APP_VERSION_INDEX = 0;
@@ -882,7 +908,7 @@ public sealed partial class UpdateMetadataCommands
if (!versionMatch.Success)
{
Console.WriteLine($"- Error: Invalid version format '{version}'. Expected format: major.minor.patch (e.g., 26.1.2)");
return new(string.Empty, 0, 0, 0);
throw new CommandExitedException(1);
}
newMajor = int.Parse(versionMatch.Groups["major"].Value);
@@ -916,13 +942,19 @@ public sealed partial class UpdateMetadataCommands
}
}
var updatedAppVersion = $"{newMajor}.{newMinor}.{newPatch}";
Console.WriteLine($"- Updating app version from '{currentAppVersionLine}' to '{updatedAppVersion}'.");
return new($"{newMajor}.{newMinor}.{newPatch}", newMajor, newMinor, newPatch);
}
lines[APP_VERSION_INDEX] = updatedAppVersion;
private async Task UpdateAppVersion(AppVersion appVersion)
{
const int APP_VERSION_INDEX = 0;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
Console.WriteLine($"- Updating app version from '{lines[APP_VERSION_INDEX].Trim()}' to '{appVersion.VersionText}'.");
lines[APP_VERSION_INDEX] = appVersion.VersionText;
await WriteMetadataLinesAsync(pathMetadata, lines);
return new(updatedAppVersion, newMajor, newMinor, newPatch);
}
private async Task UpdateLicenceYear(string licenceFilePath)
@@ -1116,7 +1148,7 @@ public sealed partial class UpdateMetadataCommands
return matches;
}
private async Task<string> ReadCommandOutput(string workingDirectory, string program, string command, bool showLiveOutput = false)
private async Task<CommandOutput> ReadCommandOutput(string workingDirectory, string program, string command, bool showLiveOutput = false)
{
var processInfo = new ProcessStartInfo
{
@@ -1129,35 +1161,36 @@ public sealed partial class UpdateMetadataCommands
CreateNoWindow = true,
};
//
// Both streams deliver their lines on threads of their own, so the lines are collected under
// a lock. The handlers are in place before reading starts; otherwise, the first lines may get
// lost, and with them the diagnostic which says why a build failed:
//
var sb = new StringBuilder();
var sbLock = new Lock();
void CollectLine(string? line)
{
if(string.IsNullOrWhiteSpace(line))
return;
lock (sbLock)
{
if(showLiveOutput)
Console.WriteLine(line);
sb.AppendLine(line);
}
}
using var process = new Process();
process.StartInfo = processInfo;
process.OutputDataReceived += (_, args) => CollectLine(args.Data);
process.ErrorDataReceived += (_, args) => CollectLine(args.Data);
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.OutputDataReceived += (_, args) =>
{
if(!string.IsNullOrWhiteSpace(args.Data))
{
if(showLiveOutput)
Console.WriteLine(args.Data);
sb.AppendLine(args.Data);
}
};
process.ErrorDataReceived += (_, args) =>
{
if(!string.IsNullOrWhiteSpace(args.Data))
{
if(showLiveOutput)
Console.WriteLine(args.Data);
sb.AppendLine(args.Data);
}
};
await process.WaitForExitAsync();
return sb.ToString();
return new(sb.ToString(), process.ExitCode);
}
private async Task<int> IncreaseBuildNumber()
+12
View File
@@ -0,0 +1,12 @@
namespace Build.Tools;
/// <summary>
/// What an external tool wrote, and how it ended.
/// </summary>
/// <remarks>
/// The text alone cannot tell whether a tool failed: a build may stop on a diagnostic which no filter
/// recognizes, or without writing any at all. Only the exit code answers that question reliably.
/// </remarks>
/// <param name="Text">Everything the tool wrote to its standard output and its standard error, line by line.</param>
/// <param name="ExitCode">The exit code of the tool, where anything but zero is a failure.</param>
public readonly record struct CommandOutput(string Text, int ExitCode);