Finished compile time plugin loading. Refactored plugin loading. Extended build helper script
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,20 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Database;
|
||||
using Moonlight.ApiServer.Interfaces.Startup;
|
||||
using Moonlight.ApiServer.Plugins;
|
||||
|
||||
namespace Moonlight.ApiServer.Implementations.Startup;
|
||||
|
||||
[PluginStartup]
|
||||
public class CoreStartup : IPluginStartup
|
||||
{
|
||||
private readonly AppConfiguration Configuration;
|
||||
|
||||
public CoreStartup(AppConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public Task BuildApplication(IHostApplicationBuilder builder)
|
||||
public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder)
|
||||
{
|
||||
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
|
||||
|
||||
#region Api Docs
|
||||
|
||||
if (Configuration.Development.EnableApiDocs)
|
||||
if (configuration.Development.EnableApiDocs)
|
||||
{
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
@@ -53,14 +49,16 @@ public class CoreStartup : IPluginStartup
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ConfigureApplication(IApplicationBuilder app)
|
||||
public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder)
|
||||
public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder)
|
||||
{
|
||||
if(Configuration.Development.EnableApiDocs)
|
||||
var configuration = serviceProvider.GetRequiredService<AppConfiguration>();
|
||||
|
||||
if(configuration.Development.EnableApiDocs)
|
||||
routeBuilder.MapSwagger("/api/swagger/{documentName}");
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.ApiServer.Models;
|
||||
|
||||
public class FrontendConfigurationOption
|
||||
{
|
||||
public string[] Scripts { get; set; } = [];
|
||||
public string[] Styles { get; set; } = [];
|
||||
}
|
||||
@@ -1,70 +1,62 @@
|
||||
<?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>
|
||||
|
||||
|
||||
</Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<PackageTags>apiserver</PackageTags>
|
||||
</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.15" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.15" />
|
||||
<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>
|
||||
</Project>
|
||||
8
Moonlight.ApiServer/Plugins/IPluginStartup.cs
Normal file
8
Moonlight.ApiServer/Plugins/IPluginStartup.cs
Normal 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);
|
||||
}
|
||||
7
Moonlight.ApiServer/Plugins/PluginStartupAttribute.cs
Normal file
7
Moonlight.ApiServer/Plugins/PluginStartupAttribute.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.ApiServer.Plugins;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PluginStartupAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Hangfire;
|
||||
using Hangfire.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MoonCore.Configuration;
|
||||
using MoonCore.EnvConfiguration;
|
||||
using MoonCore.Extended.Abstractions;
|
||||
using MoonCore.Extended.Extensions;
|
||||
@@ -15,15 +12,14 @@ using MoonCore.Extended.Helpers;
|
||||
using MoonCore.Extended.JwtInvalidation;
|
||||
using MoonCore.Extensions;
|
||||
using MoonCore.Helpers;
|
||||
using MoonCore.Services;
|
||||
using Moonlight.ApiServer.Configuration;
|
||||
using Moonlight.ApiServer.Database;
|
||||
using Moonlight.ApiServer.Database.Entities;
|
||||
using Moonlight.ApiServer.Helpers;
|
||||
using Moonlight.ApiServer.Implementations;
|
||||
using Moonlight.ApiServer.Implementations.Startup;
|
||||
using Moonlight.ApiServer.Interfaces;
|
||||
using Moonlight.ApiServer.Interfaces.Startup;
|
||||
using Moonlight.ApiServer.Models;
|
||||
using Moonlight.ApiServer.Plugins;
|
||||
using Moonlight.ApiServer.Services;
|
||||
|
||||
namespace Moonlight.ApiServer;
|
||||
@@ -34,8 +30,6 @@ namespace Moonlight.ApiServer;
|
||||
public class Startup
|
||||
{
|
||||
private string[] Args;
|
||||
private Assembly[] AdditionalAssemblies;
|
||||
private PluginManifest[] AdditionalPluginManifests;
|
||||
|
||||
// Logging
|
||||
private ILoggerProvider[] LoggerProviders;
|
||||
@@ -51,24 +45,20 @@ public class Startup
|
||||
private WebApplicationBuilder WebApplicationBuilder;
|
||||
|
||||
// Plugin Loading
|
||||
private PluginService PluginService;
|
||||
private AssemblyLoadContext PluginLoadContext;
|
||||
|
||||
private IPluginStartup[] PluginStartups;
|
||||
private IPluginStartup[] AdditionalPlugins;
|
||||
private IServiceProvider PluginLoadServiceProvider;
|
||||
|
||||
public async Task Run(string[] args, Assembly[]? additionalAssemblies = null,
|
||||
PluginManifest[]? additionalManifests = null)
|
||||
public async Task Run(string[] args, IPluginStartup[]? additionalPlugins = null)
|
||||
{
|
||||
Args = args;
|
||||
AdditionalAssemblies = additionalAssemblies ?? [];
|
||||
AdditionalPluginManifests = additionalManifests ?? [];
|
||||
AdditionalPlugins = additionalPlugins ?? [];
|
||||
|
||||
await PrintVersion();
|
||||
|
||||
await CreateStorage();
|
||||
await SetupAppConfiguration();
|
||||
await SetupLogging();
|
||||
await LoadPlugins();
|
||||
await InitializePlugins();
|
||||
|
||||
await CreateWebApplicationBuilder();
|
||||
@@ -139,17 +129,13 @@ public class Startup
|
||||
|
||||
// Add pre-existing services
|
||||
WebApplicationBuilder.Services.AddSingleton(Configuration);
|
||||
WebApplicationBuilder.Services.AddSingleton(PluginService);
|
||||
|
||||
// Configure controllers
|
||||
var mvcBuilder = WebApplicationBuilder.Services.AddControllers();
|
||||
|
||||
// Add plugin and additional assemblies as application parts
|
||||
foreach (var pluginAssembly in PluginLoadContext.Assemblies)
|
||||
mvcBuilder.AddApplicationPart(pluginAssembly);
|
||||
|
||||
foreach (var additionalAssembly in AdditionalAssemblies)
|
||||
mvcBuilder.AddApplicationPart(additionalAssembly);
|
||||
// Add plugin assemblies as application parts
|
||||
foreach (var pluginStartup in PluginStartups.Select(x => x.GetType().Assembly).Distinct())
|
||||
mvcBuilder.AddApplicationPart(pluginStartup.GetType().Assembly);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -199,90 +185,34 @@ public class Startup
|
||||
|
||||
#region Plugin Loading
|
||||
|
||||
private async Task LoadPlugins()
|
||||
{
|
||||
// Load plugins
|
||||
PluginService = new PluginService(
|
||||
LoggerFactory.CreateLogger<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);
|
||||
|
||||
startupSc.AddLogging(builder =>
|
||||
serviceCollection.AddSingleton(Configuration);
|
||||
|
||||
serviceCollection.AddLogging(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.AddProviders(LoggerProviders);
|
||||
});
|
||||
|
||||
//
|
||||
var startupSp = startupSc.BuildServiceProvider();
|
||||
PluginLoadServiceProvider = serviceCollection.BuildServiceProvider();
|
||||
|
||||
// Collect startups
|
||||
var pluginStartups = new List<IPluginStartup>();
|
||||
|
||||
pluginStartups.Add(new CoreStartup());
|
||||
|
||||
pluginStartups.AddRange(AdditionalPlugins); // Used by the development server
|
||||
|
||||
// Do NOT remove the following comment, as its used to place the plugin startup register calls
|
||||
// MLBUILD_PLUGIN_STARTUP_HERE
|
||||
|
||||
// Initialize plugin startups
|
||||
var startups = new List<IPluginStartup>();
|
||||
var startupType = typeof(IPluginStartup);
|
||||
|
||||
var assembliesToScan = new List<Assembly>();
|
||||
|
||||
assembliesToScan.Add(typeof(Startup).Assembly);
|
||||
assembliesToScan.AddRange(PluginLoadContext.Assemblies);
|
||||
assembliesToScan.AddRange(AdditionalAssemblies);
|
||||
|
||||
foreach (var pluginAssembly in assembliesToScan)
|
||||
{
|
||||
var startupTypes = pluginAssembly
|
||||
.ExportedTypes
|
||||
.Where(x => !x.IsAbstract && !x.IsInterface && x.IsAssignableTo(startupType))
|
||||
.ToArray();
|
||||
|
||||
foreach (var type in startupTypes)
|
||||
{
|
||||
var startup = ActivatorUtilities.CreateInstance(startupSp, type) as IPluginStartup;
|
||||
|
||||
if (startup == null)
|
||||
continue;
|
||||
|
||||
startups.Add(startup);
|
||||
}
|
||||
}
|
||||
|
||||
PluginStartups = startups.ToArray();
|
||||
|
||||
PluginStartups = pluginStartups.ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -298,11 +228,6 @@ public class Startup
|
||||
FileProvider = new BundleAssetFileProvider()
|
||||
});
|
||||
|
||||
WebApplication.UseStaticFiles(new StaticFileOptions()
|
||||
{
|
||||
FileProvider = PluginService.WwwRootFileProvider
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -314,7 +239,7 @@ public class Startup
|
||||
{
|
||||
try
|
||||
{
|
||||
await pluginAppStartup.BuildApplication(WebApplicationBuilder);
|
||||
await pluginAppStartup.BuildApplication(PluginLoadServiceProvider, WebApplicationBuilder);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -333,7 +258,7 @@ public class Startup
|
||||
{
|
||||
try
|
||||
{
|
||||
await pluginAppStartup.ConfigureApplication(WebApplication);
|
||||
await pluginAppStartup.ConfigureApplication(PluginLoadServiceProvider, WebApplication);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -352,7 +277,7 @@ public class Startup
|
||||
{
|
||||
try
|
||||
{
|
||||
await pluginEndpointStartup.ConfigureEndpoints(WebApplication);
|
||||
await pluginEndpointStartup.ConfigureEndpoints(PluginLoadServiceProvider, WebApplication);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,71 +1,62 @@
|
||||
<?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>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<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>
|
||||
|
||||
<!--
|
||||
<StaticWebAssetsEnabled>True</StaticWebAssetsEnabled>
|
||||
<PackageTags>frontend</PackageTags>
|
||||
</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.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.15" 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>
|
||||
|
||||
</Project>
|
||||
<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>
|
||||
9
Moonlight.Client/Plugins/IPluginStartup.cs
Normal file
9
Moonlight.Client/Plugins/IPluginStartup.cs
Normal 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);
|
||||
}
|
||||
7
Moonlight.Client/Plugins/PluginStartupAttribute.cs
Normal file
7
Moonlight.Client/Plugins/PluginStartupAttribute.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.Client.Plugins;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PluginStartupAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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<FrontendConfiguration>(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<HttpClient>();
|
||||
@@ -146,7 +146,7 @@ public class Startup
|
||||
WebAssemblyHostBuilder.Services.AddScoped<LocalStorageService>();
|
||||
|
||||
WebAssemblyHostBuilder.Services.AddScoped<ThemeService>();
|
||||
|
||||
|
||||
WebAssemblyHostBuilder.Services.AutoAddServices<Program>();
|
||||
|
||||
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<IPluginStartup>();
|
||||
var startupType = typeof(IPluginStartup);
|
||||
|
||||
var assembliesToScan = new List<Assembly>();
|
||||
|
||||
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<IPluginStartup>();
|
||||
|
||||
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<RemoteAuthStateManager>();
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,20 +1,20 @@
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Title>Moonlight.Shared</Title>
|
||||
<PackageTags>shared</PackageTags>
|
||||
</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>
|
||||
</Project>
|
||||
370
Resources/Scripts/Commands/PackCommand.cs
Normal file
370
Resources/Scripts/Commands/PackCommand.cs
Normal file
@@ -0,0 +1,370 @@
|
||||
using System.IO.Compression;
|
||||
using System.Xml.Linq;
|
||||
using Cocona;
|
||||
using Scripts.Helpers;
|
||||
|
||||
namespace Scripts.Commands;
|
||||
|
||||
public class PackCommand
|
||||
{
|
||||
private readonly CommandHelper CommandHelper;
|
||||
private readonly string TmpDir = "/tmp/mlbuild";
|
||||
|
||||
public PackCommand(CommandHelper commandHelper)
|
||||
{
|
||||
CommandHelper = commandHelper;
|
||||
}
|
||||
|
||||
[Command("pack", Description = "Packs the specified folder/solution into nuget packages")]
|
||||
public async Task Pack(
|
||||
[Argument] string solutionDirectory,
|
||||
[Argument] string outputLocation,
|
||||
[Option] string buildConfiguration = "Debug"
|
||||
)
|
||||
{
|
||||
if (!Directory.Exists(solutionDirectory))
|
||||
{
|
||||
Console.WriteLine("The specified solution directory does not exist");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(outputLocation))
|
||||
Directory.CreateDirectory(outputLocation);
|
||||
|
||||
if (Directory.Exists(TmpDir))
|
||||
Directory.Delete(TmpDir, true);
|
||||
|
||||
Directory.CreateDirectory(TmpDir);
|
||||
|
||||
// Find the project files
|
||||
Console.WriteLine("Searching for projects inside the specified folder");
|
||||
var csProjFiles = Directory.GetFiles(solutionDirectory, "*csproj", SearchOption.AllDirectories);
|
||||
|
||||
// Show the user
|
||||
Console.WriteLine($"Found {csProjFiles.Length} project(s) to check:");
|
||||
|
||||
foreach (var csProjFile in csProjFiles)
|
||||
Console.WriteLine($"- {Path.GetFullPath(csProjFile)}");
|
||||
|
||||
// Filter out project files which have specific tags specified
|
||||
Console.WriteLine("Filtering projects by tags");
|
||||
|
||||
List<string> apiServerProjects = [];
|
||||
List<string> frontendProjects = [];
|
||||
List<string> sharedProjects = [];
|
||||
|
||||
foreach (var csProjFile in csProjFiles)
|
||||
{
|
||||
await using var fs = File.Open(
|
||||
csProjFile,
|
||||
FileMode.Open,
|
||||
FileAccess.ReadWrite,
|
||||
FileShare.ReadWrite
|
||||
);
|
||||
|
||||
var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None);
|
||||
fs.Close();
|
||||
|
||||
// Search for tag definitions
|
||||
var packageTagsElements = document.Descendants("PackageTags").ToArray();
|
||||
|
||||
if (packageTagsElements.Length == 0)
|
||||
{
|
||||
Console.WriteLine($"No package tags found in {Path.GetFullPath(csProjFile)}. Skipping it when packing");
|
||||
continue;
|
||||
}
|
||||
|
||||
var packageTags = packageTagsElements.First().Value;
|
||||
|
||||
if (packageTags.Contains("apiserver", StringComparison.InvariantCultureIgnoreCase))
|
||||
apiServerProjects.Add(csProjFile);
|
||||
|
||||
if (packageTags.Contains("frontend", StringComparison.InvariantCultureIgnoreCase))
|
||||
frontendProjects.Add(csProjFile);
|
||||
|
||||
if (packageTags.Contains("shared", StringComparison.InvariantCultureIgnoreCase))
|
||||
sharedProjects.Add(csProjFile);
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"Found {apiServerProjects.Count} api server project(s), {frontendProjects.Count} frontend project(s) and {sharedProjects.Count} shared project(s)");
|
||||
|
||||
// Now build all these projects so we can pack them
|
||||
Console.WriteLine("Building and packing api server project(s)");
|
||||
|
||||
foreach (var apiServerProject in apiServerProjects)
|
||||
{
|
||||
await BuildProject(
|
||||
apiServerProject,
|
||||
buildConfiguration
|
||||
);
|
||||
|
||||
var nugetFilePath = await PackProject(
|
||||
apiServerProject,
|
||||
TmpDir,
|
||||
buildConfiguration
|
||||
);
|
||||
|
||||
var nugetPackage = ZipFile.Open(
|
||||
nugetFilePath,
|
||||
ZipArchiveMode.Update
|
||||
);
|
||||
|
||||
await RemoveContentFiles(nugetPackage);
|
||||
|
||||
await CleanDependencies(nugetPackage);
|
||||
|
||||
Console.WriteLine("Finishing package and copying to output directory");
|
||||
|
||||
nugetPackage.Dispose();
|
||||
File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true);
|
||||
}
|
||||
|
||||
Console.WriteLine("Building and packing frontend projects");
|
||||
|
||||
foreach (var frontendProject in frontendProjects)
|
||||
{
|
||||
await BuildProject(
|
||||
frontendProject,
|
||||
buildConfiguration
|
||||
);
|
||||
|
||||
var nugetFilePath = await PackProject(
|
||||
frontendProject,
|
||||
TmpDir,
|
||||
buildConfiguration
|
||||
);
|
||||
|
||||
var nugetPackage = ZipFile.Open(
|
||||
nugetFilePath,
|
||||
ZipArchiveMode.Update
|
||||
);
|
||||
|
||||
foreach (var entry in nugetPackage.Entries.ToArray())
|
||||
{
|
||||
if (!entry.FullName.StartsWith("staticwebassets/_framework"))
|
||||
continue;
|
||||
|
||||
Console.WriteLine($"Removing framework file: {entry.FullName}");
|
||||
entry.Delete();
|
||||
}
|
||||
|
||||
var buildTargetEntry = nugetPackage.Entries.FirstOrDefault(x =>
|
||||
x.FullName == "build/Microsoft.AspNetCore.StaticWebAssets.props"
|
||||
);
|
||||
|
||||
if (buildTargetEntry != null)
|
||||
{
|
||||
Console.WriteLine("Removing framework file references");
|
||||
|
||||
await ModifyXmlInPackage(nugetPackage, buildTargetEntry,
|
||||
document => document
|
||||
.Descendants("StaticWebAsset")
|
||||
.Where(x =>
|
||||
{
|
||||
var relativePath = x.Element("RelativePath")!.Value;
|
||||
|
||||
if (relativePath.StartsWith("_framework"))
|
||||
return true;
|
||||
|
||||
if (relativePath.StartsWith("css/style.min.css"))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await CleanDependencies(nugetPackage);
|
||||
|
||||
await RemoveContentFiles(nugetPackage);
|
||||
|
||||
// Pack razor and html files into src folder
|
||||
var additionalSrcFiles = new List<string>();
|
||||
var basePath = Path.GetDirectoryName(frontendProject)!;
|
||||
|
||||
additionalSrcFiles.AddRange(
|
||||
Directory.GetFiles(basePath, "*.razor", SearchOption.AllDirectories)
|
||||
);
|
||||
|
||||
additionalSrcFiles.AddRange(
|
||||
Directory.GetFiles(basePath, "index.html", SearchOption.AllDirectories)
|
||||
);
|
||||
|
||||
foreach (var additionalSrcFile in additionalSrcFiles)
|
||||
{
|
||||
var relativePath = "src/" + additionalSrcFile.Replace(basePath, "").Trim('/');
|
||||
|
||||
Console.WriteLine($"Adding additional files as src: {relativePath}");
|
||||
|
||||
await using var fs = File.Open(
|
||||
additionalSrcFile,
|
||||
FileMode.Open,
|
||||
FileAccess.ReadWrite,
|
||||
FileShare.ReadWrite
|
||||
);
|
||||
|
||||
var entry = nugetPackage.CreateEntry(relativePath);
|
||||
await using var entryFs = entry.Open();
|
||||
|
||||
await fs.CopyToAsync(entryFs);
|
||||
await entryFs.FlushAsync();
|
||||
|
||||
fs.Close();
|
||||
entryFs.Close();
|
||||
}
|
||||
|
||||
Console.WriteLine("Finishing package and copying to output directory");
|
||||
|
||||
nugetPackage.Dispose();
|
||||
File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true);
|
||||
}
|
||||
|
||||
Console.WriteLine("Building and packing shared projects");
|
||||
|
||||
foreach (var sharedProject in sharedProjects)
|
||||
{
|
||||
await BuildProject(
|
||||
sharedProject,
|
||||
buildConfiguration
|
||||
);
|
||||
|
||||
var nugetFilePath = await PackProject(
|
||||
sharedProject,
|
||||
TmpDir,
|
||||
buildConfiguration
|
||||
);
|
||||
|
||||
File.Move(nugetFilePath, Path.Combine(outputLocation, Path.GetFileName(nugetFilePath)), true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BuildProject(string file, string configuration)
|
||||
{
|
||||
var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!);
|
||||
var fileName = Path.GetFileName(file);
|
||||
|
||||
await CommandHelper.Run(
|
||||
"/usr/bin/dotnet",
|
||||
$"build {fileName} --configuration {configuration}",
|
||||
basePath
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<string> PackProject(string file, string output, string configuration)
|
||||
{
|
||||
var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!);
|
||||
var fileName = Path.GetFileName(file);
|
||||
|
||||
await CommandHelper.Run(
|
||||
"/usr/bin/dotnet",
|
||||
$"pack {fileName} --output {output} --configuration {configuration}",
|
||||
basePath
|
||||
);
|
||||
|
||||
var nugetFilesPaths = Directory.GetFiles(TmpDir, "*.nupkg", SearchOption.TopDirectoryOnly);
|
||||
|
||||
if (nugetFilesPaths.Length == 0)
|
||||
throw new Exception("No nuget packages were built");
|
||||
|
||||
if (nugetFilesPaths.Length > 1)
|
||||
throw new Exception("More than one nuget package has been built");
|
||||
|
||||
return nugetFilesPaths.First();
|
||||
}
|
||||
|
||||
private async Task CleanDependencies(ZipArchive nugetPackage)
|
||||
{
|
||||
var nuspecEntry = nugetPackage.Entries.FirstOrDefault(x => x.Name.EndsWith(".nuspec"));
|
||||
|
||||
if (nuspecEntry == null)
|
||||
{
|
||||
Console.WriteLine("No nuspec file to modify found in nuget package");
|
||||
return;
|
||||
}
|
||||
|
||||
await ModifyXmlInPackage(nugetPackage, nuspecEntry, document =>
|
||||
{
|
||||
var ns = document.Root!.GetDefaultNamespace();
|
||||
|
||||
var metadata = document.Root!.Element(ns + "metadata")!;
|
||||
|
||||
var id = metadata.Element(ns + "id")!.Value;
|
||||
|
||||
// Skip the removal of moonlight references when
|
||||
// we are packing moonlight itself
|
||||
if (id.StartsWith("Moonlight."))
|
||||
return [];
|
||||
|
||||
return document
|
||||
.Descendants(ns + "dependency")
|
||||
.Where(x => x.Attribute("id")?.Value.StartsWith("Moonlight.") ?? false);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<ZipArchiveEntry> ModifyXmlInPackage(
|
||||
ZipArchive archive,
|
||||
ZipArchiveEntry entry,
|
||||
Func<XDocument, IEnumerable<XElement>> filter
|
||||
)
|
||||
{
|
||||
var oldPath = entry.FullName;
|
||||
await using var oldFs = entry.Open();
|
||||
|
||||
var document = await XDocument.LoadAsync(
|
||||
oldFs,
|
||||
LoadOptions.None,
|
||||
CancellationToken.None
|
||||
);
|
||||
|
||||
var itemsToRemove = filter.Invoke(document);
|
||||
var items = itemsToRemove.ToArray();
|
||||
|
||||
foreach (var item in items)
|
||||
item.Remove();
|
||||
|
||||
oldFs.Close();
|
||||
entry.Delete();
|
||||
|
||||
var newEntry = archive.CreateEntry(oldPath);
|
||||
var newFs = newEntry.Open();
|
||||
|
||||
await document.SaveAsync(newFs, SaveOptions.None, CancellationToken.None);
|
||||
|
||||
await newFs.FlushAsync();
|
||||
newFs.Close();
|
||||
|
||||
return newEntry;
|
||||
}
|
||||
|
||||
private async Task RemoveContentFiles(ZipArchive nugetPackage)
|
||||
{
|
||||
// Remove all content files
|
||||
foreach (var entry in nugetPackage.Entries.ToArray())
|
||||
{
|
||||
if (!entry.FullName.StartsWith("contentFiles") && !entry.FullName.StartsWith("content"))
|
||||
continue;
|
||||
|
||||
Console.WriteLine($"Removing content file: {entry.FullName}");
|
||||
entry.Delete();
|
||||
}
|
||||
|
||||
// Remove references to those files in the nuspec file
|
||||
var nuspecFile = nugetPackage.Entries.FirstOrDefault(x => x.Name.EndsWith(".nuspec"));
|
||||
|
||||
if (nuspecFile != null)
|
||||
{
|
||||
Console.WriteLine("Removing references to content files in the nuspec files");
|
||||
|
||||
await ModifyXmlInPackage(
|
||||
nugetPackage,
|
||||
nuspecFile,
|
||||
document =>
|
||||
{
|
||||
var ns = document.Root!.GetDefaultNamespace();
|
||||
return document.Descendants(ns + "contentFiles");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
414
Resources/Scripts/Commands/PreBuildCommand.cs
Normal file
414
Resources/Scripts/Commands/PreBuildCommand.cs
Normal file
@@ -0,0 +1,414 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using Cocona;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Scripts.Helpers;
|
||||
|
||||
namespace Scripts.Commands;
|
||||
|
||||
public class PreBuildCommand
|
||||
{
|
||||
private readonly CommandHelper CommandHelper;
|
||||
private const string GeneratedStart = "// MLBUILD Generated Start";
|
||||
private const string GeneratedEnd = "// MLBUILD Generated End";
|
||||
private const string GeneratedHook = "// MLBUILD_PLUGIN_STARTUP_HERE";
|
||||
|
||||
public PreBuildCommand(CommandHelper commandHelper)
|
||||
{
|
||||
CommandHelper = commandHelper;
|
||||
}
|
||||
|
||||
[Command("prebuild")]
|
||||
public async Task Prebuild(
|
||||
[Argument] string moonlightDir,
|
||||
[Argument] string nugetDir
|
||||
)
|
||||
{
|
||||
var dependencies = await GetDependenciesFromNuget(nugetDir);
|
||||
|
||||
Console.WriteLine("Following plugins found:");
|
||||
|
||||
foreach (var dependency in dependencies)
|
||||
{
|
||||
Console.WriteLine($"{dependency.Id} ({dependency.Version}) [{dependency.Tags}]");
|
||||
}
|
||||
|
||||
var csProjFiles = Directory.GetFiles(moonlightDir, "*.csproj", SearchOption.AllDirectories);
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Adjusting csproj files");
|
||||
foreach (var csProjFile in csProjFiles)
|
||||
{
|
||||
await using var fs = File.Open(
|
||||
csProjFile,
|
||||
FileMode.Open,
|
||||
FileAccess.ReadWrite,
|
||||
FileShare.ReadWrite
|
||||
);
|
||||
|
||||
var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None);
|
||||
fs.Close();
|
||||
|
||||
// Search for tag definitions
|
||||
var packageTagsElements = document.Descendants("PackageTags").ToArray();
|
||||
|
||||
if (packageTagsElements.Length == 0)
|
||||
{
|
||||
Console.WriteLine($"No package tags found in {Path.GetFullPath(csProjFile)}. Skipping it");
|
||||
continue;
|
||||
}
|
||||
|
||||
var packageTags = packageTagsElements.First().Value;
|
||||
|
||||
var dependenciesToAdd = dependencies
|
||||
.Where(x => x.Tags.Contains(packageTags, StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
await RemoveDependencies(csProjFile);
|
||||
await AddDependencies(csProjFile, dependenciesToAdd);
|
||||
}
|
||||
|
||||
Console.WriteLine("Restoring projects");
|
||||
foreach (var csProjFile in csProjFiles)
|
||||
{
|
||||
await RestoreProject(csProjFile, nugetDir);
|
||||
}
|
||||
|
||||
Console.WriteLine("Generating plugin startup");
|
||||
|
||||
string[] validTags = ["apiserver", "frontend"];
|
||||
|
||||
foreach (var currentTag in validTags)
|
||||
{
|
||||
Console.WriteLine($"Checking for '{currentTag}' projects");
|
||||
|
||||
foreach (var csProjFile in csProjFiles)
|
||||
{
|
||||
var tags = await GetTagsFromCsproj(csProjFile);
|
||||
|
||||
if (string.IsNullOrEmpty(tags))
|
||||
{
|
||||
Console.WriteLine($"No package tags found in {Path.GetFullPath(csProjFile)}. Skipping it");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!tags.Contains(currentTag))
|
||||
continue;
|
||||
|
||||
var currentDeps = dependencies
|
||||
.Where(x => x.Tags.Contains(currentTag))
|
||||
.ToArray();
|
||||
|
||||
var classPaths = await FindStartupClasses(currentDeps);
|
||||
|
||||
var code = new StringBuilder();
|
||||
|
||||
code.AppendLine(GeneratedStart);
|
||||
|
||||
foreach (var path in classPaths)
|
||||
code.AppendLine($"pluginStartups.Add(new global::{path}());");
|
||||
|
||||
code.AppendLine(GeneratedEnd);
|
||||
|
||||
var filesToSearch = Directory.GetFiles(
|
||||
Path.GetDirectoryName(csProjFile)!,
|
||||
"*.cs",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
|
||||
foreach (var file in filesToSearch)
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(file);
|
||||
|
||||
if (!content.Contains(GeneratedHook, StringComparison.InvariantCultureIgnoreCase))
|
||||
continue;
|
||||
|
||||
Console.WriteLine($"Injecting generated code to: {Path.GetFullPath(file)}");
|
||||
|
||||
content = content.Replace(
|
||||
GeneratedHook,
|
||||
code.ToString(),
|
||||
StringComparison.InvariantCultureIgnoreCase
|
||||
);
|
||||
|
||||
await File.WriteAllTextAsync(file, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Console.WriteLine("An error occured while prebuilding moonlight. Removing csproj modifications");
|
||||
|
||||
foreach (var csProjFile in csProjFiles)
|
||||
await RemoveDependencies(csProjFile);
|
||||
|
||||
await RemoveGeneratedCode(moonlightDir);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[Command("prebuild-reset")]
|
||||
public async Task PrebuildReset(
|
||||
[Argument] string moonlightDir
|
||||
)
|
||||
{
|
||||
var csProjFiles = Directory.GetFiles(moonlightDir, "*.csproj", SearchOption.AllDirectories);
|
||||
|
||||
Console.WriteLine("Reverting csproj changes");
|
||||
|
||||
foreach (var csProjFile in csProjFiles)
|
||||
await RemoveDependencies(csProjFile);
|
||||
|
||||
Console.WriteLine("Removing generated code");
|
||||
await RemoveGeneratedCode(moonlightDir);
|
||||
}
|
||||
|
||||
[Command("test")]
|
||||
public async Task Test(
|
||||
[Argument] string nugetDir
|
||||
)
|
||||
{
|
||||
var dependencies = await GetDependenciesFromNuget(nugetDir);
|
||||
|
||||
await FindStartupClasses(dependencies);
|
||||
}
|
||||
|
||||
private async Task<Dependency[]> GetDependenciesFromNuget(string nugetDir)
|
||||
{
|
||||
var nugetFiles = Directory.GetFiles(nugetDir, "*.nupkg", SearchOption.AllDirectories);
|
||||
var dependencies = new List<Dependency>();
|
||||
|
||||
foreach (var nugetFile in nugetFiles)
|
||||
{
|
||||
var dependency = await GetDependencyFromPackage(nugetFile);
|
||||
dependencies.Add(dependency);
|
||||
}
|
||||
|
||||
return dependencies.ToArray();
|
||||
}
|
||||
|
||||
private async Task<Dependency> GetDependencyFromPackage(string path)
|
||||
{
|
||||
using var nugetPackage = ZipFile.Open(path, ZipArchiveMode.Read);
|
||||
|
||||
var nuspecEntry = nugetPackage.Entries.First(x => x.Name.EndsWith(".nuspec"));
|
||||
await using var nuspecFs = nuspecEntry.Open();
|
||||
|
||||
var nuspec = await XDocument.LoadAsync(nuspecFs, LoadOptions.None, CancellationToken.None);
|
||||
|
||||
var ns = nuspec.Root!.GetDefaultNamespace();
|
||||
var metadata = nuspec.Root!.Element(ns + "metadata")!;
|
||||
|
||||
var id = metadata.Element(ns + "id")!.Value;
|
||||
var version = metadata.Element(ns + "version")!.Value;
|
||||
var tags = metadata.Element(ns + "tags")!.Value;
|
||||
|
||||
nuspecFs.Close();
|
||||
|
||||
return new Dependency()
|
||||
{
|
||||
Id = id,
|
||||
Version = version,
|
||||
Tags = tags
|
||||
};
|
||||
}
|
||||
|
||||
private async Task AddDependencies(string path, Dependency[] dependencies)
|
||||
{
|
||||
await using var fs = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||
fs.Position = 0;
|
||||
|
||||
var csProj = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None);
|
||||
|
||||
var project = csProj.Element("Project")!;
|
||||
|
||||
var itemGroup = new XElement("ItemGroup");
|
||||
itemGroup.SetAttributeValue("Label", "MoonlightBuildDeps");
|
||||
|
||||
foreach (var dependency in dependencies)
|
||||
{
|
||||
var depElement = new XElement("PackageReference");
|
||||
depElement.SetAttributeValue("Include", dependency.Id);
|
||||
depElement.SetAttributeValue("Version", dependency.Version);
|
||||
|
||||
itemGroup.Add(depElement);
|
||||
}
|
||||
|
||||
project.Add(itemGroup);
|
||||
|
||||
fs.Position = 0;
|
||||
await csProj.SaveAsync(fs, SaveOptions.None, CancellationToken.None);
|
||||
}
|
||||
|
||||
private Task RemoveDependencies(string path)
|
||||
{
|
||||
var csProj = XDocument.Load(path, LoadOptions.None);
|
||||
|
||||
var itemGroupsToRemove = csProj
|
||||
.Descendants("ItemGroup")
|
||||
.Where(x => x.Attribute("Label")?.Value.Contains("MoonlightBuildDeps") ?? false)
|
||||
.ToArray();
|
||||
|
||||
itemGroupsToRemove.Remove();
|
||||
|
||||
csProj.Save(path, SaveOptions.None);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task RestoreProject(string file, string nugetPath)
|
||||
{
|
||||
var basePath = Path.GetFullPath(Path.GetDirectoryName(file)!);
|
||||
var fileName = Path.GetFileName(file);
|
||||
var nugetPathFull = Path.GetFullPath(nugetPath);
|
||||
|
||||
Console.WriteLine($"Restore: {basePath} - {fileName}");
|
||||
|
||||
await CommandHelper.Run(
|
||||
"/usr/bin/dotnet",
|
||||
$"restore {fileName} --source {nugetPathFull}",
|
||||
basePath
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<string[]> FindStartupClasses(Dependency[] dependencies)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
var nugetPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
".nuget",
|
||||
"packages"
|
||||
);
|
||||
|
||||
var filesToScan = dependencies.SelectMany(dependency =>
|
||||
{
|
||||
var dependencySrcPath = Path.Combine(nugetPath, dependency.Id.ToLower(), dependency.Version, "src");
|
||||
|
||||
Console.WriteLine($"Checking {dependencySrcPath}");
|
||||
|
||||
if (!Directory.Exists(dependencySrcPath))
|
||||
return [];
|
||||
|
||||
return Directory.GetFiles(dependencySrcPath, "*.cs", SearchOption.AllDirectories);
|
||||
}
|
||||
).ToArray();
|
||||
|
||||
var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
|
||||
|
||||
var trees = new List<SyntaxTree>();
|
||||
|
||||
foreach (var file in filesToScan)
|
||||
{
|
||||
Console.WriteLine($"Reading {file}");
|
||||
|
||||
var content = await File.ReadAllTextAsync(file);
|
||||
var tree = CSharpSyntaxTree.ParseText(content);
|
||||
trees.Add(tree);
|
||||
}
|
||||
|
||||
var compilation = CSharpCompilation.Create("Analysis", trees, [mscorlib]);
|
||||
|
||||
foreach (var tree in trees)
|
||||
{
|
||||
var model = compilation.GetSemanticModel(tree);
|
||||
var root = await tree.GetRootAsync();
|
||||
|
||||
var classDeclarations = root
|
||||
.DescendantNodes()
|
||||
.OfType<ClassDeclarationSyntax>();
|
||||
|
||||
foreach (var classDeclaration in classDeclarations)
|
||||
{
|
||||
var symbol = model.GetDeclaredSymbol(classDeclaration);
|
||||
|
||||
if (symbol == null)
|
||||
continue;
|
||||
|
||||
var hasAttribute = symbol.GetAttributes().Any(attr =>
|
||||
{
|
||||
if (attr.AttributeClass == null)
|
||||
return false;
|
||||
|
||||
return attr.AttributeClass.Name == "PluginStartup";
|
||||
});
|
||||
|
||||
if (!hasAttribute)
|
||||
continue;
|
||||
|
||||
var classPath =
|
||||
$"{symbol.ContainingNamespace.ToDisplayString()}.{classDeclaration.Identifier.ValueText}";
|
||||
|
||||
Console.WriteLine($"Detected startup in class: {classPath}");
|
||||
result.Add(classPath);
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private async Task<string> GetTagsFromCsproj(string csProjFile)
|
||||
{
|
||||
await using var fs = File.Open(
|
||||
csProjFile,
|
||||
FileMode.Open,
|
||||
FileAccess.ReadWrite,
|
||||
FileShare.ReadWrite
|
||||
);
|
||||
|
||||
var document = await XDocument.LoadAsync(fs, LoadOptions.None, CancellationToken.None);
|
||||
fs.Close();
|
||||
|
||||
// Search for tag definitions
|
||||
var packageTagsElements = document.Descendants("PackageTags").ToArray();
|
||||
|
||||
if (packageTagsElements.Length == 0)
|
||||
return "";
|
||||
|
||||
return packageTagsElements.First().Value;
|
||||
}
|
||||
|
||||
private async Task RemoveGeneratedCode(string dir)
|
||||
{
|
||||
var filesToSearch = Directory.GetFiles(
|
||||
dir,
|
||||
"*.cs",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
|
||||
foreach (var file in filesToSearch)
|
||||
{
|
||||
// We dont want to replace ourself
|
||||
if (file.Contains("PreBuildCommand.cs"))
|
||||
continue;
|
||||
|
||||
var content = await File.ReadAllTextAsync(file);
|
||||
|
||||
if (!content.Contains(GeneratedStart) || !content.Contains(GeneratedEnd))
|
||||
continue;
|
||||
|
||||
var startIndex = content.IndexOf(GeneratedStart, StringComparison.InvariantCultureIgnoreCase);
|
||||
var endIndex = content.IndexOf(GeneratedEnd, startIndex, StringComparison.InvariantCultureIgnoreCase) +
|
||||
GeneratedEnd.Length;
|
||||
|
||||
var cutOut = content.Substring(startIndex, endIndex - startIndex);
|
||||
|
||||
content = content.Replace(cutOut, GeneratedHook);
|
||||
|
||||
await File.WriteAllTextAsync(file, content);
|
||||
}
|
||||
}
|
||||
|
||||
private record Dependency
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string Tags { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Scripts.Functions;
|
||||
|
||||
public class TagsFunctions
|
||||
{
|
||||
public static async Task Run(string[] args)
|
||||
{
|
||||
if (args.Length < 3)
|
||||
{
|
||||
Console.WriteLine("You need to specify a directory, tag and at least one command");
|
||||
return;
|
||||
}
|
||||
|
||||
var rootDir = args[0];
|
||||
var tag = args[1];
|
||||
var commands = args.Skip(2).ToArray();
|
||||
|
||||
var csprojFiles = Directory.GetFiles(
|
||||
rootDir,
|
||||
"*.csproj",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
|
||||
foreach (var csprojFile in csprojFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load the .csproj file
|
||||
var csprojXml = XElement.Load(csprojFile);
|
||||
|
||||
// Check if <PackageTags> exists within the .csproj file
|
||||
var packageTagsElement = csprojXml.Descendants("PackageTags").FirstOrDefault();
|
||||
if (packageTagsElement != null)
|
||||
{
|
||||
if(!packageTagsElement.Value.Contains(tag))
|
||||
continue;
|
||||
|
||||
var projectName = Path.GetFileName(Path.GetDirectoryName(csprojFile))!;
|
||||
var projectFile = Path.GetFileName(csprojFile);
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
// Replace PROJECT_NAME and PROJECT_FILE with the actual values
|
||||
var bashCommand = command.Replace("PROJECT_NAME", projectName).Replace("PROJECT_FILE", projectFile);
|
||||
RunBashCommand(bashCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error processing {csprojFile}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void RunBashCommand(string command)
|
||||
{
|
||||
try
|
||||
{
|
||||
var processStartInfo = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "bash",
|
||||
Arguments = $"-c \"{command}\"",
|
||||
};
|
||||
|
||||
var process = System.Diagnostics.Process.Start(processStartInfo);
|
||||
process!.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error running bash command: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Resources/Scripts/Helpers/CommandHelper.cs
Normal file
30
Resources/Scripts/Helpers/CommandHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
46
Resources/Scripts/Helpers/StartupClassDetector.cs
Normal file
46
Resources/Scripts/Helpers/StartupClassDetector.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Scripts.Helpers;
|
||||
|
||||
public class StartupClassDetector
|
||||
{
|
||||
public bool Check(string content, out string fullName)
|
||||
{
|
||||
var tree = CSharpSyntaxTree.ParseText(content);
|
||||
var root = tree.GetCompilationUnitRoot();
|
||||
|
||||
var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
|
||||
var compilation = CSharpCompilation.Create("MyAnalysis", [tree], [mscorlib]);
|
||||
|
||||
var model = compilation.GetSemanticModel(tree);
|
||||
|
||||
var classDeclarations = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
|
||||
|
||||
foreach (var classDecl in classDeclarations)
|
||||
{
|
||||
var symbol = model.GetDeclaredSymbol(classDecl);
|
||||
|
||||
if(symbol == null)
|
||||
continue;
|
||||
|
||||
var hasAttribute = symbol.GetAttributes().Any(attribute =>
|
||||
{
|
||||
if (attribute.AttributeClass == null)
|
||||
return false;
|
||||
|
||||
return attribute.AttributeClass.Name.Contains("PluginStartup");
|
||||
});
|
||||
|
||||
if (hasAttribute)
|
||||
{
|
||||
fullName = symbol.ContainingNamespace.ToDisplayString() + "." + classDecl.Identifier.ValueText;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
fullName = "";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,19 @@
|
||||
using Scripts.Functions;
|
||||
using Cocona;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Scripts.Commands;
|
||||
using Scripts.Helpers;
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Console.WriteLine("You need to specify a module to run");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine("Moonlight Build Helper Script");
|
||||
Console.WriteLine();
|
||||
|
||||
var module = args[0];
|
||||
var moduleArgs = args.Skip(1).ToArray();
|
||||
var builder = CoconaApp.CreateBuilder(args);
|
||||
|
||||
switch (module)
|
||||
{
|
||||
case "staticWebAssets":
|
||||
await StaticWebAssetsFunctions.Run(moduleArgs);
|
||||
break;
|
||||
case "content":
|
||||
await ContentFunctions.Run(moduleArgs);
|
||||
break;
|
||||
case "src":
|
||||
await SrcFunctions.Run(moduleArgs);
|
||||
break;
|
||||
case "tags":
|
||||
await TagsFunctions.Run(moduleArgs);
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine($"No module named {module} found");
|
||||
break;
|
||||
}
|
||||
builder.Services.AddSingleton<CommandHelper>();
|
||||
builder.Services.AddSingleton<StartupClassDetector>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.AddCommands<PackCommand>();
|
||||
app.AddCommands<PreBuildCommand>();
|
||||
|
||||
await app.RunAsync();
|
||||
@@ -1,10 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<Folder Include="Modules\" />
|
||||
<Folder Include="outputDir\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.13.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user