diff --git a/Doki.sln b/Doki.sln index de5371a..32a18f1 100644 --- a/Doki.sln +++ b/Doki.sln @@ -34,6 +34,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doki.TestAssembly", "tests\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doki.Tests.Common", "tests\Doki.Tests.Common\Doki.Tests.Common.csproj", "{0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Doki.CommandLine.Tests", "tests\Doki.CommandLine.Tests\Doki.CommandLine.Tests.csproj", "{67B12AF1-2696-411F-ADFD-B5C32A43BA71}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +54,7 @@ Global {6CCD9EE6-B3FC-485F-9155-553165141B20} = {8C7B5305-B599-4F08-B28B-DD9F1715DD51} {0293D689-DFDC-4A78-80D8-BFC11DB0A175} = {08041208-BE3D-4BE8-9AF7-806B73985275} {0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC} = {8C7B5305-B599-4F08-B28B-DD9F1715DD51} + {67B12AF1-2696-411F-ADFD-B5C32A43BA71} = {8C7B5305-B599-4F08-B28B-DD9F1715DD51} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6F31B87A-2BD3-4FB4-8C08-7E059A338D4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -110,5 +113,9 @@ Global {0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}.Debug|Any CPU.Build.0 = Debug|Any CPU {0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}.Release|Any CPU.ActiveCfg = Release|Any CPU {0FA0FF7A-6EDA-4CB1-8D81-2DFB1A4077CC}.Release|Any CPU.Build.0 = Release|Any CPU + {67B12AF1-2696-411F-ADFD-B5C32A43BA71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67B12AF1-2696-411F-ADFD-B5C32A43BA71}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67B12AF1-2696-411F-ADFD-B5C32A43BA71}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67B12AF1-2696-411F-ADFD-B5C32A43BA71}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Doki.CommandLine/Commands/GenerateCommand.Outputs.cs b/src/Doki.CommandLine/Commands/GenerateCommand.Outputs.cs index 50aa0c0..2cfd0ed 100644 --- a/src/Doki.CommandLine/Commands/GenerateCommand.Outputs.cs +++ b/src/Doki.CommandLine/Commands/GenerateCommand.Outputs.cs @@ -34,7 +34,7 @@ internal partial class GenerateCommand var nugetFolder = Path.Combine(workingDirectory.FullName, ".doki", "nuget"); - using var nugetLoader = new NuGetLoader(output.From); + using var nugetLoader = new NuGetLoader(_logger, output.From); var assemblyPath = await nugetLoader.LoadPackageAsync(output.Type, nugetFolder, allowPreview, cancellationToken); diff --git a/src/Doki.CommandLine/Doki.CommandLine.csproj b/src/Doki.CommandLine/Doki.CommandLine.csproj index 2b84899..0787513 100644 --- a/src/Doki.CommandLine/Doki.CommandLine.csproj +++ b/src/Doki.CommandLine/Doki.CommandLine.csproj @@ -21,18 +21,22 @@ ..\..\nuget + + + + - + - + - + diff --git a/src/Doki.CommandLine/NuGet/NuGetLoader.cs b/src/Doki.CommandLine/NuGet/NuGetLoader.cs index c05e1c0..48f03e0 100644 --- a/src/Doki.CommandLine/NuGet/NuGetLoader.cs +++ b/src/Doki.CommandLine/NuGet/NuGetLoader.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyModel; -using NuGet.Common; using NuGet.Configuration; using NuGet.Frameworks; using NuGet.Packaging; @@ -16,12 +15,13 @@ internal class NuGetLoader : IDisposable { private readonly SourceCacheContext _cacheContext = new(); - //TODO use console logger (ASCII) - private readonly ILogger _logger = NullLogger.Instance; + private readonly NuGetLogger _logger; private readonly SourceRepositoryProvider _sourceRepositoryProvider; - public NuGetLoader(string? source = null) + public NuGetLoader(Microsoft.Extensions.Logging.ILogger logger, string? source = null) { + _logger = new NuGetLogger(logger); + var sources = new List { new("https://api.nuget.org/v3/index.json") @@ -39,19 +39,27 @@ public async Task LoadPackageAsync(string packageId, string destinationD { ArgumentNullException.ThrowIfNull(packageId, nameof(packageId)); - var packageIdentity = await GetPackageIdentityAsync(packageId, allowPreview, cancellationToken); + var currentVersion = NuGetVersion.Parse(typeof(DocumentationObject).Assembly.GetName().Version!.ToString()); + + var packageIdentities = await GetPackageIdentitiesAsync(packageId, allowPreview, cancellationToken); + foreach (var packageIdentity in packageIdentities) + { + var dependencyInfos = new HashSet(); + var result = await ScanPackagesAsync(currentVersion, packageIdentity, dependencyInfos, cancellationToken); + if (!result) continue; - var dependencyInfos = new HashSet(); - await CollectPackagesAsync(packageIdentity, dependencyInfos, cancellationToken); + var packagesToInstall = GetPackagesToInstall(packageId, dependencyInfos.ToArray()); - var packagesToInstall = GetPackagesToInstall(packageId, dependencyInfos.ToArray()); + var settings = Settings.LoadDefaultSettings(destinationDirectory); - var settings = Settings.LoadDefaultSettings(destinationDirectory); + await InstallPackagesAsync(packagesToInstall, destinationDirectory, settings, cancellationToken); - await InstallPackagesAsync(packagesToInstall, destinationDirectory, settings, cancellationToken); + return Path.Combine(destinationDirectory, $"{packageIdentity.Id}.{packageIdentity.Version}", "lib", + "net8.0", + $"{packageIdentity.Id}.dll"); + } - return Path.Combine(destinationDirectory, $"{packageIdentity.Id}.{packageIdentity.Version}", "lib", "net8.0", - $"{packageIdentity.Id}.dll"); + throw new InvalidOperationException($"No applicable package '{packageId}' found for version: {currentVersion}"); } private async Task InstallPackagesAsync(IEnumerable packagesToInstall, @@ -90,9 +98,9 @@ private IEnumerable GetPackagesToInstall(string pac var resolverContext = new PackageResolverContext( DependencyBehavior.Lowest, new[] { packageId }, - Enumerable.Empty(), - Enumerable.Empty(), - Enumerable.Empty(), + [], + [], + [], dependencyInfos, _sourceRepositoryProvider.GetRepositories().Select(s => s.PackageSource), _logger); @@ -103,10 +111,10 @@ private IEnumerable GetPackagesToInstall(string pac .Select(p => dependencyInfos.Single(x => PackageIdentityComparer.Default.Equals(x, p))); } - private async Task CollectPackagesAsync(PackageIdentity package, ICollection packages, - CancellationToken cancellationToken) + private async Task ScanPackagesAsync(NuGetVersion currentVersion, PackageIdentity package, + ICollection packages, CancellationToken cancellationToken) { - if (packages.Contains(package)) return; + if (packages.Contains(package)) return true; foreach (var repository in _sourceRepositoryProvider.GetRepositories()) { @@ -120,24 +128,45 @@ private async Task CollectPackagesAsync(PackageIdentity package, ICollection(); + foreach (var dependency in dependencyInfo.Dependencies) + { + if (dependency.Id is "Doki" or "Doki.Abstractions" or "Doki.Output.Extensions" + or "Doki.Output.Abstractions") + { + if (dependency.VersionRange.Satisfies(currentVersion)) continue; + + _logger.LogDebug( + $"Doki dependency '{dependency.Id}' version '{dependency.VersionRange}' does not satisfy current version '{currentVersion}'"); + + return false; + } + + if (!IsDependencyProvided(dependency)) dependencies.Add(dependency); + } + var filteredSourceDependency = new SourcePackageDependencyInfo( dependencyInfo.Id, dependencyInfo.Version, - dependencyInfo.Dependencies.Where(d => !IsDependencyProvided(d)), + dependencies, dependencyInfo.Listed, dependencyInfo.Source); packages.Add(filteredSourceDependency); - foreach (var dependency in filteredSourceDependency.Dependencies) + foreach (var dependency in dependencies) { - await CollectPackagesAsync(new PackageIdentity(dependency.Id, dependency.VersionRange.MinVersion), - packages, cancellationToken); + var result = await ScanPackagesAsync(currentVersion, + new PackageIdentity(dependency.Id, dependency.VersionRange.MinVersion), packages, + cancellationToken); + if (!result) return false; } } + + return true; } - private async Task GetPackageIdentityAsync(string packageId, bool allowPreview, + private async Task> GetPackageIdentitiesAsync(string packageId, bool allowPreview, CancellationToken cancellationToken) { foreach (var repository in _sourceRepositoryProvider.GetRepositories()) @@ -147,10 +176,12 @@ private async Task GetPackageIdentityAsync(string packageId, bo var versions = await packages.GetAllVersionsAsync(packageId, _cacheContext, _logger, cancellationToken); - var latestVersion = versions.Where(v => allowPreview || !v.IsPrerelease).MaxBy(v => v); - if (latestVersion == null) continue; + var allowedVersions = versions.Where(v => allowPreview || !v.IsPrerelease).ToArray(); - return new PackageIdentity(packageId, latestVersion); + if (allowedVersions.Length != 0) + return allowedVersions + .OrderByDescending(v => v) + .Select(v => new PackageIdentity(packageId, v)); } throw new InvalidOperationException($"Package '{packageId}' not found."); @@ -158,15 +189,13 @@ private async Task GetPackageIdentityAsync(string packageId, bo private static bool IsDependencyProvided(PackageDependency dependency) { - if (dependency.Id is "Doki" or "Doki.Abstractions" or "Doki.Output.Abstractions") return true; - var runtimeLibrary = DependencyContext.Default!.RuntimeLibraries.FirstOrDefault(r => r.Name == dependency.Id); if (runtimeLibrary == null) return false; var parsedLibVersion = NuGetVersion.Parse(runtimeLibrary.Version); - return parsedLibVersion.IsPrerelease || dependency.VersionRange.Satisfies(parsedLibVersion); + return dependency.VersionRange.Satisfies(parsedLibVersion); } public void Dispose() diff --git a/src/Doki.CommandLine/NuGet/NuGetLogger.cs b/src/Doki.CommandLine/NuGet/NuGetLogger.cs new file mode 100644 index 0000000..bed22be --- /dev/null +++ b/src/Doki.CommandLine/NuGet/NuGetLogger.cs @@ -0,0 +1,85 @@ +using Microsoft.Extensions.Logging; +using NuGet.Common; +using ILogger = NuGet.Common.ILogger; +using LogLevel = NuGet.Common.LogLevel; + +namespace Doki.CommandLine.NuGet; + +internal class NuGetLogger(Microsoft.Extensions.Logging.ILogger logger) : ILogger +{ + public void LogDebug(string data) + { + logger.LogDebug(data); + } + + public void LogVerbose(string data) + { + logger.LogTrace(data); + } + + public void LogInformation(string data) + { + logger.LogInformation(data); + } + + public void LogMinimal(string data) + { + logger.LogInformation(data); + } + + public void LogWarning(string data) + { + logger.LogWarning(data); + } + + public void LogError(string data) + { + logger.LogError(data); + } + + public void LogInformationSummary(string data) + { + logger.LogInformation(data); + } + + public void Log(LogLevel level, string data) + { + switch (level) + { + case LogLevel.Debug: + LogDebug(data); + break; + case LogLevel.Verbose: + LogVerbose(data); + break; + case LogLevel.Information: + LogInformation(data); + break; + case LogLevel.Minimal: + LogMinimal(data); + break; + case LogLevel.Warning: + LogWarning(data); + break; + case LogLevel.Error: + LogError(data); + break; + } + } + + public Task LogAsync(LogLevel level, string data) + { + Log(level, data); + return Task.CompletedTask; + } + + public void Log(ILogMessage message) + { + Log(message.Level, message.Message); + } + + public Task LogAsync(ILogMessage message) + { + return LogAsync(message.Level, message.Message); + } +} \ No newline at end of file diff --git a/tests/Doki.CommandLine.Tests/Doki.CommandLine.Tests.csproj b/tests/Doki.CommandLine.Tests/Doki.CommandLine.Tests.csproj new file mode 100644 index 0000000..39c484e --- /dev/null +++ b/tests/Doki.CommandLine.Tests/Doki.CommandLine.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/Doki.CommandLine.Tests/GlobalUsings.cs b/tests/Doki.CommandLine.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/Doki.CommandLine.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/tests/Doki.CommandLine.Tests/NuGetTests.cs b/tests/Doki.CommandLine.Tests/NuGetTests.cs new file mode 100644 index 0000000..f8b452c --- /dev/null +++ b/tests/Doki.CommandLine.Tests/NuGetTests.cs @@ -0,0 +1,27 @@ +using Doki.CommandLine.NuGet; +using Doki.Tests.Common; +using Xunit.Abstractions; + +namespace Doki.CommandLine.Tests; + +public class NuGetTests(ITestOutputHelper testOutputHelper) +{ + [Fact] + public async Task NuGetLoader_TestAsync() + { + const string packageId = "Doki.Output.Json"; + var tmpDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + + var logger = new TestOutputLogger(testOutputHelper); + + using var loader = new NuGetLoader(logger); + + await loader.LoadPackageAsync(packageId, tmpDirectory); + + Assert.False(logger.HadError); + + var dllPath = Path.Combine(tmpDirectory, $"{packageId}.1.0.0", "lib", "net8.0", $"{packageId}.dll"); + + Assert.True(File.Exists(dllPath)); + } +} \ No newline at end of file