mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 03:57:59 +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.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
using Build.Tools;
|
||||||
|
|
||||||
using SharedTools;
|
using SharedTools;
|
||||||
|
|
||||||
namespace Build.Commands;
|
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)
|
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
|
||||||
{
|
{
|
||||||
if(!Environment.IsWorkingDirectoryValid())
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
return;
|
throw new CommandExitedException(1);
|
||||||
|
|
||||||
// Validate parameters: either action or version must be specified, but not both:
|
// Validate parameters: either action or version must be specified, but not both:
|
||||||
if (action == PrepareAction.NONE && string.IsNullOrWhiteSpace(version))
|
if (action == PrepareAction.NONE && string.IsNullOrWhiteSpace(version))
|
||||||
{
|
{
|
||||||
Console.WriteLine("- Error: You must specify either --action (-a) or --version (-v).");
|
Console.WriteLine("- Error: You must specify either --action (-a) or --version (-v).");
|
||||||
return;
|
throw new CommandExitedException(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action != PrepareAction.NONE && !string.IsNullOrWhiteSpace(version))
|
if (action != PrepareAction.NONE && !string.IsNullOrWhiteSpace(version))
|
||||||
{
|
{
|
||||||
Console.WriteLine("- Error: You cannot specify both --action and --version. Please use only one.");
|
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 version is specified, use SET action:
|
||||||
if (!string.IsNullOrWhiteSpace(version))
|
if (!string.IsNullOrWhiteSpace(version))
|
||||||
action = PrepareAction.SET;
|
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.PerformPrepare(action, true, version);
|
||||||
|
|
||||||
await this.BuildPreparedRelease(offline);
|
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)
|
[Option("offline", Description = "Skip downloads and use locally available build dependencies")] bool offline = false)
|
||||||
{
|
{
|
||||||
if(!Environment.IsWorkingDirectoryValid())
|
if(!Environment.IsWorkingDirectoryValid())
|
||||||
return;
|
throw new CommandExitedException(1);
|
||||||
|
|
||||||
Console.WriteLine("==============================");
|
Console.WriteLine("==============================");
|
||||||
Console.WriteLine("- Prepare a new build of the current release ...");
|
Console.WriteLine("- Prepare a new build of the current release ...");
|
||||||
@@ -63,7 +66,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
catch (InvalidOperationException exception)
|
catch (InvalidOperationException exception)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"- Error: {exception.Message}");
|
Console.WriteLine($"- Error: {exception.Message}");
|
||||||
return;
|
throw new CommandExitedException(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.ApplyRebuildReleaseState(releaseState, DateTime.UtcNow);
|
await this.ApplyRebuildReleaseState(releaseState, DateTime.UtcNow);
|
||||||
@@ -76,7 +79,8 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
private async Task BuildPreparedRelease(bool offline)
|
private async Task BuildPreparedRelease(bool offline)
|
||||||
{
|
{
|
||||||
// Build once to allow the Rust compiler to read the changed metadata
|
// 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);
|
await this.Build(offline);
|
||||||
|
|
||||||
// Now, we update the web assets (which may were updated by the first build):
|
// 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 ...");
|
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))
|
if (!string.IsNullOrWhiteSpace(appVersion.VersionText))
|
||||||
{
|
{
|
||||||
// The changelog is the source for the AppStream description. Check it before we write
|
// 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);
|
var changelogPath = GetChangelogPath(appVersion.VersionText);
|
||||||
if (!File.Exists(changelogPath))
|
if (!File.Exists(changelogPath))
|
||||||
{
|
{
|
||||||
Console.WriteLine($"- Error: The changelog file '{Path.GetFileName(changelogPath)}' does not exist.");
|
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 buildNumber = await this.IncreaseBuildNumber();
|
||||||
var buildTime = await this.UpdateBuildTime();
|
var buildTime = await this.UpdateBuildTime();
|
||||||
await this.UpdateChangelog(buildNumber, appVersion.VersionText, buildTime);
|
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)
|
[Option("skip-verify", Description = "Skip the quality gate which otherwise runs before anything is built")] bool skipVerify = false)
|
||||||
{
|
{
|
||||||
if(!Environment.IsWorkingDirectoryValid())
|
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
|
// 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()} ...");
|
Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ...");
|
||||||
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}");
|
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 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;
|
var foundIssue = false;
|
||||||
foreach (var buildOutputLine in dotnetBuildOutputLines)
|
foreach (var buildOutputLine in dotnetBuildOutputLines)
|
||||||
{
|
{
|
||||||
@@ -275,11 +281,24 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
|
|
||||||
if(foundIssue)
|
if(foundIssue)
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
else
|
else if(dotnetBuildOutput.ExitCode is 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine(" completed successfully.");
|
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:
|
// Prepare the .NET artifact to be used by Tauri as sidecar:
|
||||||
//
|
//
|
||||||
@@ -301,7 +320,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
if (string.IsNullOrWhiteSpace(tauriSidecarArtifactName))
|
if (string.IsNullOrWhiteSpace(tauriSidecarArtifactName))
|
||||||
{
|
{
|
||||||
Console.WriteLine($"- Error: Unsupported rid '{rid.AsMicrosoftRid()}'.");
|
Console.WriteLine($"- Error: Unsupported rid '{rid.AsMicrosoftRid()}'.");
|
||||||
return;
|
throw new CommandExitedException(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
var dotnetArtifactPath = Path.Combine(pathApp, "bin", "dist");
|
var dotnetArtifactPath = Path.Combine(pathApp, "bin", "dist");
|
||||||
@@ -322,7 +341,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine($"- Error: Published .NET artifact not found: '{dotnetPublishedPath}'.");
|
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 ...");
|
Console.Write($"- Move the .NET artifact to the Tauri sidecar destination ...");
|
||||||
@@ -333,8 +352,11 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
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(" failed.");
|
||||||
Console.WriteLine($" - Error: {e.Message}");
|
Console.WriteLine($" - Error: {e.Message}");
|
||||||
|
throw new CommandExitedException(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -344,7 +366,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
|
|
||||||
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
||||||
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --no-bundle", true);
|
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;
|
var foundRustIssue = false;
|
||||||
foreach (var buildOutputLine in rustBuildOutputLines)
|
foreach (var buildOutputLine in rustBuildOutputLines)
|
||||||
{
|
{
|
||||||
@@ -362,11 +384,15 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(foundRustIssue)
|
Console.WriteLine();
|
||||||
Console.WriteLine();
|
if(rustBuildOutput.ExitCode is not 0)
|
||||||
else
|
{
|
||||||
|
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("- Compilation completed successfully.");
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
}
|
}
|
||||||
@@ -555,7 +581,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
if (!ReleasesStartRegex().IsMatch(await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8)))
|
if (!ReleasesStartRegex().IsMatch(await File.ReadAllTextAsync(metainfoPath, Encoding.UTF8)))
|
||||||
throw new InvalidOperationException("The AppStream metainfo does not contain a <releases> element.");
|
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))
|
if (!GitCommitHashRegex().IsMatch(headCommitHash))
|
||||||
throw new InvalidOperationException("The current Git commit hash could not be determined.");
|
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 pathMetadata = Environment.GetMetadataPath();
|
||||||
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
|
||||||
var currentCommitHash = lines[COMMIT_HASH_INDEX].Trim();
|
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 first10Chars = headCommitHash[..11];
|
||||||
var updatedCommitHash = $"{first10Chars}, release";
|
var updatedCommitHash = $"{first10Chars}, release";
|
||||||
|
|
||||||
@@ -860,7 +886,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
await WriteMetadataLinesAsync(pathMetadata, lines);
|
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;
|
const int APP_VERSION_INDEX = 0;
|
||||||
|
|
||||||
@@ -882,7 +908,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
if (!versionMatch.Success)
|
if (!versionMatch.Success)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"- Error: Invalid version format '{version}'. Expected format: major.minor.patch (e.g., 26.1.2)");
|
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);
|
newMajor = int.Parse(versionMatch.Groups["major"].Value);
|
||||||
@@ -916,13 +942,19 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedAppVersion = $"{newMajor}.{newMinor}.{newPatch}";
|
return new($"{newMajor}.{newMinor}.{newPatch}", newMajor, newMinor, newPatch);
|
||||||
Console.WriteLine($"- Updating app version from '{currentAppVersionLine}' to '{updatedAppVersion}'.");
|
}
|
||||||
|
|
||||||
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);
|
await WriteMetadataLinesAsync(pathMetadata, lines);
|
||||||
|
|
||||||
return new(updatedAppVersion, newMajor, newMinor, newPatch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateLicenceYear(string licenceFilePath)
|
private async Task UpdateLicenceYear(string licenceFilePath)
|
||||||
@@ -1116,7 +1148,7 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
return matches;
|
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
|
var processInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
@@ -1129,35 +1161,36 @@ public sealed partial class UpdateMetadataCommands
|
|||||||
CreateNoWindow = true,
|
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 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();
|
using var process = new Process();
|
||||||
process.StartInfo = processInfo;
|
process.StartInfo = processInfo;
|
||||||
|
process.OutputDataReceived += (_, args) => CollectLine(args.Data);
|
||||||
|
process.ErrorDataReceived += (_, args) => CollectLine(args.Data);
|
||||||
process.Start();
|
process.Start();
|
||||||
process.BeginOutputReadLine();
|
process.BeginOutputReadLine();
|
||||||
process.BeginErrorReadLine();
|
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();
|
await process.WaitForExitAsync();
|
||||||
return sb.ToString();
|
return new(sb.ToString(), process.ExitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> IncreaseBuildNumber()
|
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