Migrated build script to C# (#401)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, deb) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2025-04-14 19:29:56 +02:00
1 parent 3fc15d9789
commit c715b38f41
20 files changed
+1007 -411

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Build</RootNamespace>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>build</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cocona" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharedTools\SharedTools.csproj" />
</ItemGroup>
</Project>
+21
View File
@@ -0,0 +1,21 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
namespace Build.Commands;
public sealed class CheckRidsCommand
{
[Command("check-rids", Description = "Check the RIDs for the current OS")]
public void GetRids()
{
if(!Environment.IsWorkingDirectoryValid())
return;
var rids = Environment.GetRidsForCurrentOS();
Console.WriteLine("The following RIDs are available for the current OS:");
foreach (var rid in rids)
{
Console.WriteLine($"- {rid}");
}
}
}
@@ -0,0 +1,167 @@
using System.Text.RegularExpressions;
using SharedTools;
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
namespace Build.Commands;
public sealed partial class CollectI18NKeysCommand
{
[Command("collect-i18n", Description = "Collect I18N keys")]
public async Task CollectI18NKeys()
{
if(!Environment.IsWorkingDirectoryValid())
return;
Console.WriteLine("=========================");
Console.Write("- Collecting I18N keys ...");
var cwd = Environment.GetAIStudioDirectory();
var binPath = Path.Join(cwd, "bin");
var objPath = Path.Join(cwd, "obj");
var wwwrootPath = Path.Join(cwd, "wwwroot");
var allFiles = Directory.EnumerateFiles(cwd, "*", SearchOption.AllDirectories);
var counter = 0;
var sb = new StringBuilder();
foreach (var filePath in allFiles)
{
counter++;
if(filePath.StartsWith(binPath, StringComparison.OrdinalIgnoreCase))
continue;
if(filePath.StartsWith(objPath, StringComparison.OrdinalIgnoreCase))
continue;
if(filePath.StartsWith(wwwrootPath, StringComparison.OrdinalIgnoreCase))
continue;
var content = await File.ReadAllTextAsync(filePath, Encoding.UTF8);
var matches = this.FindAllTextTags(content);
if (matches.Count == 0)
continue;
var ns = this.DetermineNamespace(filePath);
var fileInfo = new FileInfo(filePath);
var name = fileInfo.Name.Replace(fileInfo.Extension, string.Empty);
var langNamespace = $"{ns}::{name}".ToUpperInvariant().Replace(".", "::");
foreach (var match in matches)
{
var key = $"root::{langNamespace}::T{match.ToFNV32()}";
}
}
Console.WriteLine($" {counter:###,###} files processed.");
Console.WriteLine();
}
private List<string> FindAllTextTags(ReadOnlySpan<char> fileContent)
{
const string START_TAG = """
T("
""";
const string END_TAG = """
")
""";
var matches = new List<string>();
var startIdx = fileContent.IndexOf(START_TAG);
var content = fileContent;
while (startIdx > -1)
{
content = content[(startIdx + START_TAG.Length)..];
var endIdx = content.IndexOf(END_TAG);
if (endIdx == -1)
break;
var match = content[..endIdx];
matches.Add(match.ToString());
startIdx = content.IndexOf(START_TAG);
}
return matches;
}
private string? DetermineNamespace(string filePath)
{
// Is it a C# file? Then we can read the namespace from it:
if (filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
return this.ReadNamespaceFromCSharp(filePath);
// Is it a Razor file? Then, it depends:
if (filePath.EndsWith(".razor", StringComparison.OrdinalIgnoreCase))
{
// Check if the file contains a namespace declaration:
var blazorNamespace = this.ReadNamespaceFromRazor(filePath);
if (blazorNamespace != null)
return blazorNamespace;
// Alright, no namespace declaration. Let's check the corresponding C# file:
var csFilePath = $"{filePath}.cs";
if (File.Exists(csFilePath))
{
var csNamespace = this.ReadNamespaceFromCSharp(csFilePath);
if (csNamespace != null)
return csNamespace;
Console.WriteLine($"- Error: Neither the blazor file '{filePath}' nor the corresponding C# file '{csFilePath}' contain a namespace declaration.");
return null;
}
Console.WriteLine($"- Error: The blazor file '{filePath}' does not contain a namespace declaration and the corresponding C# file '{csFilePath}' does not exist.");
return null;
}
// Not a C# or Razor file. We can't determine the namespace:
Console.WriteLine($"- Error: The file '{filePath}' is neither a C# nor a Razor file. We can't determine the namespace.");
return null;
}
private string? ReadNamespaceFromCSharp(string filePath)
{
var content = File.ReadAllText(filePath, Encoding.UTF8);
var matches = CSharpNamespaceRegex().Matches(content);
if (matches.Count == 0)
return null;
if (matches.Count > 1)
{
Console.WriteLine($"The file '{filePath}' contains multiple namespaces. This scenario is not supported.");
return null;
}
var match = matches[0];
return match.Groups[1].Value;
}
private string? ReadNamespaceFromRazor(string filePath)
{
var content = File.ReadAllText(filePath, Encoding.UTF8);
var matches = BlazorNamespaceRegex().Matches(content);
if (matches.Count == 0)
return null;
if (matches.Count > 1)
{
Console.WriteLine($"The file '{filePath}' contains multiple namespaces. This scenario is not supported.");
return null;
}
var match = matches[0];
return match.Groups[1].Value;
}
[GeneratedRegex("""@namespace\s+([a-zA-Z0-9_.]+)""")]
private static partial Regex BlazorNamespaceRegex();
[GeneratedRegex("""namespace\s+([a-zA-Z0-9_.]+)""")]
private static partial Regex CSharpNamespaceRegex();
}
+10
View File
@@ -0,0 +1,10 @@
namespace Build.Commands;
public enum PrepareAction
{
NONE,
PATCH,
MINOR,
MAJOR,
}
@@ -0,0 +1,557 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using Build.Tools;
namespace Build.Commands;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
public sealed partial class UpdateMetadataCommands
{
[Command("release", Description = "Prepare & build the next release")]
public async Task Release(PrepareAction action)
{
if(!Environment.IsWorkingDirectoryValid())
return;
// Prepare the metadata for the next release:
await this.Prepare(action);
// Build once to allow the Rust compiler to read the changed metadata
// and to update all .NET artifacts:
await this.Build();
// Now, we update the web assets (which may were updated by the first build):
new UpdateWebAssetsCommand().UpdateWebAssets();
// Collect the I18N keys from the source code. This step yields a I18N file
// that must be part of the final release:
await new CollectI18NKeysCommand().CollectI18NKeys();
// Build the final release, where Rust knows the updated metadata, the .NET
// artifacts are already in place, and .NET knows the updated web assets, etc.:
await this.Build();
}
[Command("prepare", Description = "Prepare the metadata for the next release")]
public async Task Prepare(PrepareAction action)
{
if(!Environment.IsWorkingDirectoryValid())
return;
Console.WriteLine("==============================");
Console.WriteLine("- Prepare the metadata for the next release ...");
var appVersion = await this.UpdateAppVersion(action);
if (!string.IsNullOrWhiteSpace(appVersion))
{
var buildNumber = await this.IncreaseBuildNumber();
var buildTime = await this.UpdateBuildTime();
await this.UpdateChangelog(buildNumber, appVersion, buildTime);
await this.UpdateDotnetVersion();
await this.UpdateRustVersion();
await this.UpdateMudBlazorVersion();
await this.UpdateTauriVersion();
await this.UpdateProjectCommitHash();
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md")));
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "About.razor.cs")));
Console.WriteLine();
}
}
[Command("build", Description = "Build MindWork AI Studio")]
public async Task Build()
{
if(!Environment.IsWorkingDirectoryValid())
return;
//
// Build the .NET project:
//
var pathApp = Environment.GetAIStudioDirectory();
var rids = Environment.GetRidsForCurrentOS();
foreach (var rid in rids)
{
Console.WriteLine("==============================");
Console.Write($"- Start .NET build for '{rid.ToName()}' ...");
await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.ToName()}");
var dotnetBuildOutput = await this.ReadCommandOutput(pathApp, "dotnet", $"publish --configuration release --runtime {rid.ToName()} --disable-build-servers --force");
var dotnetBuildOutputLines = dotnetBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var foundIssue = false;
foreach (var buildOutputLine in dotnetBuildOutputLines)
{
if(buildOutputLine.Contains(" error ") || buildOutputLine.Contains("#warning"))
{
if(!foundIssue)
{
foundIssue = true;
Console.WriteLine();
Console.WriteLine("- Build has issues:");
}
Console.Write(" - ");
Console.WriteLine(buildOutputLine);
}
}
if(foundIssue)
Console.WriteLine();
else
{
Console.WriteLine(" completed successfully.");
}
//
// Prepare the .NET artifact to be used by Tauri as sidecar:
//
var os = Environment.GetOS();
var tauriSidecarArtifactName = rid switch
{
RID.WIN_X64 => "mindworkAIStudioServer-x86_64-pc-windows-msvc.exe",
RID.WIN_ARM64 => "mindworkAIStudioServer-aarch64-pc-windows-msvc.exe",
RID.LINUX_X64 => "mindworkAIStudioServer-x86_64-unknown-linux-gnu",
RID.LINUX_ARM64 => "mindworkAIStudioServer-aarch64-unknown-linux-gnu",
RID.OSX_ARM64 => "mindworkAIStudioServer-aarch64-apple-darwin",
RID.OSX_X64 => "mindworkAIStudioServer-x86_64-apple-darwin",
_ => string.Empty,
};
if (string.IsNullOrWhiteSpace(tauriSidecarArtifactName))
{
Console.WriteLine($"- Error: Unsupported rid '{rid.ToName()}'.");
return;
}
var dotnetArtifactPath = Path.Combine(pathApp, "bin", "dist");
if(!Directory.Exists(dotnetArtifactPath))
Directory.CreateDirectory(dotnetArtifactPath);
var dotnetArtifactFilename = os switch
{
"windows" => "mindworkAIStudio.exe",
_ => "mindworkAIStudio",
};
var dotnetPublishedPath = Path.Combine(pathApp, "bin", "release", Environment.DOTNET_VERSION, rid.ToName(), "publish", dotnetArtifactFilename);
var finalDestination = Path.Combine(dotnetArtifactPath, tauriSidecarArtifactName);
if(File.Exists(dotnetPublishedPath))
Console.WriteLine("- Published .NET artifact found.");
else
{
Console.WriteLine($"- Error: Published .NET artifact not found: '{dotnetPublishedPath}'.");
return;
}
Console.Write($"- Move the .NET artifact to the Tauri sidecar destination ...");
try
{
File.Move(dotnetPublishedPath, finalDestination, true);
Console.WriteLine(" done.");
}
catch (Exception e)
{
Console.WriteLine(" failed.");
Console.WriteLine($" - Error: {e.Message}");
}
Console.WriteLine();
}
//
// Build the Rust project / runtime:
//
Console.WriteLine("==============================");
Console.WriteLine("- Start building the Rust runtime ...");
var pathRuntime = Environment.GetRustRuntimeDirectory();
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --bundles none", true);
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
var foundRustIssue = false;
foreach (var buildOutputLine in rustBuildOutputLines)
{
if(buildOutputLine.Contains("error", StringComparison.OrdinalIgnoreCase) || buildOutputLine.Contains("warning"))
{
if(!foundRustIssue)
{
foundRustIssue = true;
Console.WriteLine();
Console.WriteLine("- Build has issues:");
}
Console.Write(" - ");
Console.WriteLine(buildOutputLine);
}
}
if(foundRustIssue)
Console.WriteLine();
else
{
Console.WriteLine();
Console.WriteLine("- Compilation completed successfully.");
Console.WriteLine();
}
}
private async Task UpdateChangelog(int buildNumber, string appVersion, string buildTime)
{
var pathChangelogs = Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog");
var expectedLogFilename = $"v{appVersion}.md";
var expectedLogFilePath = Path.Combine(pathChangelogs, expectedLogFilename);
if(!File.Exists(expectedLogFilePath))
{
Console.WriteLine($"- Error: The changelog file '{expectedLogFilename}' does not exist.");
return;
}
// Right now, the build time is formatted as "yyyy-MM-dd HH:mm:ss UTC", but must remove the seconds:
buildTime = buildTime[..^7] + " UTC";
const string CODE_START =
"""
LOGS =
[
""";
var changelogCodePath = Path.Join(Environment.GetAIStudioDirectory(), "Components", "Changelog.Logs.cs");
var changelogCode = await File.ReadAllTextAsync(changelogCodePath, Encoding.UTF8);
var updatedCode =
$"""
{CODE_START}
new ({buildNumber}, "v{appVersion}, build {buildNumber} ({buildTime})", "{expectedLogFilename}"),
""";
changelogCode = changelogCode.Replace(CODE_START, updatedCode);
await File.WriteAllTextAsync(changelogCodePath, changelogCode, Environment.UTF8_NO_BOM);
}
private async Task UpdateProjectCommitHash()
{
const int COMMIT_HASH_INDEX = 8;
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 first10Chars = headCommitHash[..11];
var updatedCommitHash = $"{first10Chars}, release";
Console.WriteLine($"- Updating commit hash from '{currentCommitHash}' to '{updatedCommitHash}'.");
lines[COMMIT_HASH_INDEX] = updatedCommitHash;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task<string> UpdateAppVersion(PrepareAction action)
{
const int APP_VERSION_INDEX = 0;
if (action == PrepareAction.NONE)
{
Console.WriteLine("- No action specified. Skipping app version update.");
return string.Empty;
}
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentAppVersionLine = lines[APP_VERSION_INDEX].Trim();
var currentAppVersion = AppVersionRegex().Match(currentAppVersionLine);
var currentPatch = int.Parse(currentAppVersion.Groups["patch"].Value);
var currentMinor = int.Parse(currentAppVersion.Groups["minor"].Value);
var currentMajor = int.Parse(currentAppVersion.Groups["major"].Value);
switch (action)
{
case PrepareAction.PATCH:
currentPatch++;
break;
case PrepareAction.MINOR:
currentPatch = 0;
currentMinor++;
break;
case PrepareAction.MAJOR:
currentPatch = 0;
currentMinor = 0;
currentMajor++;
break;
}
var updatedAppVersion = $"{currentMajor}.{currentMinor}.{currentPatch}";
Console.WriteLine($"- Updating app version from '{currentAppVersionLine}' to '{updatedAppVersion}'.");
lines[APP_VERSION_INDEX] = updatedAppVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
return updatedAppVersion;
}
private async Task UpdateLicenceYear(string licenceFilePath)
{
var currentYear = DateTime.UtcNow.Year.ToString();
var lines = await File.ReadAllLinesAsync(licenceFilePath, Encoding.UTF8);
var found = false;
var copyrightYear = string.Empty;
var updatedLines = new List<string>(lines.Length);
foreach (var line in lines)
{
var match = FindCopyrightRegex().Match(line);
if (match.Success)
{
copyrightYear = match.Groups["year"].Value;
if(!found && copyrightYear != currentYear)
Console.WriteLine($"- Updating the licence's year in '{Path.GetFileName(licenceFilePath)}' from '{copyrightYear}' to '{currentYear}'.");
updatedLines.Add(ReplaceCopyrightYearRegex().Replace(line, currentYear));
found = true;
}
else
updatedLines.Add(line);
}
await File.WriteAllLinesAsync(licenceFilePath, updatedLines, Environment.UTF8_NO_BOM);
if (!found)
Console.WriteLine($"- Error: No copyright year found in '{Path.GetFileName(licenceFilePath)}'.");
else if (copyrightYear == currentYear)
Console.WriteLine($"- The copyright year in '{Path.GetFileName(licenceFilePath)}' is already up to date.");
}
private async Task UpdateTauriVersion()
{
const int TAURI_VERSION_INDEX = 7;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentTauriVersion = lines[TAURI_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion("Tauri", Environment.GetRustRuntimeDirectory(), TauriVersionRegex(), "cargo", "tree --depth 1");
if (matches.Count == 0)
return;
var updatedTauriVersion = matches[0].Groups["version"].Value;
if(currentTauriVersion == updatedTauriVersion)
{
Console.WriteLine("- The Tauri version is already up to date.");
return;
}
Console.WriteLine($"- Updated Tauri version from {currentTauriVersion} to {updatedTauriVersion}.");
lines[TAURI_VERSION_INDEX] = updatedTauriVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateMudBlazorVersion()
{
const int MUD_BLAZOR_VERSION_INDEX = 6;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentMudBlazorVersion = lines[MUD_BLAZOR_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion("MudBlazor", Environment.GetAIStudioDirectory(), MudBlazorVersionRegex(), "dotnet", "list package");
if (matches.Count == 0)
return;
var updatedMudBlazorVersion = matches[0].Groups["version"].Value;
if(currentMudBlazorVersion == updatedMudBlazorVersion)
{
Console.WriteLine("- The MudBlazor version is already up to date.");
return;
}
Console.WriteLine($"- Updated MudBlazor version from {currentMudBlazorVersion} to {updatedMudBlazorVersion}.");
lines[MUD_BLAZOR_VERSION_INDEX] = updatedMudBlazorVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateRustVersion()
{
const int RUST_VERSION_INDEX = 5;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentRustVersion = lines[RUST_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion("Rust", Environment.GetRustRuntimeDirectory(), RustVersionRegex(), "rustc", "-Vv");
if (matches.Count == 0)
return;
var updatedRustVersion = matches[0].Groups["version"].Value + " (commit " + matches[0].Groups["commit"].Value + ")";
if(currentRustVersion == updatedRustVersion)
{
Console.WriteLine("- Rust version is already up to date.");
return;
}
Console.WriteLine($"- Updated Rust version from {currentRustVersion} to {updatedRustVersion}.");
lines[RUST_VERSION_INDEX] = updatedRustVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task UpdateDotnetVersion()
{
const int DOTNET_VERSION_INDEX = 4;
const int DOTNET_SDK_VERSION_INDEX = 3;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentDotnetVersion = lines[DOTNET_VERSION_INDEX].Trim();
var currentDotnetSdkVersion = lines[DOTNET_SDK_VERSION_INDEX].Trim();
var matches = await this.DetermineVersion(".NET", Environment.GetAIStudioDirectory(), DotnetVersionRegex(), "dotnet", "--info");
if (matches.Count == 0)
return;
var updatedDotnetVersion = matches[0].Groups["hostVersion"].Value + " (commit " + matches[0].Groups["hostCommit"].Value + ")";
var updatedDotnetSdkVersion = matches[0].Groups["sdkVersion"].Value + " (commit " + matches[0].Groups["sdkCommit"].Value + ")";
if(currentDotnetVersion == updatedDotnetVersion && currentDotnetSdkVersion == updatedDotnetSdkVersion)
{
Console.WriteLine("- .NET version is already up to date.");
return;
}
Console.WriteLine($"- Updated .NET SDK version from {currentDotnetSdkVersion} to {updatedDotnetSdkVersion}.");
Console.WriteLine($"- Updated .NET version from {currentDotnetVersion} to {updatedDotnetVersion}.");
lines[DOTNET_VERSION_INDEX] = updatedDotnetVersion;
lines[DOTNET_SDK_VERSION_INDEX] = updatedDotnetSdkVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
private async Task<IList<Match>> DetermineVersion(string name, string workingDirectory, Regex regex, string program, string command)
{
var processInfo = new ProcessStartInfo
{
WorkingDirectory = workingDirectory,
FileName = program,
Arguments = command,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process();
process.StartInfo = processInfo;
process.Start();
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
var matches = regex.Matches(output);
if (matches.Count == 0)
{
Console.WriteLine($"- Error: Was not able to determine the {name} version.");
return [];
}
return matches;
}
private async Task<string> ReadCommandOutput(string workingDirectory, string program, string command, bool showLiveOutput = false)
{
var processInfo = new ProcessStartInfo
{
WorkingDirectory = workingDirectory,
FileName = program,
Arguments = command,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
var sb = new StringBuilder();
using var process = new Process();
process.StartInfo = processInfo;
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();
}
private async Task<int> IncreaseBuildNumber()
{
const int BUILD_NUMBER_INDEX = 2;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var buildNumber = int.Parse(lines[BUILD_NUMBER_INDEX]) + 1;
Console.WriteLine($"- Updating build number from '{lines[BUILD_NUMBER_INDEX]}' to '{buildNumber}'.");
lines[BUILD_NUMBER_INDEX] = buildNumber.ToString();
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
return buildNumber;
}
private async Task<string> UpdateBuildTime()
{
const int BUILD_TIME_INDEX = 1;
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var buildTime = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") + " UTC";
Console.WriteLine($"- Updating build time from '{lines[BUILD_TIME_INDEX]}' to '{buildTime}'.");
lines[BUILD_TIME_INDEX] = buildTime;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
return buildTime;
}
[GeneratedRegex("""(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?<sdkVersion>[0-9.]+).+Commit:\s+(?<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?<hostVersion>[0-9.]+).+Commit:\s+(?<hostCommit>[a-zA-Z0-9]+)""")]
private static partial Regex DotnetVersionRegex();
[GeneratedRegex("""rustc (?<version>[0-9.]+)(?:-nightly)? \((?<commit>[a-zA-Z0-9]+)""")]
private static partial Regex RustVersionRegex();
[GeneratedRegex("""MudBlazor\s+(?<version>[0-9.]+)""")]
private static partial Regex MudBlazorVersionRegex();
[GeneratedRegex("""tauri\s+v(?<version>[0-9.]+)""")]
private static partial Regex TauriVersionRegex();
[GeneratedRegex("""^\s*Copyright\s+(?<year>[0-9]{4})""")]
private static partial Regex FindCopyrightRegex();
[GeneratedRegex("""([0-9]{4})""")]
private static partial Regex ReplaceCopyrightYearRegex();
[GeneratedRegex("""(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<patch>[0-9]+)""")]
private static partial Regex AppVersionRegex();
}
@@ -0,0 +1,49 @@
// ReSharper disable ClassNeverInstantiated.Global
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
using Build.Tools;
namespace Build.Commands;
public sealed class UpdateWebAssetsCommand
{
[Command("update-web", Description = "Update web assets")]
public void UpdateWebAssets()
{
if(!Environment.IsWorkingDirectoryValid())
return;
Console.WriteLine("=========================");
Console.Write("- Updating web assets ...");
var rid = Environment.GetRidsForCurrentOS().First();
var cwd = Environment.GetAIStudioDirectory();
var contentPath = Path.Join(cwd, "bin", "release", Environment.DOTNET_VERSION, rid.ToName(), "publish", "wwwroot", "_content");
var isMudBlazorDirectoryPresent = Directory.Exists(Path.Join(contentPath, "MudBlazor"));
if (!isMudBlazorDirectoryPresent)
{
Console.WriteLine();
Console.WriteLine($"- Error: No web assets found for RID '{rid}'. Please publish the project first.");
return;
}
Directory.CreateDirectory(Path.Join(cwd, "wwwroot", "system"));
var sourcePaths = Directory.EnumerateFiles(contentPath, "*", SearchOption.AllDirectories);
var counter = 0;
foreach(var sourcePath in sourcePaths)
{
counter++;
var relativePath = Path.GetRelativePath(cwd, sourcePath);
var targetPath = Path.Join(cwd, "wwwroot", relativePath);
var targetDirectory = Path.GetDirectoryName(targetPath);
if (targetDirectory != null)
Directory.CreateDirectory(targetDirectory);
File.Copy(sourcePath, targetPath, true);
}
Console.WriteLine($" {counter:###,###} web assets updated successfully.");
Console.WriteLine();
}
}
+7
View File
@@ -0,0 +1,7 @@
// Global using directives
global using System.Text;
global using Cocona;
global using Environment = Build.Tools.Environment;
+9
View File
@@ -0,0 +1,9 @@
using Build.Commands;
var builder = CoconaApp.CreateBuilder();
var app = builder.Build();
app.AddCommands<CheckRidsCommand>();
app.AddCommands<UpdateMetadataCommands>();
app.AddCommands<UpdateWebAssetsCommand>();
app.AddCommands<CollectI18NKeysCommand>();
app.Run();
+77
View File
@@ -0,0 +1,77 @@
using System.Runtime.InteropServices;
namespace Build.Tools;
public static class Environment
{
public const string DOTNET_VERSION = "net9.0";
public static readonly Encoding UTF8_NO_BOM = new UTF8Encoding(false);
private static readonly Dictionary<RID, string> ALL_RIDS = Enum.GetValues<RID>().Select(rid => new KeyValuePair<RID, string>(rid, rid.ToName())).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
public static bool IsWorkingDirectoryValid()
{
var currentDirectory = Directory.GetCurrentDirectory();
var mainFile = Path.Combine(currentDirectory, "Program.cs");
var projectFile = Path.Combine(currentDirectory, "Build Script.csproj");
if (!currentDirectory.EndsWith("Build", StringComparison.Ordinal) || !File.Exists(mainFile) || !File.Exists(projectFile))
{
Console.WriteLine("The current directory is not a valid working directory for the build script. Go to the /app/Build directory within the git repository.");
return false;
}
return true;
}
public static string GetAIStudioDirectory()
{
var currentDirectory = Directory.GetCurrentDirectory();
var directory = Path.Combine(currentDirectory, "..", "MindWork AI Studio");
return Path.GetFullPath(directory);
}
public static string GetRustRuntimeDirectory()
{
var currentDirectory = Directory.GetCurrentDirectory();
var directory = Path.Combine(currentDirectory, "..", "..", "runtime");
return Path.GetFullPath(directory);
}
public static string GetMetadataPath()
{
var currentDirectory = Directory.GetCurrentDirectory();
var directory = Path.Combine(currentDirectory, "..", "..", "metadata.txt");
return Path.GetFullPath(directory);
}
public static string? GetOS()
{
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return "windows";
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return "linux";
if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return "darwin";
Console.WriteLine($"Error: Unsupported OS '{RuntimeInformation.OSDescription}'");
return null;
}
public static IEnumerable<RID> GetRidsForCurrentOS()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return ALL_RIDS.Where(rid => rid.Value.StartsWith("win-", StringComparison.Ordinal)).Select(n => n.Key);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return ALL_RIDS.Where(rid => rid.Value.StartsWith("osx-", StringComparison.Ordinal)).Select(n => n.Key);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return ALL_RIDS.Where(rid => rid.Value.StartsWith("linux-", StringComparison.Ordinal)).Select(n => n.Key);
Console.WriteLine($"Error: Unsupported OS '{RuntimeInformation.OSDescription}'");
return [];
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace Build.Tools;
public enum RID
{
NONE,
WIN_X64,
WIN_ARM64,
LINUX_X64,
LINUX_ARM64,
OSX_X64,
OSX_ARM64,
}
+18
View File
@@ -0,0 +1,18 @@
namespace Build.Tools;
public static class RIDExtensions
{
public static string ToName(this RID rid) => rid switch
{
RID.WIN_X64 => "win-x64",
RID.WIN_ARM64 => "win-arm64",
RID.LINUX_X64 => "linux-x64",
RID.LINUX_ARM64 => "linux-arm64",
RID.OSX_X64 => "osx-x64",
RID.OSX_ARM64 => "osx-arm64",
_ => string.Empty,
};
}
+12
View File
@@ -4,6 +4,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindW
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build Script", "Build\Build Script.csproj", "{447A5590-68E1-4EF8-9451-A41AF5FBE571}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedTools", "SharedTools\SharedTools.csproj", "{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,6 +22,14 @@ Global
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.Build.0 = Release|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Debug|Any CPU.Build.0 = Debug|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.ActiveCfg = Release|Any CPU
{447A5590-68E1-4EF8-9451-A41AF5FBE571}.Release|Any CPU.Build.0 = Release|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
+2
View File
@@ -8,7 +8,9 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RAG/@EntryIndexedValue">RAG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RID/@EntryIndexedValue">RID</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=agentic/@EntryIndexedValue">True</s:Boolean>
@@ -3,6 +3,8 @@ using AIStudio.Tools.PluginSystem;
using Microsoft.AspNetCore.Components;
using SharedTools;
namespace AIStudio.Components;
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver, ILang
@@ -56,6 +56,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SharedTools\SharedTools.csproj" />
<ProjectReference Include="..\SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
-388
View File
@@ -1,388 +0,0 @@
#!/usr/bin/env nu
def main [] {}
def are_assets_exist [rid: string] {
$"bin/release/net9.0/($rid)/publish/wwwroot/_content/MudBlazor/MudBlazor.min.css" | path exists
}
def "main help" [] {
print "Usage: nu build.nu [action]"
print ""
print "Optional Actions:"
print "-----------------"
print " fix_web_assets Prepare the web assets; run this once for each release on one platform; changes will be committed."
print ""
print " metadata Update the metadata file; run this on every platform right before the release; changes will be"
print " committed once; there should be no differences between the platforms."
print ""
print "Actions:"
print "---------"
print " prepare [action] Prepare the project for a release; increases the version & build numbers, updates the build time,"
print " and runs fix_web_assets; run this once for each release on one platform; changes will be committed."
print " The action can be 'major', 'minor', or 'patch'. The version will be updated accordingly."
print ""
print " publish Publish the project for all supported RIDs; run this on every platform."
print ""
}
def "main prepare" [action: string] {
if (update_app_version $action) {
main fix_web_assets
inc_build_number
update_build_time
update_changelog
main metadata
}
}
def "main metadata" [] {
update_dotnet_version
update_rust_version
update_mudblazor_version
update_tauri_version
update_project_commit_hash
update_license_year "../../LICENSE.md"
update_license_year "Pages/About.razor.cs"
}
def "main fix_web_assets" [] {
# Get the matching RIDs for the current OS:
let rids = get_rids
# We chose the first RID to copy the assets from:
let rid = $rids.0
if (are_assets_exist $rid) == false {
print $"Web assets do not exist for ($rid). Please build the project first."
return
}
# Ensure, that the dist directory exists:
mkdir wwwroot/system
# Copy the web assets from the first RID to the source project:
let source_paths = glob --depth 99 bin/release/net9.0/($rid)/publish/wwwroot/_content/*
for source_path in $source_paths {
cp --recursive --force --update $source_path wwwroot/system/
}
}
def "main publish" [] {
main metadata
# Ensure, that the dist directory exists:
mkdir bin/dist
# Get the matching RIDs for the current OS:
let rids = get_rids
if ($rids | length) == 0 {
print "No RIDs to build for."
return
}
let current_os = get_os
let published_filename_dotnet = match $current_os {
"windows" => "mindworkAIStudio.exe",
_ => "mindworkAIStudio"
}
# Build for each RID:
for rid in $rids {
print "=============================="
print $"Start building for ($rid)..."
^dotnet publish --configuration release --runtime $rid --disable-build-servers --force
let final_filename = match $rid {
"win-x64" => "mindworkAIStudioServer-x86_64-pc-windows-msvc.exe",
"win-arm64" => "mindworkAIStudioServer-aarch64-pc-windows-msvc.exe",
"linux-x64" => "mindworkAIStudioServer-x86_64-unknown-linux-gnu",
"linux-arm64" => "mindworkAIStudioServer-aarch64-unknown-linux-gnu",
"osx-arm64" => "mindworkAIStudioServer-aarch64-apple-darwin",
"osx-x64" => "mindworkAIStudioServer-x86_64-apple-darwin",
_ => {
print $"Unsupported RID for final filename: ($rid)"
return
}
}
let published_path = $"bin/release/net9.0/($rid)/publish/($published_filename_dotnet)"
let final_path = $"bin/dist/($final_filename)"
if ($published_path | path exists) {
print $"Published file ($published_path) exists."
} else {
print $"Published file ($published_path) does not exist. Compiling might failed?"
return
}
print $"Moving ($published_path) to ($final_path)..."
mv --force $published_path $final_path
}
print "=============================="
print "Start building runtime..."
cd ../../runtime
try {
cargo tauri build --bundles none
};
cd "../app/MindWork AI Studio"
print "=============================="
print "Building done."
}
def get_rids [] {
# Define the list of RIDs to build for, cf. https://learn.microsoft.com/en-us/dotnet/core/rid-catalog:
let rids = ["win-x64", "win-arm64", "linux-x64", "linux-arm64", "osx-arm64", "osx-x64"]
# Get the current OS:
let current_os = get_os
let current_os_dotnet = match $current_os {
"windows" => "win-",
"linux" => "linux-",
"darwin" => "osx-",
_ => {
print $"Unsupported OS: ($current_os)"
return
}
}
# Filter the RIDs to build for the current OS:
let rids = $rids | where $it =~ $current_os_dotnet
# Return the list of RIDs to build for:
$rids
}
def get_os [] {
let os = (sys host).name | str downcase
if $os =~ "linux" {
return "linux"
}
$os
}
def update_build_time [] {
mut meta_lines = open --raw ../../metadata.txt | lines
mut build_time = $meta_lines.1
let updated_build_time = (date now | date to-timezone UTC | format date "%Y-%m-%d %H:%M:%S")
print $"Updated build time from ($build_time) to ($updated_build_time) UTC."
$build_time = $"($updated_build_time) UTC"
$meta_lines.1 = $build_time
$meta_lines | save --raw --force ../../metadata.txt
}
def inc_build_number [] {
mut meta_lines = open --raw ../../metadata.txt | lines
mut build_number = $meta_lines.2 | into int
let updated_build_number = ([$build_number, 1] | math sum)
print $"Incremented build number from ($build_number) to ($updated_build_number)."
$build_number = $updated_build_number
$meta_lines.2 = ($build_number | into string)
$meta_lines | save --raw --force ../../metadata.txt
}
def update_dotnet_version [] {
mut meta_lines = open --raw ../../metadata.txt | lines
mut dotnet_sdk_version = $meta_lines.3
mut dotnet_version = $meta_lines.4
let dotnet_data = (^dotnet --info) | collect | parse --regex '(?ms).?(NET\s+SDK|SDK\s+\.NET)\s*:\s+Version:\s+(?P<sdkVersion>[0-9.]+).+Commit:\s+(?P<sdkCommit>[a-zA-Z0-9]+).+Host:\s+Version:\s+(?P<hostVersion>[0-9.]+).+Commit:\s+(?P<hostCommit>[a-zA-Z0-9]+)'
let sdk_version = $dotnet_data.sdkVersion.0
let host_version = $dotnet_data.hostVersion.0
let sdkCommit = $dotnet_data.sdkCommit.0
let hostCommit = $dotnet_data.hostCommit.0
print $"Updated .NET SDK version from ($dotnet_sdk_version) to ($sdk_version) \(commit ($sdkCommit)\)."
$meta_lines.3 = $"($sdk_version) \(commit ($sdkCommit)\)"
print $"Updated .NET version from ($dotnet_version) to ($host_version) \(commit ($hostCommit)\)."
$meta_lines.4 = $"($host_version) \(commit ($hostCommit)\)"
$meta_lines | save --raw --force ../../metadata.txt
}
def update_rust_version [] {
mut meta_lines = open --raw ../../metadata.txt | lines
mut rust_version = $meta_lines.5
let rust_data = (^rustc -Vv) | parse --regex 'rustc (?<version>[0-9.]+)(?:-nightly)? \((?<commit>[a-zA-Z0-9]+)'
let version = $rust_data.version.0
let commit = $rust_data.commit.0
print $"Updated Rust version from ($rust_version) to ($version) \(commit ($commit)\)."
$meta_lines.5 = $"($version) \(commit ($commit)\)"
$meta_lines | save --raw --force ../../metadata.txt
}
def update_mudblazor_version [] {
mut meta_lines = open --raw ../../metadata.txt | lines
mut mudblazor_version = $meta_lines.6
let mudblazor_data = (^dotnet list package) | parse --regex 'MudBlazor\s+(?<version>[0-9.]+)'
let version = $mudblazor_data.version.0
print $"Updated MudBlazor version from ($mudblazor_version) to ($version)."
$meta_lines.6 = $version
$meta_lines | save --raw --force ../../metadata.txt
}
def update_tauri_version [] {
mut meta_lines = open --raw ../../metadata.txt | lines
mut tauri_version = $meta_lines.7
cd ../../runtime
let tauri_data = (^cargo tree --depth 1) | parse --regex 'tauri\s+v(?<version>[0-9.]+)'
let version = $tauri_data.version.0
cd "../app/MindWork AI Studio"
print $"Updated Tauri version from ($tauri_version) to ($version)."
$meta_lines.7 = $version
$meta_lines | save --raw --force ../../metadata.txt
}
def update_license_year [licence_file: string] {
let current_year = (date now | date to-timezone UTC | format date "%Y")
let license_text = open --raw $licence_file | lines
print $"Updating the license's year in ($licence_file) to ($current_year)."
# Target line looks like `Copyright 2024 Thorsten Sommer`.
# Perhaps, there are whitespaces at the beginning. Using
# a regex to match the year.
let updated_license_text = $license_text | each { |it|
if $it =~ '^\s*Copyright\s+[0-9]{4}' {
$it | str replace --regex '([0-9]{4})' $"($current_year)"
} else {
$it
}
}
$updated_license_text | save --raw --force $licence_file
}
def update_app_version [action: string] {
mut meta_lines = open --raw ../../metadata.txt | lines
mut app_version = $meta_lines.0
let version_data = $app_version | parse --regex '(?P<major>[0-9]+)\.(?P<minor>[0-9]+)\.(?P<patch>[0-9]+)'
if $action == "major" {
mut major = $version_data.major | into int
$major = ([$major.0, 1] | math sum)
let updated_version = [$major, 0, 0] | str join "."
print $"Updated app version from ($app_version) to ($updated_version)."
$meta_lines.0 = $updated_version
} else if $action == "minor" {
let major = $version_data.major | into int
mut minor = $version_data.minor | into int
$minor = ([$minor.0, 1] | math sum)
let updated_version = [$major.0, $minor, 0] | str join "."
print $"Updated app version from ($app_version) to ($updated_version)."
$meta_lines.0 = $updated_version
} else if $action == "patch" {
let major = $version_data.major | into int
let minor = $version_data.minor | into int
mut patch = $version_data.patch | into int
$patch = ([$patch.0, 1] | math sum)
let updated_version = [$major.0, $minor.0, $patch] | str join "."
print $"Updated app version from ($app_version) to ($updated_version)."
$meta_lines.0 = $updated_version
} else {
print $"Invalid action '($action)'. Please use 'major', 'minor', or 'patch'."
return false
}
$meta_lines | save --raw --force ../../metadata.txt
return true
}
def update_project_commit_hash [] {
mut meta_lines = open --raw ../../metadata.txt | lines
mut commit_hash = $meta_lines.8
# Check, if the work directory is clean. We allow, that the metadata file is dirty:
let git_status = (^git status --porcelain) | lines
let dirty_files = $git_status | length
let first_is_metadata = ($dirty_files > 0 and $git_status.0 =~ '^\sM\s+metadata.txt$')
let git_tag_response = ^git describe --tags --exact-match | complete
let state = {
num_dirty: $dirty_files,
first_is_metadata: $first_is_metadata,
git_tag_present: ($git_tag_response.exit_code == 0),
git_tag: $git_tag_response.stdout
}
let commit_postfix = match $state {
{ num_dirty: $num_dirty, first_is_metadata: _, git_tag_present: _, git_tag: _ } if $num_dirty > 1 => ", dev debug",
{ num_dirty: $num_dirty, first_is_metadata: false, git_tag_present: _, git_tag: _ } if $num_dirty == 1 => ", dev debug",
{ num_dirty: $num_dirty, first_is_metadata: true, git_tag_present: false, git_tag: _ } if $num_dirty == 1 => ", dev testing",
{ num_dirty: $num_dirty, first_is_metadata: false, git_tag_present: false, git_tag: _ } if $num_dirty == 0 => ", dev testing",
{ num_dirty: $num_dirty, first_is_metadata: true, git_tag_present: true, git_tag: $tag } if $num_dirty == 1 => $", release $tag",
{ num_dirty: $num_dirty, first_is_metadata: false, git_tag_present: true, git_tag: $tag } if $num_dirty == 0 => $", release $tag",
_ => "-dev unknown"
}
# Use the first ten characters of the commit hash:
let updated_commit_hash = (^git rev-parse HEAD) | str substring 0..10 | append $commit_postfix | str join
print $"Updated commit hash from ($commit_hash) to ($updated_commit_hash)."
$meta_lines.8 = $updated_commit_hash
$meta_lines | save --raw --force ../../metadata.txt
}
def update_changelog [] {
# Get all changelog files:
let all_changelog_files = glob wwwroot/changelog/*.md
# Create a table with the build numbers and the corresponding file names:
let table = $all_changelog_files | reduce --fold [] { |it, acc|
let header_line = open --raw $it | lines | first
let file_name = $it | path basename
let header_data = $header_line | parse --regex '#\s+(?P<header>(?P<version>v[0-9.]+)[,\s]+build\s+(?P<build_num>[0-9]+)[,\s]+\((?P<build_time>[0-9-?\s:UTC]+)\))'
$acc ++ [[build_num, file_name, header]; [$header_data.build_num.0, $file_name, $header_data.header.0]]
} | sort-by build_num --natural --reverse
# Now, we build the necessary C# code:
const tab = " "; # using 4 spaces as one tab
let code_rows = $table | reduce --fold "" { |it, acc|
$acc ++ $"($tab)($tab)new \(($it.build_num), \"($it.header)\", \"($it.file_name)\"\),\n"
}
let code = ($"LOGS = \n($tab)[\n($code_rows)\n($tab)];")
# Next, update the Changelog.Logs.cs file:
let changelog_logs_source_file = open --raw "Components/Changelog.Logs.cs"
let result = $changelog_logs_source_file | str replace --regex '(?ms)LOGS =\s+\[[\w\s".,-:()?]+\];' $code
# Save the updated file:
$result | save --raw --force "Components/Changelog.Logs.cs"
let number_change_logs = $table | length
print $"Updated Changelog.Logs.cs with ($number_change_logs) change logs."
}
@@ -205,6 +205,9 @@
"type": "Transitive",
"resolved": "0.16.9",
"contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ=="
},
"sharedtools": {
"type": "Project"
}
},
"net9.0/osx-arm64": {}
@@ -1,5 +1,5 @@
// ReSharper disable MemberCanBePrivate.Global
namespace AIStudio.Tools;
namespace SharedTools;
/// <summary>
/// Implements the Fowler–Noll–Vo hash function for 32-bit and 64-bit hashes.
+10
View File
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>