Finished compile time plugin loading. Refactored plugin loading. Extended build helper script

This commit is contained in:
2025-05-13 20:48:50 +02:00
parent 8126250d1a
commit a579dd4759
28 changed files with 1169 additions and 741 deletions

View File

@@ -0,0 +1,370 @@
using System.IO.Compression;
using System.Xml.Linq;
using Cocona;
using Scripts.Helpers;
namespace Scripts.Commands;
public class PackCommand
{
private readonly CommandHelper CommandHelper;
private readonly string TmpDir = "/tmp/mlbuild";
public PackCommand(CommandHelper commandHelper)
{
CommandHelper = commandHelper;
}
[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"
)
{
if (!Directory.Exists(solutionDirectory))
{
Console.WriteLine("The specified solution directory does not exist");
return;
}
if (!Directory.Exists(outputLocation))
Directory.CreateDirectory(outputLocation);
if (Directory.Exists(TmpDir))
Directory.Delete(TmpDir, true);
Directory.CreateDirectory(TmpDir);
// Find the project files
Console.WriteLine("Searching for projects inside the specified folder");
var csProjFiles = Directory.GetFiles(solutionDirectory, "*csproj", SearchOption.AllDirectories);
// Show the user
Console.WriteLine($"Found {csProjFiles.Length} project(s) to check:");
foreach (var csProjFile in csProjFiles)
Console.WriteLine($"- {Path.GetFullPath(csProjFile)}");
// Filter out project files which have specific tags specified
Console.WriteLine("Filtering projects by tags");
List<string> apiServerProjects = [];
List<string> frontendProjects = [];
List<string> sharedProjects = [];
foreach (var csProjFile in csProjFiles)
{
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)
{
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
Console.WriteLine("Building and packing api server project(s)");
foreach (var apiServerProject in apiServerProjects)
{
await BuildProject(
apiServerProject,
buildConfiguration
);
var nugetFilePath = await PackProject(
apiServerProject,
TmpDir,
buildConfiguration
);
var nugetPackage = ZipFile.Open(
nugetFilePath,
ZipArchiveMode.Update
);
await RemoveContentFiles(nugetPackage);
await CleanDependencies(nugetPackage);
Console.WriteLine("Finishing package and copying to output directory");
nugetPackage.Dispose();
File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true);
}
Console.WriteLine("Building and packing frontend projects");
foreach (var frontendProject in frontendProjects)
{
await BuildProject(
frontendProject,
buildConfiguration
);
var nugetFilePath = await PackProject(
frontendProject,
TmpDir,
buildConfiguration
);
var nugetPackage = ZipFile.Open(
nugetFilePath,
ZipArchiveMode.Update
);
foreach (var entry in nugetPackage.Entries.ToArray())
{
if (!entry.FullName.StartsWith("staticwebassets/_framework"))
continue;
Console.WriteLine($"Removing framework file: {entry.FullName}");
entry.Delete();
}
var buildTargetEntry = nugetPackage.Entries.FirstOrDefault(x =>
x.FullName == "build/Microsoft.AspNetCore.StaticWebAssets.props"
);
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)!;
additionalSrcFiles.AddRange(
Directory.GetFiles(basePath, "*.razor", SearchOption.AllDirectories)
);
additionalSrcFiles.AddRange(
Directory.GetFiles(basePath, "index.html", SearchOption.AllDirectories)
);
foreach (var additionalSrcFile in additionalSrcFiles)
{
var relativePath = "src/" + additionalSrcFile.Replace(basePath, "").Trim('/');
Console.WriteLine($"Adding additional files as src: {relativePath}");
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();
File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true);
}
Console.WriteLine("Building and packing shared projects");
foreach (var sharedProject in sharedProjects)
{
await BuildProject(
sharedProject,
buildConfiguration
);
var nugetFilePath = await PackProject(
sharedProject,
TmpDir,
buildConfiguration
);
File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true);
}
}
private async Task BuildProject(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
);
}
private async Task<string> PackProject(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(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

@@ -0,0 +1,414 @@
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 Scripts.Helpers;
namespace Scripts.Commands;
public class PreBuildCommand
{
private readonly CommandHelper CommandHelper;
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)
{
CommandHelper = commandHelper;
}
[Command("prebuild")]
public async Task Prebuild(
[Argument] string moonlightDir,
[Argument] string nugetDir
)
{
var dependencies = await GetDependenciesFromNuget(nugetDir);
Console.WriteLine("Following plugins found:");
foreach (var dependency in dependencies)
{
Console.WriteLine($"{dependency.Id} ({dependency.Version}) [{dependency.Tags}]");
}
var csProjFiles = Directory.GetFiles(moonlightDir, "*.csproj", SearchOption.AllDirectories);
try
{
Console.WriteLine("Adjusting csproj files");
foreach (var csProjFile in csProjFiles)
{
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)
{
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();
await RemoveDependencies(csProjFile);
await AddDependencies(csProjFile, dependenciesToAdd);
}
Console.WriteLine("Restoring projects");
foreach (var csProjFile in csProjFiles)
{
await RestoreProject(csProjFile, nugetDir);
}
Console.WriteLine("Generating plugin startup");
string[] validTags = ["apiserver", "frontend"];
foreach (var currentTag in validTags)
{
Console.WriteLine($"Checking for '{currentTag}' projects");
foreach (var csProjFile in csProjFiles)
{
var tags = await GetTagsFromCsproj(csProjFile);
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
.Where(x => x.Tags.Contains(currentTag))
.ToArray();
var classPaths = await FindStartupClasses(currentDeps);
var code = new StringBuilder();
code.AppendLine(GeneratedStart);
foreach (var path in classPaths)
code.AppendLine($"pluginStartups.Add(new global::{path}());");
code.AppendLine(GeneratedEnd);
var filesToSearch = Directory.GetFiles(
Path.GetDirectoryName(csProjFile)!,
"*.cs",
SearchOption.AllDirectories
);
foreach (var file in filesToSearch)
{
var content = await File.ReadAllTextAsync(file);
if (!content.Contains(GeneratedHook, StringComparison.InvariantCultureIgnoreCase))
continue;
Console.WriteLine($"Injecting generated code to: {Path.GetFullPath(file)}");
content = content.Replace(
GeneratedHook,
code.ToString(),
StringComparison.InvariantCultureIgnoreCase
);
await File.WriteAllTextAsync(file, content);
}
}
}
}
catch (Exception)
{
Console.WriteLine("An error occured while prebuilding moonlight. Removing csproj modifications");
foreach (var csProjFile in csProjFiles)
await RemoveDependencies(csProjFile);
await RemoveGeneratedCode(moonlightDir);
throw;
}
}
[Command("prebuild-reset")]
public async Task PrebuildReset(
[Argument] string moonlightDir
)
{
var csProjFiles = Directory.GetFiles(moonlightDir, "*.csproj", SearchOption.AllDirectories);
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<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);
dependencies.Add(dependency);
}
return dependencies.ToArray();
}
private async Task<Dependency> GetDependencyFromPackage(string path)
{
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
);
}
private async Task<string[]> FindStartupClasses(Dependency[] dependencies)
{
var result = new List<string>();
var nugetPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".nuget",
"packages"
);
var filesToScan = dependencies.SelectMany(dependency =>
{
var dependencySrcPath = Path.Combine(nugetPath, dependency.Id.ToLower(), dependency.Version, "src");
Console.WriteLine($"Checking {dependencySrcPath}");
if (!Directory.Exists(dependencySrcPath))
return [];
return Directory.GetFiles(dependencySrcPath, "*.cs", SearchOption.AllDirectories);
}
).ToArray();
var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
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)
{
var filesToSearch = Directory.GetFiles(
dir,
"*.cs",
SearchOption.AllDirectories
);
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))
continue;
var startIndex = content.IndexOf(GeneratedStart, StringComparison.InvariantCultureIgnoreCase);
var endIndex = content.IndexOf(GeneratedEnd, startIndex, StringComparison.InvariantCultureIgnoreCase) +
GeneratedEnd.Length;
var cutOut = content.Substring(startIndex, endIndex - startIndex);
content = content.Replace(cutOut, GeneratedHook);
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

