mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-16 12:53:48 +00:00
Made the build script report failed builds through its exit code (#1067)
This commit is contained in:
1 parent
da0c4e6bd9
commit
b1e5199f62
2 files changed
+98
-53
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
Reference in new issue
Block a user