├── .gitignore ├── appveyor.yml ├── changelog.md ├── img ├── unity_editor_with_nuget.jpg ├── unitynuget.png └── unitynuget.svg ├── license.txt ├── readme.md ├── registry.json └── src ├── UnityNuGet.Server ├── ApplicationBuilderExtension.cs ├── Connected Services │ └── Application Insights │ │ └── ConnectedService.json ├── Controllers │ └── ApiController.cs ├── NuGetRedirectLogger.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── RegistryCacheSingleton.cs ├── RegistryCacheUpdater.cs ├── Startup.cs ├── UnityNuGet.Server.csproj ├── app.config ├── appsettings.Development.json └── appsettings.json ├── UnityNuGet.Tests ├── RegistryCacheTests.cs └── UnityNuGet.Tests.csproj ├── UnityNuGet.sln ├── UnityNuGet ├── JsonCommonExtensions.cs ├── JsonObjectBase.cs ├── Npm │ ├── NpmDistribution.cs │ ├── NpmError.cs │ ├── NpmObject.cs │ ├── NpmPackage.cs │ ├── NpmPackageInfo.cs │ ├── NpmPackageListAllResponse.cs │ ├── NpmPackageRegistry.cs │ ├── NpmPackageVersion.cs │ └── NpmSourceRepository.cs ├── NuGetConsoleLogger.cs ├── Registry.cs ├── RegistryCache.cs ├── RegistryEntry.cs ├── UnityMeta.cs ├── UnityNuGet.csproj └── UnityPackage.cs └── global.json /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Remove idea/jetbrains files and tmp/ folder 39 | .idea/ 40 | tmp/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # JustCode is a .NET coding add-in 134 | .JustCode 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Visual Studio code coverage results 147 | *.coverage 148 | *.coveragexml 149 | 150 | # NCrunch 151 | _NCrunch_* 152 | .*crunch*.local.xml 153 | nCrunchTemp_* 154 | 155 | # MightyMoose 156 | *.mm.* 157 | AutoTest.Net/ 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # NuGet Symbol Packages 194 | *.snupkg 195 | # The packages folder can be ignored because of Package Restore 196 | **/[Pp]ackages/* 197 | # except build/, which is used as an MSBuild target. 198 | !**/[Pp]ackages/build/ 199 | # Uncomment if necessary however generally it will be regenerated when needed 200 | #!**/[Pp]ackages/repositories.config 201 | # NuGet v3's project.json files produces more ignorable files 202 | *.nuget.props 203 | *.nuget.targets 204 | 205 | # Microsoft Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Microsoft Azure Emulator 210 | ecf/ 211 | rcf/ 212 | 213 | # Windows Store app package directories and files 214 | AppPackages/ 215 | BundleArtifacts/ 216 | Package.StoreAssociation.xml 217 | _pkginfo.txt 218 | *.appx 219 | *.appxbundle 220 | *.appxupload 221 | 222 | # Visual Studio cache files 223 | # files ending in .cache can be ignored 224 | *.[Cc]ache 225 | # but keep track of directories ending in .cache 226 | !?*.[Cc]ache/ 227 | 228 | # Others 229 | ClientBin/ 230 | ~$* 231 | *~ 232 | *.dbmdl 233 | *.dbproj.schemaview 234 | *.jfm 235 | *.pfx 236 | *.publishsettings 237 | orleans.codegen.cs 238 | 239 | # Including strong name files can present a security risk 240 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 241 | #*.snk 242 | 243 | # Since there are multiple workflows, uncomment next line to ignore bower_components 244 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 245 | #bower_components/ 246 | 247 | # RIA/Silverlight projects 248 | Generated_Code/ 249 | 250 | # Backup & report files from converting an old project file 251 | # to a newer Visual Studio version. Backup files are not needed, 252 | # because we have git ;-) 253 | _UpgradeReport_Files/ 254 | Backup*/ 255 | UpgradeLog*.XML 256 | UpgradeLog*.htm 257 | ServiceFabricBackup/ 258 | *.rptproj.bak 259 | 260 | # SQL Server files 261 | *.mdf 262 | *.ldf 263 | *.ndf 264 | 265 | # Business Intelligence projects 266 | *.rdl.data 267 | *.bim.layout 268 | *.bim_*.settings 269 | *.rptproj.rsuser 270 | *- [Bb]ackup.rdl 271 | *- [Bb]ackup ([0-9]).rdl 272 | *- [Bb]ackup ([0-9][0-9]).rdl 273 | 274 | # Microsoft Fakes 275 | FakesAssemblies/ 276 | 277 | # GhostDoc plugin setting file 278 | *.GhostDoc.xml 279 | 280 | # Node.js Tools for Visual Studio 281 | .ntvs_analysis.dat 282 | node_modules/ 283 | 284 | # Visual Studio 6 build log 285 | *.plg 286 | 287 | # Visual Studio 6 workspace options file 288 | *.opt 289 | 290 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 291 | *.vbw 292 | 293 | # Visual Studio LightSwitch build output 294 | **/*.HTMLClient/GeneratedArtifacts 295 | **/*.DesktopClient/GeneratedArtifacts 296 | **/*.DesktopClient/ModelManifest.xml 297 | **/*.Server/GeneratedArtifacts 298 | **/*.Server/ModelManifest.xml 299 | _Pvt_Extensions 300 | 301 | # Paket dependency manager 302 | .paket/paket.exe 303 | paket-files/ 304 | 305 | # FAKE - F# Make 306 | .fake/ 307 | 308 | # CodeRush personal settings 309 | .cr/personal 310 | 311 | # Python Tools for Visual Studio (PTVS) 312 | __pycache__/ 313 | *.pyc 314 | 315 | # Cake - Uncomment if you are using it 316 | # tools/** 317 | # !tools/packages.config 318 | 319 | # Tabs Studio 320 | *.tss 321 | 322 | # Telerik's JustMock configuration file 323 | *.jmconfig 324 | 325 | # BizTalk build output 326 | *.btp.cs 327 | *.btm.cs 328 | *.odx.cs 329 | *.xsd.cs 330 | 331 | # OpenCover UI analysis results 332 | OpenCover/ 333 | 334 | # Azure Stream Analytics local run output 335 | ASALocalRun/ 336 | 337 | # MSBuild Binary and Structured Log 338 | *.binlog 339 | 340 | # NVidia Nsight GPU debugger configuration file 341 | *.nvuser 342 | 343 | # MFractors (Xamarin productivity tool) working folder 344 | .mfractor/ 345 | 346 | # Local History for Visual Studio 347 | .localhistory/ 348 | 349 | # BeatPulse healthcheck temp database 350 | healthchecksdb 351 | 352 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 353 | MigrationBackup/ 354 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | image: Visual Studio 2019 3 | configuration: Release 4 | install: 5 | - ps: >- 6 | cd src 7 | 8 | nuget restore UnityNuGet.sln 9 | 10 | $env:DEPLOY_SERVER_TO_AZURE = 'false' 11 | 12 | if(-Not $env:APPVEYOR_PULL_REQUEST_NUMBER) { 13 | if($env:appveyor_repo_tag -eq 'True') { 14 | if($env:appveyor_repo_tag_name -match '^[0-9]') { 15 | $env:DEPLOY_SERVER_TO_AZURE = 'true' 16 | } 17 | } 18 | } 19 | build: 20 | project: src/UnityNuGet.sln 21 | verbosity: minimal 22 | after_build: 23 | - dotnet publish -o ..\build -c Release UnityNuGet.Server\UnityNuGet.Server.csproj 24 | artifacts: 25 | - path: src/build 26 | name: UnityNuGetDeploy 27 | type: WebDeployPackage 28 | deploy: 29 | - provider: WebDeploy 30 | server: https://unitynuget-registry.scm.azurewebsites.net:443/msdeploy.axd?site=unitynuget-registry 31 | website: unitynuget-registry 32 | username: $unitynuget-registry 33 | password: 34 | secure: j5rB9C0QezfJlpCglmalS14+5PkTuAJ1PT9fuRFNss+5UQNAPmHcW8z4pvX1CRD6htRaorT6fe+KoySIiOwYOg== 35 | remove_files: false 36 | app_offline: true 37 | artifact: UnityNuGetDeploy 38 | on: 39 | branch: master 40 | DEPLOY_SERVER_TO_AZURE: true 41 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 (10 July 2019) 4 | - Fix Guid generated for asmdef/meta files to use a stable GUID across package versions for the same asmdef 5 | 6 | ## 0.2.0 (08 July 2019) 7 | - Add System.Memory and System.Buffers (PR #2) 8 | 9 | ## 0.1.1 (08 July 2019) 10 | - Add route for root / to avoid 404 errors 11 | 12 | ## 0.1.0 (08 July 2019) 13 | - Add GeneticSharp (PR #1) 14 | 15 | ## none (07 July 2019) 16 | - Initial version 17 | -------------------------------------------------------------------------------- /img/unity_editor_with_nuget.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keijiro/UnityNuGet/f0fae272be470b7a4e07f39e9f7aa89b615eabf3/img/unity_editor_with_nuget.jpg -------------------------------------------------------------------------------- /img/unitynuget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keijiro/UnityNuGet/f0fae272be470b7a4e07f39e9f7aa89b615eabf3/img/unitynuget.png -------------------------------------------------------------------------------- /img/unitynuget.svg: -------------------------------------------------------------------------------- 1 | 2 | 24 | 26 | 27 | 29 | image/svg+xml 30 | 32 | 33 | 34 | 35 | 37 | 57 | Artboard 20 59 | 63 | 66 | 69 | 70 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Alexandre Mutel 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification 5 | , are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # UnityNuGet [![Build status](https://ci.appveyor.com/api/projects/status/xecinpa92napbd2e?svg=true)](https://ci.appveyor.com/project/xoofx/unitynuget) 2 | 3 | 4 | 5 | This project provides a seamlessly integration of a [curated list](registry.json) of NuGet packages within the Unity Package Manager. 6 | 7 | > DISCLAIMER: This is not an official service provided by Unity Technologies Inc. 8 | 9 | In order to use this service you simply need to edit the `Packages/manifest.json` in your project and add the following scoped registry: 10 | 11 | ```json 12 | { 13 | "scopedRegistries": [ 14 | { 15 | "name": "Unity NuGet", 16 | "url": "https://unitynuget-registry.azurewebsites.net", 17 | "scopes": [ 18 | "org.nuget" 19 | ] 20 | } 21 | ], 22 | "dependencies": { 23 | "org.nuget.scriban": "2.1.0" 24 | } 25 | } 26 | ``` 27 | 28 | When opening the Package Manager Window, you should see a few packages coming from NuGet (with the postfix text ` (NuGet)`) 29 | 30 | ![UnityEditorWithNuGet](img/unity_editor_with_nuget.jpg) 31 | 32 | ## Adding a package to the registry 33 | 34 | This service provides only a [curated list](registry.json) of NuGet packages 35 | 36 | Your NuGet package needs to respect a few constraints in order to be listed in the curated list: 37 | 38 | - It must have non-preview versions (e.g `1.0.0` but not `1.0.0-preview.1`) 39 | - It must provide `.NETStandard2.0` assemblies as part of its package 40 | 41 | You can send a PR to this repository to modify the [registry.json](registry.json) file (don't forget to maintain the alphabetical order) 42 | 43 | I recommend also to **specify the lowest version of your package that has support for `.NETStandard2.0`** upward so that other packages depending on your package have a chance to work with. 44 | 45 | Beware that **all transitive dependencies of the package** must be **explicitly listed** in the registry as well. 46 | 47 | > NOTE: We reserve the right to decline a package to be available through this service 48 | 49 | ## Compatibility 50 | 51 | Only compatible with **`Unity 2019.1`** and potentially with newer version. 52 | 53 | > NOTE: This service is currently only tested with **`Unity 2019.1`** 54 | > 55 | > It may not work with a more recent version of Unity 56 | 57 | ## FAQ 58 | 59 | ### Where is hosted this service? 60 | 61 | On Azure through my own Azure credits coming from my MVP subscription, enjoy! 62 | 63 | ### Why can't you add all NuGet packages? 64 | 65 | 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. 66 | 67 | 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. 68 | 69 | ### Why does it require .NETStandard2.0? 70 | 71 | Since 2019.1, Unity is now compatible with `.NETStandard2.0` and it is the .NET profile that is preferred to be used 72 | 73 | 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. 74 | 75 | ### How this service is working? 76 | 77 | This project implements a simplified compatible NPM server in C# using ASP.NET Core and converts NuGet packages to Unity packages before serving them. 78 | 79 | 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. 80 | 81 | Once converted, these packages are cached on the disk on the server. 82 | 83 | ## License 84 | 85 | This software is released under the [BSD-Clause 2 license](https://opensource.org/licenses/BSD-2-Clause). 86 | 87 | ## Author 88 | 89 | Alexandre Mutel aka [xoofx](http://xoofx.com) 90 | -------------------------------------------------------------------------------- /registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "GeneticSharp": { 3 | "listed": true, 4 | "version": "2.0.0" 5 | }, 6 | "Microsoft.Azure.Kinect.Sensor": { 7 | "listed": true, 8 | "version": "1.2.0" 9 | }, 10 | "Microsoft.CSharp": { 11 | "ignore": true 12 | }, 13 | "Newtonsoft.Json": { 14 | "listed": true, 15 | "version": "11.0.1" 16 | }, 17 | "Scriban": { 18 | "listed": true, 19 | "version": "2.1.0" 20 | }, 21 | "System.Buffers": { 22 | "listed": true, 23 | "version": "4.4.0" 24 | }, 25 | "System.Memory": { 26 | "listed": true, 27 | "version": "4.5.0" 28 | }, 29 | "System.Numerics.Vectors": { 30 | "listed": false, 31 | "version": "4.4.0" 32 | }, 33 | "System.Runtime.CompilerServices.Unsafe": { 34 | "listed": true, 35 | "version": "4.4.0" 36 | }, 37 | "System.Threading.Tasks.Extensions": { 38 | "listed": false, 39 | "version": "4.4.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/UnityNuGet.Server/ApplicationBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace UnityNuGet.Server 7 | { 8 | public static class ApplicationBuilderExtensions 9 | { 10 | /// 11 | /// Logs all request and response headers (used only in development) 12 | /// 13 | public static void LogRequestHeaders(this IApplicationBuilder app, ILoggerFactory loggerFactory) 14 | { 15 | var logger = loggerFactory.CreateLogger("Request Headers"); 16 | app.Use(async (context, next) => 17 | { 18 | var builder = new StringBuilder(Environment.NewLine); 19 | foreach (var header in context.Request.Headers) 20 | { 21 | builder.AppendLine($"{header.Key}:{header.Value}"); 22 | } 23 | logger.LogInformation("Request: " + builder.ToString()); 24 | await next.Invoke(); 25 | 26 | builder.Length = 0; 27 | builder.AppendLine(); 28 | foreach (var header in context.Response.Headers) 29 | { 30 | builder.AppendLine($"{header.Key}:{header.Value}"); 31 | } 32 | logger.LogInformation("Response: " + builder.ToString()); 33 | }); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/Connected Services/Application Insights/ConnectedService.json: -------------------------------------------------------------------------------- 1 | { 2 | "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider", 3 | "Version": "9.1.429.1", 4 | "GettingStartedDocument": { 5 | "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432" 6 | } 7 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/Controllers/ApiController.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Mvc; 3 | using UnityNuGet.Npm; 4 | 5 | namespace UnityNuGet.Server.Controllers 6 | { 7 | /// 8 | /// Main entry to emulate the following NPM endpoints: 9 | /// 10 | /// - "-/all": query all packages (return json) 11 | /// - "{packageId}": query a specific package (return json) 12 | /// - "{package_id}/-/{package_file}": download a specific package 13 | /// 14 | [Route("")] 15 | [ApiController] 16 | public class ApiController : ControllerBase 17 | { 18 | private readonly RegistryCacheSingleton _cacheSingleton; 19 | 20 | public ApiController(RegistryCacheSingleton cacheSingleton) 21 | { 22 | _cacheSingleton = cacheSingleton; 23 | } 24 | 25 | // GET / 26 | [HttpGet("")] 27 | public IActionResult Home() 28 | { 29 | return Ok(); 30 | } 31 | 32 | // GET -/all 33 | [HttpGet("-/all")] 34 | public JsonResult GetAll() 35 | { 36 | var result = _cacheSingleton.Instance.All(); 37 | return new JsonResult(result); 38 | } 39 | 40 | // GET {packageId} 41 | [HttpGet("{id}")] 42 | public JsonResult GetPackage(string id) 43 | { 44 | var package = _cacheSingleton.Instance.GetPackage(id); 45 | if (package == null) 46 | { 47 | return new JsonResult(NpmError.NotFound); 48 | } 49 | 50 | return new JsonResult(package); 51 | } 52 | 53 | // GET {package_id}/-/{package_file} 54 | [HttpGet("{id}/-/{file}")] 55 | [HttpHead("{id}/-/{file}")] 56 | public IActionResult DownloadPackage(string id, string file) 57 | { 58 | var package = _cacheSingleton.Instance.GetPackage(id); 59 | if (package == null) 60 | { 61 | return new JsonResult(NpmError.NotFound); 62 | } 63 | 64 | if (!file.StartsWith(id + "-") || !file.EndsWith(".tgz")) 65 | { 66 | return new JsonResult(NpmError.NotFound); 67 | } 68 | 69 | var filePath = _cacheSingleton.Instance.GetPackageFilePath(file); 70 | if (!System.IO.File.Exists(filePath)) 71 | { 72 | return new JsonResult(NpmError.NotFound); 73 | } 74 | 75 | // This method can be called with HEAD request, so in that case we just calculate the content length 76 | if (Request.Method.Equals("HEAD")) 77 | { 78 | Response.ContentType = "application/octet-stream"; 79 | Response.ContentLength = new FileInfo(filePath).Length; 80 | return Ok(); 81 | } 82 | else 83 | { 84 | return new PhysicalFileResult(filePath, "application/octet-stream") {FileDownloadName = file}; 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/NuGetRedirectLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using NuGet.Common; 5 | using ILogger = Microsoft.Extensions.Logging.ILogger; 6 | using LogLevel = NuGet.Common.LogLevel; 7 | 8 | namespace UnityNuGet.Server 9 | { 10 | /// 11 | /// A NuGet logger redirecting to a 12 | /// 13 | public class NuGetRedirectLogger : LoggerBase 14 | { 15 | private readonly ILogger _logger; 16 | 17 | public NuGetRedirectLogger(ILogger logger) 18 | { 19 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 20 | } 21 | 22 | public override void Log(ILogMessage message) 23 | { 24 | LoggerExtensions.Log(_logger, GetLogLevel(message.Level), message.Message); 25 | } 26 | 27 | public override Task LogAsync(ILogMessage message) 28 | { 29 | LoggerExtensions.Log(_logger, GetLogLevel(message.Level), message.Message); 30 | return Task.CompletedTask; 31 | } 32 | 33 | private static Microsoft.Extensions.Logging.LogLevel GetLogLevel(LogLevel logLevel) 34 | { 35 | switch (logLevel) 36 | { 37 | case LogLevel.Debug: 38 | return Microsoft.Extensions.Logging.LogLevel.Debug; 39 | case LogLevel.Verbose: 40 | return Microsoft.Extensions.Logging.LogLevel.Trace; 41 | case LogLevel.Information: 42 | return Microsoft.Extensions.Logging.LogLevel.Information; 43 | case LogLevel.Minimal: 44 | return Microsoft.Extensions.Logging.LogLevel.Information; 45 | case LogLevel.Warning: 46 | return Microsoft.Extensions.Logging.LogLevel.Warning; 47 | case LogLevel.Error: 48 | return Microsoft.Extensions.Logging.LogLevel.Error; 49 | default: 50 | throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null); 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace UnityNuGet.Server 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseApplicationInsights() 16 | //.UseSetting("detailedErrors", "true") 17 | .UseStartup(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "UnityNuGetServer": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "launchUrl": "-/all", 8 | "applicationUrl": "http://localhost", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/RegistryCacheSingleton.cs: -------------------------------------------------------------------------------- 1 | namespace UnityNuGet.Server 2 | { 3 | public sealed class RegistryCacheSingleton 4 | { 5 | public RegistryCacheSingleton(RegistryCache instance) 6 | { 7 | Instance = instance; 8 | } 9 | 10 | public RegistryCache Instance { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/RegistryCacheUpdater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace UnityNuGet.Server 9 | { 10 | /// 11 | /// Update the RegistryCache at a regular internal (default is 10min) 12 | /// 13 | internal sealed class RegistryCacheUpdater : IHostedService, IDisposable 14 | { 15 | private readonly RegistryCacheSingleton _currentRegistryCache; 16 | private readonly ILogger _logger; 17 | private Timer _timer; 18 | // Update the RegistryCache very 10min 19 | private static readonly TimeSpan DefaultIntervalUpdate = TimeSpan.FromMinutes(10); 20 | 21 | public RegistryCacheUpdater(RegistryCacheSingleton currentRegistryCache, ILogger logger) 22 | { 23 | _currentRegistryCache = currentRegistryCache; 24 | _logger = logger; 25 | } 26 | 27 | public Task StartAsync(CancellationToken cancellationToken) 28 | { 29 | _timer = new Timer(DoWork, null, DefaultIntervalUpdate, DefaultIntervalUpdate); 30 | return Task.CompletedTask; 31 | } 32 | 33 | private async void DoWork(object state) 34 | { 35 | try 36 | { 37 | _logger.LogInformation("Starting to update RegistryCache"); 38 | 39 | var previousRegistryCache = _currentRegistryCache.Instance; 40 | Debug.Assert(previousRegistryCache != null); 41 | var newRegistryCache = new RegistryCache(previousRegistryCache.RootUnityPackageFolder, previousRegistryCache.RootHttpUrl, previousRegistryCache.Logger); 42 | await newRegistryCache.Build(); 43 | 44 | if (newRegistryCache.HasErrors) 45 | { 46 | _logger.LogInformation("RegistryCache not updated due to errors. See previous logs"); 47 | } 48 | else 49 | { 50 | // Update the registry cache in the services 51 | _currentRegistryCache.Instance = newRegistryCache; 52 | 53 | _logger.LogInformation("RegistryCache successfully updated"); 54 | } 55 | } 56 | catch (Exception ex) 57 | { 58 | _logger.LogError($"Error while building a new registry cache. Reason: {ex}"); 59 | } 60 | } 61 | 62 | public Task StopAsync(CancellationToken cancellationToken) 63 | { 64 | _timer?.Change(Timeout.Infinite, 0); 65 | return Task.CompletedTask; 66 | } 67 | 68 | public void Dispose() 69 | { 70 | _timer?.Dispose(); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace UnityNuGet.Server 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory) 16 | { 17 | Configuration = configuration; 18 | HostingEnvironment = hostingEnvironment; 19 | LoggerFactory = loggerFactory; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | public IHostingEnvironment HostingEnvironment { get; } 25 | 26 | public ILoggerFactory LoggerFactory { get; } 27 | 28 | // This method gets called by the runtime. Use this method to add services to the container. 29 | public void ConfigureServices(IServiceCollection services) 30 | { 31 | var loggerRedirect = new NuGetRedirectLogger(LoggerFactory.CreateLogger("NuGet")); 32 | 33 | string url = "https://unitynuget-registry.azurewebsites.net/"; 34 | 35 | bool isDevelopment = HostingEnvironment.IsDevelopment(); 36 | if (isDevelopment) 37 | { 38 | var urls = Configuration[WebHostDefaults.ServerUrlsKey]; 39 | 40 | // Select HTTPS in production, HTTP in development 41 | url = urls.Split(';').FirstOrDefault(x => !x.StartsWith("https")); 42 | if (url == null) 43 | { 44 | throw new InvalidOperationException($"Unable to find a proper server URL from `{urls}`. Expecting a `http://...` URL in development"); 45 | } 46 | } 47 | 48 | // Get the current directory /home/site/unity_packages or binary folder in dev 49 | var currentDirectory = isDevelopment ? Path.GetDirectoryName(typeof(Startup).Assembly.Location) : Directory.GetCurrentDirectory(); 50 | var unityPackageFolder = Path.Combine(currentDirectory, "unity_packages"); 51 | loggerRedirect.LogInformation($"Using Unity Package folder `{unityPackageFolder}`"); 52 | 53 | // Build the cache synchronously because ConfigureServices doesn't support async Task 54 | var initialCache = new RegistryCache(unityPackageFolder, url, loggerRedirect); 55 | initialCache.Build().GetAwaiter().GetResult(); 56 | 57 | // Add the cache accessible from the services 58 | var singletonCache = new RegistryCacheSingleton(initialCache); 59 | services.AddSingleton(singletonCache); 60 | 61 | // Add the registry cache updater 62 | services.AddHostedService(); 63 | 64 | // what is that? 65 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); 66 | } 67 | 68 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 69 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 70 | { 71 | if (env.IsDevelopment()) 72 | { 73 | app.UseDeveloperExceptionPage(); 74 | app.LogRequestHeaders(LoggerFactory); 75 | } 76 | else 77 | { 78 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 79 | app.UseHsts(); 80 | app.UseHttpsRedirection(); 81 | } 82 | app.UseMvc(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Server/UnityNuGet.Server.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | /subscriptions/b6745039-70e7-4641-994b-5457cb220e2a/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/unitynuget-registry 6 | /subscriptions/b6745039-70e7-4641-994b-5457cb220e2a/resourcegroups/Default-ApplicationInsights-EastUS/providers/microsoft.insights/components/unitynuget-registry 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/UnityNuGet.Server/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/UnityNuGet.Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/UnityNuGet.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "ApplicationInsights": { 9 | "InstrumentationKey": "e1cc41b5-90d8-4827-a639-4597b122c852" 10 | } 11 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Tests/RegistryCacheTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | 5 | namespace UnityNuGet.Tests 6 | { 7 | public class RegistryCacheTests 8 | { 9 | [Test] 10 | public void TestRegistry() 11 | { 12 | var registry = Registry.GetInstance(); 13 | Assert.True(registry.Count >= 3); 14 | Assert.True(registry.ContainsKey("Scriban")); 15 | } 16 | 17 | [Test] 18 | public async Task TestBuild() 19 | { 20 | var unityPackages = Path.Combine(Path.GetDirectoryName(typeof(RegistryCacheTests).Assembly.Location), "unity_packages"); 21 | var registryCache = new RegistryCache(unityPackages, "http://localhost"); 22 | 23 | await registryCache.Build(); 24 | 25 | Assert.False(registryCache.HasErrors, "The registry failed to build, check the logs"); 26 | 27 | var allResult = registryCache.All(); 28 | Assert.True(allResult.Packages.Count >= 3); 29 | var allResultJson = allResult.ToJson(); 30 | 31 | StringAssert.Contains("org.nuget.scriban", allResultJson); 32 | StringAssert.Contains("org.nuget.system.runtime.compilerservices.unsafe", allResultJson); 33 | 34 | var scribanPackage = registryCache.GetPackage("org.nuget.scriban"); 35 | Assert.NotNull(scribanPackage); 36 | var scribanPackageJson = scribanPackage.ToJson(); 37 | StringAssert.Contains("org.nuget.scriban", scribanPackageJson); 38 | StringAssert.Contains("2.1.0", scribanPackageJson); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/UnityNuGet.Tests/UnityNuGet.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/UnityNuGet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityNuGet.Server", "UnityNuGet.Server\UnityNuGet.Server.csproj", "{C84FC40C-94EE-4314-BB48-CD1A98849D82}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityNuGet", "UnityNuGet\UnityNuGet.csproj", "{3E72FFD2-4CB6-4B44-9A49-29699722D93E}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnityNuGet.Tests", "UnityNuGet.Tests\UnityNuGet.Tests.csproj", "{EBC6FFF4-FD7C-4E03-B907-F12908C1D900}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{D2A040A5-DB8C-43BE-A801-107503057C2B}" 10 | ProjectSection(SolutionItems) = preProject 11 | ..\appveyor.yml = ..\appveyor.yml 12 | ..\changelog.md = ..\changelog.md 13 | ..\license.txt = ..\license.txt 14 | ..\readme.md = ..\readme.md 15 | ..\registry.json = ..\registry.json 16 | EndProjectSection 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {C84FC40C-94EE-4314-BB48-CD1A98849D82}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {3E72FFD2-4CB6-4B44-9A49-29699722D93E}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {EBC6FFF4-FD7C-4E03-B907-F12908C1D900}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /src/UnityNuGet/JsonCommonExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Converters; 5 | using NuGet.Versioning; 6 | 7 | namespace UnityNuGet 8 | { 9 | /// 10 | /// Extension methods for serializing NPM JSON responses. 11 | /// 12 | public static class JsonCommonExtensions 13 | { 14 | public static string ToJson(this JsonObjectBase self) => JsonConvert.SerializeObject(self, Settings); 15 | 16 | /// 17 | /// Settings used for serializing JSON objects in this project. 18 | /// 19 | public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings 20 | { 21 | MetadataPropertyHandling = MetadataPropertyHandling.Ignore, 22 | DateParseHandling = DateParseHandling.None, 23 | Converters = 24 | { 25 | new IsoDateTimeConverter { 26 | DateTimeStyles = DateTimeStyles.AssumeUniversal 27 | }, 28 | new VersionConverter(), 29 | }, 30 | }; 31 | 32 | /// 33 | /// Converter for NuGet 34 | /// 35 | private class VersionConverter : JsonConverter 36 | { 37 | public override void WriteJson(JsonWriter writer, VersionRange value, JsonSerializer serializer) 38 | { 39 | writer.WriteValue(value.ToString()); 40 | } 41 | 42 | public override VersionRange ReadJson(JsonReader reader, Type objectType, VersionRange existingValue, bool hasExistingValue, JsonSerializer serializer) 43 | { 44 | string s = (string)reader.Value; 45 | return VersionRange.Parse(s); 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/UnityNuGet/JsonObjectBase.cs: -------------------------------------------------------------------------------- 1 | namespace UnityNuGet 2 | { 3 | /// 4 | /// Base object for all JSON objects serialized by this project. 5 | /// 6 | public abstract class JsonObjectBase 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmDistribution.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace UnityNuGet.Npm 5 | { 6 | /// 7 | /// Describes how a NPM package is distributed, used by 8 | /// 9 | public class NpmDistribution : NpmObject 10 | { 11 | [JsonProperty("tarball")] 12 | public Uri Tarball { get; set; } 13 | 14 | [JsonProperty("shasum")] 15 | public string Shasum { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmError.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace UnityNuGet.Npm 4 | { 5 | /// 6 | /// A simple object to return npm errors. Used mainly for returning 7 | /// 8 | public class NpmError : NpmObject 9 | { 10 | public static readonly NpmError NotFound = new NpmError("not_found", "document not found"); 11 | 12 | public NpmError(string error, string reason) 13 | { 14 | Error = error; 15 | Reason = reason; 16 | } 17 | 18 | [JsonProperty("error")] 19 | public string Error { get; } 20 | 21 | [JsonProperty("reason")] 22 | public string Reason { get; } 23 | } 24 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmObject.cs: -------------------------------------------------------------------------------- 1 | namespace UnityNuGet.Npm 2 | { 3 | /// 4 | /// Base object for NpmObjects. 5 | /// 6 | public abstract class NpmObject : JsonObjectBase 7 | { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace UnityNuGet.Npm 6 | { 7 | /// 8 | /// Describes a full NPM package (used as a response to `{packageId}`) 9 | /// 10 | public class NpmPackage : NpmObject 11 | { 12 | public NpmPackage() 13 | { 14 | Revision = "1-0"; 15 | DistTags = new Dictionary(); 16 | Versions = new Dictionary(); 17 | Time = new Dictionary(); 18 | Users = new Dictionary(); 19 | } 20 | 21 | [JsonProperty("_id")] 22 | public string Id { get; set; } 23 | 24 | [JsonProperty("_rev")] 25 | public string Revision { get; set; } 26 | 27 | [JsonProperty("name")] 28 | public string Name { get; set; } 29 | 30 | [JsonProperty("license")] 31 | public string License { get; set; } 32 | 33 | [JsonProperty("description")] 34 | public string Description { get; set; } 35 | 36 | [JsonProperty("dist-tags")] 37 | public Dictionary DistTags { get; } 38 | 39 | [JsonProperty("versions")] 40 | public Dictionary Versions { get; } 41 | 42 | [JsonProperty("time")] 43 | public Dictionary Time { get; } 44 | 45 | [JsonProperty("repository", NullValueHandling = NullValueHandling.Ignore)] 46 | public NpmSourceRepository Repository { get; set; } 47 | 48 | [JsonProperty("users")] 49 | public Dictionary Users { get; } 50 | } 51 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmPackageInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace UnityNuGet.Npm 6 | { 7 | /// 8 | /// Describes a package for "all" listing, used by 9 | /// 10 | public class NpmPackageInfo : NpmObject 11 | { 12 | public NpmPackageInfo() 13 | { 14 | Maintainers = new List(); 15 | Versions = new Dictionary(); 16 | Keywords = new List(); 17 | } 18 | 19 | [JsonProperty("name")] 20 | public string Name { get; set; } 21 | 22 | [JsonProperty("description")] 23 | public string Description { get; set; } 24 | 25 | [JsonProperty("maintainers")] 26 | public List Maintainers { get; } 27 | 28 | [JsonProperty("versions")] 29 | public Dictionary Versions { get; } 30 | 31 | [JsonProperty("time")] 32 | public DateTimeOffset? Time { get; set; } 33 | 34 | [JsonProperty("keywords")] 35 | public List Keywords { get; } 36 | 37 | [JsonProperty("author")] 38 | public string Author { get; set; } 39 | } 40 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmPackageListAllResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | 5 | namespace UnityNuGet.Npm 6 | { 7 | /// 8 | /// Represents a `-/all` listing package NPM response 9 | /// 10 | /// 11 | /// NOTE: only used to serialize from C# to JSON (JSON To C# is not implemented) 12 | /// 13 | public class NpmPackageListAllResponse : NpmObject 14 | { 15 | public NpmPackageListAllResponse() 16 | { 17 | Unused = 99999; 18 | Packages = new Dictionary(); 19 | } 20 | 21 | [JsonProperty("_updated")] 22 | public int Unused { get; set; } 23 | 24 | [JsonIgnore] 25 | public Dictionary Packages { get; } 26 | 27 | // everything else gets stored here 28 | [JsonExtensionData] 29 | private IDictionary AdditionalData 30 | { 31 | get 32 | { 33 | var marshalPackages = new Dictionary(); 34 | foreach (var packagePair in Packages) 35 | { 36 | marshalPackages.Add(packagePair.Key, JObject.FromObject(packagePair.Value)); 37 | } 38 | 39 | return marshalPackages; 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmPackageRegistry.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace UnityNuGet.Npm 4 | { 5 | /// 6 | /// Used to store all type of responses (all packages, or single packages but also unlisted packages) 7 | /// 8 | public class NpmPackageRegistry : NpmObject 9 | { 10 | public NpmPackageRegistry() 11 | { 12 | Packages = new Dictionary(); 13 | ListedPackageInfos = new NpmPackageListAllResponse(); 14 | UnlistedPackageInfos = new NpmPackageListAllResponse(); 15 | } 16 | 17 | public Dictionary Packages { get; } 18 | 19 | public NpmPackageListAllResponse ListedPackageInfos { get; } 20 | 21 | public NpmPackageListAllResponse UnlistedPackageInfos { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmPackageVersion.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace UnityNuGet.Npm 5 | { 6 | /// 7 | /// Describes a version of a 8 | /// 9 | public class NpmPackageVersion : NpmObject 10 | { 11 | public NpmPackageVersion() 12 | { 13 | Dependencies = new Dictionary(); 14 | Distribution =new NpmDistribution(); 15 | Scripts = new Dictionary(); 16 | } 17 | 18 | [JsonProperty("name")] 19 | public string Name { get; set; } 20 | 21 | [JsonProperty("version")] 22 | public string Version { get; set; } 23 | 24 | [JsonProperty("dist")] 25 | public NpmDistribution Distribution { get; } 26 | 27 | [JsonProperty("dependencies")] 28 | public Dictionary Dependencies { get; } 29 | 30 | [JsonProperty("_id")] 31 | public string Id { get; set; } 32 | 33 | [JsonProperty("unity", NullValueHandling = NullValueHandling.Ignore)] 34 | public string Unity { get; set; } 35 | 36 | [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] 37 | public string Description { get; set; } 38 | 39 | [JsonProperty("displayName", NullValueHandling = NullValueHandling.Ignore)] 40 | public string DisplayName { get; set; } 41 | 42 | [JsonProperty("scripts", NullValueHandling = NullValueHandling.Ignore)] 43 | public Dictionary Scripts { get; } 44 | 45 | [JsonProperty("repository", NullValueHandling = NullValueHandling.Ignore)] 46 | public NpmSourceRepository Repository { get; set; } 47 | 48 | [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] 49 | public string Author { get; set; } 50 | } 51 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Npm/NpmSourceRepository.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace UnityNuGet.Npm 4 | { 5 | /// 6 | /// Describes a source repository used both by and 7 | /// 8 | public partial class NpmSourceRepository : NpmObject 9 | { 10 | [JsonProperty("type")] 11 | public string Type { get; set; } 12 | 13 | [JsonProperty("url")] 14 | public string Url { get; set; } 15 | 16 | [JsonProperty("revision")] 17 | public string Revision { get; set; } 18 | 19 | public NpmSourceRepository Clone() 20 | { 21 | return (NpmSourceRepository) MemberwiseClone(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/UnityNuGet/NuGetConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NuGet.Common; 4 | 5 | namespace UnityNuGet 6 | { 7 | /// 8 | /// A default NuGet console logger. 9 | /// 10 | public class NuGetConsoleLogger : LoggerBase 11 | { 12 | public override void Log(ILogMessage message) 13 | { 14 | if (message.Level == LogLevel.Error) 15 | { 16 | Console.Error.WriteLine(message); 17 | } 18 | else 19 | { 20 | Console.WriteLine(message); 21 | } 22 | } 23 | 24 | public override Task LogAsync(ILogMessage message) 25 | { 26 | Log(message); 27 | return Task.CompletedTask; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/UnityNuGet/Registry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Newtonsoft.Json; 5 | 6 | namespace UnityNuGet 7 | { 8 | /// 9 | /// Loads the `registry.json` file at startup 10 | /// 11 | public sealed class Registry : Dictionary 12 | { 13 | private const string RegistryFileName = "registry.json"; 14 | private static readonly object LockRead = new object(); 15 | private static Registry _registry = null; 16 | 17 | public static Registry Parse(string json) 18 | { 19 | if (json == null) throw new ArgumentNullException(nameof(json)); 20 | return JsonConvert.DeserializeObject(json, JsonCommonExtensions.Settings); 21 | } 22 | 23 | public static Registry GetInstance() 24 | { 25 | lock (LockRead) 26 | { 27 | if (_registry == null) 28 | { 29 | _registry = Parse(File.ReadAllText(Path.Combine(Path.GetDirectoryName(typeof(Registry).Assembly.Location), RegistryFileName))); 30 | } 31 | } 32 | return _registry; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/UnityNuGet/RegistryCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Security.Cryptography; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using ICSharpCode.SharpZipLib.GZip; 11 | using ICSharpCode.SharpZipLib.Tar; 12 | using NuGet.Common; 13 | using NuGet.Configuration; 14 | using NuGet.Frameworks; 15 | using NuGet.Packaging; 16 | using NuGet.Packaging.Core; 17 | using NuGet.Protocol; 18 | using NuGet.Protocol.Core.Types; 19 | using NuGet.Versioning; 20 | using NUglify.Html; 21 | using UnityNuGet.Npm; 22 | 23 | namespace UnityNuGet 24 | { 25 | /// 26 | /// Main class used to build the unity packages and create the NPM object responses. 27 | /// 28 | public class RegistryCache 29 | { 30 | private static readonly Encoding Utf8EncodingNoBom = new UTF8Encoding(false, false); 31 | private const string UnityScope = "org.nuget"; 32 | private const string MinimumUnityVersion = "2019.1"; 33 | private const string PackageNameNuGetPostFix = " (NuGet)"; 34 | private readonly ISettings _settings; 35 | private readonly SourceRepository _sourceRepository; 36 | private static readonly NuGetFramework NuGetFrameworkNetStandard20 = NuGetFramework.Parse("netstandard2.0"); 37 | private readonly SourceCacheContext _sourceCacheContext; 38 | private readonly Registry _registry; 39 | private readonly NpmPackageRegistry _npmPackageRegistry; 40 | 41 | public RegistryCache(string rootPersistentFolder, string rootHttpUrl, ILogger logger = null) 42 | { 43 | RootUnityPackageFolder = rootPersistentFolder ?? throw new ArgumentNullException(nameof(rootPersistentFolder)); 44 | RootHttpUrl = rootHttpUrl ?? throw new ArgumentNullException(nameof(rootHttpUrl)) ; 45 | 46 | if (!Directory.Exists(RootUnityPackageFolder)) 47 | { 48 | Directory.CreateDirectory(RootUnityPackageFolder); 49 | } 50 | _settings = Settings.LoadDefaultSettings(root: null); 51 | var sourceRepositoryProvider = new SourceRepositoryProvider(_settings, Repository.Provider.GetCoreV3()); 52 | _sourceRepository = sourceRepositoryProvider.GetRepositories().FirstOrDefault(); 53 | Logger = logger ?? new NuGetConsoleLogger(); 54 | _registry = Registry.GetInstance(); 55 | _sourceCacheContext = new SourceCacheContext(); 56 | _npmPackageRegistry = new NpmPackageRegistry(); 57 | } 58 | 59 | public bool HasErrors { get; private set; } 60 | 61 | public string RootUnityPackageFolder { get; } 62 | 63 | public string RootHttpUrl { get; } 64 | 65 | public ILogger Logger { get; } 66 | 67 | /// 68 | /// Get all packages registered. 69 | /// 70 | /// A list of packages registered 71 | public NpmPackageListAllResponse All() 72 | { 73 | return _npmPackageRegistry.ListedPackageInfos; 74 | } 75 | 76 | /// 77 | /// Get a specific package for the specified package id. 78 | /// 79 | /// 80 | /// A npm package or null if not found 81 | public NpmPackage GetPackage(string packageId) 82 | { 83 | if (packageId == null) throw new ArgumentNullException(nameof(packageId)); 84 | _npmPackageRegistry.Packages.TryGetValue(packageId, out var package); 85 | return package; 86 | } 87 | 88 | /// 89 | /// Gets the path for the specified package file to download 90 | /// 91 | /// The name of the package to download 92 | /// The file path of the package on the disk 93 | public string GetPackageFilePath(string packageFileName) 94 | { 95 | if (packageFileName == null) throw new ArgumentNullException(nameof(packageFileName)); 96 | packageFileName = packageFileName.Replace("/", packageFileName.Replace(".", string.Empty)); 97 | var packageFilePath = Path.Combine(RootUnityPackageFolder, packageFileName); 98 | return packageFilePath; 99 | } 100 | 101 | /// 102 | /// Build the registry cache. 103 | /// 104 | public async Task Build() 105 | { 106 | try 107 | { 108 | await BuildInternal(); 109 | } 110 | catch (Exception ex) 111 | { 112 | LogError($"Unexpected error {ex}"); 113 | } 114 | } 115 | 116 | /// 117 | /// For each package in our registry.json, query NuGet, extract package metadata, and convert them to unity packages. 118 | /// 119 | private async Task BuildInternal() 120 | { 121 | var packageMetadataResource = _sourceRepository.GetResource(); 122 | 123 | foreach (var packageDesc in _registry) 124 | { 125 | var packageName = packageDesc.Key; 126 | var packageEntry = packageDesc.Value; 127 | // A package entry is ignored but allowed in the registry (case of Microsoft.CSharp) 128 | if (packageEntry.Ignored) 129 | { 130 | continue; 131 | } 132 | 133 | var packageMetaIt = await packageMetadataResource.GetMetadataAsync(packageName, false, false, _sourceCacheContext, Logger, CancellationToken.None); 134 | var packageMetas = packageMetaIt.ToList(); 135 | foreach (var packageMeta in packageMetas) 136 | { 137 | var packageIdentity = packageMeta.Identity; 138 | var packageId = packageIdentity.Id.ToLowerInvariant(); 139 | var npmPackageId = $"{UnityScope}.{packageId}"; 140 | 141 | if (!packageEntry.Version.Satisfies(packageMeta.Identity.Version)) 142 | { 143 | continue; 144 | } 145 | 146 | PackageDependencyGroup netstd20Dependency = null; 147 | 148 | foreach (var dependencySet in packageMeta.DependencySets) 149 | { 150 | if (dependencySet.TargetFramework == NuGetFrameworkNetStandard20) 151 | { 152 | netstd20Dependency = dependencySet; 153 | break; 154 | } 155 | } 156 | 157 | if (netstd20Dependency == null) 158 | { 159 | Logger.LogWarning($"The package `{packageIdentity}` doesn't support `netstandard2.0`"); 160 | continue; 161 | } 162 | 163 | if (!_npmPackageRegistry.Packages.TryGetValue(npmPackageId, out var npmPackage)) 164 | { 165 | npmPackage = new NpmPackage(); 166 | _npmPackageRegistry.Packages.Add(npmPackageId, npmPackage); 167 | } 168 | 169 | // One NpmPackage (for package request) 170 | 171 | var packageInfoList = packageEntry.Listed ? _npmPackageRegistry.ListedPackageInfos : _npmPackageRegistry.UnlistedPackageInfos; 172 | 173 | if (!packageInfoList.Packages.TryGetValue(npmPackageId, out var npmPackageInfo)) 174 | { 175 | npmPackageInfo = new NpmPackageInfo(); 176 | packageInfoList.Packages.Add(npmPackageId, npmPackageInfo); 177 | } 178 | 179 | // Update latest version 180 | var currentVersion = packageIdentity.Version; 181 | 182 | var update = !npmPackage.DistTags.TryGetValue("latest", out var latestVersion) 183 | || (currentVersion > NuGetVersion.Parse(latestVersion)); 184 | 185 | 186 | if (update) 187 | { 188 | npmPackage.DistTags["latest"] = currentVersion.ToString(); 189 | 190 | npmPackageInfo.Versions.Clear(); 191 | npmPackageInfo.Versions[currentVersion.ToString()] = "latest"; 192 | 193 | npmPackage.Id = npmPackageId; 194 | npmPackage.License = packageMeta.LicenseMetadata?.License ?? packageMeta.LicenseUrl?.ToString(); 195 | 196 | npmPackage.Name = npmPackageId; 197 | npmPackageInfo.Name = npmPackageId; 198 | 199 | npmPackage.Description = packageMeta.Description; 200 | npmPackageInfo.Description = packageMeta.Description; 201 | 202 | npmPackageInfo.Author = packageMeta.Authors; 203 | if (packageMeta.Owners != null) 204 | { 205 | npmPackageInfo.Maintainers.Clear(); 206 | npmPackageInfo.Maintainers.AddRange(SplitCommaSeparatedString(packageMeta.Owners)); 207 | } 208 | 209 | if (packageMeta.Tags != null) 210 | { 211 | npmPackageInfo.Keywords.Clear(); 212 | npmPackageInfo.Keywords.Add("nuget"); 213 | npmPackageInfo.Keywords.AddRange(SplitCommaSeparatedString(packageMeta.Tags)); 214 | } 215 | } 216 | 217 | var npmVersion = new NpmPackageVersion 218 | { 219 | Id = $"{npmPackageId}@{currentVersion}", 220 | Version = currentVersion.ToString(), 221 | Name = npmPackageId, 222 | Description = packageMeta.Description, 223 | Author = npmPackageInfo.Author, 224 | DisplayName = packageMeta.Title + PackageNameNuGetPostFix 225 | }; 226 | npmVersion.Distribution.Tarball = new Uri($"{RootHttpUrl}/{npmPackage.Id}/-/{GetUnityPackageFileName(packageIdentity)}"); 227 | npmVersion.Unity = MinimumUnityVersion; 228 | npmPackage.Versions[npmVersion.Version] = npmVersion; 229 | 230 | bool hasDependencyErrors = false; 231 | foreach (var deps in netstd20Dependency.Packages) 232 | { 233 | var depsId = deps.Id.ToLowerInvariant(); 234 | 235 | if (!_registry.TryGetValue(deps.Id, out var packageEntryDep)) 236 | { 237 | LogError($"The package `{packageIdentity}` has a dependency on `{deps.Id}` which is not in the registry. You must add this dependency to the registry.json file."); 238 | hasDependencyErrors = true; 239 | } 240 | else if (packageEntryDep.Ignored) 241 | { 242 | // A package that is ignored is not declared as an explicit dependency 243 | continue; 244 | } 245 | else if (!deps.VersionRange.IsSubSetOrEqualTo(packageEntryDep.Version)) 246 | { 247 | LogError($"The version range `{deps.VersionRange}` for the dependency `{deps.Id}` for the package `{packageIdentity}` doesn't match the range allowed from the registry.json: `{packageEntryDep.Version}`"); 248 | hasDependencyErrors = true; 249 | } 250 | 251 | // Otherwise add the package as a dependency 252 | npmVersion.Dependencies.Add($"{UnityScope}.{depsId}", deps.VersionRange.MinVersion.ToString()); 253 | } 254 | 255 | // If we don't have any dependencies error, generate the package 256 | if (!hasDependencyErrors) 257 | { 258 | await ConvertNuGetToUnityPackageIfDoesNotExist(packageIdentity, npmPackageInfo, npmVersion, packageMeta); 259 | npmPackage.Time[currentVersion.ToString()] = packageMeta.Published?.UtcDateTime ?? GetUnityPackageFileInfo(packageIdentity).CreationTimeUtc; 260 | 261 | // Copy repository info if necessary 262 | if (update) 263 | { 264 | npmPackage.Repository = npmVersion.Repository?.Clone(); 265 | } 266 | } 267 | } 268 | } 269 | } 270 | 271 | /// 272 | /// Converts a NuGet package to Unity package if not already 273 | /// 274 | private async Task ConvertNuGetToUnityPackageIfDoesNotExist(PackageIdentity identity, NpmPackageInfo npmPackageInfo, NpmPackageVersion npmPackageVersion, IPackageSearchMetadata packageMeta) 275 | { 276 | if (!IsUnityPackageExists(identity)) 277 | { 278 | await ConvertNuGetPackageToUnity(identity, npmPackageInfo, npmPackageVersion, packageMeta); 279 | } 280 | else 281 | { 282 | npmPackageVersion.Distribution.Shasum = ReadUnityPackageSha1(identity); 283 | } 284 | } 285 | 286 | /// 287 | /// Converts a NuGet package to a Unity package. 288 | /// 289 | private async Task ConvertNuGetPackageToUnity(PackageIdentity identity, NpmPackageInfo npmPackageInfo, NpmPackageVersion npmPackageVersion, IPackageSearchMetadata packageMeta) 290 | { 291 | var unityPackageFileName = GetUnityPackageFileName(identity); 292 | var unityPackageFilePath = Path.Combine(RootUnityPackageFolder, unityPackageFileName); 293 | 294 | Logger.LogInformation($"Converting NuGet package {identity} to Unity `{unityPackageFileName}`"); 295 | 296 | var downloadResource = await _sourceRepository.GetResourceAsync(CancellationToken.None); 297 | var downloadResult = await downloadResource.GetDownloadResourceResultAsync( 298 | identity, 299 | new PackageDownloadContext(_sourceCacheContext), 300 | SettingsUtility.GetGlobalPackagesFolder(_settings), 301 | Logger, CancellationToken.None); 302 | var packageReader = downloadResult.PackageReader; 303 | 304 | // Update Repository metadata if necessary 305 | var repoMeta = packageReader.NuspecReader.GetRepositoryMetadata(); 306 | if (repoMeta != null && repoMeta.Url != null && repoMeta.Commit != null && repoMeta.Type != null) 307 | { 308 | npmPackageVersion.Repository = new NpmSourceRepository() 309 | { 310 | Revision = repoMeta.Commit, 311 | Type = repoMeta.Type, 312 | Url = repoMeta.Url, 313 | }; 314 | } 315 | else 316 | { 317 | npmPackageVersion.Repository = null; 318 | } 319 | 320 | try 321 | { 322 | var memStream = new MemoryStream(); 323 | 324 | using (var outStream = File.Create(unityPackageFilePath)) 325 | using (var gzoStream = new GZipOutputStream(outStream)) 326 | using (var tarArchive = new TarOutputStream(gzoStream)) 327 | { 328 | foreach (var item in await packageReader.GetLibItemsAsync(CancellationToken.None)) 329 | { 330 | if (item.TargetFramework != NuGetFrameworkNetStandard20) 331 | { 332 | continue; 333 | } 334 | 335 | foreach (var file in item.Items) 336 | { 337 | var fileInUnityPackage = Path.GetFileName(file); 338 | var meta = UnityMeta.GetMetaForExtension(GetStableGuid(identity, fileInUnityPackage), Path.GetExtension(fileInUnityPackage)); 339 | if (meta == null) 340 | { 341 | continue; 342 | } 343 | 344 | memStream.Position = 0; 345 | memStream.SetLength(0); 346 | 347 | var stream = packageReader.GetStream(file); 348 | stream.CopyTo(memStream); 349 | var buffer = memStream.ToArray(); 350 | 351 | // write content 352 | WriteBufferToTar(tarArchive, fileInUnityPackage, buffer); 353 | 354 | // write meta file 355 | WriteTextFileToTar(tarArchive, fileInUnityPackage + ".meta", meta); 356 | } 357 | } 358 | 359 | // Write the package,json 360 | var unityPackage = CreateUnityPackage(npmPackageInfo, npmPackageVersion); 361 | var unityPackageAsJson = unityPackage.ToJson(); 362 | const string packageJsonFileName = "package.json"; 363 | WriteTextFileToTar(tarArchive, packageJsonFileName, unityPackageAsJson); 364 | WriteTextFileToTar(tarArchive, $"{packageJsonFileName}.meta", UnityMeta.GetMetaForExtension(GetStableGuid(identity, packageJsonFileName), ".json")); 365 | 366 | // Write the license to the package if any 367 | string license = null; 368 | string licenseUrlText = null; 369 | 370 | var licenseUrl = packageMeta.LicenseMetadata?.LicenseUrl.ToString() ?? packageMeta.LicenseUrl?.ToString(); 371 | if (!string.IsNullOrEmpty(licenseUrl)) 372 | { 373 | try 374 | { 375 | // Try to fetch the license from an URL 376 | using (var httpClient = new HttpClient()) 377 | { 378 | licenseUrlText = await httpClient.GetStringAsync(licenseUrl); 379 | } 380 | 381 | // If the license text is HTML, try to remove all text 382 | if (licenseUrlText != null) 383 | { 384 | licenseUrlText = licenseUrlText.Trim(); 385 | if (licenseUrlText.StartsWith("<")) 386 | { 387 | try 388 | { 389 | licenseUrlText = NUglify.Uglify.HtmlToText(licenseUrlText, HtmlToTextOptions.KeepStructure).Code ?? licenseUrlText; 390 | } 391 | catch 392 | { 393 | // ignored 394 | } 395 | } 396 | } 397 | } 398 | catch 399 | { 400 | // ignored 401 | } 402 | } 403 | 404 | if (!string.IsNullOrEmpty(packageMeta.LicenseMetadata?.License)) 405 | { 406 | license = packageMeta.LicenseMetadata.License; 407 | } 408 | 409 | // If the license fetched from the URL is bigger, use that one to put into the file 410 | if (licenseUrlText != null && (license == null || licenseUrlText.Length > license.Length)) 411 | { 412 | license = licenseUrlText; 413 | } 414 | 415 | if (!string.IsNullOrEmpty(license)) 416 | { 417 | const string licenseMdFile = "License.md"; 418 | WriteTextFileToTar(tarArchive, licenseMdFile, license); 419 | WriteTextFileToTar(tarArchive, $"{licenseMdFile}.meta", UnityMeta.GetMetaForExtension(GetStableGuid(identity, licenseMdFile), ".md")); 420 | } 421 | } 422 | 423 | using (var stream = File.OpenRead(unityPackageFilePath)) 424 | { 425 | var sha1 = Sha1sum(stream); 426 | WriteUnityPackageSha1(identity, sha1); 427 | npmPackageVersion.Distribution.Shasum = sha1; 428 | } 429 | } 430 | catch (Exception ex) 431 | { 432 | try 433 | { 434 | File.Delete(unityPackageFilePath); 435 | } 436 | catch 437 | { 438 | // ignored 439 | } 440 | 441 | LogError($"Error while processing package `{identity}`. Reason: {ex}"); 442 | } 443 | } 444 | 445 | private static Guid GetStableGuid(PackageIdentity identity, string name) 446 | { 447 | return StringToGuid(identity.Id + $"/{name}*"); 448 | } 449 | 450 | private FileInfo GetUnityPackageFileInfo(PackageIdentity identity) 451 | { 452 | return new FileInfo(Path.Combine(RootUnityPackageFolder, GetUnityPackageFileName(identity))); 453 | } 454 | 455 | private static string GetUnityPackageFileName(PackageIdentity identity) 456 | { 457 | return $"{UnityScope}.{identity.Id.ToLowerInvariant()}-{identity.Version}.tgz"; 458 | } 459 | 460 | private static string GetUnityPackageSha1FileName(PackageIdentity identity) 461 | { 462 | return $"{UnityScope}.{identity.Id.ToLowerInvariant()}-{identity.Version}.sha1"; 463 | } 464 | 465 | private bool IsUnityPackageExists(PackageIdentity identity) 466 | { 467 | return File.Exists(Path.Combine(RootUnityPackageFolder, GetUnityPackageFileName(identity))); 468 | } 469 | 470 | private string ReadUnityPackageSha1(PackageIdentity identity) 471 | { 472 | return File.ReadAllText(Path.Combine(RootUnityPackageFolder, GetUnityPackageSha1FileName(identity))); 473 | } 474 | 475 | private void WriteUnityPackageSha1(PackageIdentity identity, string sha1) 476 | { 477 | File.WriteAllText(Path.Combine(RootUnityPackageFolder, GetUnityPackageSha1FileName(identity)), sha1); 478 | } 479 | 480 | private void WriteTextFileToTar(TarOutputStream tarOut, string filePath, string content) 481 | { 482 | if (tarOut == null) throw new ArgumentNullException(nameof(tarOut)); 483 | if (filePath == null) throw new ArgumentNullException(nameof(filePath)); 484 | if (content == null) throw new ArgumentNullException(nameof(content)); 485 | 486 | var buffer = Utf8EncodingNoBom.GetBytes(content); 487 | WriteBufferToTar(tarOut, filePath, buffer); 488 | } 489 | 490 | private void WriteBufferToTar(TarOutputStream tarOut, string filePath, byte[] buffer) 491 | { 492 | if (tarOut == null) throw new ArgumentNullException(nameof(tarOut)); 493 | if (filePath == null) throw new ArgumentNullException(nameof(filePath)); 494 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 495 | 496 | filePath = filePath.Replace(@"\", "/"); 497 | filePath = filePath.TrimStart('/'); 498 | 499 | var tarEntry = TarEntry.CreateTarEntry("package/"+ filePath); 500 | tarEntry.Size = buffer.Length; 501 | tarOut.PutNextEntry(tarEntry); 502 | tarOut.Write(buffer, 0, buffer.Length); 503 | tarOut.CloseEntry(); 504 | } 505 | 506 | private static UnityPackage CreateUnityPackage(NpmPackageInfo npmPackageInfo, NpmPackageVersion npmPackageVersion) 507 | { 508 | var unityPackage = new UnityPackage 509 | { 510 | Name = npmPackageInfo.Name, 511 | Version = npmPackageVersion.Version, 512 | Description = npmPackageInfo.Description, 513 | Unity = npmPackageVersion.Unity 514 | }; 515 | unityPackage.Dependencies.AddRange(npmPackageVersion.Dependencies); 516 | unityPackage.Keywords.AddRange(npmPackageInfo.Keywords); 517 | return unityPackage; 518 | } 519 | 520 | private static Guid StringToGuid(string text) 521 | { 522 | var guid = new byte[16]; 523 | var inputBytes = Encoding.UTF8.GetBytes(text); 524 | using (var algorithm = SHA1.Create()) 525 | { 526 | var hash = algorithm.ComputeHash(inputBytes); 527 | Array.Copy(hash, 0, guid, 0, guid.Length); 528 | } 529 | 530 | // Follow UUID for SHA1 based GUID 531 | const int version = 5; // SHA1 (3 for MD5) 532 | guid[6] = (byte) ((guid[6] & 0x0F) | (version << 4)); 533 | guid[8] = (byte) ((guid[8] & 0x3F) | 0x80); 534 | return new Guid(guid); 535 | } 536 | 537 | private static string Sha1sum(Stream stream) 538 | { 539 | using (SHA1Managed sha1 = new SHA1Managed()) 540 | { 541 | var hash = sha1.ComputeHash(stream); 542 | var sb = new StringBuilder(hash.Length * 2); 543 | 544 | foreach (byte b in hash) 545 | { 546 | sb.Append(b.ToString("x2")); 547 | } 548 | 549 | return sb.ToString(); 550 | } 551 | } 552 | 553 | private void LogError(string message, Exception ex = null) 554 | { 555 | Logger.LogError(message); 556 | HasErrors = true; 557 | } 558 | 559 | private static List SplitCommaSeparatedString(string input) 560 | { 561 | var list = new List(); 562 | if (input == null) return list; 563 | foreach (var entry in input.Split(new[] {',', ';'}, StringSplitOptions.RemoveEmptyEntries)) 564 | { 565 | list.Add(entry.Trim()); 566 | } 567 | 568 | return list; 569 | } 570 | } 571 | } -------------------------------------------------------------------------------- /src/UnityNuGet/RegistryEntry.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NuGet.Versioning; 3 | 4 | namespace UnityNuGet 5 | { 6 | /// 7 | /// An entry in the 8 | /// 9 | public class RegistryEntry 10 | { 11 | [JsonProperty("ignore")] 12 | public bool Ignored { get; set; } 13 | 14 | [JsonProperty("listed")] 15 | public bool Listed { get; set; } 16 | 17 | [JsonProperty("version")] 18 | public VersionRange Version { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/UnityNuGet/UnityMeta.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Scriban; 3 | 4 | namespace UnityNuGet 5 | { 6 | /// 7 | /// Helper methods to create Unity .meta files 8 | /// 9 | internal static class UnityMeta 10 | { 11 | public static string GetMetaForExtension(Guid guid, string extension) 12 | { 13 | switch (extension) 14 | { 15 | case ".dll": 16 | return GetMetaForDll(guid); 17 | 18 | case ".pdb": 19 | break; 20 | 21 | case ".json": 22 | case ".xml": 23 | case ".txt": 24 | case ".md": 25 | return GetMetaForText(guid); 26 | } 27 | 28 | return null; 29 | } 30 | 31 | private static string GetMetaForDll(Guid guid) 32 | { 33 | const string text = @"fileFormatVersion: 2 34 | guid: {{ guid }} 35 | PluginImporter: 36 | externalObjects: {} 37 | serializedVersion: 2 38 | iconMap: {} 39 | executionOrder: {} 40 | defineConstraints: 41 | - NET_STANDARD_2_0 42 | isPreloaded: 0 43 | isOverridable: 0 44 | isExplicitlyReferenced: 0 45 | validateReferences: 1 46 | platformData: 47 | - first: 48 | '': Any 49 | second: 50 | enabled: 0 51 | settings: 52 | Exclude Editor: 0 53 | Exclude Linux: 0 54 | Exclude Linux64: 0 55 | Exclude LinuxUniversal: 0 56 | Exclude OSXUniversal: 0 57 | Exclude Win: 0 58 | Exclude Win64: 0 59 | - first: 60 | Any: 61 | second: 62 | enabled: 1 63 | settings: {} 64 | - first: 65 | Editor: Editor 66 | second: 67 | enabled: 1 68 | settings: 69 | CPU: AnyCPU 70 | DefaultValueInitialized: true 71 | OS: AnyOS 72 | - first: 73 | Facebook: Win 74 | second: 75 | enabled: 0 76 | settings: 77 | CPU: AnyCPU 78 | - first: 79 | Facebook: Win64 80 | second: 81 | enabled: 0 82 | settings: 83 | CPU: AnyCPU 84 | - first: 85 | Standalone: Linux 86 | second: 87 | enabled: 1 88 | settings: 89 | CPU: x86 90 | - first: 91 | Standalone: Linux64 92 | second: 93 | enabled: 1 94 | settings: 95 | CPU: x86_64 96 | - first: 97 | Standalone: LinuxUniversal 98 | second: 99 | enabled: 1 100 | settings: {} 101 | - first: 102 | Standalone: OSXUniversal 103 | second: 104 | enabled: 1 105 | settings: 106 | CPU: AnyCPU 107 | - first: 108 | Standalone: Win 109 | second: 110 | enabled: 1 111 | settings: 112 | CPU: AnyCPU 113 | - first: 114 | Standalone: Win64 115 | second: 116 | enabled: 1 117 | settings: 118 | CPU: AnyCPU 119 | - first: 120 | Windows Store Apps: WindowsStoreApps 121 | second: 122 | enabled: 0 123 | settings: 124 | CPU: AnyCPU 125 | userData: 126 | assetBundleName: 127 | assetBundleVariant: 128 | "; 129 | var meta = Template.Parse(text); 130 | return meta.Render(new {guid = guid.ToString("N")}); 131 | } 132 | 133 | private static string GetMetaForText(Guid guid) 134 | { 135 | const string text = @"fileFormatVersion: 2 136 | guid: {{ guid }} 137 | TextScriptImporter: 138 | externalObjects: {} 139 | userData: 140 | assetBundleName: 141 | assetBundleVariant: 142 | "; 143 | var meta = Template.Parse(text); 144 | return meta.Render(new {guid = guid.ToString("N")}); 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/UnityNuGet/UnityNuGet.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 0.3.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/UnityNuGet/UnityPackage.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | using UnityNuGet.Npm; 4 | 5 | namespace UnityNuGet 6 | { 7 | /// 8 | /// 9 | /// 10 | public class UnityPackage : JsonObjectBase 11 | { 12 | public UnityPackage() 13 | { 14 | Keywords = new List(); 15 | Dependencies = new Dictionary(); 16 | } 17 | 18 | [JsonProperty("name")] 19 | public string Name { get; set; } 20 | 21 | [JsonProperty("version")] 22 | public string Version { get; set; } 23 | 24 | [JsonProperty("unity")] 25 | public string Unity { get; set; } 26 | 27 | [JsonProperty("description")] 28 | public string Description { get; set; } 29 | 30 | [JsonProperty("keywords")] 31 | public List Keywords { get; } 32 | 33 | [JsonProperty("category")] 34 | public string Category { get; set; } 35 | 36 | [JsonProperty("dependencies")] 37 | public Dictionary Dependencies { get; } 38 | } 39 | } -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.2.100" 4 | } 5 | } --------------------------------------------------------------------------------