Merge branch 'v2_ChangeArchitecture' into v2_ChangeArchitecture_AddDiagnose

This commit is contained in:
2025-05-17 19:40:50 +02:00
committed by GitHub
37 changed files with 1376 additions and 863 deletions

View File

@@ -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; } = "*";
}
}

View File

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

View File

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

View File

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

View File

@@ -278,9 +278,18 @@ public class OAuth2Controller : Controller
{
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);
}

View File

@@ -25,7 +25,7 @@
<form class="space-y-6" method="POST">
<div>
<label for="email" class="block text-sm font-medium leading-6 text-gray-100">Username</label>
<label for="username" class="block text-sm font-medium leading-6 text-gray-100">Username</label>
<div class="mt-2">
<input id="username" name="username" type="text" required class="block bg-white/5 w-full rounded-md border-0 py-1.5 text-gray-100 shadow-sm ring-1 ring-inset ring-gray-700 placeholder:text-gray-600 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
</div>

View File

@@ -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)
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
{
Configuration = configuration;
}
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
public Task BuildApplication(IHostApplicationBuilder builder)
{
#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<AppConfiguration>();
if(configuration.Development.EnableApiDocs)
routeBuilder.MapSwagger("/api/swagger/{documentName}");
return Task.CompletedTask;

View File

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

View File

@@ -0,0 +1,7 @@
namespace Moonlight.ApiServer.Models;
public class FrontendConfigurationOption
{
public string[] Scripts { get; set; } = [];
public string[] Styles { get; set; } = [];
}

View File

@@ -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<string, string[]> Assemblies { get; set; } = new();
}

View File

@@ -1,70 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj"/>
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj"/>
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Migrations\"/>
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
<Pack>false</Pack>
</Content>
</ItemGroup>
<PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.0</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ExCSS" Version="4.3.0"/>
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18"/>
<PackageReference Include="Hangfire.Core" Version="1.8.18"/>
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.10"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"/>
<PackageReference Include="MoonCore" Version="1.8.5"/>
<PackageReference Include="MoonCore.Extended" Version="1.3.2"/>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
<PackageReference Include="SharpZipLib" Version="1.4.2"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
<PackageReference Include="Ben.Demystifier" Version="0.4.1"/>
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="**\*.razor" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Client\Moonlight.Client.csproj" />
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Database\Migrations\" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
<Pack>false</Pack>
</Content>
</ItemGroup>
<PropertyGroup>
<PackageId>Moonlight.ApiServer</PackageId>
<Version>2.1.0</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the api server for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<PackageTags>apiserver</PackageTags>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
<PackageReference Include="Hangfire.Core" Version="1.8.18" />
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
<PackageReference Include="MoonCore" Version="1.8.6" />
<PackageReference Include="MoonCore.Extended" Version="1.3.3" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="**\*.razor" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,7 @@
namespace Moonlight.ApiServer.Plugins;
[AttributeUsage(AttributeTargets.Class)]
public class PluginStartupAttribute : Attribute
{
}

View File

@@ -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<FrontendConfigurationOption> ConfigurationOptions;
public FrontendService(
AppConfiguration configuration,
PluginService pluginService,
IWebHostEnvironment webHostEnvironment
IWebHostEnvironment webHostEnvironment,
IEnumerable<FrontendConfigurationOption> configurationOptions
)
{
Configuration = configuration;
PluginService = pluginService;
WebHostEnvironment = webHostEnvironment;
ConfigurationOptions = configurationOptions;
}
public async Task<FrontendConfiguration> GetConfiguration()
@@ -48,33 +49,15 @@ public class FrontendService
.Deserialize<Dictionary<string, string>>(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<string>();
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();

View File

@@ -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<PluginService> Logger;
private readonly string PluginRoot;
public readonly Dictionary<PluginManifest, string> LoadedPlugins = new();
public IFileProvider WwwRootFileProvider;
public PluginService(ILogger<PluginService> 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<PluginManifest, string>();
#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<PluginManifest>(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<string, string> GetAssemblies(string section)
{
var assemblyMap = new Dictionary<string, string>();
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<IFileProvider> 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);
}
}

View File

@@ -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<PluginService>()
);
// 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<IPluginStartup>();
var startupType = typeof(IPluginStartup);
// Collect startups
var pluginStartups = new List<IPluginStartup>();
var assembliesToScan = new List<Assembly>();
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)
{
@@ -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;

View File

@@ -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<ISidebarItemProvider, DefaultSidebarItemProvider>();
builder.Services.AddSingleton<IOverviewElementProvider, DefaultOverviewElementProvider>();
@@ -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;
}

