From 8126250d1aad77a49081ab4d8763a09f07a8d45a Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Sun, 11 May 2025 22:26:05 +0200 Subject: [PATCH 1/9] Implemented tag based run helper script --- Resources/Scripts/Functions/TagsFunctions.cs | 75 ++++++++++++++++++++ Resources/Scripts/Program.cs | 3 + 2 files changed, 78 insertions(+) create mode 100644 Resources/Scripts/Functions/TagsFunctions.cs diff --git a/Resources/Scripts/Functions/TagsFunctions.cs b/Resources/Scripts/Functions/TagsFunctions.cs new file mode 100644 index 00000000..290755ff --- /dev/null +++ b/Resources/Scripts/Functions/TagsFunctions.cs @@ -0,0 +1,75 @@ +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 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}"); + } + } +} \ No newline at end of file diff --git a/Resources/Scripts/Program.cs b/Resources/Scripts/Program.cs index a0dbec12..aa258951 100644 --- a/Resources/Scripts/Program.cs +++ b/Resources/Scripts/Program.cs @@ -20,6 +20,9 @@ switch (module) case "src": await SrcFunctions.Run(moduleArgs); break; + case "tags": + await TagsFunctions.Run(moduleArgs); + break; default: Console.WriteLine($"No module named {module} found"); break; From a579dd4759973de22079266f1b32fd4edbf14e8a Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Tue, 13 May 2025 20:48:50 +0200 Subject: [PATCH 2/9] Finished compile time plugin loading. Refactored plugin loading. Extended build helper script --- Moonlight.ApiServer/Dockerfile | 7 +- .../Http/Controllers/FrontendController.cs | 20 +- .../Implementations/Startup/CoreStartup.cs | 24 +- .../Interfaces/Startup/IPluginStartup.cs | 8 - .../Models/FrontendConfigurationOption.cs | 7 + .../Moonlight.ApiServer.csproj | 130 +++--- Moonlight.ApiServer/Plugins/IPluginStartup.cs | 8 + .../Plugins/PluginStartupAttribute.cs | 7 + .../Services/FrontendService.cs | 65 +-- Moonlight.ApiServer/Startup.cs | 135 ++---- .../Implementations/CoreStartup.cs | 5 +- Moonlight.Client/Interfaces/IPluginStartup.cs | 9 - Moonlight.Client/Moonlight.Client.csproj | 121 +++-- Moonlight.Client/Plugins/IPluginStartup.cs | 9 + .../Plugins/PluginStartupAttribute.cs | 7 + Moonlight.Client/Startup.cs | 107 ++--- Moonlight.Client/wwwroot/js/moonlight.js | 9 + Moonlight.Shared/Moonlight.Shared.csproj | 40 +- Resources/Scripts/Commands/PackCommand.cs | 370 ++++++++++++++++ Resources/Scripts/Commands/PreBuildCommand.cs | 414 ++++++++++++++++++ .../Scripts/Functions/ContentFunctions.cs | 60 --- Resources/Scripts/Functions/SrcFunctions.cs | 47 -- .../Functions/StaticWebAssetsFunctions.cs | 93 ---- Resources/Scripts/Functions/TagsFunctions.cs | 75 ---- Resources/Scripts/Helpers/CommandHelper.cs | 30 ++ .../Scripts/Helpers/StartupClassDetector.cs | 46 ++ Resources/Scripts/Program.cs | 42 +- Resources/Scripts/Scripts.csproj | 15 +- 28 files changed, 1169 insertions(+), 741 deletions(-) delete mode 100644 Moonlight.ApiServer/Interfaces/Startup/IPluginStartup.cs create mode 100644 Moonlight.ApiServer/Models/FrontendConfigurationOption.cs create mode 100644 Moonlight.ApiServer/Plugins/IPluginStartup.cs create mode 100644 Moonlight.ApiServer/Plugins/PluginStartupAttribute.cs delete mode 100644 Moonlight.Client/Interfaces/IPluginStartup.cs create mode 100644 Moonlight.Client/Plugins/IPluginStartup.cs create mode 100644 Moonlight.Client/Plugins/PluginStartupAttribute.cs create mode 100644 Resources/Scripts/Commands/PackCommand.cs create mode 100644 Resources/Scripts/Commands/PreBuildCommand.cs delete mode 100644 Resources/Scripts/Functions/ContentFunctions.cs delete mode 100644 Resources/Scripts/Functions/SrcFunctions.cs delete mode 100644 Resources/Scripts/Functions/StaticWebAssetsFunctions.cs delete mode 100644 Resources/Scripts/Functions/TagsFunctions.cs create mode 100644 Resources/Scripts/Helpers/CommandHelper.cs create mode 100644 Resources/Scripts/Helpers/StartupClassDetector.cs diff --git a/Moonlight.ApiServer/Dockerfile b/Moonlight.ApiServer/Dockerfile index ca790bd6..baa1e81f 100644 --- a/Moonlight.ApiServer/Dockerfile +++ b/Moonlight.ApiServer/Dockerfile @@ -1,4 +1,9 @@ -# Prepare runtime docker image +# +# OUTDATED +# Use https://github.com/Moonlight-Panel/Deploy +# + +# Prepare runtime docker image FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-chiseled AS base WORKDIR /app diff --git a/Moonlight.ApiServer/Http/Controllers/FrontendController.cs b/Moonlight.ApiServer/Http/Controllers/FrontendController.cs index ddd519b3..721ab6a6 100644 --- a/Moonlight.ApiServer/Http/Controllers/FrontendController.cs +++ b/Moonlight.ApiServer/Http/Controllers/FrontendController.cs @@ -12,31 +12,13 @@ namespace Moonlight.ApiServer.Http.Controllers; public class FrontendController : Controller { private readonly FrontendService FrontendService; - private readonly PluginService PluginService; - public FrontendController(FrontendService frontendService, PluginService pluginService) + public FrontendController(FrontendService frontendService) { FrontendService = frontendService; - PluginService = pluginService; } [HttpGet("frontend.json")] public async Task GetConfiguration() => await FrontendService.GetConfiguration(); - - [HttpGet("plugins/{assemblyName}")] - public async Task GetPluginAssembly(string assemblyName) - { - var assembliesMap = PluginService.GetAssemblies("client"); - - if (!assembliesMap.TryGetValue(assemblyName, out var path)) - throw new HttpApiException("The requested assembly could not be found", 404); - - var absolutePath = Path.Combine( - Directory.GetCurrentDirectory(), - path - ); - - await Results.File(absolutePath).ExecuteAsync(HttpContext); - } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs b/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs index 6ca274db..b5293345 100644 --- a/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs +++ b/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs @@ -1,24 +1,20 @@ using Microsoft.OpenApi.Models; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database; -using Moonlight.ApiServer.Interfaces.Startup; +using Moonlight.ApiServer.Plugins; namespace Moonlight.ApiServer.Implementations.Startup; +[PluginStartup] public class CoreStartup : IPluginStartup { - private readonly AppConfiguration Configuration; - - public CoreStartup(AppConfiguration configuration) - { - Configuration = configuration; - } - - public Task BuildApplication(IHostApplicationBuilder builder) + public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder) { + var configuration = serviceProvider.GetRequiredService(); + #region Api Docs - if (Configuration.Development.EnableApiDocs) + if (configuration.Development.EnableApiDocs) { builder.Services.AddEndpointsApiExplorer(); @@ -53,14 +49,16 @@ public class CoreStartup : IPluginStartup return Task.CompletedTask; } - public Task ConfigureApplication(IApplicationBuilder app) + public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app) { return Task.CompletedTask; } - public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder) + public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder) { - if(Configuration.Development.EnableApiDocs) + var configuration = serviceProvider.GetRequiredService(); + + if(configuration.Development.EnableApiDocs) routeBuilder.MapSwagger("/api/swagger/{documentName}"); return Task.CompletedTask; diff --git a/Moonlight.ApiServer/Interfaces/Startup/IPluginStartup.cs b/Moonlight.ApiServer/Interfaces/Startup/IPluginStartup.cs deleted file mode 100644 index c0735523..00000000 --- a/Moonlight.ApiServer/Interfaces/Startup/IPluginStartup.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Moonlight.ApiServer.Interfaces.Startup; - -public interface IPluginStartup -{ - public Task BuildApplication(IHostApplicationBuilder builder); - public Task ConfigureApplication(IApplicationBuilder app); - public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder); -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Models/FrontendConfigurationOption.cs b/Moonlight.ApiServer/Models/FrontendConfigurationOption.cs new file mode 100644 index 00000000..343a98d2 --- /dev/null +++ b/Moonlight.ApiServer/Models/FrontendConfigurationOption.cs @@ -0,0 +1,7 @@ +namespace Moonlight.ApiServer.Models; + +public class FrontendConfigurationOption +{ + public string[] Scripts { get; set; } = []; + public string[] Styles { get; set; } = []; +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index 0b648e2f..a3c03697 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -1,70 +1,62 @@ + - - - net8.0 - enable - enable - Linux - - - - - - - - - - - - - - .dockerignore - false - - - - - - Moonlight.ApiServer - 2.1.0 - Moonlight Panel - A build of the api server for moonlight development - https://github.com/Moonlight-Panel/Moonlight - true - true - true - - - - - - - - - - - - - - - - - - - - true - src - Never - - - true - src - Never - - - - - - - - + + net8.0 + enable + enable + Linux + apiserver + + + + + + + + + + + .dockerignore + false + + + + Moonlight.ApiServer + 2.1.0 + Moonlight Panel + A build of the api server for moonlight development + https://github.com/Moonlight-Panel/Moonlight + true + true + true + + + + + + + + + + + + + + + + + + true + src + Never + + + true + src + Never + + + + + + \ No newline at end of file diff --git a/Moonlight.ApiServer/Plugins/IPluginStartup.cs b/Moonlight.ApiServer/Plugins/IPluginStartup.cs new file mode 100644 index 00000000..92eac50a --- /dev/null +++ b/Moonlight.ApiServer/Plugins/IPluginStartup.cs @@ -0,0 +1,8 @@ +namespace Moonlight.ApiServer.Plugins; + +public interface IPluginStartup +{ + public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder); + public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app); + public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder); +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Plugins/PluginStartupAttribute.cs b/Moonlight.ApiServer/Plugins/PluginStartupAttribute.cs new file mode 100644 index 00000000..d38a0555 --- /dev/null +++ b/Moonlight.ApiServer/Plugins/PluginStartupAttribute.cs @@ -0,0 +1,7 @@ +namespace Moonlight.ApiServer.Plugins; + +[AttributeUsage(AttributeTargets.Class)] +public class PluginStartupAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/FrontendService.cs b/Moonlight.ApiServer/Services/FrontendService.cs index 936d6f2b..cf478f82 100644 --- a/Moonlight.ApiServer/Services/FrontendService.cs +++ b/Moonlight.ApiServer/Services/FrontendService.cs @@ -6,6 +6,7 @@ using MoonCore.Attributes; using MoonCore.Exceptions; using MoonCore.Helpers; using Moonlight.ApiServer.Configuration; +using Moonlight.ApiServer.Models; using Moonlight.Shared.Misc; namespace Moonlight.ApiServer.Services; @@ -14,18 +15,18 @@ namespace Moonlight.ApiServer.Services; public class FrontendService { private readonly AppConfiguration Configuration; - private readonly PluginService PluginService; private readonly IWebHostEnvironment WebHostEnvironment; + private readonly IEnumerable ConfigurationOptions; public FrontendService( AppConfiguration configuration, - PluginService pluginService, - IWebHostEnvironment webHostEnvironment + IWebHostEnvironment webHostEnvironment, + IEnumerable configurationOptions ) { Configuration = configuration; - PluginService = pluginService; WebHostEnvironment = webHostEnvironment; + ConfigurationOptions = configurationOptions; } public async Task GetConfiguration() @@ -48,33 +49,15 @@ public class FrontendService .Deserialize>(variablesJson) ?? new(); } - // Collect assemblies for the 'client' section - configuration.Assemblies = PluginService - .GetAssemblies("client") - .Keys - .ToArray(); - // Collect scripts to execute - configuration.Scripts = PluginService - .LoadedPlugins - .Keys + configuration.Scripts = ConfigurationOptions .SelectMany(x => x.Scripts) .ToArray(); // Collect styles - var styles = new List(); - - styles.AddRange( - PluginService - .LoadedPlugins - .Keys - .SelectMany(x => x.Styles) - ); - - // Add bundle css - styles.Add("css/bundle.min.css"); - - configuration.Styles = styles.ToArray(); + configuration.Styles = ConfigurationOptions + .SelectMany(x => x.Styles) + .ToArray(); return configuration; } @@ -111,42 +94,12 @@ public class FrontendService // Add blazor files await ArchiveFsItem(zipArchive, blazorPath, blazorPath, "_framework/"); - // Add bundle.css - var bundleContent = await File.ReadAllBytesAsync(Path.Combine("storage", "tmp", "bundle.css")); - await ArchiveBytes(zipArchive, "css/bundle.css", bundleContent); - // Add frontend.json var frontendConfig = await GetConfiguration(); frontendConfig.HostEnvironment = "Static"; var frontendJson = JsonSerializer.Serialize(frontendConfig); await ArchiveText(zipArchive, "frontend.json", frontendJson); - // Add plugin wwwroot files - foreach (var pluginPath in PluginService.LoadedPlugins.Values) - { - var wwwRootPluginPath = Path.Combine(pluginPath, "wwwroot/"); - - if (!Directory.Exists(wwwRootPluginPath)) - continue; - - await ArchiveFsItem(zipArchive, wwwRootPluginPath, wwwRootPluginPath); - } - - // Add plugin assemblies for client to the zip file - var assembliesMap = PluginService.GetAssemblies("client"); - - foreach (var assemblyName in assembliesMap.Keys) - { - var path = assembliesMap[assemblyName]; - - await ArchiveFsItem( - zipArchive, - path, - path, - $"plugins/{assemblyName}" - ); - } - // Finish zip archive and reset stream so the code calling this function can process it zipArchive.Dispose(); await memoryStream.FlushAsync(); diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index baf2ee51..c37f9343 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -1,13 +1,10 @@ -using System.Reflection; using System.Runtime.Loader; using System.Text; using System.Text.Json; using Hangfire; using Hangfire.EntityFrameworkCore; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; -using MoonCore.Configuration; using MoonCore.EnvConfiguration; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Extensions; @@ -15,15 +12,14 @@ using MoonCore.Extended.Helpers; using MoonCore.Extended.JwtInvalidation; using MoonCore.Extensions; using MoonCore.Helpers; -using MoonCore.Services; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database; using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Helpers; using Moonlight.ApiServer.Implementations; +using Moonlight.ApiServer.Implementations.Startup; using Moonlight.ApiServer.Interfaces; -using Moonlight.ApiServer.Interfaces.Startup; -using Moonlight.ApiServer.Models; +using Moonlight.ApiServer.Plugins; using Moonlight.ApiServer.Services; namespace Moonlight.ApiServer; @@ -34,8 +30,6 @@ namespace Moonlight.ApiServer; public class Startup { private string[] Args; - private Assembly[] AdditionalAssemblies; - private PluginManifest[] AdditionalPluginManifests; // Logging private ILoggerProvider[] LoggerProviders; @@ -51,24 +45,20 @@ public class Startup private WebApplicationBuilder WebApplicationBuilder; // Plugin Loading - private PluginService PluginService; - private AssemblyLoadContext PluginLoadContext; - private IPluginStartup[] PluginStartups; + private IPluginStartup[] AdditionalPlugins; + private IServiceProvider PluginLoadServiceProvider; - public async Task Run(string[] args, Assembly[]? additionalAssemblies = null, - PluginManifest[]? additionalManifests = null) + public async Task Run(string[] args, IPluginStartup[]? additionalPlugins = null) { Args = args; - AdditionalAssemblies = additionalAssemblies ?? []; - AdditionalPluginManifests = additionalManifests ?? []; + AdditionalPlugins = additionalPlugins ?? []; await PrintVersion(); await CreateStorage(); await SetupAppConfiguration(); await SetupLogging(); - await LoadPlugins(); await InitializePlugins(); await CreateWebApplicationBuilder(); @@ -139,17 +129,13 @@ public class Startup // Add pre-existing services WebApplicationBuilder.Services.AddSingleton(Configuration); - WebApplicationBuilder.Services.AddSingleton(PluginService); // Configure controllers var mvcBuilder = WebApplicationBuilder.Services.AddControllers(); - // Add plugin and additional assemblies as application parts - foreach (var pluginAssembly in PluginLoadContext.Assemblies) - mvcBuilder.AddApplicationPart(pluginAssembly); - - foreach (var additionalAssembly in AdditionalAssemblies) - mvcBuilder.AddApplicationPart(additionalAssembly); + // Add plugin assemblies as application parts + foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct()) + mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly); return Task.CompletedTask; } @@ -199,90 +185,34 @@ public class Startup #region Plugin Loading - private async Task LoadPlugins() - { - // Load plugins - PluginService = new PluginService( - LoggerFactory.CreateLogger() - ); - - // Add plugins manually if specified in the startup - foreach (var manifest in AdditionalPluginManifests) - PluginService.LoadedPlugins.Add(manifest, Directory.GetCurrentDirectory()); - - // Search and load all plugins - await PluginService.Load(); - - // Search up assemblies for the apiServer - var assemblyFiles = PluginService.GetAssemblies("apiServer") - .Values - .ToArray(); - - // Create the load context and add assemblies - PluginLoadContext = new AssemblyLoadContext(null); - - foreach (var assemblyFile in assemblyFiles) - { - try - { - PluginLoadContext.LoadFromAssemblyPath( - Path.Combine(Directory.GetCurrentDirectory(), assemblyFile) - ); - } - catch (Exception e) - { - Logger.LogError("Unable to load plugin assembly '{assemblyFile}': {e}", assemblyFile, e); - } - } - } - private Task InitializePlugins() { - // Define minimal service collection - var startupSc = new ServiceCollection(); + // Create service provider for starting up + var serviceCollection = new ServiceCollection(); - // Configure base services for initialisation - startupSc.AddSingleton(Configuration); - - startupSc.AddLogging(builder => + serviceCollection.AddSingleton(Configuration); + + serviceCollection.AddLogging(builder => { builder.ClearProviders(); builder.AddProviders(LoggerProviders); }); - // - var startupSp = startupSc.BuildServiceProvider(); + PluginLoadServiceProvider = serviceCollection.BuildServiceProvider(); + + // Collect startups + var pluginStartups = new List(); + + pluginStartups.Add(new CoreStartup()); + + pluginStartups.AddRange(AdditionalPlugins); // Used by the development server + + // Do NOT remove the following comment, as its used to place the plugin startup register calls + // MLBUILD_PLUGIN_STARTUP_HERE - // Initialize plugin startups - var startups = new List(); - var startupType = typeof(IPluginStartup); - - var assembliesToScan = new List(); - - assembliesToScan.Add(typeof(Startup).Assembly); - assembliesToScan.AddRange(PluginLoadContext.Assemblies); - assembliesToScan.AddRange(AdditionalAssemblies); - - foreach (var pluginAssembly in assembliesToScan) - { - var startupTypes = pluginAssembly - .ExportedTypes - .Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType)) - .ToArray(); - - foreach (var type in startupTypes) - { - var startup = ActivatorUtilities.CreateInstance(startupSp, type) as IPluginStartup; - - if (startup == null) - continue; - - startups.Add(startup); - } - } - - PluginStartups = startups.ToArray(); + PluginStartups = pluginStartups.ToArray(); + return Task.CompletedTask; } @@ -298,11 +228,6 @@ public class Startup FileProvider = new BundleAssetFileProvider() }); - WebApplication.UseStaticFiles(new StaticFileOptions() - { - FileProvider = PluginService.WwwRootFileProvider - }); - return Task.CompletedTask; } @@ -314,7 +239,7 @@ public class Startup { try { - await pluginAppStartup.BuildApplication(WebApplicationBuilder); + await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder); } catch (Exception e) { @@ -333,7 +258,7 @@ public class Startup { try { - await pluginAppStartup.ConfigureApplication(WebApplication); + await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication); } catch (Exception e) { @@ -352,7 +277,7 @@ public class Startup { try { - await pluginEndpointStartup.ConfigureEndpoints(WebApplication); + await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication); } catch (Exception e) { diff --git a/Moonlight.Client/Implementations/CoreStartup.cs b/Moonlight.Client/Implementations/CoreStartup.cs index 248d45ce..55005986 100644 --- a/Moonlight.Client/Implementations/CoreStartup.cs +++ b/Moonlight.Client/Implementations/CoreStartup.cs @@ -1,11 +1,12 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Moonlight.Client.Interfaces; +using Moonlight.Client.Plugins; namespace Moonlight.Client.Implementations; public class CoreStartup : IPluginStartup { - public Task BuildApplication(WebAssemblyHostBuilder builder) + public Task BuildApplication(IServiceProvider serviceProvider, WebAssemblyHostBuilder builder) { builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -13,6 +14,6 @@ public class CoreStartup : IPluginStartup return Task.CompletedTask; } - public Task ConfigureApplication(WebAssemblyHost app) + public Task ConfigureApplication(IServiceProvider serviceProvider, WebAssemblyHost app) => Task.CompletedTask; } \ No newline at end of file diff --git a/Moonlight.Client/Interfaces/IPluginStartup.cs b/Moonlight.Client/Interfaces/IPluginStartup.cs deleted file mode 100644 index 3d084ed1..00000000 --- a/Moonlight.Client/Interfaces/IPluginStartup.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; - -namespace Moonlight.Client.Interfaces; - -public interface IPluginStartup -{ - public Task BuildApplication(WebAssemblyHostBuilder builder); - public Task ConfigureApplication(WebAssemblyHost app); -} \ No newline at end of file diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index fdcf16e2..cb4db178 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -1,71 +1,62 @@ + - - - false - net8.0 - enable - enable - Linux - + + false + net8.0 + enable + enable + Linux + **\bin\**;**\obj\**;**\node_modules\**;**\Styles\*.json - True - - - - - Moonlight.Client - 2.1.0 - Moonlight Panel - A build of the client for moonlight development - https://github.com/Moonlight-Panel/Moonlight - true - true - true - - - - - - - - - - - - - - - - true - src - Never - - - true - styles - Never - - - - - - - - - - service-worker-assets.js - - - - - - - - - - - + + service-worker-assets.js + + + + + + + + \ No newline at end of file diff --git a/Moonlight.Client/Plugins/IPluginStartup.cs b/Moonlight.Client/Plugins/IPluginStartup.cs new file mode 100644 index 00000000..68f5532e --- /dev/null +++ b/Moonlight.Client/Plugins/IPluginStartup.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +namespace Moonlight.Client.Plugins; + +public interface IPluginStartup +{ + public Task BuildApplication(IServiceProvider serviceProvider, WebAssemblyHostBuilder builder); + public Task ConfigureApplication(IServiceProvider serviceProvider, WebAssemblyHost app); +} \ No newline at end of file diff --git a/Moonlight.Client/Plugins/PluginStartupAttribute.cs b/Moonlight.Client/Plugins/PluginStartupAttribute.cs new file mode 100644 index 00000000..d55d5491 --- /dev/null +++ b/Moonlight.Client/Plugins/PluginStartupAttribute.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Client.Plugins; + +[AttributeUsage(AttributeTargets.Class)] +public class PluginStartupAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/Moonlight.Client/Startup.cs b/Moonlight.Client/Startup.cs index 5c8cb94a..b8166a16 100644 --- a/Moonlight.Client/Startup.cs +++ b/Moonlight.Client/Startup.cs @@ -9,7 +9,9 @@ using MoonCore.Blazor.Tailwind.Extensions; using MoonCore.Blazor.Tailwind.Auth; using MoonCore.Extensions; using MoonCore.Helpers; +using Moonlight.Client.Implementations; using Moonlight.Client.Interfaces; +using Moonlight.Client.Plugins; using Moonlight.Client.Services; using Moonlight.Shared.Misc; using Moonlight.Client.UI; @@ -33,15 +35,14 @@ public class Startup private WebAssemblyHost WebAssemblyHost; // Plugin Loading - private AssemblyLoadContext PluginLoadContext; - private Assembly[] AdditionalAssemblies; - + private IPluginStartup[] AdditionalPlugins; private IPluginStartup[] PluginStartups; + private IServiceProvider PluginLoadServiceProvider; - public async Task Run(string[] args, Assembly[]? additionalAssemblies = null) + public async Task Run(string[] args, IPluginStartup[]? additionalPlugins = null) { Args = args; - AdditionalAssemblies = additionalAssemblies ?? []; + AdditionalPlugins = additionalPlugins ?? []; await PrintVersion(); await SetupLogging(); @@ -49,7 +50,6 @@ public class Startup await CreateWebAssemblyHostBuilder(); await LoadConfiguration(); - await LoadPlugins(); await InitializePlugins(); await RegisterLogging(); @@ -94,7 +94,7 @@ public class Startup httpClient.BaseAddress = new Uri(WebAssemblyHostBuilder.HostEnvironment.BaseAddress); var jsonText = await httpClient.GetStringAsync("frontend.json"); - + Configuration = JsonSerializer.Deserialize(jsonText, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true @@ -120,7 +120,7 @@ public class Startup BaseAddress = new Uri(Configuration.ApiUrl) } ); - + WebAssemblyHostBuilder.Services.AddScoped(sp => { var httpClient = sp.GetRequiredService(); @@ -146,7 +146,7 @@ public class Startup WebAssemblyHostBuilder.Services.AddScoped(); WebAssemblyHostBuilder.Services.AddScoped(); - + WebAssemblyHostBuilder.Services.AutoAddServices(); return Task.CompletedTask; @@ -160,39 +160,15 @@ public class Startup foreach (var scriptName in Configuration.Scripts) await jsRuntime.InvokeVoidAsync("moonlight.assets.loadJavascript", scriptName); + + foreach (var styleName in Configuration.Styles) + await jsRuntime.InvokeVoidAsync("moonlight.assets.loadStylesheet", styleName); } #endregion #region Plugins - private async Task LoadPlugins() - { - // Create everything required to stream plugins - using var clientForStreaming = new HttpClient(); - - clientForStreaming.BaseAddress = new Uri(Configuration.HostEnvironment == "ApiServer" - ? Configuration.ApiUrl - : WebAssemblyHostBuilder.HostEnvironment.BaseAddress - ); - - PluginLoadContext = new AssemblyLoadContext(null); - - foreach (var assembly in Configuration.Assemblies) - { - var assemblyStream = await clientForStreaming.GetStreamAsync($"plugins/{assembly}"); - PluginLoadContext.LoadFromStream(assemblyStream); - } - - // Add application assembly service - var appAssemblyService = new ApplicationAssemblyService(); - - appAssemblyService.Assemblies.AddRange(AdditionalAssemblies); - appAssemblyService.Assemblies.AddRange(PluginLoadContext.Assemblies); - - WebAssemblyHostBuilder.Services.AddSingleton(appAssemblyService); - } - private Task InitializePlugins() { // Define minimal service collection @@ -205,38 +181,31 @@ public class Startup builder.AddProviders(LoggerProviders); }); - // - var startupSp = startupSc.BuildServiceProvider(); - - // Initialize plugin startups - var startups = new List(); - var startupType = typeof(IPluginStartup); - - var assembliesToScan = new List(); - - assembliesToScan.Add(typeof(Startup).Assembly); - assembliesToScan.AddRange(AdditionalAssemblies); - assembliesToScan.AddRange(PluginLoadContext.Assemblies); + PluginLoadServiceProvider = startupSc.BuildServiceProvider(); - foreach (var pluginAssembly in assembliesToScan) - { - var startupTypes = pluginAssembly - .ExportedTypes - .Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType)) - .ToArray(); + // Collect startups + var pluginStartups = new List(); - foreach (var type in startupTypes) - { - var startup = ActivatorUtilities.CreateInstance(startupSp, type) as IPluginStartup; - - if(startup == null) - continue; - - startups.Add(startup); - } - } + pluginStartups.Add(new CoreStartup()); - PluginStartups = startups.ToArray(); + pluginStartups.AddRange(AdditionalPlugins); // Used by the development server + + // Do NOT remove the following comment, as its used to place the plugin startup register calls + // MLBUILD_PLUGIN_STARTUP_HERE + + + PluginStartups = pluginStartups.ToArray(); + + // Add application assembly service + var appAssemblyService = new ApplicationAssemblyService(); + + appAssemblyService.Assemblies.AddRange( + PluginStartups + .Select(x => x.GetType().Assembly) + .Distinct() + ); + + WebAssemblyHostBuilder.Services.AddSingleton(appAssemblyService); return Task.CompletedTask; } @@ -249,7 +218,7 @@ public class Startup { try { - await pluginAppStartup.BuildApplication(WebAssemblyHostBuilder); + await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebAssemblyHostBuilder); } catch (Exception e) { @@ -268,7 +237,7 @@ public class Startup { try { - await pluginAppStartup.ConfigureApplication(WebAssemblyHost); + await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebAssemblyHost); } catch (Exception e) { @@ -336,9 +305,9 @@ public class Startup { WebAssemblyHostBuilder.Services.AddAuthorizationCore(); WebAssemblyHostBuilder.Services.AddCascadingAuthenticationState(); - + WebAssemblyHostBuilder.Services.AddAuthenticationStateManager(); - + return Task.CompletedTask; } diff --git a/Moonlight.Client/wwwroot/js/moonlight.js b/Moonlight.Client/wwwroot/js/moonlight.js index fae07bd5..a57ec020 100644 --- a/Moonlight.Client/wwwroot/js/moonlight.js +++ b/Moonlight.Client/wwwroot/js/moonlight.js @@ -34,6 +34,15 @@ window.moonlight = { scriptElement.type = 'text/javascript'; (document.head || document.documentElement).appendChild(scriptElement); + }, + loadStylesheet: function (url) { + let linkElement = document.createElement('link'); + + linkElement.href = url; + linkElement.type = 'text/css'; + linkElement.rel = 'stylesheet'; + + (document.head || document.documentElement).appendChild(linkElement); } } }; \ No newline at end of file diff --git a/Moonlight.Shared/Moonlight.Shared.csproj b/Moonlight.Shared/Moonlight.Shared.csproj index 69f33b08..84b8261b 100644 --- a/Moonlight.Shared/Moonlight.Shared.csproj +++ b/Moonlight.Shared/Moonlight.Shared.csproj @@ -1,20 +1,20 @@ - - - - net8.0 - enable - enable - - - - Moonlight.Shared - 2.1.0 - Moonlight Panel - A build of the shared classes for moonlight development - https://github.com/Moonlight-Panel/Moonlight - true - true - true - - - + + + + net8.0 + enable + enable + Moonlight.Shared + shared + + + Moonlight.Shared + 2.1.0 + Moonlight Panel + A build of the shared classes for moonlight development + https://github.com/Moonlight-Panel/Moonlight + true + true + true + + \ No newline at end of file diff --git a/Resources/Scripts/Commands/PackCommand.cs b/Resources/Scripts/Commands/PackCommand.cs new file mode 100644 index 00000000..e0121d89 --- /dev/null +++ b/Resources/Scripts/Commands/PackCommand.cs @@ -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 apiServerProjects = []; + List frontendProjects = []; + List 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(); + 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 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 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"); + } + ); + } + } +} \ No newline at end of file diff --git a/Resources/Scripts/Commands/PreBuildCommand.cs b/Resources/Scripts/Commands/PreBuildCommand.cs new file mode 100644 index 00000000..d911d6e3 --- /dev/null +++ b/Resources/Scripts/Commands/PreBuildCommand.cs @@ -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 GetDependenciesFromNuget(string nugetDir) + { + var nugetFiles = Directory.GetFiles(nugetDir, "*.nupkg", SearchOption.AllDirectories); + var dependencies = new List(); + + foreach (var nugetFile in nugetFiles) + { + var dependency = await GetDependencyFromPackage(nugetFile); + dependencies.Add(dependency); + } + + return dependencies.ToArray(); + } + + private async Task 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 FindStartupClasses(Dependency[] dependencies) + { + var result = new List(); + + 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(); + + 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; + } + + 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; } + } +} \ No newline at end of file diff --git a/Resources/Scripts/Functions/ContentFunctions.cs b/Resources/Scripts/Functions/ContentFunctions.cs deleted file mode 100644 index 2319445d..00000000 --- a/Resources/Scripts/Functions/ContentFunctions.cs +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/Resources/Scripts/Functions/SrcFunctions.cs b/Resources/Scripts/Functions/SrcFunctions.cs deleted file mode 100644 index 3af2ccd7..00000000 --- a/Resources/Scripts/Functions/SrcFunctions.cs +++ /dev/null @@ -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(); - } - } -} \ No newline at end of file diff --git a/Resources/Scripts/Functions/StaticWebAssetsFunctions.cs b/Resources/Scripts/Functions/StaticWebAssetsFunctions.cs deleted file mode 100644 index de6318c1..00000000 --- a/Resources/Scripts/Functions/StaticWebAssetsFunctions.cs +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/Resources/Scripts/Functions/TagsFunctions.cs b/Resources/Scripts/Functions/TagsFunctions.cs deleted file mode 100644 index 290755ff..00000000 --- a/Resources/Scripts/Functions/TagsFunctions.cs +++ /dev/null @@ -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 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}"); - } - } -} \ No newline at end of file diff --git a/Resources/Scripts/Helpers/CommandHelper.cs b/Resources/Scripts/Helpers/CommandHelper.cs new file mode 100644 index 00000000..162759b9 --- /dev/null +++ b/Resources/Scripts/Helpers/CommandHelper.cs @@ -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 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); + } +} \ No newline at end of file diff --git a/Resources/Scripts/Helpers/StartupClassDetector.cs b/Resources/Scripts/Helpers/StartupClassDetector.cs new file mode 100644 index 00000000..cf76665d --- /dev/null +++ b/Resources/Scripts/Helpers/StartupClassDetector.cs @@ -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(); + + 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/Program.cs b/Resources/Scripts/Program.cs index aa258951..a255daf4 100644 --- a/Resources/Scripts/Program.cs +++ b/Resources/Scripts/Program.cs @@ -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; -} \ No newline at end of file +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +app.AddCommands(); +app.AddCommands(); + +await app.RunAsync(); \ No newline at end of file diff --git a/Resources/Scripts/Scripts.csproj b/Resources/Scripts/Scripts.csproj index 2150e379..46dbb3b6 100644 --- a/Resources/Scripts/Scripts.csproj +++ b/Resources/Scripts/Scripts.csproj @@ -1,10 +1,17 @@ - - + + Exe net8.0 enable enable - - + + + + + + + + + \ No newline at end of file From 73ca5e57e8417c4c82c74f8e4e5d8bd5397fda7c Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Tue, 13 May 2025 21:17:49 +0200 Subject: [PATCH 3/9] Added nuget directory option to pack command of the script --- Resources/Scripts/Commands/PackCommand.cs | 29 ++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/Resources/Scripts/Commands/PackCommand.cs b/Resources/Scripts/Commands/PackCommand.cs index e0121d89..a2f22242 100644 --- a/Resources/Scripts/Commands/PackCommand.cs +++ b/Resources/Scripts/Commands/PackCommand.cs @@ -19,7 +19,8 @@ public class PackCommand public async Task Pack( [Argument] string solutionDirectory, [Argument] string outputLocation, - [Option] string buildConfiguration = "Debug" + [Option] string buildConfiguration = "Debug", + [Option] string? nugetDir = null ) { if (!Directory.Exists(solutionDirectory)) @@ -96,13 +97,15 @@ public class PackCommand { await BuildProject( apiServerProject, - buildConfiguration + buildConfiguration, + nugetDir ); var nugetFilePath = await PackProject( apiServerProject, TmpDir, - buildConfiguration + buildConfiguration, + nugetDir ); var nugetPackage = ZipFile.Open( @@ -126,13 +129,15 @@ public class PackCommand { await BuildProject( frontendProject, - buildConfiguration + buildConfiguration, + nugetDir ); var nugetFilePath = await PackProject( frontendProject, TmpDir, - buildConfiguration + buildConfiguration, + nugetDir ); var nugetPackage = ZipFile.Open( @@ -226,39 +231,41 @@ public class PackCommand { await BuildProject( sharedProject, - buildConfiguration + buildConfiguration, + nugetDir ); var nugetFilePath = await PackProject( sharedProject, TmpDir, - buildConfiguration + buildConfiguration, + nugetDir ); File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true); } } - private async Task BuildProject(string file, string configuration) + private async Task BuildProject(string file, string configuration, string? nugetDir) { var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!); var fileName = Path.GetFileName(file); await CommandHelper.Run( "/usr/bin/dotnet", - $"build {fileName} --configuration {configuration}", + $"build {fileName} --configuration {configuration}" + (string.IsNullOrEmpty(nugetDir) ? "" : $" --source {nugetDir}"), basePath ); } - private async Task PackProject(string file, string output, string configuration) + 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}", + $"pack {fileName} --output {output} --configuration {configuration}" + (string.IsNullOrEmpty(nugetDir) ? "" : $" --source {nugetDir}"), basePath ); From 6922242b4ff27b93175830ab3692202bfa467b2e Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Tue, 13 May 2025 21:46:29 +0200 Subject: [PATCH 4/9] Disabled generation of packages during build. Re-enabled trimming in the client --- Moonlight.ApiServer/Moonlight.ApiServer.csproj | 1 - Moonlight.Client/Moonlight.Client.csproj | 2 -- Moonlight.Shared/Moonlight.Shared.csproj | 1 - 3 files changed, 4 deletions(-) diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index a3c03697..91ae5037 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -27,7 +27,6 @@ A build of the api server for moonlight development https://github.com/Moonlight-Panel/Moonlight true - true true diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index cb4db178..566804fd 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -1,7 +1,6 @@  - false net8.0 enable enable @@ -19,7 +18,6 @@ A build of the client for moonlight development https://github.com/Moonlight-Panel/Moonlight true - true true diff --git a/Moonlight.Shared/Moonlight.Shared.csproj b/Moonlight.Shared/Moonlight.Shared.csproj index 84b8261b..612b5980 100644 --- a/Moonlight.Shared/Moonlight.Shared.csproj +++ b/Moonlight.Shared/Moonlight.Shared.csproj @@ -14,7 +14,6 @@ A build of the shared classes for moonlight development https://github.com/Moonlight-Panel/Moonlight true - true true \ No newline at end of file From 0e5402c347bdaa9c1b0610e7468fbde9844f97a6 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Wed, 14 May 2025 09:15:18 +0200 Subject: [PATCH 5/9] Removed unused calls and classes from the old plugin system --- .../Configuration/AppConfiguration.cs | 1 + .../Helpers/BundleAssetFileProvider.cs | 28 ---- .../Http/Controllers/OAuth2/Register.razor | 2 +- Moonlight.ApiServer/Models/PluginManifest.cs | 14 -- Moonlight.ApiServer/Services/PluginService.cs | 139 ------------------ Moonlight.ApiServer/Startup.cs | 53 ++++--- 6 files changed, 31 insertions(+), 206 deletions(-) delete mode 100644 Moonlight.ApiServer/Helpers/BundleAssetFileProvider.cs delete mode 100644 Moonlight.ApiServer/Models/PluginManifest.cs delete mode 100644 Moonlight.ApiServer/Services/PluginService.cs diff --git a/Moonlight.ApiServer/Configuration/AppConfiguration.cs b/Moonlight.ApiServer/Configuration/AppConfiguration.cs index 4c5bd173..004f4c44 100644 --- a/Moonlight.ApiServer/Configuration/AppConfiguration.cs +++ b/Moonlight.ApiServer/Configuration/AppConfiguration.cs @@ -55,5 +55,6 @@ public class AppConfiguration public class KestrelConfig { public int UploadLimit { get; set; } = 100; + public string AllowedOrigins { get; set; } = "*"; } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Helpers/BundleAssetFileProvider.cs b/Moonlight.ApiServer/Helpers/BundleAssetFileProvider.cs deleted file mode 100644 index 06669dc5..00000000 --- a/Moonlight.ApiServer/Helpers/BundleAssetFileProvider.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.FileProviders.Physical; -using Microsoft.Extensions.Primitives; -using MoonCore.Helpers; - -namespace Moonlight.ApiServer.Helpers; - -public class BundleAssetFileProvider : IFileProvider -{ - public IDirectoryContents GetDirectoryContents(string subpath) - => NotFoundDirectoryContents.Singleton; - - public IFileInfo GetFileInfo(string subpath) - { - if(subpath != "/css/bundle.css") - return new NotFoundFileInfo(subpath); - - var physicalPath = PathBuilder.File("storage", "tmp", "bundle.css"); - - if(!File.Exists(physicalPath)) - return new NotFoundFileInfo(subpath); - - return new PhysicalFileInfo(new FileInfo(physicalPath)); - } - - public IChangeToken Watch(string filter) - => NullChangeToken.Singleton; -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor b/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor index b7bf0953..79c16b2a 100644 --- a/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor +++ b/Moonlight.ApiServer/Http/Controllers/OAuth2/Register.razor @@ -25,7 +25,7 @@
- +
diff --git a/Moonlight.ApiServer/Models/PluginManifest.cs b/Moonlight.ApiServer/Models/PluginManifest.cs deleted file mode 100644 index cda0d868..00000000 --- a/Moonlight.ApiServer/Models/PluginManifest.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Moonlight.ApiServer.Models; - -public class PluginManifest -{ - public string Id { get; set; } - public string Name { get; set; } - public string Author { get; set; } - public string[] Dependencies { get; set; } = []; - - public string[] Scripts { get; set; } = []; - public string[] Styles { get; set; } = []; - - public Dictionary Assemblies { get; set; } = new(); -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/PluginService.cs b/Moonlight.ApiServer/Services/PluginService.cs deleted file mode 100644 index 94e3c8bd..00000000 --- a/Moonlight.ApiServer/Services/PluginService.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System.Text.Json; -using Microsoft.Extensions.FileProviders; -using MoonCore.Helpers; -using Moonlight.ApiServer.Models; - -namespace Moonlight.ApiServer.Services; - -public class PluginService -{ - private readonly ILogger Logger; - private readonly string PluginRoot; - - public readonly Dictionary LoadedPlugins = new(); - public IFileProvider WwwRootFileProvider; - - public PluginService(ILogger logger) - { - Logger = logger; - - PluginRoot = PathBuilder.Dir("storage", "plugins"); - } - - public async Task Load() - { - var jsonOptions = new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = true - }; - - var pluginDirs = Directory.GetDirectories(PluginRoot); - var pluginMap = new Dictionary(); - - #region Scan plugins/ directory for plugin.json files - - foreach (var dir in pluginDirs) - { - var metaPath = PathBuilder.File(dir, "plugin.json"); - - if (!File.Exists(metaPath)) - { - Logger.LogWarning("Skipped '{dir}' as it is missing a plugin.json", dir); - continue; - } - - var json = await File.ReadAllTextAsync(metaPath); - - try - { - var meta = JsonSerializer.Deserialize(json, jsonOptions); - - if (meta == null) - throw new JsonException("Unable to parse. Return value was null"); - - pluginMap.Add(meta, dir); - } - catch (JsonException e) - { - Logger.LogError("Unable to load plugin.json at '{path}': {e}", metaPath, e); - } - } - - #endregion - - #region Depdenency check - - foreach (var plugin in pluginMap.Keys) - { - var hasMissingDep = false; - - foreach (var dependency in plugin.Dependencies) - { - if (pluginMap.Keys.All(x => x.Id != dependency)) - { - hasMissingDep = true; - Logger.LogWarning("Plugin '{name}' has missing dependency: {dep}", plugin.Name, dependency); - } - } - - if (hasMissingDep) - Logger.LogWarning("Unable to load '{name}' due to missing dependencies", plugin.Name); - else - LoadedPlugins.Add(plugin, pluginMap[plugin]); - } - - #endregion - - #region Create wwwroot file provider - - Logger.LogInformation("Creating wwwroot file provider"); - WwwRootFileProvider = CreateWwwRootProvider(); - - #endregion - - Logger.LogInformation("Loaded {count} plugins", LoadedPlugins.Count); - } - - public Dictionary GetAssemblies(string section) - { - var assemblyMap = new Dictionary(); - - foreach (var loadedPlugin in LoadedPlugins.Keys) - { - // Skip all plugins which haven't defined any assemblies in that section - if (!loadedPlugin.Assemblies.ContainsKey(section)) - continue; - - var pluginPath = LoadedPlugins[loadedPlugin]; - - foreach (var assembly in loadedPlugin.Assemblies[section]) - { - var assemblyFile = Path.GetFileName(assembly); - assemblyMap[assemblyFile] = PathBuilder.File(pluginPath, assembly); - } - } - - return assemblyMap; - } - - private IFileProvider CreateWwwRootProvider() - { - List wwwRootProviders = new(); - - foreach (var pluginFolder in LoadedPlugins.Values) - { - var wwwRootPath = Path.GetFullPath( - PathBuilder.Dir(pluginFolder, "wwwroot") - ); - - if(!Directory.Exists(wwwRootPath)) - continue; - - wwwRootProviders.Add( - new PhysicalFileProvider(wwwRootPath) - ); - } - - return new CompositeFileProvider(wwwRootProviders); - } -} \ No newline at end of file diff --git a/Moonlight.ApiServer/Startup.cs b/Moonlight.ApiServer/Startup.cs index c37f9343..84695cc1 100644 --- a/Moonlight.ApiServer/Startup.cs +++ b/Moonlight.ApiServer/Startup.cs @@ -1,8 +1,8 @@ -using System.Runtime.Loader; using System.Text; using System.Text.Json; using Hangfire; using Hangfire.EntityFrameworkCore; +using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using MoonCore.EnvConfiguration; @@ -15,12 +15,10 @@ using MoonCore.Helpers; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database; using Moonlight.ApiServer.Database.Entities; -using Moonlight.ApiServer.Helpers; using Moonlight.ApiServer.Implementations; using Moonlight.ApiServer.Implementations.Startup; using Moonlight.ApiServer.Interfaces; using Moonlight.ApiServer.Plugins; -using Moonlight.ApiServer.Services; namespace Moonlight.ApiServer; @@ -79,7 +77,6 @@ public class Startup await PrepareDatabase(); await UseCors(); - await UsePluginAssets(); // We need to move the plugin assets to the top to allow plugins to override content await UseBase(); await UseAuth(); await UseHangfire(); @@ -191,7 +188,7 @@ public class Startup var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(Configuration); - + serviceCollection.AddLogging(builder => { builder.ClearProviders(); @@ -199,20 +196,20 @@ public class Startup }); PluginLoadServiceProvider = serviceCollection.BuildServiceProvider(); - + // Collect startups var pluginStartups = new List(); - + pluginStartups.Add(new CoreStartup()); - + pluginStartups.AddRange(AdditionalPlugins); // Used by the development server - + // Do NOT remove the following comment, as its used to place the plugin startup register calls // MLBUILD_PLUGIN_STARTUP_HERE PluginStartups = pluginStartups.ToArray(); - + return Task.CompletedTask; } @@ -221,16 +218,6 @@ public class Startup return Task.CompletedTask; } - private Task UsePluginAssets() - { - WebApplication.UseStaticFiles(new StaticFileOptions() - { - FileProvider = new BundleAssetFileProvider() - }); - - return Task.CompletedTask; - } - #region Hooks private async Task HookPluginBuild() @@ -489,7 +476,7 @@ public class Startup }); WebApplicationBuilder.Services.AddAuthorization(); - + // Add local oauth2 provider if enabled if (Configuration.Authentication.EnableLocalOAuth2) WebApplicationBuilder.Services.AddScoped(); @@ -514,12 +501,30 @@ public class Startup private Task RegisterCors() { + var allowedOrigins = Configuration.Kestrel.AllowedOrigins.Split(";", StringSplitOptions.RemoveEmptyEntries); + WebApplicationBuilder.Services.AddCors(options => { - options.AddDefaultPolicy(builder => + var cors = new CorsPolicyBuilder(); + + if (allowedOrigins.Contains("*")) { - builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().Build(); - }); + cors.SetIsOriginAllowed(_ => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + } + else + { + cors.WithOrigins(allowedOrigins) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials(); + } + + options.AddDefaultPolicy( + cors.Build() + ); }); return Task.CompletedTask; From 3a804c99cedede4f0962f9f7a06364c5a6b427ad Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Thu, 15 May 2025 09:45:54 +0200 Subject: [PATCH 6/9] Upgraded to dotnet 9 --- .../Moonlight.ApiServer.csproj | 17 ++++++------- Moonlight.Client/Moonlight.Client.csproj | 24 +++++++++---------- Moonlight.Shared/Moonlight.Shared.csproj | 6 ++--- Resources/Scripts/Scripts.csproj | 6 +---- 4 files changed, 22 insertions(+), 31 deletions(-) diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index 91ae5037..bc97f0ac 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -1,11 +1,9 @@  - net8.0 + net9.0 enable enable - Linux - apiserver @@ -27,20 +25,19 @@ A build of the api server for moonlight development https://github.com/Moonlight-Panel/Moonlight true + apiserver true - - - - - - + + + + - + diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index 566804fd..2858878b 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -1,17 +1,16 @@  - net8.0 + net9.0 enable enable - Linux **\bin\**;**\obj\**;**\node_modules\**;**\Styles\*.json True - frontend + frontend Moonlight.Client 2.1.0 Moonlight Panel @@ -21,13 +20,12 @@ true - - - - - - - + + + + + + @@ -45,9 +43,9 @@ + Specify the /p:BuildPWA=true flag to build moonlight as a PWA. + This flag is by default disabled to allow nuget package generation + --> service-worker-assets.js diff --git a/Moonlight.Shared/Moonlight.Shared.csproj b/Moonlight.Shared/Moonlight.Shared.csproj index 612b5980..14145ac6 100644 --- a/Moonlight.Shared/Moonlight.Shared.csproj +++ b/Moonlight.Shared/Moonlight.Shared.csproj @@ -1,13 +1,13 @@  - net8.0 + net9.0 enable enable - Moonlight.Shared - shared + Moonlight.Shared + shared Moonlight.Shared 2.1.0 Moonlight Panel diff --git a/Resources/Scripts/Scripts.csproj b/Resources/Scripts/Scripts.csproj index 46dbb3b6..ff74cd5c 100644 --- a/Resources/Scripts/Scripts.csproj +++ b/Resources/Scripts/Scripts.csproj @@ -2,14 +2,10 @@ Exe - net8.0 + net9.0 enable enable - - - - From 9dc77e6dde68caf6b64e632e57dc7ec3ece98d80 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Thu, 15 May 2025 10:14:44 +0200 Subject: [PATCH 7/9] Workaround for https://github.com/dotnet/aspnetcore/issues/59291 --- Moonlight.Client/Moonlight.Client.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index 2858878b..7d70af68 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -18,6 +18,7 @@ https://github.com/Moonlight-Panel/Moonlight true true + false From 7ead76fbccee7a6e20bfa5327e44bdb209576169 Mon Sep 17 00:00:00 2001 From: mxritzdev Date: Sat, 17 May 2025 17:53:05 +0200 Subject: [PATCH 8/9] Implemented First User Admin Feature --- .../Configuration/AppConfiguration.cs | 2 ++ .../Http/Controllers/OAuth2/OAuth2Controller.cs | 13 +++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Moonlight.ApiServer/Configuration/AppConfiguration.cs b/Moonlight.ApiServer/Configuration/AppConfiguration.cs index 004f4c44..22096878 100644 --- a/Moonlight.ApiServer/Configuration/AppConfiguration.cs +++ b/Moonlight.ApiServer/Configuration/AppConfiguration.cs @@ -44,6 +44,8 @@ public class AppConfiguration public string? AuthorizationEndpoint { get; set; } public string? AccessEndpoint { get; set; } public string? AuthorizationRedirect { get; set; } + + public bool FirstUserAdmin { get; set; } = true; } } diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs b/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs index 37d2c555..7f2332a5 100644 --- a/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs +++ b/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs @@ -273,13 +273,22 @@ public class OAuth2Controller : Controller if (await UserRepository.Get().AnyAsync(x => x.Email == email)) throw new HttpApiException("A account with that email already exists", 400); - + var user = new User() { Username = username, Email = email, - Password = HashHelper.Hash(password) + Password = HashHelper.Hash(password), }; + + if (Configuration.Authentication.OAuth2.FirstUserAdmin) + { + var userCount = await UserRepository.Get().CountAsync(); + + if (userCount == 0) + user.PermissionsJson = "[\"*\"]"; + + } return await UserRepository.Add(user); } From d4a7600c1470759e528ef84ca773fb65d0cd3f9f Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Sat, 17 May 2025 18:04:59 +0200 Subject: [PATCH 9/9] Cleaned up scripts project --- Resources/Scripts/Commands/PackCommand.cs | 382 ++++++------------ Resources/Scripts/Commands/PreBuildCommand.cs | 381 ++++++----------- Resources/Scripts/Helpers/CodeHelper.cs | 73 ++++ Resources/Scripts/Helpers/CsprojHelper.cs | 228 +++++++++++ Resources/Scripts/Helpers/NupkgHelper.cs | 195 +++++++++ .../Scripts/Helpers/StartupClassDetector.cs | 46 --- Resources/Scripts/Models/CsprojManifest.cs | 9 + Resources/Scripts/Models/NupkgManifest.cs | 8 + Resources/Scripts/Program.cs | 9 +- Resources/Scripts/Scripts.csproj | 1 + 10 files changed, 754 insertions(+), 578 deletions(-) create mode 100644 Resources/Scripts/Helpers/CodeHelper.cs create mode 100644 Resources/Scripts/Helpers/CsprojHelper.cs create mode 100644 Resources/Scripts/Helpers/NupkgHelper.cs delete mode 100644 Resources/Scripts/Helpers/StartupClassDetector.cs create mode 100644 Resources/Scripts/Models/CsprojManifest.cs create mode 100644 Resources/Scripts/Models/NupkgManifest.cs 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