@@ -1,60 +0,0 @@
using System.IO.Compression;
using System.Text.RegularExpressions;
namespace Scripts.Functions;
public static class ContentFunctions
{
public static async Task Run(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Please provide the path to a nuget file and at least one regex expression");
return;
}
var nugetPath = args[0];
var regexs = args
.Skip(1)
.Select(x => new Regex(x))
.ToArray();
Console.WriteLine(string.Join(", ", args
.Skip(1)
.Select(x => new Regex(x))));
if (!File.Exists(nugetPath))
{
Console.WriteLine("The provided file does not exist");
return;
}
Console.WriteLine("Modding nuget package...");
using var zipFile = ZipFile.Open(nugetPath, ZipArchiveMode.Update);
foreach (var zipArchiveEntry in zipFile.Entries)
{
Console.WriteLine(zipArchiveEntry.FullName);
}
Console.WriteLine("Searching for files to remove");
var files = zipFile.Entries
.Where(x => x.FullName.Trim('/').StartsWith("content"))
.Where(x =>
{
var name = x.FullName
.Replace("contentFiles/", "")
.Replace("content/", "");
Console.WriteLine(name);
return regexs.Any(y => y.IsMatch(name));
})
.ToArray();
Console.WriteLine($"Found {files.Length} file(s) to remove");
foreach (var file in files)
file.Delete();
}
}

