Cleaned up scripts project

This commit is contained in:
2025-05-17 18:04:59 +02:00
parent 9dc77e6dde
commit d4a7600c14
10 changed files with 754 additions and 578 deletions

View File

@@ -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<PackCommand> Logger;
private readonly CsprojHelper CsprojHelper;
private readonly NupkgHelper NupkgHelper;
public PackCommand(CommandHelper commandHelper)
private readonly string[] ValidTags = ["apiserver", "frontend", "shared"];
public PackCommand(
ILogger<PackCommand> 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<string> apiServerProjects = [];
List<string> frontendProjects = [];
List<string> 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<string>();
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<string> 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<ZipArchiveEntry> ModifyXmlInPackage(
ZipArchive archive,
ZipArchiveEntry entry,
Func<XDocument, IEnumerable<XElement>> 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
);
}
}