View File

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

View File

@@ -1,71 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<PublishTrimmed>false</PublishTrimmed>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DefaultItemExcludes>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DefaultItemExcludes>
**\bin\**;**\obj\**;**\node_modules\**;**\Styles\*.json
</DefaultItemExcludes>
<StaticWebAssetsEnabled>True</StaticWebAssetsEnabled>
</PropertyGroup>
<PropertyGroup>
<PackageId>Moonlight.Client</PackageId>
<Version>2.1.0</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the client for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazor-ApexCharts" Version="4.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all"/>
<PackageReference Include="MoonCore" Version="1.8.5"/>
<PackageReference Include="MoonCore.Blazor" Version="1.2.9"/>
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.5"/>
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.4.2"/>
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="Styles\**\*" Exclude="storage\**\*;bin\**\*;obj\**\*;Styles\node_modules\**\*">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*"/>
<Content Remove="storage\**\*"/>
<None Remove="storage\**\*"/>
</ItemGroup>
<!--
Specify the /p:BuildPWA=true flag to build moonlight as a PWA.
This flag is by default disabled to allow nuget package generation
-->
<PropertyGroup Condition="'$(BuildPWA)' == 'true'">
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
<ItemGroup Condition="'$(BuildPWA)' == 'true'">
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj"/>
</ItemGroup>
<StaticWebAssetsEnabled>True</StaticWebAssetsEnabled>
</PropertyGroup>
<PropertyGroup>
<PackageTags>frontend</PackageTags>
<PackageId>Moonlight.Client</PackageId>
<Version>2.1.0</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the client for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<IsPackable>true</IsPackable>
<CompressionEnabled>false</CompressionEnabled>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazor-ApexCharts" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all" />
<PackageReference Include="MoonCore" Version="1.8.6" />
<PackageReference Include="MoonCore.Blazor" Version="1.3.0" />
<PackageReference Include="MoonCore.Blazor.Tailwind" Version="1.4.3" />
</ItemGroup>
<ItemGroup>
<None Include="**\*.cs" Exclude="storage\**\*;bin\**\*;obj\**\*">
<Pack>true</Pack>
<PackagePath>src</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Include="Styles\**\*" Exclude="storage\**\*;bin\**\*;obj\**\*;Styles\node_modules\**\*">
<Pack>true</Pack>
<PackagePath>styles</PackagePath>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<Compile Remove="storage\**\*" />
<Content Remove="storage\**\*" />
<None Remove="storage\**\*" />
</ItemGroup>
<!--
Specify the /p:BuildPWA=true flag to build moonlight as a PWA.
This flag is by default disabled to allow nuget package generation
-->
<PropertyGroup Condition="'$(BuildPWA)' == 'true'">
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
<ItemGroup Condition="'$(BuildPWA)' == 'true'">
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Moonlight.Shared\Moonlight.Shared.csproj" />
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Client.Plugins;
[AttributeUsage(AttributeTargets.Class)]
public class PluginStartupAttribute : Attribute
{
}

View File

@@ -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();
@@ -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();
PluginLoadServiceProvider = startupSc.BuildServiceProvider();
// Initialize plugin startups
var startups = new List<IPluginStartup>();
var startupType = typeof(IPluginStartup);
// Collect startups
var pluginStartups = new List<IPluginStartup>();
var assembliesToScan = new List<Assembly>();
pluginStartups.Add(new CoreStartup());
assembliesToScan.Add(typeof(Startup).Assembly);
assembliesToScan.AddRange(AdditionalAssemblies);
assembliesToScan.AddRange(PluginLoadContext.Assemblies);
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;
PluginStartups = pluginStartups.ToArray();
startups.Add(startup);
}
}
// Add application assembly service
var appAssemblyService = new ApplicationAssemblyService();
PluginStartups = startups.ToArray();
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)
{

View File

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

View File

@@ -1,20 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<PackageId>Moonlight.Shared</PackageId>
<Version>2.1.0</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the shared classes for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IsPackable>true</IsPackable>
</PropertyGroup>
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Title>Moonlight.Shared</Title>
<PackageTags>shared</PackageTags>
<PackageId>Moonlight.Shared</PackageId>
<Version>2.1.0</Version>
<Authors>Moonlight Panel</Authors>
<Description>A build of the shared classes for moonlight development</Description>
<PackageProjectUrl>https://github.com/Moonlight-Panel/Moonlight</PackageProjectUrl>
<DevelopmentDependency>true</DevelopmentDependency>
<IsPackable>true</IsPackable>
</PropertyGroup>
</Project>

View File

@@ -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<PackCommand> Logger;
private readonly CsprojHelper CsprojHelper;
private readonly NupkgHelper NupkgHelper;
private readonly string[] ValidTags = ["apiserver", "frontend", "shared"];
public PackCommand(
ILogger<PackCommand> 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<string>();
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
);
}
}
}

