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.IO.Compression;
using System.Xml.Linq; using System.Xml.Linq;
using Cocona; using Cocona;
using Microsoft.Extensions.Logging;
using Scripts.Helpers; using Scripts.Helpers;
namespace Scripts.Commands; namespace Scripts.Commands;
public class PackCommand public class PackCommand
{ {
private readonly CommandHelper CommandHelper;
private readonly string TmpDir = "/tmp/mlbuild"; 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")] [Command("pack", Description = "Packs the specified folder/solution into nuget packages")]
public async Task Pack( public async Task Pack(
[Argument] string solutionDirectory, [Argument] string solutionDirectory,
[Argument] string outputLocation, [Argument] string outputLocation,
[Option] string buildConfiguration = "Debug", [Option] string buildConfiguration = "Debug"
[Option] string? nugetDir = null
) )
{ {
if (!Directory.Exists(solutionDirectory)) if (!Directory.Exists(solutionDirectory))
{ {
Console.WriteLine("The specified solution directory does not exist"); Logger.LogError("The specified solution directory does not exist");
return; return;
} }
@@ -38,74 +48,55 @@ public class PackCommand
Directory.CreateDirectory(TmpDir); Directory.CreateDirectory(TmpDir);
// Find the project files // Find the project files
Console.WriteLine("Searching for projects inside the specified folder"); Logger.LogInformation("Searching for projects inside the specified folder");
var csProjFiles = Directory.GetFiles(solutionDirectory, "*csproj", SearchOption.AllDirectories);
var projects = await CsprojHelper.FindProjectsInPath(solutionDirectory, ValidTags);
// Show the user // 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) foreach (var path in projects.Keys)
Console.WriteLine($"- {Path.GetFullPath(csProjFile)}"); Logger.LogInformation("- {path}", Path.GetFullPath(path));
// Filter out project files which have specific tags specified // Filter out project files which have specific tags specified
Console.WriteLine("Filtering projects by tags"); Logger.LogInformation("Filtering projects by tags");
List<string> apiServerProjects = []; var apiServerProjects = projects
List<string> frontendProjects = []; .Where(x => x.Value.PackageTags.Contains("apiserver", StringComparer.InvariantCultureIgnoreCase))
List<string> sharedProjects = []; .ToArray();
foreach (var csProjFile in csProjFiles) var frontendProjects = projects
{ .Where(x => x.Value.PackageTags.Contains("frontend", StringComparer.InvariantCultureIgnoreCase))
await using var fs = File.Open( .ToArray();
csProjFile,
FileMode.Open,
FileAccess.ReadWrite,
FileShare.ReadWrite
);
var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None); var sharedProjects = projects
fs.Close(); .Where(x => x.Value.PackageTags.Contains("shared", StringComparer.InvariantCultureIgnoreCase))
.ToArray();
// Search for tag definitions Logger.LogInformation(
var packageTagsElements = document.Descendants("PackageTags").ToArray(); "Found {apiServerCount} api server project(s), {frontendCount} frontend project(s) and {sharedCount} shared project(s)",
apiServerProjects.Length,
if (packageTagsElements.Length == 0) frontendProjects.Length,
{ sharedProjects.Length
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)");
// Now build all these projects so we can pack them // 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) foreach (var apiServerProject in apiServerProjects)
{ {
await BuildProject( var csProjectFile = apiServerProject.Key;
apiServerProject, var manifest = apiServerProject.Value;
buildConfiguration,
nugetDir await CsprojHelper.Build(
csProjectFile,
buildConfiguration
); );
var nugetFilePath = await PackProject( var nugetFilePath = await CsprojHelper.Pack(
apiServerProject, csProjectFile,
TmpDir, TmpDir,
buildConfiguration, buildConfiguration
nugetDir
); );
var nugetPackage = ZipFile.Open( var nugetPackage = ZipFile.Open(
@@ -113,31 +104,41 @@ public class PackCommand
ZipArchiveMode.Update ZipArchiveMode.Update
); );
await RemoveContentFiles(nugetPackage); await NupkgHelper.RemoveContentFiles(nugetPackage);
await CleanDependencies(nugetPackage); // We don't want to clean moonlight references when we are packing moonlight,
// as it would remove references to its own shared project
Console.WriteLine("Finishing package and copying to output directory"); if (!manifest.PackageId.StartsWith("Moonlight."))
await NupkgHelper.CleanDependencies(nugetPackage, "Moonlight.");
Logger.LogInformation("Finishing package and copying to output directory");
nugetPackage.Dispose(); 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) foreach (var frontendProject in frontendProjects)
{ {
await BuildProject( var csProjectFile = frontendProject.Key;
frontendProject, var manifest = frontendProject.Value;
buildConfiguration,
nugetDir await CsprojHelper.Build(
csProjectFile,
buildConfiguration
); );
var nugetFilePath = await PackProject( var nugetFilePath = await CsprojHelper.Pack(
frontendProject, csProjectFile,
TmpDir, TmpDir,
buildConfiguration, buildConfiguration
nugetDir
); );
var nugetPackage = ZipFile.Open( var nugetPackage = ZipFile.Open(
@@ -145,48 +146,20 @@ public class PackCommand
ZipArchiveMode.Update ZipArchiveMode.Update
); );
foreach (var entry in nugetPackage.Entries.ToArray()) await NupkgHelper.RemoveStaticWebAssets(nugetPackage, "_framework");
{ await NupkgHelper.RemoveStaticWebAssets(nugetPackage, "css/style.min.css");
if (!entry.FullName.StartsWith("staticwebassets/_framework")) await NupkgHelper.RemoveContentFiles(nugetPackage);
continue;
Console.WriteLine($"Removing framework file: {entry.FullName}"); // We don't want to clean moonlight references when we are packing moonlight,
entry.Delete(); // as it would remove references to its own shared project
}
var buildTargetEntry = nugetPackage.Entries.FirstOrDefault(x => if (!manifest.PackageId.StartsWith("Moonlight."))
x.FullName == "build/Microsoft.AspNetCore.StaticWebAssets.props" 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 // Pack razor and html files into src folder
var additionalSrcFiles = new List<string>(); var additionalSrcFiles = new List<string>();
var basePath = Path.GetDirectoryName(frontendProject)!; var basePath = Path.GetDirectoryName(csProjectFile)!;
additionalSrcFiles.AddRange( additionalSrcFiles.AddRange(
Directory.GetFiles(basePath, "*.razor", SearchOption.AllDirectories) Directory.GetFiles(basePath, "*.razor", SearchOption.AllDirectories)
@@ -196,181 +169,58 @@ public class PackCommand
Directory.GetFiles(basePath, "index.html", SearchOption.AllDirectories) Directory.GetFiles(basePath, "index.html", SearchOption.AllDirectories)
); );
foreach (var additionalSrcFile in additionalSrcFiles) await NupkgHelper.AddSourceFiles(
{ nugetPackage,
var relativePath = "src/" + additionalSrcFile.Replace(basePath, "").Trim('/'); additionalSrcFiles.ToArray(),
file => "src/" + file.Replace(basePath, "").Trim('/')
);
Console.WriteLine($"Adding additional files as src: {relativePath}"); Logger.LogInformation("Finishing package and copying to output directory");
await using var fs = File.Open(
additionalSrcFile,
FileMode.Open,
FileAccess.ReadWrite,
FileShare.ReadWrite
);
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(); 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) foreach (var sharedProject in sharedProjects)
{ {
await BuildProject( var csProjectFile = sharedProject.Key;
sharedProject, var manifest = sharedProject.Value;
buildConfiguration,
nugetDir await CsprojHelper.Build(
csProjectFile,
buildConfiguration
); );
var nugetFilePath = await PackProject( var nugetFilePath = await CsprojHelper.Pack(
sharedProject, csProjectFile,
TmpDir, TmpDir,
buildConfiguration, buildConfiguration
nugetDir
); );
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) // We don't want to clean moonlight references when we are packing moonlight,
{ // as it would remove references to its own shared project
var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!);
var fileName = Path.GetFileName(file);
await CommandHelper.Run( if (!manifest.PackageId.StartsWith("Moonlight."))
"/usr/bin/dotnet", await NupkgHelper.CleanDependencies(nugetPackage, "Moonlight.");
$"build {fileName} --configuration {configuration}" + (string.IsNullOrEmpty(nugetDir) ? "" : $" --source {nugetDir}"),
basePath
);
}
private async Task<string> PackProject(string file, string output, string configuration, string? nugetDir) nugetPackage.Dispose();
{
var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!);
var fileName = Path.GetFileName(file);
await CommandHelper.Run( File.Move(
"/usr/bin/dotnet", nugetFilePath,
$"pack {fileName} --output {output} --configuration {configuration}" + (string.IsNullOrEmpty(nugetDir) ? "" : $" --source {nugetDir}"), Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)),
basePath true
);
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();
var metadata = document.Root!.Element(ns + "metadata")!;
var id = metadata.Element(ns + "id")!.Value;
// 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");
}
); );
} }
} }