View File

@@ -1,47 +0,0 @@
using System.IO.Compression;
namespace Scripts.Functions;
public static class SrcFunctions
{
public static async Task Run(string[] args)
{
if (args.Length != 3)
{
Console.WriteLine("Please provide the path to a nuget file, a search pattern and a path");
return;
}
var nugetPath = args[0];
var path = args[1];
var pattern = args[2];
if (!File.Exists(nugetPath))
{
Console.WriteLine("The provided file does not exist");
return;
}
Console.WriteLine("Modding nuget package...");
using var zipFile = ZipFile.Open(nugetPath, ZipArchiveMode.Update);
var filesToAdd = Directory.GetFiles(path, pattern, SearchOption.AllDirectories);
foreach (var file in filesToAdd)
{
var name = file.Replace(path, "").Replace("\\", "/");
Console.WriteLine($"{file} => /src/{name}");
var entry = zipFile.CreateEntry($"src/{name}");
await using var entryStream = entry.Open();
await using var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await fs.CopyToAsync(entryStream);
fs.Close();
await entryStream.FlushAsync();
entryStream.Close();
}
}
}

View File

@@ -1,93 +0,0 @@
using System.IO.Compression;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace Scripts.Functions;
public static class StaticWebAssetsFunctions
{
public static async Task Run(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Please provide the path to a nuget file and at least one regex expression");
return;
}
var nugetPath = args[0];
var regexs = args
.Skip(1)
.Select(x => new Regex(x))
.ToArray();
if (!File.Exists(nugetPath))
{
Console.WriteLine("The provided file does not exist");
return;
}
Console.WriteLine("Modding nuget package...");
using var zipFile = ZipFile.Open(nugetPath, ZipArchiveMode.Update);
Console.WriteLine("Searching for files to remove");
var files = zipFile.Entries
.Where(x => x.FullName.Trim('/').StartsWith("staticwebassets"))
.Where(x =>
{
var name = x.FullName.Replace("staticwebassets/", "");
return regexs.Any(y => y.IsMatch(name));
})
.ToArray();
Console.WriteLine($"Found {files.Length} file(s) to remove");
foreach (var file in files)
file.Delete();
Console.WriteLine("Modifying static web assets build target");
var oldBuildTargetEntry = zipFile
.Entries
.FirstOrDefault(x => x.FullName == "build/Microsoft.AspNetCore.StaticWebAssets.props");
if (oldBuildTargetEntry == null)
{
Console.WriteLine("Build target file not found in nuget packages");
return;
}
await using var oldBuildTargetStream = oldBuildTargetEntry.Open();
var contentXml = await XDocument.LoadAsync(
oldBuildTargetStream,
LoadOptions.None,
CancellationToken.None
);
oldBuildTargetStream.Close();
oldBuildTargetEntry.Delete();
var assetRefsToRemove = contentXml
.Descendants("StaticWebAsset")
.Where(asset =>
{
var element = asset.Element("RelativePath");
if (element == null)
return false;
return regexs.Any(y => y.IsMatch(element.Value));
})
.ToArray();
foreach (var asset in assetRefsToRemove)
asset.Remove();
var newBuildTargetEntry = zipFile.CreateEntry("build/Microsoft.AspNetCore.StaticWebAssets.props");
await using var newBuildTargetStream = newBuildTargetEntry.Open();
await contentXml.SaveAsync(newBuildTargetStream, SaveOptions.None, CancellationToken.None);
newBuildTargetStream.Close();
}
}

