diff --git a/Moonlight.ApiServer/Configuration/AppConfiguration.cs b/Moonlight.ApiServer/Configuration/AppConfiguration.cs index 4c5bd173..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; } } @@ -55,5 +57,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/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/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/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/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); } 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/Implementations/Startup/CoreStartup.cs b/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs index 14324f0d..3f27ab65 100644 --- a/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs +++ b/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs @@ -3,24 +3,20 @@ using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database; using Moonlight.ApiServer.Implementations.Diagnose; using Moonlight.ApiServer.Interfaces; -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(); @@ -62,14 +58,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/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/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index 0b648e2f..bc97f0ac 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -1,70 +1,58 @@ + - - - 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 - - - - - - - - + + net9.0 + enable + enable + + + + + + + + + + + .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 + apiserver + 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/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 4e691bbb..f9f4a00a 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.AspNetCore.Cors.Infrastructure; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; -using MoonCore.Configuration; using MoonCore.EnvConfiguration; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Extensions; @@ -15,16 +12,13 @@ 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.Services; +using Moonlight.ApiServer.Plugins; namespace Moonlight.ApiServer; @@ -34,8 +28,6 @@ namespace Moonlight.ApiServer; public class Startup { private string[] Args; - private Assembly[] AdditionalAssemblies; - private PluginManifest[] AdditionalPluginManifests; // Logging private ILoggerProvider[] LoggerProviders; @@ -51,24 +43,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(); @@ -89,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(); @@ -140,17 +127,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; } @@ -200,89 +183,33 @@ 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); + serviceCollection.AddSingleton(Configuration); - startupSc.AddLogging(builder => + serviceCollection.AddLogging(builder => { builder.ClearProviders(); builder.AddProviders(LoggerProviders); }); - // - var startupSp = startupSc.BuildServiceProvider(); + PluginLoadServiceProvider = serviceCollection.BuildServiceProvider(); - // Initialize plugin startups - var startups = new List(); - var startupType = typeof(IPluginStartup); + // Collect startups + var pluginStartups = new List(); - var assembliesToScan = new List(); + pluginStartups.Add(new CoreStartup()); - assembliesToScan.Add(typeof(Startup).Assembly); - assembliesToScan.AddRange(PluginLoadContext.Assemblies); - assembliesToScan.AddRange(AdditionalAssemblies); + pluginStartups.AddRange(AdditionalPlugins); // Used by the development server - foreach (var pluginAssembly in assembliesToScan) - { - var startupTypes = pluginAssembly - .ExportedTypes - .Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType)) - .ToArray(); + // Do NOT remove the following comment, as its used to place the plugin startup register calls + // MLBUILD_PLUGIN_STARTUP_HERE - 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; } @@ -292,21 +219,6 @@ public class Startup return Task.CompletedTask; } - private Task UsePluginAssets() - { - WebApplication.UseStaticFiles(new StaticFileOptions() - { - FileProvider = new BundleAssetFileProvider() - }); - - WebApplication.UseStaticFiles(new StaticFileOptions() - { - FileProvider = PluginService.WwwRootFileProvider - }); - - return Task.CompletedTask; - } - #region Hooks private async Task HookPluginBuild() @@ -315,7 +227,7 @@ public class Startup { try { - await pluginAppStartup.BuildApplication(WebApplicationBuilder); + await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder); } catch (Exception e) { @@ -334,7 +246,7 @@ public class Startup { try { - await pluginAppStartup.ConfigureApplication(WebApplication); + await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication); } catch (Exception e) { @@ -353,7 +265,7 @@ public class Startup { try { - await pluginEndpointStartup.ConfigureEndpoints(WebApplication); + await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication); } catch (Exception e) { @@ -568,7 +480,7 @@ public class Startup }); WebApplicationBuilder.Services.AddAuthorization(); - + // Add local oauth2 provider if enabled if (Configuration.Authentication.EnableLocalOAuth2) WebApplicationBuilder.Services.AddScoped(); @@ -593,12 +505,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; 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..7d70af68 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -1,71 +1,59 @@ + - - - false - net8.0 - enable - enable - Linux - + + net9.0 + enable + enable + **\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 - - - - - - - - - - - + True + + + frontend + Moonlight.Client + 2.1.0 + Moonlight Panel + A build of the client for moonlight development + https://github.com/Moonlight-Panel/Moonlight + true + true + false + + + + + + + + + + + + true + src + Never + + + true + styles + Never + + + + + + + + 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..14145ac6 100644 --- a/Moonlight.Shared/Moonlight.Shared.csproj +++ b/Moonlight.Shared/Moonlight.Shared.csproj @@ -1,20 +1,19 @@ - - - - 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 - - - + + + + net9.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 + + \ 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..5ed77716 --- /dev/null +++ b/Resources/Scripts/Commands/PackCommand.cs @@ -0,0 +1,227 @@ +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 string TmpDir = "/tmp/mlbuild"; + private readonly ILogger Logger; + private readonly CsprojHelper CsprojHelper; + private readonly NupkgHelper NupkgHelper; + + private readonly string[] ValidTags = ["apiserver", "frontend", "shared"]; + + public PackCommand( + ILogger logger, + CsprojHelper csprojHelper, + NupkgHelper nupkgHelper + ) + { + 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" + ) + { + if (!Directory.Exists(solutionDirectory)) + { + Logger.LogError("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 + Logger.LogInformation("Searching for projects inside the specified folder"); + + var projects = await CsprojHelper.FindProjectsInPath(solutionDirectory, ValidTags); + + // Show the user + Logger.LogInformation("Found {count} project(s) to check:", projects.Count); + + foreach (var path in projects.Keys) + Logger.LogInformation("- {path}", Path.GetFullPath(path)); + + // Filter out project files which have specific tags specified + Logger.LogInformation("Filtering projects by tags"); + + var apiServerProjects = projects + .Where(x => x.Value.PackageTags.Contains("apiserver", StringComparer.InvariantCultureIgnoreCase)) + .ToArray(); + + var frontendProjects = projects + .Where(x => x.Value.PackageTags.Contains("frontend", StringComparer.InvariantCultureIgnoreCase)) + .ToArray(); + + var sharedProjects = projects + .Where(x => x.Value.PackageTags.Contains("shared", StringComparer.InvariantCultureIgnoreCase)) + .ToArray(); + + 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 + Logger.LogInformation("Building and packing api server project(s)"); + + foreach (var apiServerProject in apiServerProjects) + { + var csProjectFile = apiServerProject.Key; + var manifest = apiServerProject.Value; + + await CsprojHelper.Build( + csProjectFile, + buildConfiguration + ); + + var nugetFilePath = await CsprojHelper.Pack( + csProjectFile, + TmpDir, + buildConfiguration + ); + + var nugetPackage = ZipFile.Open( + nugetFilePath, + ZipArchiveMode.Update + ); + + await NupkgHelper.RemoveContentFiles(nugetPackage); + + // 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 + ); + } + + Logger.LogInformation("Building and packing frontend projects"); + + foreach (var frontendProject in frontendProjects) + { + var csProjectFile = frontendProject.Key; + var manifest = frontendProject.Value; + + await CsprojHelper.Build( + csProjectFile, + buildConfiguration + ); + + var nugetFilePath = await CsprojHelper.Pack( + csProjectFile, + TmpDir, + buildConfiguration + ); + + var nugetPackage = ZipFile.Open( + nugetFilePath, + ZipArchiveMode.Update + ); + + await NupkgHelper.RemoveStaticWebAssets(nugetPackage, "_framework"); + await NupkgHelper.RemoveStaticWebAssets(nugetPackage, "css/style.min.css"); + await NupkgHelper.RemoveContentFiles(nugetPackage); + + // 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."); + + + // Pack razor and html files into src folder + var additionalSrcFiles = new List(); + var basePath = Path.GetDirectoryName(csProjectFile)!; + + additionalSrcFiles.AddRange( + Directory.GetFiles(basePath, "*.razor", SearchOption.AllDirectories) + ); + + additionalSrcFiles.AddRange( + Directory.GetFiles(basePath, "index.html", SearchOption.AllDirectories) + ); + + await NupkgHelper.AddSourceFiles( + nugetPackage, + additionalSrcFiles.ToArray(), + file => "src/" + file.Replace(basePath, "").Trim('/') + ); + + Logger.LogInformation("Finishing package and copying to output directory"); + + nugetPackage.Dispose(); + + File.Move( + nugetFilePath, + Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), + true + ); + } + + Logger.LogInformation("Building and packing shared projects"); + + foreach (var sharedProject in sharedProjects) + { + var csProjectFile = sharedProject.Key; + var manifest = sharedProject.Value; + + await CsprojHelper.Build( + csProjectFile, + buildConfiguration + ); + + var nugetFilePath = await CsprojHelper.Pack( + csProjectFile, + TmpDir, + buildConfiguration + ); + + var nugetPackage = ZipFile.Open( + nugetFilePath, + ZipArchiveMode.Update + ); + + // 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."); + + nugetPackage.Dispose(); + + File.Move( + nugetFilePath, + Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), + true + ); + } + } +} \ 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..cef8cf76 --- /dev/null +++ b/Resources/Scripts/Commands/PreBuildCommand.cs @@ -0,0 +1,265 @@ +using System.IO.Compression; +using System.Text; +using System.Xml.Linq; +using Cocona; +using Microsoft.Extensions.Logging; +using Scripts.Helpers; +using Scripts.Models; + +namespace Scripts.Commands; + +public class PreBuildCommand +{ + 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"; + + private readonly string[] ValidTags = ["frontend", "apiserver", "shared"]; + + public PreBuildCommand( + CsprojHelper csprojHelper, + NupkgHelper nupkgHelper, + CodeHelper codeHelper, + ILogger logger + ) + { + CsprojHelper = csprojHelper; + NupkgHelper = nupkgHelper; + CodeHelper = codeHelper; + Logger = logger; + } + + [Command("prebuild")] + public async Task Prebuild( + [Argument] string moonlightDirectory, + [Argument] string pluginsDirectory + ) + { + var projects = await CsprojHelper.FindProjectsInPath(moonlightDirectory, ValidTags); + + var nugetManifests = await GetNugetManifests(pluginsDirectory); + + Logger.LogInformation("Following plugins found:"); + + foreach (var manifest in nugetManifests) + { + Logger.LogInformation( + "- {id} ({version}) [{tags}]", + manifest.Id, + manifest.Version, + string.Join(", ", manifest.Tags) + ); + } + + try + { + Logger.LogInformation("Adjusting csproj files"); + + foreach (var project in projects) + { + var csProjectPath = project.Key; + + await using var fs = File.Open( + csProjectPath, + FileMode.Open, + FileAccess.ReadWrite, + FileShare.ReadWrite + ); + + var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None); + fs.Position = 0; + + var dependenciesToAdd = nugetManifests + .Where(x => x.Tags.Any(tag => + project.Value.PackageTags.Contains(tag, StringComparer.InvariantCultureIgnoreCase))) + .ToArray(); + + 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(); + } + + Logger.LogInformation("Restoring projects"); + + foreach (var csProjectPath in projects.Keys) + await CsprojHelper.Restore(csProjectPath); + + Logger.LogInformation("Generating plugin startup"); + + foreach (var currentTag in ValidTags) + { + Logger.LogInformation("Checking for '{currentTag}' projects", currentTag); + + var projectsWithTag = projects + .Where(x => + x.Value.PackageTags.Contains(currentTag, StringComparer.InvariantCultureIgnoreCase) + ) + .ToArray(); + + foreach (var project in projectsWithTag) + { + var csProjectPath = project.Key; + + var currentDependencies = nugetManifests + .Where(x => x.Tags.Contains(currentTag)) + .ToArray(); + + var classPaths = await FindStartupClasses(currentDependencies); + + var code = new StringBuilder(); + + code.AppendLine(GeneratedStart); + + foreach (var path in classPaths) + code.AppendLine($"pluginStartups.Add(new global::{path}());"); + + code.Append(GeneratedEnd); + + var filesToSearch = Directory.GetFiles( + Path.GetDirectoryName(csProjectPath)!, + "*.cs", + SearchOption.AllDirectories + ); + + foreach (var file in filesToSearch) + { + var content = await File.ReadAllTextAsync(file); + + if (!content.Contains(GeneratedHook, StringComparison.InvariantCultureIgnoreCase)) + continue; + + Logger.LogInformation("Injecting generated code to: {path}", Path.GetFullPath(file)); + + content = content.Replace( + GeneratedHook, + code.ToString(), + StringComparison.InvariantCultureIgnoreCase + ); + + await File.WriteAllTextAsync(file, content); + } + } + } + } + catch (Exception) + { + Logger.LogInformation("An error occured while prebuilding moonlight. Removing csproj modifications"); + + foreach (var project in projects) + { + await CsprojHelper.CleanDependencies(project.Key, "MoonlightBuildDeps"); + + var path = Path.GetDirectoryName(project.Key)!; + await RemoveGeneratedCode(path); + } + + throw; + } + } + + [Command("prebuild-reset")] + public async Task PrebuildReset( + [Argument] string moonlightDir + ) + { + var projects = await CsprojHelper.FindProjectsInPath(moonlightDir, ValidTags); + + Logger.LogInformation("Reverting csproj changes"); + + foreach (var project in projects) + { + 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); + } + } + + private async Task GetNugetManifests(string nugetDir) + { + 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(NupkgManifest[] dependencies) + { + 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"); + + Logger.LogDebug("Checking {dependencySrcPath}", dependencySrcPath); + + if (!Directory.Exists(dependencySrcPath)) + return []; + + return Directory.GetFiles(dependencySrcPath, "*.cs", SearchOption.AllDirectories); + }) + .ToArray(); + + return await CodeHelper.FindPluginStartups(filesToScan); + } + + private async Task RemoveGeneratedCode(string dir) + { + var filesToSearch = Directory.GetFiles( + dir, + "*.cs", + SearchOption.AllDirectories + ); + + foreach (var file in filesToSearch) + { + 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); + } + } +} \ 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/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/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/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/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 a0dbec12..33c12965 100644 --- a/Resources/Scripts/Program.cs +++ b/Resources/Scripts/Program.cs @@ -1,26 +1,26 @@ -using Scripts.Functions; +using Cocona; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using MoonCore.Extensions; +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; - default: - Console.WriteLine($"No module named {module} found"); - break; -} \ No newline at end of file +builder.Logging.ClearProviders(); +builder.Logging.AddMoonCore(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +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..fd6b6464 100644 --- a/Resources/Scripts/Scripts.csproj +++ b/Resources/Scripts/Scripts.csproj @@ -1,10 +1,14 @@ - - + + Exe - net8.0 + net9.0 enable enable - - + + + + + + \ No newline at end of file