View File

@@ -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<PreBuildCommand> 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<PreBuildCommand> 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<NupkgManifest[]> GetNugetManifests(string nugetDir)
{
var nugetFiles = Directory.GetFiles(
nugetDir,
"*.nupkg",
SearchOption.AllDirectories
);
var manifests = new List<NupkgManifest>();
foreach (var nugetFilePath in nugetFiles)
{
using var nugetPackage = ZipFile.Open(nugetFilePath, ZipArchiveMode.Read);
var manifest = await NupkgHelper.GetManifest(nugetPackage);
if (manifest == null)
continue;
manifests.Add(manifest);
}
return manifests.ToArray();
}
private async Task<string[]> FindStartupClasses(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);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Extensions.Logging;
namespace Scripts.Helpers;
public class CodeHelper
{
private readonly ILogger<CodeHelper> Logger;
public CodeHelper(ILogger<CodeHelper> logger)
{
Logger = logger;
}
public async Task<string[]> FindPluginStartups(string[] filesToSearch)
{
var result = new List<string>();
var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
var trees = new List<SyntaxTree>();
foreach (var file in filesToSearch)
{
Logger.LogDebug("Reading {file}", file);
var content = await File.ReadAllTextAsync(file);
var tree = CSharpSyntaxTree.ParseText(content);
trees.Add(tree);
}
var compilation = CSharpCompilation.Create("Analysis", trees, [mscorlib]);
foreach (var tree in trees)
{
var model = compilation.GetSemanticModel(tree);
var root = await tree.GetRootAsync();
var classDeclarations = root
.DescendantNodes()
.OfType<ClassDeclarationSyntax>();
foreach (var classDeclaration in classDeclarations)
{
var symbol = model.GetDeclaredSymbol(classDeclaration);
if (symbol == null)
continue;
var hasAttribute = symbol.GetAttributes().Any(attr =>
{
if (attr.AttributeClass == null)
return false;
return attr.AttributeClass.Name == "PluginStartup";
});
if (!hasAttribute)
continue;
var classPath = $"{symbol.ContainingNamespace.ToDisplayString()}.{classDeclaration.Identifier.ValueText}";
Logger.LogInformation("Detected startup in class: {classPath}", classPath);
result.Add(classPath);
}
}
return result.ToArray();
}
}

View File

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

View File

