From 7f1482c5d77b1b5f7e369ade925d2351d7623fa1 Mon Sep 17 00:00:00 2001 From: vnugent Date: Fri, 6 Sep 2024 20:50:06 -0400 Subject: cleanup and add fluent-ftp as an publishing location --- Taskfile.yaml | 2 +- src/BuildFailedException.cs | 16 ++ src/BuildPipeline.cs | 3 +- src/BuildPublisher.cs | 430 ---------------------------------- src/BuildStepFailedException.cs | 20 +- src/Commands/BaseCommand.cs | 14 +- src/Commands/PublishCommand.cs | 97 ++++++-- src/Constants/Utils.cs | 7 +- src/Extensions/BuildExtensions.cs | 2 +- src/Extensions/ProjectExtensions.cs | 16 +- src/GpgSigner.cs | 52 ----- src/MinioUploadManager.cs | 60 ----- src/Model/IUploadManager.cs | 6 - src/Publishing/BuildPublisher.cs | 434 +++++++++++++++++++++++++++++++++++ src/Publishing/FtpUploadManager.cs | 98 ++++++++ src/Publishing/GpgSigner.cs | 54 +++++ src/Publishing/MinioUploadManager.cs | 43 ++++ src/Publishing/SleetFeedManager.cs | 52 +++++ src/SleetFeedManager.cs | 53 ----- src/TaskFile.cs | 18 +- src/vnbuild.csproj | 11 +- 21 files changed, 831 insertions(+), 657 deletions(-) create mode 100644 src/BuildFailedException.cs delete mode 100644 src/BuildPublisher.cs delete mode 100644 src/GpgSigner.cs delete mode 100644 src/MinioUploadManager.cs create mode 100644 src/Publishing/BuildPublisher.cs create mode 100644 src/Publishing/FtpUploadManager.cs create mode 100644 src/Publishing/GpgSigner.cs create mode 100644 src/Publishing/MinioUploadManager.cs create mode 100644 src/Publishing/SleetFeedManager.cs delete mode 100644 src/SleetFeedManager.cs diff --git a/Taskfile.yaml b/Taskfile.yaml index 0e42d83..9333200 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -17,7 +17,7 @@ tasks: - powershell -Command "Get-ChildItem -Include *.cs,*.csproj -Recurse | Where { \$_.FullName -notlike '*\obj\*' } | Resolve-Path -Relative | tar --files-from - -czf '{{.TARGET}}/src.tgz'" #run post in debug mode - - for: [ win-x64, linux-x64, osx-x64, linux-arm64, linux-arm ] + - for: [ win-x64, linux-x64, osx-x64, linux-arm64, linux-arm ] task: postbuild vars: { BUILD_MODE: debug, TARGET_OS: '{{ .ITEM }}'} diff --git a/src/BuildFailedException.cs b/src/BuildFailedException.cs new file mode 100644 index 0000000..7f8f695 --- /dev/null +++ b/src/BuildFailedException.cs @@ -0,0 +1,16 @@ +using System; + +namespace VNLib.Tools.Build.Executor +{ + internal class BuildFailedException : Exception + { + public BuildFailedException() + { } + + public BuildFailedException(string? message) : base(message) + { } + + public BuildFailedException(string? message, Exception? innerException) : base(message, innerException) + { } + } +} \ No newline at end of file diff --git a/src/BuildPipeline.cs b/src/BuildPipeline.cs index 2435566..70f6fd8 100644 --- a/src/BuildPipeline.cs +++ b/src/BuildPipeline.cs @@ -11,9 +11,10 @@ using VNLib.Tools.Build.Executor.Model; using VNLib.Tools.Build.Executor.Modules; using VNLib.Tools.Build.Executor.Extensions; using VNLib.Tools.Build.Executor.Constants; +using VNLib.Tools.Build.Executor.Publishing; namespace VNLib.Tools.Build.Executor -{ +{ public sealed class BuildPipeline(Logger Log) : IDisposable { diff --git a/src/BuildPublisher.cs b/src/BuildPublisher.cs deleted file mode 100644 index a2dc2bf..0000000 --- a/src/BuildPublisher.cs +++ /dev/null @@ -1,430 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using System.Collections.Generic; - -using LibGit2Sharp; - -using Semver; - -using VNLib.Tools.Build.Executor.Constants; -using VNLib.Tools.Build.Executor.Model; -using VNLib.Tools.Build.Executor.Extensions; -using VNLib.Tools.Build.Executor.Projects; -using static VNLib.Tools.Build.Executor.Constants.Config; - -namespace VNLib.Tools.Build.Executor -{ - - public sealed class BuildPublisher(BuildConfig config, GpgSigner signer) - { - public bool SignEnabled => signer.IsEnabled; - - /// - /// Prepares the module output and its collection of file details for publishing - /// then runs the upload step - /// - /// - /// A task that completes when the module's output has been created - public async Task PrepareModuleOutput(IModuleData module) - { - //Copy project artifacts to output directory - await CopyProjectOutputToModuleOutputAsync(module); - - //Copy source archive - string? archiveFile = await CopySourceArchiveToOutput(module, module.FileManager); - - Log.Information("Building module {mod} catalog and git history", module.ModuleName); - - //Build module catalog - await BuildModuleCatalogAsync(module, config.SemverStyle, archiveFile); - - Log.Information("Building module {mod} git history", module.ModuleName); - - //Build git history - await BuildModuleGitHistoryAsync(module); - - Log.Information("Building module {mod} version history", module.ModuleName); - - //build version history - await BuildModuleVersionHistory(module); - - Log.Information("Moving module {mod} artifacts to the output", module.ModuleName); - } - - /// - /// Uploads the modules output to the remote - /// - /// The module containing the information to upload - /// - public Task UploadModuleOutput(IUploadManager Uploader, IModuleData module) - { - Log.Information("Uploading module {mod}", module.ModuleName); - - //Upload the entire output directory - return Uploader.UploadDirectoryAsync(module.FileManager.OutputDir); - } - - /* - * Builds the project catalog file and publishes it to the module file manager - */ - private async Task BuildModuleCatalogAsync(IModuleData mod, SemVersionStyles style, string? archiveFile) - { - /* - * Builds the index.json file for the module. It - * contains an array of projects and their metadata - */ - - string moduleVersion = mod.GetModuleCiVersion(config.DefaultCiVersion, style); - - using MemoryStream ms = new(); - - using (Utf8JsonWriter writer = new(ms)) - { - //Open initial object - writer.WriteStartObject(); - - InitModuleFile(writer, mod); - - //Add the archive path if it was created in the module - if (archiveFile != null) - { - writer.WriteStartObject("archive"); - - //Archive path is in the build directory - writer.WriteString("path", config.SourceArchiveName); - - //Get the checksum of the archive - string checksum = await new FileInfo(archiveFile).ComputeFileHashStringAsync(); - writer.WriteString(config.HashFuncName, checksum); - writer.WriteString("sha_file", $"{config.SourceArchiveName}.{config.HashFuncName}"); - - //If signing is enabled, add the signature file (it is constant) - if (SignEnabled) - { - writer.WriteString("signature", $"{config.SourceArchiveName}.sig"); - } - - writer.WriteEndObject(); - } - - //Build project array - writer.WriteStartArray("projects"); - - foreach (IProject project in mod.Projects) - { - //start object for each project - writer.WriteStartObject(); - - //Write the project info - await WriteProjectInfoAsync(mod.FileManager, project, mod.Repository.Head.Tip.Sha, moduleVersion, writer); - - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - - //Close object - writer.WriteEndObject(); - - writer.Flush(); - } - - ms.Seek(0, SeekOrigin.Begin); - - await mod.FileManager.WriteFileAsync(ModuleFileType.Catalog, ms.ToArray()); - } - - private static async Task BuildModuleVersionHistory(IModuleData mod) - { - /* - * Builds the index.json file for the module. It - * contains an array of projects and their metadata - */ - - using MemoryStream ms = new(); - - using (Utf8JsonWriter writer = new(ms)) - { - //Open initial object - writer.WriteStartObject(); - - InitModuleFile(writer, mod); - - //Set the head pointer to the latest commit we build - writer.WriteString("head", mod.Repository.Head.Tip.Sha); - - //Build project array - writer.WriteStartArray("versions"); - - //Write all git hashes from head back to the first commit - foreach(Commit commit in mod.Repository.Commits) - { - writer.WriteStringValue(commit.Sha); - } - - writer.WriteEndArray(); - - //Releases will be an array of objects containing the tag and the hash - writer.WriteStartArray("releases"); - - //Write all git tags - foreach(Tag tag in mod.Repository.Tags.OrderByDescending(static p => p.FriendlyName)) - { - writer.WriteStartObject(); - - writer.WriteString("tag", tag.FriendlyName); - writer.WriteString("hash", tag.Target.Sha); - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - - //Close object - writer.WriteEndObject(); - - writer.Flush(); - } - - ms.Seek(0, SeekOrigin.Begin); - - await mod.FileManager.WriteFileAsync(ModuleFileType.VersionHistory, ms.ToArray()); - } - - - /* - * Builds the project's git history file and publishes it to the module file manager - * - * Also updates the latest hash file - */ - private static async Task BuildModuleGitHistoryAsync(IModuleData mod) - { - using MemoryStream ms = new(); - - using (Utf8JsonWriter writer = new(ms)) - { - //Open initial object - writer.WriteStartObject(); - - InitModuleFile(writer, mod); - - //Write the head commit - writer.WriteStartObject("head"); - writer.WriteString("branch", mod.Repository.Head.FriendlyName); - WriteSingleCommit(writer, mod.Repository.Head.Tip); - writer.WriteEndObject(); - - //Write commit history - WriteCommitHistory(writer, mod.Repository); - - //Close object - writer.WriteEndObject(); - - writer.Flush(); - } - - ms.Seek(0, SeekOrigin.Begin); - - await mod.FileManager.WriteFileAsync(ModuleFileType.GitHistory, ms.ToArray()); - - await mod.FileManager.WriteFileAsync(ModuleFileType.LatestHash, Encoding.UTF8.GetBytes(mod.Repository.Head.Tip.Sha)); - } - - /* - * Captures all of the project artiacts and copies them to the module output directory - */ - private async Task CopyProjectOutputToModuleOutputAsync(IModuleData mod) - { - //Copy build artifacts to module output directory - await mod.Projects.RunAllAsync(project => - { - //Get all output files from the project build, and copy them to the module output directory - return project.GetProjectBuildFiles(config) - .RunAllAsync(artifact => mod.FileManager.CopyArtifactToOutputAsync(project, artifact)); - }); - - /* - * If signing is enabled, we can sign all project files synchronously - */ - if (SignEnabled) - { - Log.Information("GPG Siginig is enabled, signing all artifacts for module {mod}", mod.ModuleName); - - /* - * Get all of the artifacts from the module's projects that match the target output - * file type, and sign them - */ - IEnumerable artifacts = mod.Projects.SelectMany( - p => mod.FileManager.GetArtifactOutputDir(p) - .EnumerateFiles(config.OutputFileType, SearchOption.TopDirectoryOnly) - ); - - //Sign synchronously - foreach(FileInfo artifact in artifacts) - { - await signer.SignFileAsync(artifact); - } - } - } - - - private static void InitModuleFile(Utf8JsonWriter writer, IModuleData mod) - { - //Set object name - writer.WriteString("module_name", mod.ModuleName); - //Modified date - writer.WriteString("modifed_date", DateTime.UtcNow); - } - - private static void WriteCommitHistory(Utf8JsonWriter writer, Repository repo) - { - writer.WriteStartArray("commits"); - - //Write commit history for current repo - foreach (Commit commit in repo.Head.Commits) - { - writer.WriteStartObject(); - - WriteSingleCommit(writer, commit); - - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - - //Write tag history for current repo - writer.WriteStartArray("tags"); - - foreach (Tag tag in repo.Tags) - { - //clamp message length and ellipsis if too long - string? message = tag.Annotation?.Message; - if (message != null && message.Length > 120) - { - message = $"{message[..120]}..."; - } - - writer.WriteStartObject(); - writer.WriteString("name", tag.FriendlyName); - writer.WriteString("sha", tag.Target.Sha); - writer.WriteString("message", message); - writer.WriteString("author", tag.Annotation?.Tagger.Name); - writer.WriteString("date", (tag.Annotation?.Tagger.When ?? default)); - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - } - - private static void WriteSingleCommit(Utf8JsonWriter writer, Commit commit) - { - writer.WriteString("sha", commit.Sha); - writer.WriteString("message", commit.Message); - writer.WriteString("author", commit.Author.Name); - writer.WriteString("commiter", commit.Committer.Name); - writer.WriteString("date", commit.Committer.When); - writer.WriteString("message_short", commit.MessageShort); - } - - /// - /// Builds and writes the projects information to the - /// - /// - /// - /// The to write the project - /// information to - /// - /// A task that completes when the write operation has completed - private async Task WriteProjectInfoAsync(IModuleFileManager man, IProject project, string latestSha, string version, Utf8JsonWriter writer) - { - //Reload the project dom after execute because semversion may be updated after build step - if (project is ModuleProject mp) - { - await mp.LoadProjectDom(); - } - - writer.WriteString("name", project.ProjectName); - writer.WriteString("repo_url", project.ProjectData.RepoUrl); - writer.WriteString("description", project.ProjectData.Description); - writer.WriteString("version", version); - writer.WriteString("copyright", project.ProjectData.Copyright); - writer.WriteString("author", project.ProjectData.Authors); - writer.WriteString("product", project.ProjectData.Product); - writer.WriteString("company", project.ProjectData.CompanyName); - writer.WriteString("commit", latestSha); - //Write target framework if it exsits - writer.WriteString("target_framework", project.ProjectData["TargetFramework"]); - - //Start file array - writer.WriteStartArray("files"); - - //Get only tar files, do not include the sha files - foreach (FileInfo output in GetProjOutputFiles(man, project)) - { - //beging file object - writer.WriteStartObject(); - - writer.WriteString("name", output.Name); - writer.WriteString("path", $"{project.GetSafeProjectName()}/{output.Name}"); - writer.WriteString("date", output.LastWriteTimeUtc); - writer.WriteNumber("size", output.Length); - - //Compute the file hash - string hashHex = await output.ComputeFileHashStringAsync(); - writer.WriteString(config.HashFuncName, hashHex); - - //Path to sha-file - writer.WriteString("sha_file", $"{project.GetSafeProjectName()}/{output.Name}.{config.HashFuncName}"); - - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - } - - private IEnumerable GetProjOutputFiles(IModuleFileManager man, IProject project) - { - return man.GetArtifactOutputDir(project).EnumerateFiles("*.*", SearchOption.TopDirectoryOnly) - .Where(p => p.Extension != $".{config.HashFuncName}"); - } - - /// - /// Copies the source git archive tgz file to the output directory - /// - /// - /// - /// - private async Task CopySourceArchiveToOutput(IModuleData mod, IModuleFileManager man) - { - //Try to get a source archive in the module directory - string? archiveFile = Directory.EnumerateFiles(mod.Repository.Info.WorkingDirectory, config.SourceArchiveName, SearchOption.TopDirectoryOnly).FirstOrDefault(); - - //If archive is null ignore and continue - if(string.IsNullOrWhiteSpace(archiveFile)) - { - Log.Information("No archive file found for module {mod}", mod.ModuleName); - return null; - } - - Log.Information("Found source archive for module {mod}, copying to output...", mod.ModuleName); - - //Otherwise copy to output - byte[] archive = await File.ReadAllBytesAsync(archiveFile); - FileInfo output = await man.WriteFileAsync(ModuleFileType.Archive, archive); - - //Compute the hash of the file - await output.ComputeFileHashAsync(config.HashFuncName); - - if (SignEnabled) - { - //Sign the file if signing is enabled - await signer.SignFileAsync(output); - } - - return archiveFile; - } - } -} \ No newline at end of file diff --git a/src/BuildStepFailedException.cs b/src/BuildStepFailedException.cs index 0b78bf3..3ba5633 100644 --- a/src/BuildStepFailedException.cs +++ b/src/BuildStepFailedException.cs @@ -2,7 +2,7 @@ namespace VNLib.Tools.Build.Executor { - sealed class BuildStepFailedException : Exception + internal sealed class BuildStepFailedException : BuildFailedException { public string? ArtifactName { get; set; } @@ -13,17 +13,17 @@ namespace VNLib.Tools.Build.Executor { } - public BuildStepFailedException(string? message, Exception? innerException) : base(message, innerException) + public BuildStepFailedException(string? message, Exception? innerException) + : base(message, innerException) { } - public BuildStepFailedException(string? message, Exception? innerException, string name) : base(message, innerException) - { - ArtifactName = name; - } + public BuildStepFailedException(string? message, Exception? innerException, string name) + : base(message, innerException) + => ArtifactName = name; - public BuildStepFailedException(string message, string artifactName):base(message) - { - this.ArtifactName = artifactName; - } + public BuildStepFailedException(string message, string artifactName) : base(message) + => ArtifactName = artifactName; + + public override string Message => $"in: {ArtifactName} msg -> {base.Message}"; } } \ No newline at end of file diff --git a/src/Commands/BaseCommand.cs b/src/Commands/BaseCommand.cs index 6362c22..25dcb0a 100644 --- a/src/Commands/BaseCommand.cs +++ b/src/Commands/BaseCommand.cs @@ -21,7 +21,7 @@ namespace VNLib.Tools.Build.Executor.Commands [CommandOption("force", 'f', Description = "Forces the operation even if steps are required")] public bool Force { get; set; } - [CommandOption("modules", 'm', Description = "Only use the specified modules, comma separated list")] + [CommandOption("include", 'i', Description = "Only use the specified modules, comma separated list")] public string? Modules { get; set; } [CommandOption("exclude", 'x', Description = "Ignores the specified modules, comma separated list")] @@ -79,11 +79,17 @@ namespace VNLib.Tools.Build.Executor.Commands } catch (OperationCanceledException) { - console.WithForegroundColor(ConsoleColor.Red, static o => o.Output.WriteLine("Operation cancelled")); + console.WithForegroundColor( + ConsoleColor.Red, + static o => o.Error.WriteLine("Operation cancelled") + ); } - catch(BuildStepFailedException be) + catch(BuildFailedException be) { - console.WithForegroundColor(ConsoleColor.Red, o => o.Output.WriteLine("FATAL: Build step failed on module {0}, msg -> {1}", be.ArtifactName, be.Message)); + console.WithForegroundColor( + ConsoleColor.Red, + o => o.Error.WriteLine("FATAL: Build step failed {0}", be.Message) + ); } } diff --git a/src/Commands/PublishCommand.cs b/src/Commands/PublishCommand.cs index 4179f86..33a7329 100644 --- a/src/Commands/PublishCommand.cs +++ b/src/Commands/PublishCommand.cs @@ -2,20 +2,25 @@ using System; using System.Linq; using System.Threading.Tasks; +using System.Collections.Generic; using Typin.Console; using Typin.Attributes; using VNLib.Tools.Build.Executor.Model; using VNLib.Tools.Build.Executor.Constants; +using VNLib.Tools.Build.Executor.Publishing; namespace VNLib.Tools.Build.Executor.Commands { [Command("publish", Description = "Runs publishig build steps on a completed build")] public sealed class PublishCommand(BuildPipeline pipeline, ConfigManager bm) : BaseCommand(pipeline, bm) { - [CommandOption("upload-path", 'p', Description = "The path to upload the build artifacts")] - public string? UploadPath { get; set; } + [CommandOption("minio", Description = "The path to upload the build artifacts")] + public string? MinioPath { get; set; } + + [CommandOption("ftp", Description = "The FTP server address to upload the build artifacts. Enables FTP mode over s3")] + public string? FtpServerAddress { get; set; } [CommandOption("sign", 's', Description = "Enables gpg signing of build artifacts")] public bool Sign { get; set; } = false; @@ -35,46 +40,102 @@ namespace VNLib.Tools.Build.Executor.Commands public override async ValueTask ExecStepsAsync(IConsole console) { //Specify custom output dir - (base.Config.Index as Dirs)!.OutputDir = BuildDirs.GetOrCreateDir(Constants.Config.OUTPUT_DIR, CustomOutDir); + (Config.Index as Dirs)!.OutputDir = BuildDirs.GetOrCreateDir(Constants.Config.OUTPUT_DIR, CustomOutDir); - IUploadManager? uploads = MinioUploadManager.Create(UploadPath); + IUploadManager uploads = GetUploadManager(console); IFeedManager? feed = Feeds.FirstOrDefault(); //Optional gpg signer for signing published artifacts BuildPublisher pub = new(Config, new GpgSigner(Sign, GpgKey)); - console.WithForegroundColor(ConsoleColor.DarkGreen, static o => o.Output.WriteLine("Publishing modules")); + console.WithForegroundColor( + ConsoleColor.DarkGreen, + static o => o.Output.WriteLine("Publishing modules") + ); //Run publish steps - await pipeline.OnPublishingAsync().ConfigureAwait(false); + await pipeline.OnPublishingAsync() + .ConfigureAwait(false); - console.WithForegroundColor(ConsoleColor.DarkGreen, static o => o.Output.WriteLine("Preparing module output for upload")); + console.WithForegroundColor( + ConsoleColor.DarkGreen, + static o => o.Output.WriteLine("Preparing module output for upload") + ); //Prepare the output - await pipeline.PrepareOutputAsync(pub).ConfigureAwait(false); - - if(uploads is null) - { - console.WithForegroundColor(ConsoleColor.DarkYellow, static o => o.Output.WriteLine("No upload path specified. Skipping upload")); - console.WithForegroundColor(ConsoleColor.Green, static o => o.Output.WriteLine("Upload build complete")); - return; - } + await pipeline.PrepareOutputAsync(pub) + .ConfigureAwait(false); //Run upload - await pipeline.ManualUpload(pub, uploads).ConfigureAwait(false); + await pipeline.ManualUpload(pub, uploads) + .ConfigureAwait(false); //Publish feeds if (feed is not null) { - console.WithForegroundColor(ConsoleColor.DarkGreen, static o => o.Output.WriteLine("Uploading feeds...")); + console.WithForegroundColor( + ConsoleColor.DarkGreen, + static o => o.Output.WriteLine("Uploading feeds...") + ); //Exec feed upload await uploads.UploadDirectoryAsync(feed.FeedOutputDir); } - console.WithForegroundColor(ConsoleColor.Green, static o => o.Output.WriteLine("Upload build complete")); + console.WithForegroundColor( + ConsoleColor.Green, + static o => o.Output.WriteLine("Upload build complete") + ); } public override IFeedManager[] Feeds => SleetPath is null ? [] : [SleetFeedManager.GetSleetFeed(SleetPath)]; + + private MultiUploadManager GetUploadManager(IConsole console) + { + try + { + IUploadManager[] uploadMan = []; + + if (!string.IsNullOrWhiteSpace(MinioPath)) + { + console.Output.WriteLine("Creating Minio publisher"); + + uploadMan = [MinioUploadManager.Create(MinioPath), ..uploadMan]; + } + + if (!string.IsNullOrWhiteSpace(FtpServerAddress)) + { + console.Output.WriteLine("Using FTP publisher"); + + uploadMan = [FtpUploadManager.Create(FtpServerAddress), .. uploadMan]; + } + + if(uploadMan.Length == 0) + { + console.WithForegroundColor( + ConsoleColor.DarkYellow, + static o => o.Output.WriteLine("No upload manager specified, output will be skipped") + ); + } + + return new MultiUploadManager(uploadMan); + } + catch(UriFormatException urie) + { + throw new BuildFailedException("Invalid server address", urie); + } + } + + private sealed class MultiUploadManager(params IUploadManager[] managers) : IUploadManager + { + private readonly IUploadManager[] _managers = managers; + + public async Task UploadDirectoryAsync(string path) + { + IEnumerable tasks = _managers.Select(m => m.UploadDirectoryAsync(path)); + + await Task.WhenAll(tasks); + } + } } } \ No newline at end of file diff --git a/src/Constants/Utils.cs b/src/Constants/Utils.cs index 6047c35..80e8ceb 100644 --- a/src/Constants/Utils.cs +++ b/src/Constants/Utils.cs @@ -20,7 +20,12 @@ namespace VNLib.Tools.Build.Executor.Constants /// The name of the process to run /// CLI arguments to pass to the process /// The process exit code - public static async Task RunProcessAsync(string process, string? workingDir, string[] args, IReadOnlyDictionary? env = null) + public static async Task RunProcessAsync( + string process, + string? workingDir, + string[] args, + IReadOnlyDictionary? env = null + ) { //Init new console cancellation token using ConsoleCancelToken ctToken = new(); diff --git a/src/Extensions/BuildExtensions.cs b/src/Extensions/BuildExtensions.cs index 24af05e..a7e5931 100644 --- a/src/Extensions/BuildExtensions.cs +++ b/src/Extensions/BuildExtensions.cs @@ -103,7 +103,7 @@ namespace VNLib.Tools.Build.Executor.Extensions SemVersion baseVersion; //Get latest version tag from git - Tag? vTag = mod.Repository.Tags.OrderByDescending(p => SemVersion.Parse(p.FriendlyName, style)).FirstOrDefault(); + Tag? vTag = mod.Repository.Tags.OrderByDescending(p => SemVersion.Parse(p.FriendlyName, style), SemVersion.SortOrderComparer).FirstOrDefault(); //Find the number of commits since the last tag if (vTag != null) diff --git a/src/Extensions/ProjectExtensions.cs b/src/Extensions/ProjectExtensions.cs index a24e6c5..9bb55c1 100644 --- a/src/Extensions/ProjectExtensions.cs +++ b/src/Extensions/ProjectExtensions.cs @@ -61,12 +61,14 @@ namespace VNLib.Tools.Build.Executor.Extensions //realtive file path outDir = Path.Combine(project.WorkingDir.FullName, outDir); - return new DirectoryInfo(outDir).EnumerateFiles(config.OutputFileType, SearchOption.TopDirectoryOnly); - } - else - { - return project.WorkingDir.EnumerateFiles(config.OutputFileType, SearchOption.AllDirectories); + if (Directory.Exists(outDir)) + { + return new DirectoryInfo(outDir) + .EnumerateFiles(config.OutputFileType, SearchOption.TopDirectoryOnly); + } } + + return project.WorkingDir.EnumerateFiles(config.OutputFileType, SearchOption.AllDirectories); } /// @@ -122,7 +124,9 @@ namespace VNLib.Tools.Build.Executor.Extensions public static string GetSafeProjectName(this IProject project) { - return project.ProjectName.Replace('/', '-').Replace('\\','-'); + return project.ProjectName + .Replace('/', '-') + .Replace('\\','-'); } } } \ No newline at end of file diff --git a/src/GpgSigner.cs b/src/GpgSigner.cs deleted file mode 100644 index 76943f9..0000000 --- a/src/GpgSigner.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using System.Collections.Generic; - -using VNLib.Tools.Build.Executor.Constants; - -namespace VNLib.Tools.Build.Executor -{ - public sealed class GpgSigner(bool enabled, string? defaultKey) - { - public bool IsEnabled { get; } = enabled; - - public async Task SignFileAsync(FileInfo file) - { - if (!IsEnabled) - { - return; - } - - List args = [ "--detach-sign" ]; - - if (!string.IsNullOrWhiteSpace(defaultKey)) - { - //Set the preferred key - args.Add("--default-key"); - args.Add(defaultKey); - } - - //Add input file - args.Add(file.FullName); - - //Delete an original file - string sigFile = $"{file.FullName}.sig"; - if (File.Exists(sigFile)) - { - File.Delete(sigFile); - } - - int result = await Utils.RunProcessAsync("gpg", null, args.ToArray()); - - switch (result) - { - case 2: - case 0: - break; - default: - throw new Exception($"Failed to sign file {file.FullName}"); - } - } - } -} \ No newline at end of file diff --git a/src/MinioUploadManager.cs b/src/MinioUploadManager.cs deleted file mode 100644 index 8337438..0000000 --- a/src/MinioUploadManager.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; - -using VNLib.Tools.Build.Executor.Model; - -using static VNLib.Tools.Build.Executor.Constants.Utils; - -namespace VNLib.Tools.Build.Executor -{ - internal class MinioUploadManager : IUploadManager - { - private readonly string _minioPath; - - private MinioUploadManager(string minioPath) - { - _minioPath = minioPath; - } - - public Task CleanAllAsync(string path) - { - throw new System.NotImplementedException(); - } - - public Task DeleteFileAsync(string filePath) - { - throw new System.NotImplementedException(); - } - - public async Task UploadDirectoryAsync(string path) - { - //Recursivley copy all files in the working directory - string[] args = - { - "cp", - "--recursive", - ".", - _minioPath - }; - - //Set working dir to the supplied dir path, and run the command - int result = await RunProcessAsync("mc", path, args); - - if(result != 0) - { - throw new BuildStepFailedException($"Failed to upload directory {path} with status code {result:x}"); - } - } - - public Task UploadFileAsync(string filePath) - { - throw new System.NotImplementedException(); - } - - [return:NotNullIfNotNull(nameof(uploadPath))] - public static IUploadManager? Create(string? uploadPath) - { - return string.IsNullOrWhiteSpace(uploadPath) ? null : new MinioUploadManager(uploadPath); - } - } -} \ No newline at end of file diff --git a/src/Model/IUploadManager.cs b/src/Model/IUploadManager.cs index 01d7081..a132af7 100644 --- a/src/Model/IUploadManager.cs +++ b/src/Model/IUploadManager.cs @@ -4,12 +4,6 @@ namespace VNLib.Tools.Build.Executor.Model { public interface IUploadManager { - Task CleanAllAsync(string path); - - Task DeleteFileAsync(string filePath); - Task UploadDirectoryAsync(string path); - - Task UploadFileAsync(string filePath); } } \ No newline at end of file diff --git a/src/Publishing/BuildPublisher.cs b/src/Publishing/BuildPublisher.cs new file mode 100644 index 0000000..67807d5 --- /dev/null +++ b/src/Publishing/BuildPublisher.cs @@ -0,0 +1,434 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections.Generic; + +using LibGit2Sharp; + +using Semver; + +using VNLib.Tools.Build.Executor.Constants; +using VNLib.Tools.Build.Executor.Model; +using VNLib.Tools.Build.Executor.Extensions; +using VNLib.Tools.Build.Executor.Projects; +using static VNLib.Tools.Build.Executor.Constants.Config; + +namespace VNLib.Tools.Build.Executor.Publishing +{ + + public sealed class BuildPublisher(BuildConfig config, GpgSigner signer) + { + public bool SignEnabled => signer.IsEnabled; + + /// + /// Prepares the module output and its collection of file details for publishing + /// then runs the upload step + /// + /// + /// A task that completes when the module's output has been created + public async Task PrepareModuleOutput(IModuleData module) + { + //Copy project artifacts to output directory + await CopyProjectOutputToModuleOutputAsync(module); + + //Copy source archive + string? archiveFile = await CopySourceArchiveToOutput(module, module.FileManager); + + Log.Information("Building module {mod} catalog and git history", module.ModuleName); + + //Build module catalog + await BuildModuleCatalogAsync(module, config.SemverStyle, archiveFile); + + Log.Information("Building module {mod} git history", module.ModuleName); + + //Build git history + await BuildModuleGitHistoryAsync(module); + + Log.Information("Building module {mod} version history", module.ModuleName); + + //build version history + await BuildModuleVersionHistory(module); + + Log.Information("Moving module {mod} artifacts to the output", module.ModuleName); + } + + /// + /// Uploads the modules output to the remote + /// + /// The module containing the information to upload + /// + public Task UploadModuleOutput(IUploadManager Uploader, IModuleData module) + { + Log.Information("Uploading module {mod}", module.ModuleName); + + //Upload the entire output directory + return Uploader.UploadDirectoryAsync(module.FileManager.OutputDir); + } + + /* + * Builds the project catalog file and publishes it to the module file manager + */ + private async Task BuildModuleCatalogAsync(IModuleData mod, SemVersionStyles style, string? archiveFile) + { + /* + * Builds the index.json file for the module. It + * contains an array of projects and their metadata + */ + + string moduleVersion = mod.GetModuleCiVersion(config.DefaultCiVersion, style); + + using MemoryStream ms = new(); + + using (Utf8JsonWriter writer = new(ms)) + { + //Open initial object + writer.WriteStartObject(); + + InitModuleFile(writer, mod); + + //Add the archive path if it was created in the module + if (archiveFile != null) + { + writer.WriteStartObject("archive"); + + //Archive path is in the build directory + writer.WriteString("path", config.SourceArchiveName); + + //Get the checksum of the archive + string checksum = await new FileInfo(archiveFile).ComputeFileHashStringAsync(); + writer.WriteString(config.HashFuncName, checksum); + writer.WriteString("sha_file", $"{config.SourceArchiveName}.{config.HashFuncName}"); + + //If signing is enabled, add the signature file (it is constant) + if (SignEnabled) + { + writer.WriteString("signature", $"{config.SourceArchiveName}.sig"); + } + + writer.WriteEndObject(); + } + + //Build project array + writer.WriteStartArray("projects"); + + foreach (IProject project in mod.Projects) + { + //start object for each project + writer.WriteStartObject(); + + //Write the project info + await WriteProjectInfoAsync(mod.FileManager, project, mod.Repository.Head.Tip.Sha, moduleVersion, writer); + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + + //Close object + writer.WriteEndObject(); + + writer.Flush(); + } + + ms.Seek(0, SeekOrigin.Begin); + + await mod.FileManager.WriteFileAsync(ModuleFileType.Catalog, ms.ToArray()); + } + + private static async Task BuildModuleVersionHistory(IModuleData mod) + { + /* + * Builds the index.json file for the module. It + * contains an array of projects and their metadata + */ + + using MemoryStream ms = new(); + + using (Utf8JsonWriter writer = new(ms)) + { + //Open initial object + writer.WriteStartObject(); + + InitModuleFile(writer, mod); + + //Set the head pointer to the latest commit we build + writer.WriteString("head", mod.Repository.Head.Tip.Sha); + + //Build project array + writer.WriteStartArray("versions"); + + //Write all git hashes from head back to the first commit + foreach (Commit commit in mod.Repository.Commits) + { + writer.WriteStringValue(commit.Sha); + } + + writer.WriteEndArray(); + + //Releases will be an array of objects containing the tag and the hash + writer.WriteStartArray("releases"); + + //Write all git tags + foreach (Tag tag in mod.Repository.Tags.OrderByDescending(static p => p.FriendlyName)) + { + writer.WriteStartObject(); + + writer.WriteString("tag", tag.FriendlyName); + writer.WriteString("hash", tag.Target.Sha); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + + //Close object + writer.WriteEndObject(); + + writer.Flush(); + } + + ms.Seek(0, SeekOrigin.Begin); + + await mod.FileManager.WriteFileAsync(ModuleFileType.VersionHistory, ms.ToArray()); + } + + + /* + * Builds the project's git history file and publishes it to the module file manager + * + * Also updates the latest hash file + */ + private static async Task BuildModuleGitHistoryAsync(IModuleData mod) + { + using MemoryStream ms = new(); + + using (Utf8JsonWriter writer = new(ms)) + { + //Open initial object + writer.WriteStartObject(); + + InitModuleFile(writer, mod); + + //Write the head commit + writer.WriteStartObject("head"); + writer.WriteString("branch", mod.Repository.Head.FriendlyName); + WriteSingleCommit(writer, mod.Repository.Head.Tip); + writer.WriteEndObject(); + + //Write commit history + WriteCommitHistory(writer, mod.Repository); + + //Close object + writer.WriteEndObject(); + + writer.Flush(); + } + + ms.Seek(0, SeekOrigin.Begin); + + await mod.FileManager.WriteFileAsync(ModuleFileType.GitHistory, ms.ToArray()); + + await mod.FileManager.WriteFileAsync( + ModuleFileType.LatestHash, + Encoding.UTF8.GetBytes(mod.Repository.Head.Tip.Sha) + ); + } + + /* + * Captures all of the project artiacts and copies them to the module output directory + */ + private async Task CopyProjectOutputToModuleOutputAsync(IModuleData mod) + { + //Copy build artifacts to module output directory + await mod.Projects.RunAllAsync(project => + { + //Get all output files from the project build, and copy them to the module output directory + return project.GetProjectBuildFiles(config) + .RunAllAsync(artifact => mod.FileManager.CopyArtifactToOutputAsync(project, artifact)); + }); + + /* + * If signing is enabled, we can sign all project files synchronously + */ + if (SignEnabled) + { + Log.Information("GPG Siginig is enabled, signing all artifacts for module {mod}", mod.ModuleName); + + /* + * Get all of the artifacts from the module's projects that match the target output + * file type, and sign them + */ + IEnumerable artifacts = mod.Projects.SelectMany( + p => mod.FileManager.GetArtifactOutputDir(p) + .EnumerateFiles(config.OutputFileType, SearchOption.TopDirectoryOnly) + ); + + //Sign synchronously + foreach (FileInfo artifact in artifacts) + { + await signer.SignFileAsync(artifact); + } + } + } + + + private static void InitModuleFile(Utf8JsonWriter writer, IModuleData mod) + { + //Set object name + writer.WriteString("module_name", mod.ModuleName); + //Modified date + writer.WriteString("modifed_date", DateTime.UtcNow); + } + + private static void WriteCommitHistory(Utf8JsonWriter writer, Repository repo) + { + writer.WriteStartArray("commits"); + + //Write commit history for current repo + foreach (Commit commit in repo.Head.Commits) + { + writer.WriteStartObject(); + + WriteSingleCommit(writer, commit); + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + + //Write tag history for current repo + writer.WriteStartArray("tags"); + + foreach (Tag tag in repo.Tags) + { + //clamp message length and ellipsis if too long + string? message = tag.Annotation?.Message; + if (message != null && message.Length > 120) + { + message = $"{message[..120]}..."; + } + + writer.WriteStartObject(); + writer.WriteString("name", tag.FriendlyName); + writer.WriteString("sha", tag.Target.Sha); + writer.WriteString("message", message); + writer.WriteString("author", tag.Annotation?.Tagger.Name); + writer.WriteString("date", tag.Annotation?.Tagger.When ?? default); + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + private static void WriteSingleCommit(Utf8JsonWriter writer, Commit commit) + { + writer.WriteString("sha", commit.Sha); + writer.WriteString("message", commit.Message); + writer.WriteString("author", commit.Author.Name); + writer.WriteString("commiter", commit.Committer.Name); + writer.WriteString("date", commit.Committer.When); + writer.WriteString("message_short", commit.MessageShort); + } + + /// + /// Builds and writes the projects information to the + /// + /// + /// + /// The to write the project + /// information to + /// + /// A task that completes when the write operation has completed + private async Task WriteProjectInfoAsync(IModuleFileManager man, IProject project, string latestSha, string version, Utf8JsonWriter writer) + { + //Reload the project dom after execute because semversion may be updated after build step + if (project is ModuleProject mp) + { + await mp.LoadProjectDom(); + } + + writer.WriteString("name", project.ProjectName); + writer.WriteString("repo_url", project.ProjectData.RepoUrl); + writer.WriteString("description", project.ProjectData.Description); + writer.WriteString("version", version); + writer.WriteString("copyright", project.ProjectData.Copyright); + writer.WriteString("author", project.ProjectData.Authors); + writer.WriteString("product", project.ProjectData.Product); + writer.WriteString("company", project.ProjectData.CompanyName); + writer.WriteString("commit", latestSha); + //Write target framework if it exsits + writer.WriteString("target_framework", project.ProjectData["TargetFramework"]); + + //Start file array + writer.WriteStartArray("files"); + + //Get only tar files, do not include the sha files + foreach (FileInfo output in GetProjOutputFiles(man, project)) + { + //beging file object + writer.WriteStartObject(); + + writer.WriteString("name", output.Name); + writer.WriteString("path", $"{project.GetSafeProjectName()}/{output.Name}"); + writer.WriteString("date", output.LastWriteTimeUtc); + writer.WriteNumber("size", output.Length); + + //Compute the file hash + string hashHex = await output.ComputeFileHashStringAsync(); + writer.WriteString(config.HashFuncName, hashHex); + + //Path to sha-file + writer.WriteString("sha_file", $"{project.GetSafeProjectName()}/{output.Name}.{config.HashFuncName}"); + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } + + private IEnumerable GetProjOutputFiles(IModuleFileManager man, IProject project) + { + return man.GetArtifactOutputDir(project) + .EnumerateFiles("*.*", SearchOption.TopDirectoryOnly) + .Where(p => p.Extension != $".{config.HashFuncName}"); + } + + /// + /// Copies the source git archive tgz file to the output directory + /// + /// + /// + /// + private async Task CopySourceArchiveToOutput(IModuleData mod, IModuleFileManager man) + { + //Try to get a source archive in the module directory + string? archiveFile = Directory.EnumerateFiles(mod.Repository.Info.WorkingDirectory, config.SourceArchiveName, SearchOption.TopDirectoryOnly).FirstOrDefault(); + + //If archive is null ignore and continue + if (string.IsNullOrWhiteSpace(archiveFile)) + { + Log.Information("No archive file found for module {mod}", mod.ModuleName); + return null; + } + + Log.Information("Found source archive for module {mod}, copying to output...", mod.ModuleName); + + //Otherwise copy to output + byte[] archive = await File.ReadAllBytesAsync(archiveFile); + FileInfo output = await man.WriteFileAsync(ModuleFileType.Archive, archive); + + //Compute the hash of the file + await output.ComputeFileHashAsync(config.HashFuncName); + + if (SignEnabled) + { + //Sign the file if signing is enabled + await signer.SignFileAsync(output); + } + + return archiveFile; + } + } +} \ No newline at end of file diff --git a/src/Publishing/FtpUploadManager.cs b/src/Publishing/FtpUploadManager.cs new file mode 100644 index 0000000..a955bb8 --- /dev/null +++ b/src/Publishing/FtpUploadManager.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; + +using FluentFTP; + +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +using VNLib.Tools.Build.Executor.Model; +using VNLib.Tools.Build.Executor.Constants; + +namespace VNLib.Tools.Build.Executor.Publishing +{ + internal sealed class FtpUploadManager(AsyncFtpClient client, string remotePath) : IUploadManager + { + public async Task UploadDirectoryAsync(string path) + { + path = Directory.GetParent(path)!.FullName; + + await client.AutoConnect(); + + var res = await client.UploadDirectory( + localFolder: path, + remoteFolder: remotePath, + FtpFolderSyncMode.Update, + FtpRemoteExists.Overwrite, + FtpVerify.Throw | FtpVerify.Retry + ); + + foreach(FtpResult fileResult in res) + { + switch (fileResult.ToStatus()) + { + case FtpStatus.Success: + Config.Log.Information( + "Uploaded {size} bytes, {0} -> {1}", + fileResult.Size, + fileResult.LocalPath, + fileResult.RemotePath + ); + break; + + case FtpStatus.Skipped: + Config.Log.Information("Skipped {0} -> {1}", fileResult.LocalPath, fileResult.RemotePath); + break; + + case FtpStatus.Failed: + Config.Log.Warning( + "Failed to upload {0}, reason: {exp}", + fileResult.LocalPath, + fileResult.Exception?.Message + ); + break; + } + } + } + + [return: NotNullIfNotNull(nameof(serverAddress))] + public static IUploadManager? Create(string? serverAddress) + { + if(string.IsNullOrWhiteSpace(serverAddress)) + { + return null; + } + + //Convert to uri, this may throw but this is currently the best way to validate the address + Uri serverUri = new(serverAddress); + + //Initlaize the client + AsyncFtpClient client = new() + { + Host = serverUri.Host, + Port = serverUri.Port, + + + Config = new() + { + LogToConsole = Config.Log.IsEnabled(Serilog.Events.LogEventLevel.Verbose), + + //Disable senstive logging in case running in automated CI pipelines where logs may be published + LogUserName = false, + LogPassword = false, + //EncryptionMode = FtpEncryptionMode.Auto, + RetryAttempts = 3, + }, + + Credentials = new() + { + //Pull credentials from the environment instead of command line + UserName = Environment.GetEnvironmentVariable("FTP_USERNAME"), + Password = Environment.GetEnvironmentVariable("FTP_PASSWORD") + }, + }; + + return new FtpUploadManager(client, serverUri.LocalPath); + } + } +} \ No newline at end of file diff --git a/src/Publishing/GpgSigner.cs b/src/Publishing/GpgSigner.cs new file mode 100644 index 0000000..3ed216a --- /dev/null +++ b/src/Publishing/GpgSigner.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Tools.Build.Executor.Constants; + +namespace VNLib.Tools.Build.Executor.Publishing +{ + public sealed class GpgSigner(bool enabled, string? defaultKey) + { + public bool IsEnabled { get; } = enabled; + + public async Task SignFileAsync(FileInfo file) + { + if (!IsEnabled) + { + return; + } + + List args = [ + "--detach-sign" + ]; + + if (!string.IsNullOrWhiteSpace(defaultKey)) + { + //Set the preferred key + args.Add("--default-key"); + args.Add(defaultKey); + } + + //Add input file + args.Add(file.FullName); + + //Delete an original file + string sigFile = $"{file.FullName}.sig"; + if (File.Exists(sigFile)) + { + File.Delete(sigFile); + } + + int result = await Utils.RunProcessAsync("gpg", null, args.ToArray()); + + switch (result) + { + case 2: + case 0: + break; + default: + throw new BuildFailedException($"Failed to sign file {file.FullName}"); + } + } + } +} \ No newline at end of file diff --git a/src/Publishing/MinioUploadManager.cs b/src/Publishing/MinioUploadManager.cs new file mode 100644 index 0000000..502392f --- /dev/null +++ b/src/Publishing/MinioUploadManager.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Tools.Build.Executor.Model; + +using static VNLib.Tools.Build.Executor.Constants.Utils; + +namespace VNLib.Tools.Build.Executor.Publishing +{ + + internal sealed class MinioUploadManager : IUploadManager + { + private readonly string _minioPath; + + private MinioUploadManager(string minioPath) => _minioPath = minioPath; + + public async Task UploadDirectoryAsync(string path) + { + //Recursivley copy all files in the working directory + string[] args = + { + "cp", + "--recursive", + ".", + _minioPath + }; + + //Set working dir to the supplied dir path, and run the command + int result = await RunProcessAsync("mc", path, args); + + if (result != 0) + { + throw new BuildFailedException($"Failed to upload directory {path} with status code {result:x}"); + } + } + + [return: NotNullIfNotNull(nameof(uploadPath))] + public static IUploadManager? Create(string? uploadPath) + { + return string.IsNullOrWhiteSpace(uploadPath) ? null : new MinioUploadManager(uploadPath); + } + } +} \ No newline at end of file diff --git a/src/Publishing/SleetFeedManager.cs b/src/Publishing/SleetFeedManager.cs new file mode 100644 index 0000000..997dcf2 --- /dev/null +++ b/src/Publishing/SleetFeedManager.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text.Json; +using VNLib.Tools.Build.Executor.Model; + +namespace VNLib.Tools.Build.Executor.Publishing +{ + internal sealed class SleetFeedManager : IFeedManager + { + private readonly string SleetConfigFile; + + /// + public string FeedOutputDir { get; } + + private SleetFeedManager(string indexFilex, string outputDir) + { + //Search for the sleet file in the build dir + SleetConfigFile = indexFilex; + FeedOutputDir = outputDir; + } + + /// + public void AddVariables(TaskfileVars vars) + { + vars.Set("SLEET_DIR", FeedOutputDir); + vars.Set("SLEET_CONFIG_PATH", SleetConfigFile); + } + + /// + /// Attempts to create a new sleet feed manager from the given directory index + /// if the index contains a sleet feed. Returns null if no sleet feed was found + /// + /// The feed manager if found, null otherwise + /// + /// + [return: NotNullIfNotNull(nameof(feedPath))] + public static IFeedManager? GetSleetFeed(string? feedPath) + { + if (string.IsNullOrWhiteSpace(feedPath)) + { + return null; + } + + //Read the sleet config file + byte[] sleetIndexFile = File.ReadAllBytes(feedPath); + using JsonDocument doc = JsonDocument.Parse(sleetIndexFile); + string rootDir = doc.RootElement.GetProperty("root").GetString() ?? throw new ArgumentException("The sleet output directory was not specified"); + return new SleetFeedManager(feedPath, rootDir); + } + } +} \ No newline at end of file diff --git a/src/SleetFeedManager.cs b/src/SleetFeedManager.cs deleted file mode 100644 index bc77ff1..0000000 --- a/src/SleetFeedManager.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Text.Json; - -using VNLib.Tools.Build.Executor.Model; - -namespace VNLib.Tools.Build.Executor -{ - internal sealed class SleetFeedManager : IFeedManager - { - private readonly string SleetConfigFile; - - /// - public string FeedOutputDir { get; } - - private SleetFeedManager(string indexFilex, string outputDir) - { - //Search for the sleet file in the build dir - SleetConfigFile = indexFilex; - FeedOutputDir = outputDir; - } - - /// - public void AddVariables(TaskfileVars vars) - { - vars.Set("SLEET_DIR", FeedOutputDir); - vars.Set("SLEET_CONFIG_PATH", SleetConfigFile); - } - - /// - /// Attempts to create a new sleet feed manager from the given directory index - /// if the index contains a sleet feed. Returns null if no sleet feed was found - /// - /// The feed manager if found, null otherwise - /// - /// - [return:NotNullIfNotNull(nameof(feedPath))] - public static IFeedManager? GetSleetFeed(string? feedPath) - { - if(string.IsNullOrWhiteSpace(feedPath)) - { - return null; - } - - //Read the sleet config file - byte[] sleetIndexFile = File.ReadAllBytes(feedPath); - using JsonDocument doc = JsonDocument.Parse(sleetIndexFile); - string rootDir = doc.RootElement.GetProperty("root").GetString() ?? throw new ArgumentException("The sleet output directory was not specified"); - return new SleetFeedManager(feedPath, rootDir); - } - } -} \ No newline at end of file diff --git a/src/TaskFile.cs b/src/TaskFile.cs index 6eef779..e8fa26e 100644 --- a/src/TaskFile.cs +++ b/src/TaskFile.cs @@ -56,7 +56,7 @@ namespace VNLib.Tools.Build.Executor args.Add(GetCommand(command)); //Exec task in the module dir - int result = await RunProcessAsync(taskFilePath, scope.WorkingDir.FullName, args.ToArray(), vars); + int result = await RunProcessAsync(taskFilePath, scope.WorkingDir.FullName, [.. args], vars); if(throwIfFailed) { @@ -68,14 +68,14 @@ namespace VNLib.Tools.Build.Executor { return cmd switch { - TaskfileComamnd.Clean => "clean", - TaskfileComamnd.Build => "build", - TaskfileComamnd.Upload => "upload", - TaskfileComamnd.Update => "update", - TaskfileComamnd.PostbuildSuccess => "postbuild_success", - TaskfileComamnd.PostbuildFailure => "postbuild_failed", - TaskfileComamnd.Publish => "publish", - TaskfileComamnd.Test => "test", + TaskfileComamnd.Clean => "clean", + TaskfileComamnd.Build => "build", + TaskfileComamnd.Upload => "upload", + TaskfileComamnd.Update => "update", + TaskfileComamnd.PostbuildSuccess => "postbuild_success", + TaskfileComamnd.PostbuildFailure => "postbuild_failed", + TaskfileComamnd.Publish => "publish", + TaskfileComamnd.Test => "test", _ => throw new NotImplementedException() }; } diff --git a/src/vnbuild.csproj b/src/vnbuild.csproj index b3e32df..b506f59 100644 --- a/src/vnbuild.csproj +++ b/src/vnbuild.csproj @@ -33,13 +33,14 @@ + - - - - - + + + + + -- cgit