diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f64877d..e53e3137 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Install .NET Core uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Build & test (Release) run: dotnet test src -c Release --logger "console;verbosity=normal" diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..b6cefc44 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "editorconfig.editorconfig", + "ms-dotnettools.csharp" + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7538bf22..42adbc71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG TARGETARCH WORKDIR /app @@ -16,7 +16,7 @@ COPY . ./ RUN dotnet publish src -a $TARGETARCH -c Release -o /app/src/out # Build runtime image -FROM mcr.microsoft.com/dotnet/aspnet:8.0 +FROM mcr.microsoft.com/dotnet/aspnet:9.0 WORKDIR /app COPY --from=build /app/src/out . ENTRYPOINT ["dotnet", "UnityNuGet.Server.dll"] diff --git a/examples/docker/docker-compose.yml b/examples/docker/docker-compose.yml index f2182a61..364d750c 100644 --- a/examples/docker/docker-compose.yml +++ b/examples/docker/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.9" +--- services: unitynuget: build: ../.. diff --git a/readme.md b/readme.md index d82e1cd0..73acd887 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # UnityNuGet [![Build Status](https://github.com/xoofx/UnityNuGet/workflows/ci/badge.svg?branch=master)](https://github.com/xoofx/UnityNuGet/actions) [![Static Badge](https://img.shields.io/badge/server-status-blue)](https://unitynuget-registry.azurewebsites.net/status) [![Static Badge](https://img.shields.io/badge/server-feed-blue)](https://unitynuget-registry.azurewebsites.net/-/all) - +UnityNuGet logo This project provides a seamlessly integration of a [curated list](registry.json) of NuGet packages within the Unity Package Manager. @@ -8,7 +8,7 @@ This project provides a seamlessly integration of a [curated list](registry.json ## Installation -### Add scope registry (manifest.json): +### Add scope registry (manifest.json) In order to use this service you simply need to edit the `Packages/manifest.json` in your project and add the following scoped registry: @@ -24,16 +24,16 @@ In order to use this service you simply need to edit the `Packages/manifest.json } ], "dependencies": { - "org.nuget.scriban": "2.1.0" + "org.nuget.scriban": "2.1.0" } } ``` -### Add scope registry (Package Manager UI): +### Add scope registry (Package Manager UI) -Instructions: https://docs.unity3d.com/Manual/class-PackageManager.html +Instructions: -``` +```yaml Name: Unity NuGet Url: https://unitynuget-registry.azurewebsites.net @@ -43,21 +43,21 @@ Scope(s): org.nuget ### Disable Assembly Version Validation -This step is necessary to ensure that binding redirects for [strongly named assemblies](https://learn.microsoft.com/en-us/dotnet/standard/assembly/strong-named) in NuGet packages resolve correctly to paths _within the Unity project_. +This step is necessary to ensure that binding redirects for [strongly named assemblies](https://learn.microsoft.com/en-us/dotnet/standard/assembly/strong-named) in NuGet packages resolve correctly to paths _within the Unity project_. - In Unity 2022.2+, this is the [default behavior](https://forum.unity.com/threads/editor-assembly-loading-issues-unloading-broken-assembly-could-not-load-signature.754508/#post-8647791), so no action is required. - For earlier Unity versions, uncheck "Project Settings > Player > Other Settings > Configuration > Assembly Version Validation" ### Verify installation -> WARNING: If you are encountering weird compilation errors with UnityNuGet and you have been using UnityNuGet already, +> WARNING: If you are encountering weird compilation errors with UnityNuGet and you have been using UnityNuGet already, > it could be that we have updated packages on the server, and in that case, you need to clear the cache containing -> all Unity NPM packages downdloaded from the `unitynuget-registry.azurewebsites.net` registry. +> all Unity NPM packages downloaded from the `unitynuget-registry.azurewebsites.net` registry. > On Windows, this cache is located at: `%localappdata%\Unity\cache\npm\unitynuget-registry.azurewebsites.net` > -> Cache locations by OS: https://docs.unity3d.com/Manual/upm-cache.html +> Cache locations by OS: -When opening the Package Manager Window, you should see a few packages coming from NuGet (with the postfix text ` (NuGet)`) +When opening the Package Manager Window, you should see a few packages coming from NuGet (with the postfix text `‎ (NuGet)`) ![UnityEditorWithNuGet](img/unity_editor_with_nuget.jpg) @@ -68,7 +68,7 @@ This service provides only a [curated list](registry.json) of NuGet packages Your NuGet package needs to respect a few constraints in order to be listed in the curated list: - It must have non-preview versions (e.g `1.0.0` but not `1.0.0-preview.1`) -- It must provide `.NETStandard2.0` assemblies as part of its package +- It must provide at least `.NETStandard2.0` (and optionally `.NETStandard2.1`) assemblies as part of its package You can send a PR to this repository to modify the [registry.json](registry.json) file (don't forget to maintain the alphabetical order) @@ -76,7 +76,7 @@ You also need to **specify the lowest version of your package that has support f Beware that **all transitive dependencies of the package** must be **explicitly listed** in the registry as well. -> NOTE: +> NOTE: > * We reserve the right to decline a package to be available through this service > * The server will be updated only when a new version tag is pushed on the main branch. @@ -84,15 +84,16 @@ Beware that **all transitive dependencies of the package** must be **explicitly Only compatible with **`Unity 2019.1`** and potentially with newer version. -> NOTE: This service is currently only tested with **`Unity 2019.x, 2020.x and 2021.x`** +> NOTE: This service is currently only tested with **`Unity 2019.x, 2020.x, 2021.x, 2022.x, 2023.x and 6`** > > It may not work with a more recent version of Unity ## Docker > Available in [ghcr (GitHub Container Registry)](https://github.com/xoofx/UnityNuGet/pkgs/container/unitynuget). -> +> > Supported platforms: +> > - linux/amd64 > - linux/arm64 @@ -144,7 +145,7 @@ On Azure through my own Azure credits coming from my MVP subscription, enjoy! ### **Why can't you add all NuGet packages?** -The reason is that many NuGet packages are not compatible with Unity, or do not provide `.NETStandard2.0` assemblies or are not relevant for being used within Unity. +The reason is that many NuGet packages are not compatible with Unity, or do not provide `.NETStandard2.0` or `.NETStandard2.1` assemblies or are not relevant for being used within Unity. Also currently the Package Manager doesn't provide a way to filter easily packages, so the UI is currently not adequate to list lots of packages. @@ -154,19 +155,21 @@ Since 2019.1.x, Unity is compatible with `.NETStandard2.0` and it is the .NET pr Having a `.NETStandard2.0` for NuGet packages for Unity can ensure that the experience to add a package to your project is consistent and well supported. -> More information: https://docs.unity3d.com/Manual/dotnetProfileSupport.html +As of Unity 2021.x it also supports `.NETStandard2.1` so packages providing this target will be compatible with this version of Unity or newer. + +> More information: ### **How this service is working?** -This project implements a simplified compatible NPM server in C# using ASP.NET Core and converts NuGet packages to Unity packages before serving them. +This project implements a simplified compatible NPM server in C# using ASP.NET Core and converts NuGet packages to Unity packages before serving them. -Every 10min, packages are updated from NuGet so that if a new version is published, from the curated list of NuGet packages, it will be available through this service. +Every 10 minutes, packages are updated from NuGet so that if a new version is published, from the curated list of NuGet packages, it will be available through this service. Once converted, these packages are cached on the disk on the server. ## License -This software is released under the [BSD-Clause 2 license](https://opensource.org/licenses/BSD-2-Clause). +This software is released under the [BSD-Clause 2 license](https://opensource.org/licenses/BSD-2-Clause). ## Author diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b91d59b2..370d3f4c 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,7 @@ enable + true true diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 1c7de547..053e0020 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,24 +1,24 @@ - - - true - - - - - - - - - - - - - - - - - - - - + + + true + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/UnityNuGet.Server/Program.cs b/src/UnityNuGet.Server/Program.cs index d154f6c1..00328f47 100644 --- a/src/UnityNuGet.Server/Program.cs +++ b/src/UnityNuGet.Server/Program.cs @@ -1,36 +1,44 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using UnityNuGet; +using UnityNuGet.Server; + +var builder = WebApplication.CreateBuilder(args); -namespace UnityNuGet.Server -{ - public static class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } +// Add the registry cache initializer +builder.Services.AddHostedService(); +// Add the registry cache updater +builder.Services.AddHostedService(); +// Add the registry cache report +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.Configure(builder.Configuration.GetSection("Registry")); +builder.Services.AddSingleton, ValidateRegistryOptions>(); +builder.Services.AddOptionsWithValidateOnStart(); + +builder.Services.AddApplicationInsightsTelemetry(); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - //webBuilder.UseSetting("detailedErrors", "true"); - webBuilder.ConfigureServices((context, services) => - { - // Add the registry cache initializer - services.AddHostedService(); - // Add the registry cache updater - services.AddHostedService(); - // Add the registry cache report - services.AddSingleton(); - services.AddSingleton(); +// Also enable NewtonsoftJson serialization +builder.Services.AddControllers().AddNewtonsoftJson(); + +var app = builder.Build(); - services.AddOptions() - .Bind(context.Configuration.GetSection("Registry")) - .ValidateDataAnnotations(); - }); - webBuilder.UseStartup(); - }); - } +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); + app.LogRequestHeaders(app.Services.GetRequiredService()); +} +else +{ + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); } +app.UseRouting(); +app.MapControllers(); +app.MapStatus(); + +app.Run(); diff --git a/src/UnityNuGet.Server/RegistryCacheInitializer.cs b/src/UnityNuGet.Server/RegistryCacheInitializer.cs index 4fdc8f65..e8d9079c 100644 --- a/src/UnityNuGet.Server/RegistryCacheInitializer.cs +++ b/src/UnityNuGet.Server/RegistryCacheInitializer.cs @@ -44,7 +44,7 @@ public Task StartAsync(CancellationToken cancellationToken) if (isDevelopment) { - var currentDirectory = Path.GetDirectoryName(typeof(Startup).Assembly.Location)!; + var currentDirectory = Path.GetDirectoryName(AppContext.BaseDirectory)!; unityPackageFolder = Path.Combine(currentDirectory, new DirectoryInfo(registryOptions.RootPersistentFolder!).Name); } else diff --git a/src/UnityNuGet.Server/RegistryCacheUpdater.cs b/src/UnityNuGet.Server/RegistryCacheUpdater.cs index aed12d3f..4de59968 100644 --- a/src/UnityNuGet.Server/RegistryCacheUpdater.cs +++ b/src/UnityNuGet.Server/RegistryCacheUpdater.cs @@ -59,10 +59,19 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await Task.Delay((int)_registryOptions.UpdateInterval.TotalMilliseconds, stoppingToken); } + } + catch (TaskCanceledException) + { + string message = "RegistryCache update canceled"; + + _logger.LogInformation("{Message}", message); + + _registryCacheReport.AddInformation($"{message}."); + _registryCacheReport.Complete(); } catch (Exception ex) { - string message = "Error while building a new registry cache."; + string message = "Error while building a new registry cache"; _logger.LogError(ex, "{Message}", message); diff --git a/src/UnityNuGet.Server/Startup.cs b/src/UnityNuGet.Server/Startup.cs deleted file mode 100644 index 0e26fdef..00000000 --- a/src/UnityNuGet.Server/Startup.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace UnityNuGet.Server -{ - public class Startup - { - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddApplicationInsightsTelemetry(); - - // Also enable NewtonsoftJson serialization - services.AddControllers().AddNewtonsoftJson(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostEnvironment env, ILoggerFactory loggerFactory) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.LogRequestHeaders(loggerFactory); - } - else - { - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - - endpoints.MapStatus(); - }); - } - } -} diff --git a/src/UnityNuGet.Server/UnityNuGet.Server.csproj b/src/UnityNuGet.Server/UnityNuGet.Server.csproj index 7aabd46f..746efbf3 100644 --- a/src/UnityNuGet.Server/UnityNuGet.Server.csproj +++ b/src/UnityNuGet.Server/UnityNuGet.Server.csproj @@ -1,10 +1,11 @@  - net8.0 + net9.0 /subscriptions/b6745039-70e7-4641-994b-5457cb220e2a/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/unitynuget-registry /subscriptions/b6745039-70e7-4641-994b-5457cb220e2a/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/unitynuget-registry 1be0a769-8d75-4a27-99e0-128afcc0ffee + false diff --git a/src/UnityNuGet.Tests/UnityNuGet.Tests.csproj b/src/UnityNuGet.Tests/UnityNuGet.Tests.csproj index b0f45a29..3b8c8484 100644 --- a/src/UnityNuGet.Tests/UnityNuGet.Tests.csproj +++ b/src/UnityNuGet.Tests/UnityNuGet.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 false false diff --git a/src/UnityNuGet/Registry.cs b/src/UnityNuGet/Registry.cs index a9c19297..1b011c9c 100644 --- a/src/UnityNuGet/Registry.cs +++ b/src/UnityNuGet/Registry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using Newtonsoft.Json; namespace UnityNuGet @@ -12,7 +13,7 @@ namespace UnityNuGet public sealed class Registry : Dictionary { private const string RegistryFileName = "registry.json"; - private static readonly object LockRead = new(); + private static readonly Lock LockRead = new(); private static Registry? _registry = null; // A comparer is established for cases where the dependency name is not set to the correct case. @@ -31,7 +32,7 @@ public static Registry GetInstance() { lock (LockRead) { - _registry ??= Parse(File.ReadAllText(Path.Combine(Path.GetDirectoryName(typeof(Registry).Assembly.Location)!, RegistryFileName))); + _registry ??= Parse(File.ReadAllText(Path.Combine(Path.GetDirectoryName(AppContext.BaseDirectory)!, RegistryFileName))); } return _registry; } diff --git a/src/UnityNuGet/RegistryOptions.cs b/src/UnityNuGet/RegistryOptions.cs index b61bc157..ee6cdc6c 100644 --- a/src/UnityNuGet/RegistryOptions.cs +++ b/src/UnityNuGet/RegistryOptions.cs @@ -1,45 +1,47 @@ -using System; -using System.ComponentModel.DataAnnotations; -using Newtonsoft.Json; -using NuGet.Frameworks; - -namespace UnityNuGet -{ - public class RegistryOptions - { - [Required] - public Uri? RootHttpUrl { get; set; } - - [Required] - [RegularExpression(@"[a-z]+\.[a-z]+")] - public string? UnityScope { get; set; } - - [Required] - [RegularExpression(@"\d+\.\d+")] - public string? MinimumUnityVersion { get; set; } - - [Required] - public string? PackageNameNuGetPostFix { get; set; } - - [Required] - public string? RootPersistentFolder { get; set; } - - [Required] - public TimeSpan UpdateInterval { get; set; } - - [Required] - public RegistryTargetFramework[]? TargetFrameworks { get; set; } - } - - public class RegistryTargetFramework - { - [Required] - public string? Name { get; set; } - - [Required] - public string[]? DefineConstraints { get; set; } - - [JsonIgnore] - internal NuGetFramework? Framework { get; set; } - } -} +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using NuGet.Frameworks; + +namespace UnityNuGet +{ + public class RegistryOptions + { + [Required] + public Uri? RootHttpUrl { get; set; } + + [Required] + [RegularExpression(@"[a-z]+\.[a-z]+")] + public string? UnityScope { get; set; } + + [Required] + [RegularExpression(@"\d+\.\d+")] + public string? MinimumUnityVersion { get; set; } + + [Required] + public string? PackageNameNuGetPostFix { get; set; } + + [Required] + public string? RootPersistentFolder { get; set; } + + [Required] + public TimeSpan UpdateInterval { get; set; } + + [Required] + [ValidateEnumeratedItems] + public RegistryTargetFramework[]? TargetFrameworks { get; set; } + } + + public class RegistryTargetFramework + { + [Required] + public string? Name { get; set; } + + [Required] + public string[]? DefineConstraints { get; set; } + + [JsonIgnore] + internal NuGetFramework? Framework { get; set; } + } +} diff --git a/src/UnityNuGet/UnityNuGet.csproj b/src/UnityNuGet/UnityNuGet.csproj index 6943335e..fa3c9ee8 100644 --- a/src/UnityNuGet/UnityNuGet.csproj +++ b/src/UnityNuGet/UnityNuGet.csproj @@ -1,8 +1,9 @@  - net8.0 + net9.0 0.14.0 + true diff --git a/src/UnityNuGet/ValidateRegistryOptions.cs b/src/UnityNuGet/ValidateRegistryOptions.cs new file mode 100644 index 00000000..15adc9aa --- /dev/null +++ b/src/UnityNuGet/ValidateRegistryOptions.cs @@ -0,0 +1,7 @@ +using Microsoft.Extensions.Options; + +namespace UnityNuGet +{ + [OptionsValidator] + public sealed partial class ValidateRegistryOptions : IValidateOptions; +} diff --git a/src/global.json b/src/global.json index f7fb55b4..40584df3 100644 --- a/src/global.json +++ b/src/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.100", "rollForward": "latestMinor", "allowPrerelease": false }