@@ -0,0 +1,228 @@
using System.Collections.Frozen;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Scripts.Models;
namespace Scripts.Helpers;
public class CsprojHelper
{
private readonly ILogger<CsprojHelper> Logger;
private readonly CommandHelper CommandHelper;
public CsprojHelper(ILogger<CsprojHelper> logger, CommandHelper commandHelper)
{
Logger = logger;
CommandHelper = commandHelper;
}
#region Add dependencies
public async Task AddDependencies(string path, NupkgManifest[] dependencies, string label)
{
await using var fs = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
fs.Position = 0;
await AddDependencies(fs, dependencies, label);
await fs.FlushAsync();
fs.Close();
}
public async Task AddDependencies(Stream stream, NupkgManifest[] dependencies, string label)
{
var xmlDocument = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
await AddDependencies(xmlDocument, dependencies, label);
stream.Position = 0;
await xmlDocument.SaveAsync(stream, SaveOptions.DisableFormatting, CancellationToken.None);
}
public Task AddDependencies(XDocument document, NupkgManifest[] dependencies, string label)
{
var project = document.Element("Project")!;
var itemGroup = new XElement("ItemGroup");
itemGroup.SetAttributeValue("Label", label);
foreach (var dependency in dependencies)
{
var depElement = new XElement("PackageReference");
depElement.SetAttributeValue("Include", dependency.Id);
depElement.SetAttributeValue("Version", dependency.Version);
itemGroup.Add(depElement);
}
project.Add(itemGroup);
return Task.CompletedTask;
}
#endregion
#region Clean dependencies
public async Task CleanDependencies(string path, string label)
{
var document = XDocument.Load(path, LoadOptions.None);
await CleanDependencies(document, label);
document.Save(path, SaveOptions.DisableFormatting);
}
public async Task CleanDependencies(Stream stream, string label)
{
var xmlDocument = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
await CleanDependencies(xmlDocument, label);
stream.Position = 0;
await xmlDocument.SaveAsync(stream, SaveOptions.DisableFormatting, CancellationToken.None);
}
public Task CleanDependencies(XDocument document, string label)
{
var itemGroupsToRemove = document
.Descendants("ItemGroup")
.Where(x => x.Attribute("Label")?.Value.Contains(label) ?? false)
.ToArray();
itemGroupsToRemove.Remove();
return Task.CompletedTask;
}
#endregion
#region Read
public async Task<CsprojManifest> Read(string path)
{
await using var fileStream = File.Open(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite
);
var manifest = await Read(fileStream);
fileStream.Close();
return manifest;
}
public async Task<CsprojManifest> Read(Stream stream)
{
var xmlDocument = await XDocument.LoadAsync(stream, LoadOptions.None, CancellationToken.None);
return await Read(xmlDocument);
}
public Task<CsprojManifest> Read(XDocument document)
{
var manifest = new CsprojManifest();
var ns = document.Root!.GetDefaultNamespace();
manifest.IsPackable = document
.Descendants(ns + "IsPackable")
.FirstOrDefault()?.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase) ?? false;
manifest.PackageId = document
.Descendants(ns + "PackageId")
.FirstOrDefault()?.Value ?? "N/A";
manifest.Version = document
.Descendants(ns + "Version")
.FirstOrDefault()?.Value ?? "N/A";
manifest.PackageTags = document
.Descendants(ns + "PackageTags")
.FirstOrDefault()?.Value
.Split(";", StringSplitOptions.RemoveEmptyEntries) ?? [];
return Task.FromResult(manifest);
}
#endregion
public async Task Restore(string path)
{
var basePath = Path.GetFullPath(Path.GetDirectoryName(path)!);
var fileName = Path.GetFileName(path);
Logger.LogInformation("Restore: {basePath} - {fileName}", basePath, fileName);
await CommandHelper.Run(
"/usr/bin/dotnet",
$"restore {fileName}",
basePath
);
}
public async Task Build(string file, string configuration)
{
var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!);
var fileName = Path.GetFileName(file);
await CommandHelper.Run(
"/usr/bin/dotnet",
$"build {fileName} --configuration {configuration}",
basePath
);
}
public async Task<string> Pack(string file, string output, string configuration)
{
var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!);
var fileName = Path.GetFileName(file);
await CommandHelper.Run(
"/usr/bin/dotnet",
$"pack {fileName} --output {output} --configuration {configuration}",
basePath
);
var nugetFilesPaths = Directory.GetFiles(
output,
"*.nupkg",
SearchOption.TopDirectoryOnly
);
if (nugetFilesPaths.Length == 0)
throw new Exception("No nuget packages were built");
if (nugetFilesPaths.Length > 1)
throw new Exception("More than one nuget package has been built");
return nugetFilesPaths.First();
}
public async Task<FrozenDictionary<string, CsprojManifest>> FindProjectsInPath(string path, string[] validTags)
{
var projectFiles = Directory.GetFiles(
path,
"*.csproj",
SearchOption.AllDirectories
);
var projects = new Dictionary<string, CsprojManifest>();
foreach (var projectFile in projectFiles)
{
var manifest = await Read(projectFile);
// Ignore all projects which have no matching tags
if (!manifest.PackageTags.Any(projectTag =>
validTags.Contains(projectTag, StringComparer.InvariantCultureIgnoreCase)))
continue;
projects.Add(projectFile, manifest);
}
return projects.ToFrozenDictionary();
}
}

View File