View File

@@ -1,75 +0,0 @@
using System.Xml.Linq;
namespace Scripts.Functions;
public class TagsFunctions
{
public static async Task Run(string[] args)
{
if (args.Length < 3)
{
Console.WriteLine("You need to specify a directory, tag and at least one command");
return;
}
var rootDir = args[0];
var tag = args[1];
var commands = args.Skip(2).ToArray();
var csprojFiles = Directory.GetFiles(
rootDir,
"*.csproj",
SearchOption.AllDirectories
);
foreach (var csprojFile in csprojFiles)
{
try
{
// Load the .csproj file
var csprojXml = XElement.Load(csprojFile);
// Check if <PackageTags> exists within the .csproj file
var packageTagsElement = csprojXml.Descendants("PackageTags").FirstOrDefault();
if (packageTagsElement != null)
{
if(!packageTagsElement.Value.Contains(tag))
continue;
var projectName = Path.GetFileName(Path.GetDirectoryName(csprojFile))!;
var projectFile = Path.GetFileName(csprojFile);
foreach (var command in commands)
{
// Replace PROJECT_NAME and PROJECT_FILE with the actual values
var bashCommand = command.Replace("PROJECT_NAME", projectName).Replace("PROJECT_FILE", projectFile);
RunBashCommand(bashCommand);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error processing {csprojFile}: {ex.Message}");
}
}
}
static void RunBashCommand(string command)
{
try
{
var processStartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "bash",
Arguments = $"-c \"{command}\"",
};
var process = System.Diagnostics.Process.Start(processStartInfo);
process!.WaitForExit();
}
catch (Exception ex)
{
Console.WriteLine($"Error running bash command: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Diagnostics;
namespace Scripts.Helpers;
public class CommandHelper
{
public async Task Run(string program, string arguments, string? workingDir = null)
{
var process = await RunRaw(program, arguments, workingDir);
await process.WaitForExitAsync();
if (process.ExitCode != 0)
throw new Exception($"The command '{program} {arguments}' failed with exit code: {process.ExitCode}");
}
private Task<Process> RunRaw(string program, string arguments, string? workingDir = null)
{
var psi = new ProcessStartInfo()
{
FileName = program,
Arguments = arguments,
WorkingDirectory = string.IsNullOrEmpty(workingDir) ? Directory.GetCurrentDirectory() : workingDir
};
var process = Process.Start(psi)!;
return Task.FromResult(process);
}
}

View File

@@ -0,0 +1,46 @@
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

@@ -1,29 +1,19 @@
using Scripts.Functions;
using Cocona;
using Microsoft.Extensions.DependencyInjection;
using Scripts.Commands;
using Scripts.Helpers;
if (args.Length == 0)
{
Console.WriteLine("You need to specify a module to run");
return;
}
Console.WriteLine("Moonlight Build Helper Script");
Console.WriteLine();
var module = args[0];
var moduleArgs = args.Skip(1).ToArray();
var builder = CoconaApp.CreateBuilder(args);
switch (module)
{
case "staticWebAssets":
await StaticWebAssetsFunctions.Run(moduleArgs);
break;
case "content":
await ContentFunctions.Run(moduleArgs);
break;
case "src":
await SrcFunctions.Run(moduleArgs);
break;
case "tags":
await TagsFunctions.Run(moduleArgs);
break;
default:
Console.WriteLine($"No module named {module} found");
break;
}
builder.Services.AddSingleton<CommandHelper>();
builder.Services.AddSingleton<StartupClassDetector>();
var app = builder.Build();
app.AddCommands<PackCommand>();
app.AddCommands<PreBuildCommand>();
await app.RunAsync();

View File

@@ -1,10 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
<ItemGroup>
<Folder Include="Modules\" />
<Folder Include="outputDir\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Cocona" Version="2.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" />
</ItemGroup>
</Project>