View File

@@ -2,108 +2,119 @@ using System.IO.Compression;
using System.Text; using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
using Cocona; using Cocona;
using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Scripts.Helpers; using Scripts.Helpers;
using Scripts.Models;
namespace Scripts.Commands; namespace Scripts.Commands;
public class PreBuildCommand public class PreBuildCommand
{ {
private readonly CommandHelper CommandHelper; private readonly NupkgHelper NupkgHelper;
private readonly CsprojHelper CsprojHelper;
private readonly CodeHelper CodeHelper;
private readonly ILogger<PreBuildCommand> Logger;
private const string GeneratedStart = "// MLBUILD Generated Start"; private const string GeneratedStart = "// MLBUILD Generated Start";
private const string GeneratedEnd = "// MLBUILD Generated End"; private const string GeneratedEnd = "// MLBUILD Generated End";
private const string GeneratedHook = "// MLBUILD_PLUGIN_STARTUP_HERE"; 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<PreBuildCommand> logger
)
{ {
CommandHelper = commandHelper; CsprojHelper = csprojHelper;
NupkgHelper = nupkgHelper;
CodeHelper = codeHelper;
Logger = logger;
} }
[Command("prebuild")] [Command("prebuild")]
public async Task Prebuild( public async Task Prebuild(
[Argument] string moonlightDir, [Argument] string moonlightDirectory,
[Argument] string nugetDir [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 try
{ {
Console.WriteLine("Adjusting csproj files"); Logger.LogInformation("Adjusting csproj files");
foreach (var csProjFile in csProjFiles)
foreach (var project in projects)
{ {
var csProjectPath = project.Key;
await using var fs = File.Open( await using var fs = File.Open(
csProjFile, csProjectPath,
FileMode.Open, FileMode.Open,
FileAccess.ReadWrite, FileAccess.ReadWrite,
FileShare.ReadWrite FileShare.ReadWrite
); );
var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None); var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None);
fs.Close(); fs.Position = 0;
// Search for tag definitions var dependenciesToAdd = nugetManifests
var packageTagsElements = document.Descendants("PackageTags").ToArray(); .Where(x => x.Tags.Any(tag =>
project.Value.PackageTags.Contains(tag, StringComparer.InvariantCultureIgnoreCase)))
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))
.ToArray(); .ToArray();
await RemoveDependencies(csProjFile); await CsprojHelper.CleanDependencies(document, "MoonlightBuildDeps");
await AddDependencies(csProjFile, dependenciesToAdd); 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"); Logger.LogInformation("Restoring projects");
foreach (var csProjFile in csProjFiles)
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 project in projectsWithTag)
foreach (var currentTag in validTags)
{
Console.WriteLine($"Checking for '{currentTag}' projects");
foreach (var csProjFile in csProjFiles)
{ {
var tags = await GetTagsFromCsproj(csProjFile); var csProjectPath = project.Key;
if (string.IsNullOrEmpty(tags)) var currentDependencies = nugetManifests
{
Console.WriteLine($"No package tags found in {Path.GetFullPath(csProjFile)}. Skipping it");
continue;
}
if (!tags.Contains(currentTag))
continue;
var currentDeps = dependencies
.Where(x => x.Tags.Contains(currentTag)) .Where(x => x.Tags.Contains(currentTag))
.ToArray(); .ToArray();
var classPaths = await FindStartupClasses(currentDeps); var classPaths = await FindStartupClasses(currentDependencies);
var code = new StringBuilder(); var code = new StringBuilder();
@@ -112,10 +123,10 @@ public class PreBuildCommand
foreach (var path in classPaths) foreach (var path in classPaths)
code.AppendLine($"pluginStartups.Add(new global::{path}());"); code.AppendLine($"pluginStartups.Add(new global::{path}());");
code.AppendLine(GeneratedEnd); code.Append(GeneratedEnd);
var filesToSearch = Directory.GetFiles( var filesToSearch = Directory.GetFiles(
Path.GetDirectoryName(csProjFile)!, Path.GetDirectoryName(csProjectPath)!,
"*.cs", "*.cs",
SearchOption.AllDirectories SearchOption.AllDirectories
); );
@@ -127,7 +138,7 @@ public class PreBuildCommand
if (!content.Contains(GeneratedHook, StringComparison.InvariantCultureIgnoreCase)) if (!content.Contains(GeneratedHook, StringComparison.InvariantCultureIgnoreCase))
continue; continue;
Console.WriteLine($"Injecting generated code to: {Path.GetFullPath(file)}"); Logger.LogInformation("Injecting generated code to: {path}", Path.GetFullPath(file));
content = content.Replace( content = content.Replace(
GeneratedHook, GeneratedHook,
@@ -142,12 +153,15 @@ public class PreBuildCommand
} }
catch (Exception) 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) foreach (var project in projects)
await RemoveDependencies(csProjFile); {
await CsprojHelper.CleanDependencies(project.Key, "MoonlightBuildDeps");
await RemoveGeneratedCode(moonlightDir); var path = Path.GetDirectoryName(project.Key)!;
await RemoveGeneratedCode(path);
}
throw; throw;
} }
@@ -158,220 +172,68 @@ public class PreBuildCommand
[Argument] string moonlightDir [Argument] string moonlightDir
) )
{ {
var csProjFiles = Directory.GetFiles(moonlightDir, "*.csproj", SearchOption.AllDirectories); var projects = await CsprojHelper.FindProjectsInPath(moonlightDir, ValidTags);
Console.WriteLine("Reverting csproj changes"); Logger.LogInformation("Reverting csproj changes");
foreach (var csProjFile in csProjFiles) foreach (var project in projects)
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<Dependency[]> GetDependenciesFromNuget(string nugetDir)
{
var nugetFiles = Directory.GetFiles(nugetDir, "*.nupkg", SearchOption.AllDirectories);
var dependencies = new List<Dependency>();
foreach (var nugetFile in nugetFiles)
{ {
var dependency = await GetDependencyFromPackage(nugetFile); Logger.LogInformation("Removing dependencies: {project}", project.Key);
dependencies.Add(dependency); 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<Dependency> GetDependencyFromPackage(string path) private async Task<NupkgManifest[]> GetNugetManifests(string nugetDir)
{ {
using var nugetPackage = ZipFile.Open(path, ZipArchiveMode.Read); var nugetFiles = Directory.GetFiles(
nugetDir,
var nuspecEntry = nugetPackage.Entries.First(x => x.Name.EndsWith(".nuspec")); "*.nupkg",
await using var nuspecFs = nuspecEntry.Open(); SearchOption.AllDirectories
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 manifests = new List<NupkgManifest>();
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<string[]> FindStartupClasses(Dependency[] dependencies) private async Task<string[]> FindStartupClasses(NupkgManifest[] dependencies)
{ {
var result = new List<string>();
var nugetPath = Path.Combine( var nugetPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".nuget", ".nuget",
"packages" "packages"
); );
var filesToScan = dependencies.SelectMany(dependency => var filesToScan = dependencies
.SelectMany(dependency =>
{ {
var dependencySrcPath = Path.Combine(nugetPath, dependency.Id.ToLower(), dependency.Version, "src"); var dependencySrcPath = Path.Combine(nugetPath, dependency.Id.ToLower(), dependency.Version, "src");
Console.WriteLine($"Checking {dependencySrcPath}"); Logger.LogDebug("Checking {dependencySrcPath}", dependencySrcPath);
if (!Directory.Exists(dependencySrcPath)) if (!Directory.Exists(dependencySrcPath))
return []; return [];
return Directory.GetFiles(dependencySrcPath, "*.cs", SearchOption.AllDirectories); return Directory.GetFiles(dependencySrcPath, "*.cs", SearchOption.AllDirectories);
} })
).ToArray(); .ToArray();
var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); return await CodeHelper.FindPluginStartups(filesToScan);
var trees = new List<SyntaxTree>();
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<ClassDeclarationSyntax>();
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<string> 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;
} }
private async Task RemoveGeneratedCode(string dir) private async Task RemoveGeneratedCode(string dir)
@@ -384,10 +246,6 @@ public class PreBuildCommand
foreach (var file in filesToSearch) foreach (var file in filesToSearch)
{ {
// We dont want to replace ourself
if (file.Contains("PreBuildCommand.cs"))
continue;
var content = await File.ReadAllTextAsync(file); var content = await File.ReadAllTextAsync(file);
if (!content.Contains(GeneratedStart) || !content.Contains(GeneratedEnd)) if (!content.Contains(GeneratedStart) || !content.Contains(GeneratedEnd))
@@ -404,11 +262,4 @@ public class PreBuildCommand
await File.WriteAllTextAsync(file, content); await File.WriteAllTextAsync(file, content);
} }
} }
private record Dependency
{
public string Id { get; set; }
public string Version { get; set; }
public string Tags { get; set; }
}
} }

View File

@@ -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<CodeHelper> Logger;
public CodeHelper(ILogger<CodeHelper> logger)
{
Logger = logger;
}
public async Task<string[]> FindPluginStartups(string[] filesToSearch)
{
var result = new List<string>();
var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
var trees = new List<SyntaxTree>();
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<ClassDeclarationSyntax>();
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();
}
}

View File

@@ -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<CsprojHelper> Logger;
private readonly CommandHelper CommandHelper;
public CsprojHelper(ILogger<CsprojHelper> 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<CsprojManifest> 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<CsprojManifest> Read(Stream stream)
{
var xmlDocument = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
return await Read(xmlDocument);
}
public Task<CsprojManifest> 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<string> 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<FrozenDictionary<string, CsprojManifest>> FindProjectsInPath(string path, string[] validTags)
{
var projectFiles = Directory.GetFiles(
path,
"*.csproj",
SearchOption.AllDirectories
);
var projects = new Dictionary<string, CsprojManifest>();
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();
}
}

View File

@@ -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<NupkgHelper> Logger;
public NupkgHelper(ILogger<NupkgHelper> logger)
{
Logger = logger;
}
public async Task<NupkgManifest?> 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<ZipArchiveEntry> ModifyXmlInPackage(
ZipArchive nugetPackage,
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 = 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<string, string> 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();
}
}
}

View File

@@ -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<ClassDeclarationSyntax>();
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;
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -1,5 +1,7 @@
using Cocona; using Cocona;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MoonCore.Extensions;
using Scripts.Commands; using Scripts.Commands;
using Scripts.Helpers; using Scripts.Helpers;
@@ -8,8 +10,13 @@ Console.WriteLine();
var builder = CoconaApp.CreateBuilder(args); var builder = CoconaApp.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddMoonCore();
builder.Services.AddSingleton<CommandHelper>(); builder.Services.AddSingleton<CommandHelper>();
builder.Services.AddSingleton<StartupClassDetector>(); builder.Services.AddSingleton<NupkgHelper>();
builder.Services.AddSingleton<CsprojHelper>();
builder.Services.AddSingleton<CodeHelper>();
var app = builder.Build(); var app = builder.Build();

View File

@@ -9,5 +9,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Cocona" Version="2.2.0" /> <PackageReference Include="Cocona" Version="2.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" />
<PackageReference Include="MoonCore" Version="1.8.6" />
</ItemGroup> </ItemGroup>
</Project> </Project>