@@ -0,0 +1,195 @@
using System.IO.Compression;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
using Scripts.Models;
namespace Scripts.Helpers;
public class NupkgHelper
{
private readonly ILogger<NupkgHelper> Logger;
public NupkgHelper(ILogger<NupkgHelper> logger)
{
Logger = logger;
}
public async Task<NupkgManifest?> GetManifest(ZipArchive nugetPackage)
{
var nuspecEntry = nugetPackage.Entries.FirstOrDefault(
x => x.Name.EndsWith(".nuspec")
);
if (nuspecEntry == null)
return null;
await using var fs = nuspecEntry.Open();
var nuspec = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None);
var ns = nuspec.Root!.GetDefaultNamespace();
var metadata = nuspec.Root!.Element(ns + "metadata")!;
var id = metadata.Element(ns + "id")!.Value;
var version = metadata.Element(ns + "version")!.Value;
var tags = metadata.Element(ns + "tags")!.Value;
return new NupkgManifest()
{
Id = id,
Version = version,
Tags = tags.Split(";", StringSplitOptions.RemoveEmptyEntries)
};
}
public async Task CleanDependencies(ZipArchive nugetPackage, string filter)
{
var nuspecEntry = nugetPackage.Entries.FirstOrDefault(
x => x.Name.EndsWith(".nuspec")
);
if (nuspecEntry == null)
{
Logger.LogWarning("No nuspec file to modify found in nuget package");
return;
}
await ModifyXmlInPackage(nugetPackage, nuspecEntry, document =>
{
var ns = document.Root!.GetDefaultNamespace();
return document
.Descendants(ns + "dependency")
.Where(x => x.Attribute("id")?.Value.StartsWith(filter) ?? false);
});
}
public async Task RemoveContentFiles(ZipArchive nugetPackage)
{
foreach (var entry in nugetPackage.Entries.ToArray())
{
if (!entry.FullName.StartsWith("contentFiles") && !entry.FullName.StartsWith("content"))
continue;
Logger.LogDebug("Removing content file: {path}", entry.FullName);
entry.Delete();
}
var nuspecFile = nugetPackage
.Entries
.FirstOrDefault(x => x.Name.EndsWith(".nuspec"));
if (nuspecFile == null)
{
Logger.LogWarning("Nuspec file missing. Unable to remove content files references from nuspec file");
return;
}
await ModifyXmlInPackage(
nugetPackage,
nuspecFile,
document =>
{
var ns = document.Root!.GetDefaultNamespace();
return document.Descendants(ns + "contentFiles");
}
);
}
public async Task<ZipArchiveEntry> ModifyXmlInPackage(
ZipArchive nugetPackage,
ZipArchiveEntry entry,
Func<XDocument, IEnumerable<XElement>> filter
)
{
var oldPath = entry.FullName;
await using var oldFs = entry.Open();
var document = await XDocument.LoadAsync(
oldFs,
LoadOptions.None,
CancellationToken.None
);
var itemsToRemove = filter.Invoke(document);
var items = itemsToRemove.ToArray();
foreach (var item in items)
item.Remove();
oldFs.Close();
entry.Delete();
var newEntry = nugetPackage.CreateEntry(oldPath);
var newFs = newEntry.Open();
await document.SaveAsync(newFs, SaveOptions.None, CancellationToken.None);
await newFs.FlushAsync();
newFs.Close();
return newEntry;
}
public async Task RemoveStaticWebAssets(ZipArchive nugetPackage, string filter)
{
var filterWithPath = $"staticwebassets/{filter}";
foreach (var entry in nugetPackage.Entries.ToArray())
{
if (!entry.FullName.StartsWith(filterWithPath))
continue;
Logger.LogDebug("Removing file: {name}", entry.FullName);
entry.Delete();
}
var buildTargetEntry = nugetPackage.Entries.FirstOrDefault(x =>
x.FullName == "build/Microsoft.AspNetCore.StaticWebAssets.props"
);
if (buildTargetEntry == null)
{
Logger.LogWarning("Unable to find Microsoft.AspNetCore.StaticWebAssets.props to remove file references");
return;
}
Logger.LogDebug("Removing file references");
await ModifyXmlInPackage(nugetPackage, buildTargetEntry,
document => document
.Descendants("StaticWebAsset")
.Where(x =>
{
var relativePath = x.Element("RelativePath")!.Value;
return relativePath.StartsWith(filter);
})
);
}
public async Task AddSourceFiles(ZipArchive nugetPackage, string[] files, Func<string, string> buildPath)
{
foreach (var sourceFile in files)
{
var path = buildPath.Invoke(sourceFile);
Logger.LogDebug("Adding additional files as src: {path}", path);
await using var fs = File.Open(
sourceFile,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite
);
var entry = nugetPackage.CreateEntry(path);
await using var entryFs = entry.Open();
await fs.CopyToAsync(entryFs);
await entryFs.FlushAsync();
fs.Close();
entryFs.Close();
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Scripts.Models;
public class CsprojManifest
{
public bool IsPackable { get; set; }
public string Version { get; set; }
public string PackageId { get; set; }
public string[] PackageTags { get; set; }
}

View File

@@ -0,0 +1,8 @@
namespace Scripts.Models;
public class NupkgManifest
{
public string Id { get; set; }
public string Version { get; set; }
public string[] Tags { get; set; }
}

View File

@@ -1,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;
}
builder.Logging.ClearProviders();
builder.Logging.AddMoonCore();
builder.Services.AddSingleton<CommandHelper>();
builder.Services.AddSingleton<NupkgHelper>();
builder.Services.AddSingleton<CsprojHelper>();
builder.Services.AddSingleton<CodeHelper>();
var app = builder.Build();
app.AddCommands<PackCommand>();
app.AddCommands<PreBuildCommand>();
await app.RunAsync();

View File

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