diff --git a/Resources/Scripts/Commands/PackCommand.cs b/Resources/Scripts/Commands/PackCommand.cs index a2f22242..5ed77716 100644 --- a/Resources/Scripts/Commands/PackCommand.cs +++ b/Resources/Scripts/Commands/PackCommand.cs @@ -1,31 +1,41 @@ using System.IO.Compression; using System.Xml.Linq; using Cocona; +using Microsoft.Extensions.Logging; using Scripts.Helpers; namespace Scripts.Commands; public class PackCommand { - private readonly CommandHelper CommandHelper; private readonly string TmpDir = "/tmp/mlbuild"; + private readonly ILogger Logger; + private readonly CsprojHelper CsprojHelper; + private readonly NupkgHelper NupkgHelper; - public PackCommand(CommandHelper commandHelper) + private readonly string[] ValidTags = ["apiserver", "frontend", "shared"]; + + public PackCommand( + ILogger logger, + CsprojHelper csprojHelper, + NupkgHelper nupkgHelper + ) { - CommandHelper = commandHelper; + CsprojHelper = csprojHelper; + NupkgHelper = nupkgHelper; + Logger = logger; } [Command("pack", Description = "Packs the specified folder/solution into nuget packages")] public async Task Pack( [Argument] string solutionDirectory, [Argument] string outputLocation, - [Option] string buildConfiguration = "Debug", - [Option] string? nugetDir = null + [Option] string buildConfiguration = "Debug" ) { if (!Directory.Exists(solutionDirectory)) { - Console.WriteLine("The specified solution directory does not exist"); + Logger.LogError("The specified solution directory does not exist"); return; } @@ -38,74 +48,55 @@ public class PackCommand Directory.CreateDirectory(TmpDir); // Find the project files - Console.WriteLine("Searching for projects inside the specified folder"); - var csProjFiles = Directory.GetFiles(solutionDirectory, "*csproj", SearchOption.AllDirectories); + Logger.LogInformation("Searching for projects inside the specified folder"); + + var projects = await CsprojHelper.FindProjectsInPath(solutionDirectory, ValidTags); // Show the user - Console.WriteLine($"Found {csProjFiles.Length} project(s) to check:"); + Logger.LogInformation("Found {count} project(s) to check:", projects.Count); - foreach (var csProjFile in csProjFiles) - Console.WriteLine($"- {Path.GetFullPath(csProjFile)}"); + foreach (var path in projects.Keys) + Logger.LogInformation("- {path}", Path.GetFullPath(path)); // Filter out project files which have specific tags specified - Console.WriteLine("Filtering projects by tags"); + Logger.LogInformation("Filtering projects by tags"); - List apiServerProjects = []; - List frontendProjects = []; - List sharedProjects = []; + var apiServerProjects = projects + .Where(x => x.Value.PackageTags.Contains("apiserver", StringComparer.InvariantCultureIgnoreCase)) + .ToArray(); - foreach (var csProjFile in csProjFiles) - { - await using var fs = File.Open( - csProjFile, - FileMode.Open, - FileAccess.ReadWrite, - FileShare.ReadWrite - ); + var frontendProjects = projects + .Where(x => x.Value.PackageTags.Contains("frontend", StringComparer.InvariantCultureIgnoreCase)) + .ToArray(); - var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None); - fs.Close(); + var sharedProjects = projects + .Where(x => x.Value.PackageTags.Contains("shared", StringComparer.InvariantCultureIgnoreCase)) + .ToArray(); - // Search for tag definitions - var packageTagsElements = document.Descendants("PackageTags").ToArray(); - - if (packageTagsElements.Length == 0) - { - Console.WriteLine($"No package tags found in {Path.GetFullPath(csProjFile)}. Skipping it when packing"); - continue; - } - - var packageTags = packageTagsElements.First().Value; - - if (packageTags.Contains("apiserver", StringComparison.InvariantCultureIgnoreCase)) - apiServerProjects.Add(csProjFile); - - if (packageTags.Contains("frontend", StringComparison.InvariantCultureIgnoreCase)) - frontendProjects.Add(csProjFile); - - if (packageTags.Contains("shared", StringComparison.InvariantCultureIgnoreCase)) - sharedProjects.Add(csProjFile); - } - - Console.WriteLine( - $"Found {apiServerProjects.Count} api server project(s), {frontendProjects.Count} frontend project(s) and {sharedProjects.Count} shared project(s)"); + Logger.LogInformation( + "Found {apiServerCount} api server project(s), {frontendCount} frontend project(s) and {sharedCount} shared project(s)", + apiServerProjects.Length, + frontendProjects.Length, + sharedProjects.Length + ); // Now build all these projects so we can pack them - Console.WriteLine("Building and packing api server project(s)"); + Logger.LogInformation("Building and packing api server project(s)"); foreach (var apiServerProject in apiServerProjects) { - await BuildProject( - apiServerProject, - buildConfiguration, - nugetDir + var csProjectFile = apiServerProject.Key; + var manifest = apiServerProject.Value; + + await CsprojHelper.Build( + csProjectFile, + buildConfiguration ); - var nugetFilePath = await PackProject( - apiServerProject, + var nugetFilePath = await CsprojHelper.Pack( + csProjectFile, TmpDir, - buildConfiguration, - nugetDir + buildConfiguration ); var nugetPackage = ZipFile.Open( @@ -113,31 +104,41 @@ public class PackCommand ZipArchiveMode.Update ); - await RemoveContentFiles(nugetPackage); + await NupkgHelper.RemoveContentFiles(nugetPackage); - await CleanDependencies(nugetPackage); - - Console.WriteLine("Finishing package and copying to output directory"); + // We don't want to clean moonlight references when we are packing moonlight, + // as it would remove references to its own shared project + + if (!manifest.PackageId.StartsWith("Moonlight.")) + await NupkgHelper.CleanDependencies(nugetPackage, "Moonlight."); + + Logger.LogInformation("Finishing package and copying to output directory"); nugetPackage.Dispose(); - File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true); + + File.Move( + nugetFilePath, + Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), + true + ); } - Console.WriteLine("Building and packing frontend projects"); + Logger.LogInformation("Building and packing frontend projects"); foreach (var frontendProject in frontendProjects) { - await BuildProject( - frontendProject, - buildConfiguration, - nugetDir + var csProjectFile = frontendProject.Key; + var manifest = frontendProject.Value; + + await CsprojHelper.Build( + csProjectFile, + buildConfiguration ); - var nugetFilePath = await PackProject( - frontendProject, + var nugetFilePath = await CsprojHelper.Pack( + csProjectFile, TmpDir, - buildConfiguration, - nugetDir + buildConfiguration ); var nugetPackage = ZipFile.Open( @@ -145,48 +146,20 @@ public class PackCommand ZipArchiveMode.Update ); - foreach (var entry in nugetPackage.Entries.ToArray()) - { - if (!entry.FullName.StartsWith("staticwebassets/_framework")) - continue; + await NupkgHelper.RemoveStaticWebAssets(nugetPackage, "_framework"); + await NupkgHelper.RemoveStaticWebAssets(nugetPackage, "css/style.min.css"); + await NupkgHelper.RemoveContentFiles(nugetPackage); - Console.WriteLine($"Removing framework file: {entry.FullName}"); - entry.Delete(); - } + // We don't want to clean moonlight references when we are packing moonlight, + // as it would remove references to its own shared project - var buildTargetEntry = nugetPackage.Entries.FirstOrDefault(x => - x.FullName == "build/Microsoft.AspNetCore.StaticWebAssets.props" - ); + if (!manifest.PackageId.StartsWith("Moonlight.")) + await NupkgHelper.CleanDependencies(nugetPackage, "Moonlight."); - if (buildTargetEntry != null) - { - Console.WriteLine("Removing framework file references"); - - await ModifyXmlInPackage(nugetPackage, buildTargetEntry, - document => document - .Descendants("StaticWebAsset") - .Where(x => - { - var relativePath = x.Element("RelativePath")!.Value; - - if (relativePath.StartsWith("_framework")) - return true; - - if (relativePath.StartsWith("css/style.min.css")) - return true; - - return false; - }) - ); - } - - await CleanDependencies(nugetPackage); - - await RemoveContentFiles(nugetPackage); // Pack razor and html files into src folder var additionalSrcFiles = new List(); - var basePath = Path.GetDirectoryName(frontendProject)!; + var basePath = Path.GetDirectoryName(csProjectFile)!; additionalSrcFiles.AddRange( Directory.GetFiles(basePath, "*.razor", SearchOption.AllDirectories) @@ -196,181 +169,58 @@ public class PackCommand Directory.GetFiles(basePath, "index.html", SearchOption.AllDirectories) ); - foreach (var additionalSrcFile in additionalSrcFiles) - { - var relativePath = "src/" + additionalSrcFile.Replace(basePath, "").Trim('/'); + await NupkgHelper.AddSourceFiles( + nugetPackage, + additionalSrcFiles.ToArray(), + file => "src/" + file.Replace(basePath, "").Trim('/') + ); - Console.WriteLine($"Adding additional files as src: {relativePath}"); - - await using var fs = File.Open( - additionalSrcFile, - FileMode.Open, - FileAccess.ReadWrite, - FileShare.ReadWrite - ); + Logger.LogInformation("Finishing package and copying to output directory"); - var entry = nugetPackage.CreateEntry(relativePath); - await using var entryFs = entry.Open(); - - await fs.CopyToAsync(entryFs); - await entryFs.FlushAsync(); - - fs.Close(); - entryFs.Close(); - } - - Console.WriteLine("Finishing package and copying to output directory"); - nugetPackage.Dispose(); - File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true); + + File.Move( + nugetFilePath, + Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), + true + ); } - - Console.WriteLine("Building and packing shared projects"); + + Logger.LogInformation("Building and packing shared projects"); foreach (var sharedProject in sharedProjects) { - await BuildProject( - sharedProject, - buildConfiguration, - nugetDir + var csProjectFile = sharedProject.Key; + var manifest = sharedProject.Value; + + await CsprojHelper.Build( + csProjectFile, + buildConfiguration ); - var nugetFilePath = await PackProject( - sharedProject, + var nugetFilePath = await CsprojHelper.Pack( + csProjectFile, TmpDir, - buildConfiguration, - nugetDir + buildConfiguration ); - File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true); - } - } + var nugetPackage = ZipFile.Open( + nugetFilePath, + ZipArchiveMode.Update + ); - private async Task BuildProject(string file, string configuration, string? nugetDir) - { - var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!); - var fileName = Path.GetFileName(file); + // We don't want to clean moonlight references when we are packing moonlight, + // as it would remove references to its own shared project - await CommandHelper.Run( - "/usr/bin/dotnet", - $"build {fileName} --configuration {configuration}" + (string.IsNullOrEmpty(nugetDir) ? "" : $" --source {nugetDir}"), - basePath - ); - } - - private async Task PackProject(string file, string output, string configuration, string? nugetDir) - { - var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!); - var fileName = Path.GetFileName(file); - - await CommandHelper.Run( - "/usr/bin/dotnet", - $"pack {fileName} --output {output} --configuration {configuration}" + (string.IsNullOrEmpty(nugetDir) ? "" : $" --source {nugetDir}"), - basePath - ); - - var nugetFilesPaths = Directory.GetFiles(TmpDir, "*.nupkg", SearchOption.TopDirectoryOnly); - - if (nugetFilesPaths.Length == 0) - throw new Exception("No nuget packages were built"); - - if (nugetFilesPaths.Length > 1) - throw new Exception("More than one nuget package has been built"); - - return nugetFilesPaths.First(); - } - - private async Task CleanDependencies(ZipArchive nugetPackage) - { - var nuspecEntry = nugetPackage.Entries.FirstOrDefault(x => x.Name.EndsWith(".nuspec")); - - if (nuspecEntry == null) - { - Console.WriteLine("No nuspec file to modify found in nuget package"); - return; - } - - await ModifyXmlInPackage(nugetPackage, nuspecEntry, document => - { - var ns = document.Root!.GetDefaultNamespace(); + if (!manifest.PackageId.StartsWith("Moonlight.")) + await NupkgHelper.CleanDependencies(nugetPackage, "Moonlight."); - var metadata = document.Root!.Element(ns + "metadata")!; - - var id = metadata.Element(ns + "id")!.Value; + nugetPackage.Dispose(); - // Skip the removal of moonlight references when - // we are packing moonlight itself - if (id.StartsWith("Moonlight.")) - return []; - - return document - .Descendants(ns + "dependency") - .Where(x => x.Attribute("id")?.Value.StartsWith("Moonlight.") ?? false); - }); - } - - private async Task ModifyXmlInPackage( - ZipArchive archive, - ZipArchiveEntry entry, - Func> filter - ) - { - var oldPath = entry.FullName; - await using var oldFs = entry.Open(); - - var document = await XDocument.LoadAsync( - oldFs, - LoadOptions.None, - CancellationToken.None - ); - - var itemsToRemove = filter.Invoke(document); - var items = itemsToRemove.ToArray(); - - foreach (var item in items) - item.Remove(); - - oldFs.Close(); - entry.Delete(); - - var newEntry = archive.CreateEntry(oldPath); - var newFs = newEntry.Open(); - - await document.SaveAsync(newFs, SaveOptions.None, CancellationToken.None); - - await newFs.FlushAsync(); - newFs.Close(); - - return newEntry; - } - - private async Task RemoveContentFiles(ZipArchive nugetPackage) - { - // Remove all content files - foreach (var entry in nugetPackage.Entries.ToArray()) - { - if (!entry.FullName.StartsWith("contentFiles") && !entry.FullName.StartsWith("content")) - continue; - - Console.WriteLine($"Removing content file: {entry.FullName}"); - entry.Delete(); - } - - // Remove references to those files in the nuspec file - var nuspecFile = nugetPackage.Entries.FirstOrDefault(x => x.Name.EndsWith(".nuspec")); - - if (nuspecFile != null) - { - Console.WriteLine("Removing references to content files in the nuspec files"); - - await ModifyXmlInPackage( - nugetPackage, - nuspecFile, - document => - { - var ns = document.Root!.GetDefaultNamespace(); - return document.Descendants(ns + "contentFiles"); - } + File.Move( + nugetFilePath, + Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), + true ); } } diff --git a/Resources/Scripts/Commands/PreBuildCommand.cs b/Resources/Scripts/Commands/PreBuildCommand.cs index d911d6e3..cef8cf76 100644 --- a/Resources/Scripts/Commands/PreBuildCommand.cs +++ b/Resources/Scripts/Commands/PreBuildCommand.cs @@ -2,108 +2,119 @@ using System.IO.Compression; using System.Text; using System.Xml.Linq; using Cocona; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Logging; using Scripts.Helpers; +using Scripts.Models; namespace Scripts.Commands; public class PreBuildCommand { - private readonly CommandHelper CommandHelper; + private readonly NupkgHelper NupkgHelper; + private readonly CsprojHelper CsprojHelper; + private readonly CodeHelper CodeHelper; + private readonly ILogger Logger; + private const string GeneratedStart = "// MLBUILD Generated Start"; private const string GeneratedEnd = "// MLBUILD Generated End"; private const string GeneratedHook = "// MLBUILD_PLUGIN_STARTUP_HERE"; - public PreBuildCommand(CommandHelper commandHelper) + private readonly string[] ValidTags = ["frontend", "apiserver", "shared"]; + + public PreBuildCommand( + CsprojHelper csprojHelper, + NupkgHelper nupkgHelper, + CodeHelper codeHelper, + ILogger logger + ) { - CommandHelper = commandHelper; + CsprojHelper = csprojHelper; + NupkgHelper = nupkgHelper; + CodeHelper = codeHelper; + Logger = logger; } [Command("prebuild")] public async Task Prebuild( - [Argument] string moonlightDir, - [Argument] string nugetDir + [Argument] string moonlightDirectory, + [Argument] string pluginsDirectory ) { - var dependencies = await GetDependenciesFromNuget(nugetDir); + var projects = await CsprojHelper.FindProjectsInPath(moonlightDirectory, ValidTags); - Console.WriteLine("Following plugins found:"); + var nugetManifests = await GetNugetManifests(pluginsDirectory); - foreach (var dependency in dependencies) + Logger.LogInformation("Following plugins found:"); + + foreach (var manifest in nugetManifests) { - Console.WriteLine($"{dependency.Id} ({dependency.Version}) [{dependency.Tags}]"); + Logger.LogInformation( + "- {id} ({version}) [{tags}]", + manifest.Id, + manifest.Version, + string.Join(", ", manifest.Tags) + ); } - var csProjFiles = Directory.GetFiles(moonlightDir, "*.csproj", SearchOption.AllDirectories); - try { - Console.WriteLine("Adjusting csproj files"); - foreach (var csProjFile in csProjFiles) + Logger.LogInformation("Adjusting csproj files"); + + foreach (var project in projects) { + var csProjectPath = project.Key; + await using var fs = File.Open( - csProjFile, + csProjectPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite ); var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None); - fs.Close(); + fs.Position = 0; - // Search for tag definitions - var packageTagsElements = document.Descendants("PackageTags").ToArray(); - - if (packageTagsElements.Length == 0) - { - Console.WriteLine($"No package tags found in {Path.GetFullPath(csProjFile)}. Skipping it"); - continue; - } - - var packageTags = packageTagsElements.First().Value; - - var dependenciesToAdd = dependencies - .Where(x => x.Tags.Contains(packageTags, StringComparison.InvariantCultureIgnoreCase)) + var dependenciesToAdd = nugetManifests + .Where(x => x.Tags.Any(tag => + project.Value.PackageTags.Contains(tag, StringComparer.InvariantCultureIgnoreCase))) .ToArray(); - await RemoveDependencies(csProjFile); - await AddDependencies(csProjFile, dependenciesToAdd); + await CsprojHelper.CleanDependencies(document, "MoonlightBuildDeps"); + await CsprojHelper.AddDependencies(document, dependenciesToAdd, "MoonlightBuildDeps"); + + fs.Position = 0; + await document.SaveAsync(fs, SaveOptions.None, CancellationToken.None); + + await fs.FlushAsync(); + fs.Close(); } - Console.WriteLine("Restoring projects"); - foreach (var csProjFile in csProjFiles) + Logger.LogInformation("Restoring projects"); + + foreach (var csProjectPath in projects.Keys) + await CsprojHelper.Restore(csProjectPath); + + Logger.LogInformation("Generating plugin startup"); + + foreach (var currentTag in ValidTags) { - await RestoreProject(csProjFile, nugetDir); - } + Logger.LogInformation("Checking for '{currentTag}' projects", currentTag); - Console.WriteLine("Generating plugin startup"); + var projectsWithTag = projects + .Where(x => + x.Value.PackageTags.Contains(currentTag, StringComparer.InvariantCultureIgnoreCase) + ) + .ToArray(); - string[] validTags = ["apiserver", "frontend"]; - - foreach (var currentTag in validTags) - { - Console.WriteLine($"Checking for '{currentTag}' projects"); - - foreach (var csProjFile in csProjFiles) + foreach (var project in projectsWithTag) { - var tags = await GetTagsFromCsproj(csProjFile); + var csProjectPath = project.Key; - if (string.IsNullOrEmpty(tags)) - { - Console.WriteLine($"No package tags found in {Path.GetFullPath(csProjFile)}. Skipping it"); - continue; - } - - if (!tags.Contains(currentTag)) - continue; - - var currentDeps = dependencies + var currentDependencies = nugetManifests .Where(x => x.Tags.Contains(currentTag)) .ToArray(); - var classPaths = await FindStartupClasses(currentDeps); + var classPaths = await FindStartupClasses(currentDependencies); var code = new StringBuilder(); @@ -112,10 +123,10 @@ public class PreBuildCommand foreach (var path in classPaths) code.AppendLine($"pluginStartups.Add(new global::{path}());"); - code.AppendLine(GeneratedEnd); + code.Append(GeneratedEnd); var filesToSearch = Directory.GetFiles( - Path.GetDirectoryName(csProjFile)!, + Path.GetDirectoryName(csProjectPath)!, "*.cs", SearchOption.AllDirectories ); @@ -127,7 +138,7 @@ public class PreBuildCommand if (!content.Contains(GeneratedHook, StringComparison.InvariantCultureIgnoreCase)) continue; - Console.WriteLine($"Injecting generated code to: {Path.GetFullPath(file)}"); + Logger.LogInformation("Injecting generated code to: {path}", Path.GetFullPath(file)); content = content.Replace( GeneratedHook, @@ -142,12 +153,15 @@ public class PreBuildCommand } catch (Exception) { - Console.WriteLine("An error occured while prebuilding moonlight. Removing csproj modifications"); + Logger.LogInformation("An error occured while prebuilding moonlight. Removing csproj modifications"); - foreach (var csProjFile in csProjFiles) - await RemoveDependencies(csProjFile); - - await RemoveGeneratedCode(moonlightDir); + foreach (var project in projects) + { + await CsprojHelper.CleanDependencies(project.Key, "MoonlightBuildDeps"); + + var path = Path.GetDirectoryName(project.Key)!; + await RemoveGeneratedCode(path); + } throw; } @@ -158,220 +172,68 @@ public class PreBuildCommand [Argument] string moonlightDir ) { - var csProjFiles = Directory.GetFiles(moonlightDir, "*.csproj", SearchOption.AllDirectories); + var projects = await CsprojHelper.FindProjectsInPath(moonlightDir, ValidTags); + + Logger.LogInformation("Reverting csproj changes"); - Console.WriteLine("Reverting csproj changes"); - - foreach (var csProjFile in csProjFiles) - await RemoveDependencies(csProjFile); - - Console.WriteLine("Removing generated code"); - await RemoveGeneratedCode(moonlightDir); - } - - [Command("test")] - public async Task Test( - [Argument] string nugetDir - ) - { - var dependencies = await GetDependenciesFromNuget(nugetDir); - - await FindStartupClasses(dependencies); - } - - private async Task GetDependenciesFromNuget(string nugetDir) - { - var nugetFiles = Directory.GetFiles(nugetDir, "*.nupkg", SearchOption.AllDirectories); - var dependencies = new List(); - - foreach (var nugetFile in nugetFiles) + foreach (var project in projects) { - var dependency = await GetDependencyFromPackage(nugetFile); - dependencies.Add(dependency); + Logger.LogInformation("Removing dependencies: {project}", project.Key); + await CsprojHelper.CleanDependencies(project.Key, "MoonlightBuildDeps"); + + Logger.LogInformation("Removing generated code: {project}", project.Key); + var path = Path.GetDirectoryName(project.Key)!; + await RemoveGeneratedCode(path); } - - return dependencies.ToArray(); } - private async Task GetDependencyFromPackage(string path) + private async Task GetNugetManifests(string nugetDir) { - using var nugetPackage = ZipFile.Open(path, ZipArchiveMode.Read); - - var nuspecEntry = nugetPackage.Entries.First(x => x.Name.EndsWith(".nuspec")); - await using var nuspecFs = nuspecEntry.Open(); - - var nuspec = await XDocument.LoadAsync(nuspecFs, LoadOptions.None, CancellationToken.None); - - var ns = nuspec.Root!.GetDefaultNamespace(); - var metadata = nuspec.Root!.Element(ns + "metadata")!; - - var id = metadata.Element(ns + "id")!.Value; - var version = metadata.Element(ns + "version")!.Value; - var tags = metadata.Element(ns + "tags")!.Value; - - nuspecFs.Close(); - - return new Dependency() - { - Id = id, - Version = version, - Tags = tags - }; - } - - private async Task AddDependencies(string path, Dependency[] dependencies) - { - await using var fs = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); - fs.Position = 0; - - var csProj = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None); - - var project = csProj.Element("Project")!; - - var itemGroup = new XElement("ItemGroup"); - itemGroup.SetAttributeValue("Label", "MoonlightBuildDeps"); - - foreach (var dependency in dependencies) - { - var depElement = new XElement("PackageReference"); - depElement.SetAttributeValue("Include", dependency.Id); - depElement.SetAttributeValue("Version", dependency.Version); - - itemGroup.Add(depElement); - } - - project.Add(itemGroup); - - fs.Position = 0; - await csProj.SaveAsync(fs, SaveOptions.None, CancellationToken.None); - } - - private Task RemoveDependencies(string path) - { - var csProj = XDocument.Load(path, LoadOptions.None); - - var itemGroupsToRemove = csProj - .Descendants("ItemGroup") - .Where(x => x.Attribute("Label")?.Value.Contains("MoonlightBuildDeps") ?? false) - .ToArray(); - - itemGroupsToRemove.Remove(); - - csProj.Save(path, SaveOptions.None); - - return Task.CompletedTask; - } - - private async Task RestoreProject(string file, string nugetPath) - { - var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!); - var fileName = Path.GetFileName(file); - var nugetPathFull = Path.GetFullPath(nugetPath); - - Console.WriteLine($"Restore: {basePath} - {fileName}"); - - await CommandHelper.Run( - "/usr/bin/dotnet", - $"restore {fileName} --source {nugetPathFull}", - basePath + var nugetFiles = Directory.GetFiles( + nugetDir, + "*.nupkg", + SearchOption.AllDirectories ); + + var manifests = new List(); + + foreach (var nugetFilePath in nugetFiles) + { + using var nugetPackage = ZipFile.Open(nugetFilePath, ZipArchiveMode.Read); + var manifest = await NupkgHelper.GetManifest(nugetPackage); + + if (manifest == null) + continue; + + manifests.Add(manifest); + } + + return manifests.ToArray(); } - private async Task FindStartupClasses(Dependency[] dependencies) + private async Task FindStartupClasses(NupkgManifest[] dependencies) { - var result = new List(); - var nugetPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages" ); - var filesToScan = dependencies.SelectMany(dependency => + var filesToScan = dependencies + .SelectMany(dependency => { var dependencySrcPath = Path.Combine(nugetPath, dependency.Id.ToLower(), dependency.Version, "src"); - Console.WriteLine($"Checking {dependencySrcPath}"); + Logger.LogDebug("Checking {dependencySrcPath}", dependencySrcPath); if (!Directory.Exists(dependencySrcPath)) return []; return Directory.GetFiles(dependencySrcPath, "*.cs", SearchOption.AllDirectories); - } - ).ToArray(); + }) + .ToArray(); - var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - - var trees = new List(); - - foreach (var file in filesToScan) - { - Console.WriteLine($"Reading {file}"); - - var content = await File.ReadAllTextAsync(file); - var tree = CSharpSyntaxTree.ParseText(content); - trees.Add(tree); - } - - var compilation = CSharpCompilation.Create("Analysis", trees, [mscorlib]); - - foreach (var tree in trees) - { - var model = compilation.GetSemanticModel(tree); - var root = await tree.GetRootAsync(); - - var classDeclarations = root - .DescendantNodes() - .OfType(); - - foreach (var classDeclaration in classDeclarations) - { - var symbol = model.GetDeclaredSymbol(classDeclaration); - - if (symbol == null) - continue; - - var hasAttribute = symbol.GetAttributes().Any(attr => - { - if (attr.AttributeClass == null) - return false; - - return attr.AttributeClass.Name == "PluginStartup"; - }); - - if (!hasAttribute) - continue; - - var classPath = - $"{symbol.ContainingNamespace.ToDisplayString()}.{classDeclaration.Identifier.ValueText}"; - - Console.WriteLine($"Detected startup in class: {classPath}"); - result.Add(classPath); - } - } - - return result.ToArray(); - } - - private async Task GetTagsFromCsproj(string csProjFile) - { - await using var fs = File.Open( - csProjFile, - FileMode.Open, - FileAccess.ReadWrite, - FileShare.ReadWrite - ); - - var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None); - fs.Close(); - - // Search for tag definitions - var packageTagsElements = document.Descendants("PackageTags").ToArray(); - - if (packageTagsElements.Length == 0) - return ""; - - return packageTagsElements.First().Value; + return await CodeHelper.FindPluginStartups(filesToScan); } private async Task RemoveGeneratedCode(string dir) @@ -384,10 +246,6 @@ public class PreBuildCommand foreach (var file in filesToSearch) { - // We dont want to replace ourself - if (file.Contains("PreBuildCommand.cs")) - continue; - var content = await File.ReadAllTextAsync(file); if (!content.Contains(GeneratedStart) || !content.Contains(GeneratedEnd)) @@ -404,11 +262,4 @@ public class PreBuildCommand await File.WriteAllTextAsync(file, content); } } - - private record Dependency - { - public string Id { get; set; } - public string Version { get; set; } - public string Tags { get; set; } - } } \ No newline at end of file diff --git a/Resources/Scripts/Helpers/CodeHelper.cs b/Resources/Scripts/Helpers/CodeHelper.cs new file mode 100644 index 00000000..92ea1da6 --- /dev/null +++ b/Resources/Scripts/Helpers/CodeHelper.cs @@ -0,0 +1,73 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.Logging; + +namespace Scripts.Helpers; + +public class CodeHelper +{ + private readonly ILogger Logger; + + public CodeHelper(ILogger logger) + { + Logger = logger; + } + + public async Task FindPluginStartups(string[] filesToSearch) + { + var result = new List(); + + var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); + + var trees = new List(); + + foreach (var file in filesToSearch) + { + Logger.LogDebug("Reading {file}", file); + + var content = await File.ReadAllTextAsync(file); + var tree = CSharpSyntaxTree.ParseText(content); + trees.Add(tree); + } + + var compilation = CSharpCompilation.Create("Analysis", trees, [mscorlib]); + + foreach (var tree in trees) + { + var model = compilation.GetSemanticModel(tree); + var root = await tree.GetRootAsync(); + + var classDeclarations = root + .DescendantNodes() + .OfType(); + + foreach (var classDeclaration in classDeclarations) + { + var symbol = model.GetDeclaredSymbol(classDeclaration); + + if (symbol == null) + continue; + + var hasAttribute = symbol.GetAttributes().Any(attr => + { + if (attr.AttributeClass == null) + return false; + + return attr.AttributeClass.Name == "PluginStartup"; + }); + + if (!hasAttribute) + continue; + + var classPath = $"{symbol.ContainingNamespace.ToDisplayString()}.{classDeclaration.Identifier.ValueText}"; + + Logger.LogInformation("Detected startup in class: {classPath}", classPath); + + result.Add(classPath); + } + } + + return result.ToArray(); + } +} \ No newline at end of file diff --git a/Resources/Scripts/Helpers/CsprojHelper.cs b/Resources/Scripts/Helpers/CsprojHelper.cs new file mode 100644 index 00000000..22f91fd6 --- /dev/null +++ b/Resources/Scripts/Helpers/CsprojHelper.cs @@ -0,0 +1,228 @@ +using System.Collections.Frozen; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Scripts.Models; + +namespace Scripts.Helpers; + +public class CsprojHelper +{ + private readonly ILogger Logger; + private readonly CommandHelper CommandHelper; + + public CsprojHelper(ILogger logger, CommandHelper commandHelper) + { + Logger = logger; + CommandHelper = commandHelper; + } + + #region Add dependencies + + public async Task AddDependencies(string path, NupkgManifest[] dependencies, string label) + { + await using var fs = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); + fs.Position = 0; + + await AddDependencies(fs, dependencies, label); + + await fs.FlushAsync(); + fs.Close(); + } + + public async Task AddDependencies(Stream stream, NupkgManifest[] dependencies, string label) + { + var xmlDocument = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None); + + await AddDependencies(xmlDocument, dependencies, label); + + stream.Position = 0; + await xmlDocument.SaveAsync(stream, SaveOptions.DisableFormatting, CancellationToken.None); + } + + public Task AddDependencies(XDocument document, NupkgManifest[] dependencies, string label) + { + var project = document.Element("Project")!; + + var itemGroup = new XElement("ItemGroup"); + itemGroup.SetAttributeValue("Label", label); + + foreach (var dependency in dependencies) + { + var depElement = new XElement("PackageReference"); + depElement.SetAttributeValue("Include", dependency.Id); + depElement.SetAttributeValue("Version", dependency.Version); + + itemGroup.Add(depElement); + } + + project.Add(itemGroup); + + return Task.CompletedTask; + } + + #endregion + + #region Clean dependencies + + public async Task CleanDependencies(string path, string label) + { + var document = XDocument.Load(path, LoadOptions.None); + + await CleanDependencies(document, label); + + document.Save(path, SaveOptions.DisableFormatting); + } + + public async Task CleanDependencies(Stream stream, string label) + { + var xmlDocument = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None); + + await CleanDependencies(xmlDocument, label); + + stream.Position = 0; + await xmlDocument.SaveAsync(stream, SaveOptions.DisableFormatting, CancellationToken.None); + } + + public Task CleanDependencies(XDocument document, string label) + { + var itemGroupsToRemove = document + .Descendants("ItemGroup") + .Where(x => x.Attribute("Label")?.Value.Contains(label) ?? false) + .ToArray(); + + itemGroupsToRemove.Remove(); + + return Task.CompletedTask; + } + + #endregion + + #region Read + + public async Task Read(string path) + { + await using var fileStream = File.Open( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite + ); + + var manifest = await Read(fileStream); + + fileStream.Close(); + + return manifest; + } + + public async Task Read(Stream stream) + { + var xmlDocument = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None); + return await Read(xmlDocument); + } + + public Task Read(XDocument document) + { + var manifest = new CsprojManifest(); + + var ns = document.Root!.GetDefaultNamespace(); + + manifest.IsPackable = document + .Descendants(ns + "IsPackable") + .FirstOrDefault()?.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase) ?? false; + + manifest.PackageId = document + .Descendants(ns + "PackageId") + .FirstOrDefault()?.Value ?? "N/A"; + + manifest.Version = document + .Descendants(ns + "Version") + .FirstOrDefault()?.Value ?? "N/A"; + + manifest.PackageTags = document + .Descendants(ns + "PackageTags") + .FirstOrDefault()?.Value + .Split(";", StringSplitOptions.RemoveEmptyEntries) ?? []; + + return Task.FromResult(manifest); + } + + #endregion + + public async Task Restore(string path) + { + var basePath = Path.GetFullPath(Path.GetDirectoryName(path)!); + var fileName = Path.GetFileName(path); + + Logger.LogInformation("Restore: {basePath} - {fileName}", basePath, fileName); + + await CommandHelper.Run( + "/usr/bin/dotnet", + $"restore {fileName}", + basePath + ); + } + + public async Task Build(string file, string configuration) + { + var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!); + var fileName = Path.GetFileName(file); + + await CommandHelper.Run( + "/usr/bin/dotnet", + $"build {fileName} --configuration {configuration}", + basePath + ); + } + + public async Task Pack(string file, string output, string configuration) + { + var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!); + var fileName = Path.GetFileName(file); + + await CommandHelper.Run( + "/usr/bin/dotnet", + $"pack {fileName} --output {output} --configuration {configuration}", + basePath + ); + + var nugetFilesPaths = Directory.GetFiles( + output, + "*.nupkg", + SearchOption.TopDirectoryOnly + ); + + if (nugetFilesPaths.Length == 0) + throw new Exception("No nuget packages were built"); + + if (nugetFilesPaths.Length > 1) + throw new Exception("More than one nuget package has been built"); + + return nugetFilesPaths.First(); + } + + public async Task> FindProjectsInPath(string path, string[] validTags) + { + var projectFiles = Directory.GetFiles( + path, + "*.csproj", + SearchOption.AllDirectories + ); + + var projects = new Dictionary(); + + foreach (var projectFile in projectFiles) + { + var manifest = await Read(projectFile); + + // Ignore all projects which have no matching tags + if (!manifest.PackageTags.Any(projectTag => + validTags.Contains(projectTag, StringComparer.InvariantCultureIgnoreCase))) + continue; + + projects.Add(projectFile, manifest); + } + + return projects.ToFrozenDictionary(); + } +} \ No newline at end of file diff --git a/Resources/Scripts/Helpers/NupkgHelper.cs b/Resources/Scripts/Helpers/NupkgHelper.cs new file mode 100644 index 00000000..b0158553 --- /dev/null +++ b/Resources/Scripts/Helpers/NupkgHelper.cs @@ -0,0 +1,195 @@ +using System.IO.Compression; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using Scripts.Models; + +namespace Scripts.Helpers; + +public class NupkgHelper +{ + private readonly ILogger Logger; + + public NupkgHelper(ILogger logger) + { + Logger = logger; + } + + public async Task GetManifest(ZipArchive nugetPackage) + { + var nuspecEntry = nugetPackage.Entries.FirstOrDefault( + x => x.Name.EndsWith(".nuspec") + ); + + if (nuspecEntry == null) + return null; + + await using var fs = nuspecEntry.Open(); + + var nuspec = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None); + + var ns = nuspec.Root!.GetDefaultNamespace(); + var metadata = nuspec.Root!.Element(ns + "metadata")!; + + var id = metadata.Element(ns + "id")!.Value; + var version = metadata.Element(ns + "version")!.Value; + var tags = metadata.Element(ns + "tags")!.Value; + + return new NupkgManifest() + { + Id = id, + Version = version, + Tags = tags.Split(";", StringSplitOptions.RemoveEmptyEntries) + }; + } + + public async Task CleanDependencies(ZipArchive nugetPackage, string filter) + { + var nuspecEntry = nugetPackage.Entries.FirstOrDefault( + x => x.Name.EndsWith(".nuspec") + ); + + if (nuspecEntry == null) + { + Logger.LogWarning("No nuspec file to modify found in nuget package"); + return; + } + + await ModifyXmlInPackage(nugetPackage, nuspecEntry, document => + { + var ns = document.Root!.GetDefaultNamespace(); + + return document + .Descendants(ns + "dependency") + .Where(x => x.Attribute("id")?.Value.StartsWith(filter) ?? false); + }); + } + + public async Task RemoveContentFiles(ZipArchive nugetPackage) + { + foreach (var entry in nugetPackage.Entries.ToArray()) + { + if (!entry.FullName.StartsWith("contentFiles") && !entry.FullName.StartsWith("content")) + continue; + + Logger.LogDebug("Removing content file: {path}", entry.FullName); + entry.Delete(); + } + + var nuspecFile = nugetPackage + .Entries + .FirstOrDefault(x => x.Name.EndsWith(".nuspec")); + + if (nuspecFile == null) + { + Logger.LogWarning("Nuspec file missing. Unable to remove content files references from nuspec file"); + return; + } + + await ModifyXmlInPackage( + nugetPackage, + nuspecFile, + document => + { + var ns = document.Root!.GetDefaultNamespace(); + return document.Descendants(ns + "contentFiles"); + } + ); + } + + public async Task ModifyXmlInPackage( + ZipArchive nugetPackage, + ZipArchiveEntry entry, + Func> filter + ) + { + var oldPath = entry.FullName; + await using var oldFs = entry.Open(); + + var document = await XDocument.LoadAsync( + oldFs, + LoadOptions.None, + CancellationToken.None + ); + + var itemsToRemove = filter.Invoke(document); + var items = itemsToRemove.ToArray(); + + foreach (var item in items) + item.Remove(); + + oldFs.Close(); + entry.Delete(); + + var newEntry = nugetPackage.CreateEntry(oldPath); + var newFs = newEntry.Open(); + + await document.SaveAsync(newFs, SaveOptions.None, CancellationToken.None); + + await newFs.FlushAsync(); + newFs.Close(); + + return newEntry; + } + + public async Task RemoveStaticWebAssets(ZipArchive nugetPackage, string filter) + { + var filterWithPath = $"staticwebassets/{filter}"; + + foreach (var entry in nugetPackage.Entries.ToArray()) + { + if (!entry.FullName.StartsWith(filterWithPath)) + continue; + + Logger.LogDebug("Removing file: {name}", entry.FullName); + entry.Delete(); + } + + var buildTargetEntry = nugetPackage.Entries.FirstOrDefault(x => + x.FullName == "build/Microsoft.AspNetCore.StaticWebAssets.props" + ); + + if (buildTargetEntry == null) + { + Logger.LogWarning("Unable to find Microsoft.AspNetCore.StaticWebAssets.props to remove file references"); + return; + } + + Logger.LogDebug("Removing file references"); + + await ModifyXmlInPackage(nugetPackage, buildTargetEntry, + document => document + .Descendants("StaticWebAsset") + .Where(x => + { + var relativePath = x.Element("RelativePath")!.Value; + return relativePath.StartsWith(filter); + }) + ); + } + + public async Task AddSourceFiles(ZipArchive nugetPackage, string[] files, Func buildPath) + { + foreach (var sourceFile in files) + { + var path = buildPath.Invoke(sourceFile); + + Logger.LogDebug("Adding additional files as src: {path}", path); + + await using var fs = File.Open( + sourceFile, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite + ); + + var entry = nugetPackage.CreateEntry(path); + await using var entryFs = entry.Open(); + + await fs.CopyToAsync(entryFs); + await entryFs.FlushAsync(); + + fs.Close(); + entryFs.Close(); + } + } +} \ No newline at end of file diff --git a/Resources/Scripts/Helpers/StartupClassDetector.cs b/Resources/Scripts/Helpers/StartupClassDetector.cs deleted file mode 100644 index cf76665d..00000000 --- a/Resources/Scripts/Helpers/StartupClassDetector.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Scripts.Helpers; - -public class StartupClassDetector -{ - public bool Check(string content, out string fullName) - { - var tree = CSharpSyntaxTree.ParseText(content); - var root = tree.GetCompilationUnitRoot(); - - var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); - var compilation = CSharpCompilation.Create("MyAnalysis", [tree], [mscorlib]); - - var model = compilation.GetSemanticModel(tree); - - var classDeclarations = root.DescendantNodes().OfType(); - - foreach (var classDecl in classDeclarations) - { - var symbol = model.GetDeclaredSymbol(classDecl); - - if(symbol == null) - continue; - - var hasAttribute = symbol.GetAttributes().Any(attribute => - { - if (attribute.AttributeClass == null) - return false; - - return attribute.AttributeClass.Name.Contains("PluginStartup"); - }); - - if (hasAttribute) - { - fullName = symbol.ContainingNamespace.ToDisplayString() + "." + classDecl.Identifier.ValueText; - return true; - } - } - - fullName = ""; - return false; - } -} \ No newline at end of file diff --git a/Resources/Scripts/Models/CsprojManifest.cs b/Resources/Scripts/Models/CsprojManifest.cs new file mode 100644 index 00000000..9d21fc6f --- /dev/null +++ b/Resources/Scripts/Models/CsprojManifest.cs @@ -0,0 +1,9 @@ +namespace Scripts.Models; + +public class CsprojManifest +{ + public bool IsPackable { get; set; } + public string Version { get; set; } + public string PackageId { get; set; } + public string[] PackageTags { get; set; } +} \ No newline at end of file diff --git a/Resources/Scripts/Models/NupkgManifest.cs b/Resources/Scripts/Models/NupkgManifest.cs new file mode 100644 index 00000000..4b2f3bc8 --- /dev/null +++ b/Resources/Scripts/Models/NupkgManifest.cs @@ -0,0 +1,8 @@ +namespace Scripts.Models; + +public class NupkgManifest +{ + public string Id { get; set; } + public string Version { get; set; } + public string[] Tags { get; set; } +} \ No newline at end of file diff --git a/Resources/Scripts/Program.cs b/Resources/Scripts/Program.cs index a255daf4..33c12965 100644 --- a/Resources/Scripts/Program.cs +++ b/Resources/Scripts/Program.cs @@ -1,5 +1,7 @@ using Cocona; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using MoonCore.Extensions; using Scripts.Commands; using Scripts.Helpers; @@ -8,8 +10,13 @@ Console.WriteLine(); var builder = CoconaApp.CreateBuilder(args); +builder.Logging.ClearProviders(); +builder.Logging.AddMoonCore(); + builder.Services.AddSingleton(); -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/Resources/Scripts/Scripts.csproj b/Resources/Scripts/Scripts.csproj index ff74cd5c..fd6b6464 100644 --- a/Resources/Scripts/Scripts.csproj +++ b/Resources/Scripts/Scripts.csproj @@ -9,5 +9,6 @@ + \ No newline at end of file