aboutsummaryrefslogtreecommitdiff
path: root/src/BuildPublisher.cs
blob: a2dc2bf85dbb84e4ab1d9a6af20791764610fc46 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
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;

        /// <summary>
        /// Prepares the module output and its collection of file details for publishing
        /// then runs the upload step
        /// </summary>
        /// <param name="module"></param>
        /// <returns>A task that completes when the module's output has been created</returns>
        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);
        }

        /// <summary>
        /// Uploads the modules output to the remote
        /// </summary>
        /// <param name="module">The module containing the information to upload</param>
        /// <returns></returns>
        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<FileInfo> 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);
        }

        /// <summary>
        /// Builds and writes the projects information to the <see cref="Utf8JsonWriter"/>
        /// </summary>
        /// <param name="project"></param>
        /// <param name="writer">
        /// The <see cref="Utf8JsonWriter"/> to write the project
        /// information to
        /// </param>
        /// <returns>A task that completes when the write operation has completed</returns>
        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<FileInfo> GetProjOutputFiles(IModuleFileManager man, IProject project)
        {
            return man.GetArtifactOutputDir(project).EnumerateFiles("*.*", SearchOption.TopDirectoryOnly)
                .Where(p => p.Extension != $".{config.HashFuncName}");
        }

        /// <summary>
        /// Copies the source git archive tgz file to the output directory
        /// </summary>
        /// <param name="mod"></param>
        /// <param name="man"></param>
        /// <returns></returns>
        private async Task<string?> 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;
        }
    }
}