diff --git a/app/Build/Build Script.csproj b/app/Build/Build Script.csproj new file mode 100644 index 00000000..5694b509 --- /dev/null +++ b/app/Build/Build Script.csproj @@ -0,0 +1,21 @@ + + + + Exe + net9.0 + Build + latest + enable + enable + build + + + + + + + + + + + diff --git a/app/Build/Commands/CheckRidsCommand.cs b/app/Build/Commands/CheckRidsCommand.cs new file mode 100644 index 00000000..62bf3662 --- /dev/null +++ b/app/Build/Commands/CheckRidsCommand.cs @@ -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}"); + } + } +} \ No newline at end of file diff --git a/app/Build/Commands/CollectI18NKeysCommand.cs b/app/Build/Commands/CollectI18NKeysCommand.cs new file mode 100644 index 00000000..8b2554bf --- /dev/null +++ b/app/Build/Commands/CollectI18NKeysCommand.cs @@ -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 FindAllTextTags(ReadOnlySpan fileContent) + { + const string START_TAG = """ + T(" + """; + + const string END_TAG = """ + ") + """; + + var matches = new List(); + 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(); +} \ No newline at end of file diff --git a/app/Build/Commands/PrepareAction.cs b/app/Build/Commands/PrepareAction.cs new file mode 100644 index 00000000..2f2ffcb2 --- /dev/null +++ b/app/Build/Commands/PrepareAction.cs @@ -0,0 +1,10 @@ +namespace Build.Commands; + +public enum PrepareAction +{ + NONE, + + PATCH, + MINOR, + MAJOR, +} \ No newline at end of file diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs new file mode 100644 index 00000000..d7055d4a --- /dev/null +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -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 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(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> 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 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 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 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+(?[0-9.]+).+Commit:\s+(?[a-zA-Z0-9]+).+Host:\s+Version:\s+(?[0-9.]+).+Commit:\s+(?[a-zA-Z0-9]+)""")] + private static partial Regex DotnetVersionRegex(); + + [GeneratedRegex("""rustc (?[0-9.]+)(?:-nightly)? \((?[a-zA-Z0-9]+)""")] + private static partial Regex RustVersionRegex(); + + [GeneratedRegex("""MudBlazor\s+(?[0-9.]+)""")] + private static partial Regex MudBlazorVersionRegex(); + + [GeneratedRegex("""tauri\s+v(?[0-9.]+)""")] + private static partial Regex TauriVersionRegex(); + + [GeneratedRegex("""^\s*Copyright\s+(?[0-9]{4})""")] + private static partial Regex FindCopyrightRegex(); + + [GeneratedRegex("""([0-9]{4})""")] + private static partial Regex ReplaceCopyrightYearRegex(); + + [GeneratedRegex("""(?[0-9]+)\.(?[0-9]+)\.(?[0-9]+)""")] + private static partial Regex AppVersionRegex(); +} \ No newline at end of file diff --git a/app/Build/Commands/UpdateWebAssetsCommand.cs b/app/Build/Commands/UpdateWebAssetsCommand.cs new file mode 100644 index 00000000..33dc9b04 --- /dev/null +++ b/app/Build/Commands/UpdateWebAssetsCommand.cs @@ -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(); + } +} \ No newline at end of file diff --git a/app/Build/GlobalUsings.cs b/app/Build/GlobalUsings.cs new file mode 100644 index 00000000..e15bc8b9 --- /dev/null +++ b/app/Build/GlobalUsings.cs @@ -0,0 +1,7 @@ +// Global using directives + +global using System.Text; + +global using Cocona; + +global using Environment = Build.Tools.Environment; \ No newline at end of file diff --git a/app/Build/Program.cs b/app/Build/Program.cs new file mode 100644 index 00000000..e7744b36 --- /dev/null +++ b/app/Build/Program.cs @@ -0,0 +1,9 @@ +using Build.Commands; + +var builder = CoconaApp.CreateBuilder(); +var app = builder.Build(); +app.AddCommands(); +app.AddCommands(); +app.AddCommands(); +app.AddCommands(); +app.Run(); \ No newline at end of file diff --git a/app/Build/Tools/Environment.cs b/app/Build/Tools/Environment.cs new file mode 100644 index 00000000..9776b633 --- /dev/null +++ b/app/Build/Tools/Environment.cs @@ -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 ALL_RIDS = Enum.GetValues().Select(rid => new KeyValuePair(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 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 []; + } +} \ No newline at end of file diff --git a/app/Build/Tools/RID.cs b/app/Build/Tools/RID.cs new file mode 100644 index 00000000..73ca86ee --- /dev/null +++ b/app/Build/Tools/RID.cs @@ -0,0 +1,15 @@ +namespace Build.Tools; + +public enum RID +{ + NONE, + + WIN_X64, + WIN_ARM64, + + LINUX_X64, + LINUX_ARM64, + + OSX_X64, + OSX_ARM64, +} \ No newline at end of file diff --git a/app/Build/Tools/RIDExtensions.cs b/app/Build/Tools/RIDExtensions.cs new file mode 100644 index 00000000..60fe4711 --- /dev/null +++ b/app/Build/Tools/RIDExtensions.cs @@ -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, + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio.sln b/app/MindWork AI Studio.sln index 37871ac7..0bb1ab52 100644 --- a/app/MindWork AI Studio.sln +++ b/app/MindWork AI Studio.sln @@ -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 diff --git a/app/MindWork AI Studio.sln.DotSettings b/app/MindWork AI Studio.sln.DotSettings index 4a94c5cc..a626186b 100644 --- a/app/MindWork AI Studio.sln.DotSettings +++ b/app/MindWork AI Studio.sln.DotSettings @@ -8,7 +8,9 @@ LLM LM MSG + OS RAG + RID UI URL True diff --git a/app/MindWork AI Studio/Components/MSGComponentBase.cs b/app/MindWork AI Studio/Components/MSGComponentBase.cs index 4e904c02..a70a5257 100644 --- a/app/MindWork AI Studio/Components/MSGComponentBase.cs +++ b/app/MindWork AI Studio/Components/MSGComponentBase.cs @@ -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 diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index a4fae3cb..f70535ce 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -56,6 +56,7 @@ + diff --git a/app/MindWork AI Studio/build.nu b/app/MindWork AI Studio/build.nu deleted file mode 100755 index 3c9c2e4e..00000000 --- a/app/MindWork AI Studio/build.nu +++ /dev/null @@ -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[0-9.]+).+Commit:\s+(?P[a-zA-Z0-9]+).+Host:\s+Version:\s+(?P[0-9.]+).+Commit:\s+(?P[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 (?[0-9.]+)(?:-nightly)? \((?[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+(?[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(?[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[0-9]+)\.(?P[0-9]+)\.(?P[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
(?Pv[0-9.]+)[,\s]+build\s+(?P[0-9]+)[,\s]+\((?P[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." -} \ No newline at end of file diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 7c4e67da..b35b2e3d 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -205,6 +205,9 @@ "type": "Transitive", "resolved": "0.16.9", "contentHash": "7WaVMHklpT3Ye2ragqRIwlFRsb6kOk63BOGADV0fan3ulVfGLUYkDi5yNUsZS/7FVNkWbtHAlDLmu4WnHGfqvQ==" + }, + "sharedtools": { + "type": "Project" } }, "net9.0/osx-arm64": {} diff --git a/app/MindWork AI Studio/Tools/FNVHash.cs b/app/SharedTools/FNVHash.cs similarity index 98% rename from app/MindWork AI Studio/Tools/FNVHash.cs rename to app/SharedTools/FNVHash.cs index cc47645e..a60beeff 100644 --- a/app/MindWork AI Studio/Tools/FNVHash.cs +++ b/app/SharedTools/FNVHash.cs @@ -1,5 +1,5 @@ // ReSharper disable MemberCanBePrivate.Global -namespace AIStudio.Tools; +namespace SharedTools; /// /// Implements the Fowler–Noll–Vo hash function for 32-bit and 64-bit hashes. diff --git a/app/SharedTools/SharedTools.csproj b/app/SharedTools/SharedTools.csproj new file mode 100644 index 00000000..e0439ac8 --- /dev/null +++ b/app/SharedTools/SharedTools.csproj @@ -0,0 +1,10 @@ + + + + net9.0 + latest + enable + enable + + + diff --git a/documentation/Build.md b/documentation/Build.md index b25a27ef..41b874d9 100644 --- a/documentation/Build.md +++ b/documentation/Build.md @@ -5,43 +5,46 @@ You just want to use the app? Then simply [download the appropriate setup for yo 1. Install the [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). 2. [Install the Rust compiler](https://www.rust-lang.org/tools/install) in the latest version. 3. Met the prerequisites for building [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/). Node.js is **not** required, though. -4. Install the Tauri CLI by running `cargo install --version 1.6.2 tauri-cli`. -5. [Install NuShell](https://www.nushell.sh/). NuShell works on all operating systems and is required because the build script is written in NuShell. -6. Clone the repository. +4. Clone the repository. ## One-time mandatory steps Regardless of whether you want to build the app locally for yourself (not trusting the pre-built binaries) or test your changes before creating a PR, you have to run the following commands at least once: -1. Open a terminal using NuShell. -2. Navigate to the `/app/MindWork AI Studio` directory within the repository. -3. Run `dotnet restore` to bring up the .NET dependencies. -4. Run `nu build.nu publish` to build the entire app. +1. Open a terminal. +2. Install the Tauri CLI by running `cargo install --version 1.6.2 tauri-cli`. +3. Navigate to the `/app/Build` directory within the repository. +4. Run `dotnet run build` to build the entire app. This is necessary because the build script and the Tauri framework assume that the .NET app is available as a so-called "sidecar." Although the sidecar is only necessary for the final release and shipping, Tauri requires it to be present during development as well. ## Build AI Studio from source In order to build MindWork AI Studio from source instead of using the pre-built binaries, follow these steps: 1. Ensure you have met all the prerequisites. -2. Open a terminal with NuShell. -3. Navigate to the `/app/MindWork AI Studio` directory within the repository. -4. To build the current version, run `nu build.nu publish` to build the entire app. +2. Open a terminal. +3. Navigate to the `/app/Build` directory within the repository. +4. To build the current version, run `dotnet run build` to build the entire app. - This will build the app for the current operating system, for both x64 (Intel, AMD) and ARM64 (e.g., Apple Silicon, Raspberry Pi). - - The final setup program will be located in `runtime/target/release/bundle` afterward. -5. In order to create a new release: - 1. Before finishing the PR, make sure to create a changelog file in the `/app/MindWork AI Studio/wwwroot/changelog` directory. The file should be named `vX.Y.Z.md` and contain the changes made in the release (your changes and any other changes that are part of the release). - 2. To prepare a new release, run `nu build.nu prepare `, where `` is either `patch`, `minor`, or `major`. - 3. The actual release will be built by our GitHub Workflow. For this to work, you need to create a PR with your changes. - 4. Your proposed changes will be reviewed and merged. - 5. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`. - 6. The GitHub Workflow will then build the release and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). - 7. Building the release including virus scanning takes some time. Please be patient. + - The final setup program will be located in `runtime/target/release` afterward. ## Run the app locally with all your changes Do you want to test your changes before creating a PR? Follow these steps: 1. Ensure you have met all the prerequisites. -2. At least once, you have to run the `nu build.nu publish` command (see above, "Build instructions"). This is necessary because the Tauri framework checks whether the .NET app as so-called "sidecar" is available. Although the sidecar is only necessary for the final release and shipping, Tauri requires it to be present during development. -3. Open a terminal (in this case, it doesn't have to be NuShell). +2. At least once, you have to run the `dotnet run build` command (see above, "Build instructions"). This is necessary because the Tauri framework checks whether the .NET app as so-called "sidecar" is available. Although the sidecar is only necessary for the final release and shipping, Tauri requires it to be present during development. +3. Open a terminal. 4. Navigate to the `runtime` directory within the repository, e.g. `cd repos/mindwork-ai-studio/runtime`. 5. Run `cargo tauri dev --no-watch`. -Cargo will compile the Rust code and start the runtime. The runtime will then start the .NET compiler. When the .NET source code is compiled, the app will start. You can now test your changes. \ No newline at end of file +Cargo will compile the Rust code and start the runtime. The runtime will then start the .NET compiler. When the .NET source code is compiled, the app will start. You can now test your changes. + +## Create a release +In order to create a release: +1. To create a new release, you need to be a maintainer of the repository—see step 8. +2. Make sure there's a changelog file for the version you want to create in the `/app/MindWork AI Studio/wwwroot/changelog` directory. Name the file `vX.Y.Z.md` and include all release changes—your updates and any others included in this version. +3. After you have created the changelog file, you must commit the changes to the repository. +4. To prepare a new release, open a terminal, go to `/app/Build` and run `dotnet run release --action `, where `` is either `patch` (creating a patch version), `minor` (creating a minor version), or `major` (creating a major version). +5. Now wait until all process steps have been completed. Among other things, the version number will be incremented, the new changelog registered, and the version numbers of central dependencies updated, etc. +6. The actual release will be built by our GitHub Workflow. For this to work, you need to create a PR with your changes. +7. Your proposed changes will be reviewed and merged. +8. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`. +9. The GitHub Workflow will then build the release and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). +10. Building the release including virus scanning takes some time. Please be patient. \ No newline at end of file