diff --git a/Mono.ApiTools.NuGetDiff.Tests/NuGetManagerTests.cs b/Mono.ApiTools.NuGetDiff.Tests/NuGetManagerTests.cs new file mode 100644 index 0000000..4610b16 --- /dev/null +++ b/Mono.ApiTools.NuGetDiff.Tests/NuGetManagerTests.cs @@ -0,0 +1,105 @@ +using NuGet.Versioning; +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Mono.ApiTools.Tests +{ + public class NuGetManagerTests + { + [Fact] + public async Task TestNuGetManagerCanOpenPackage() + { + var manager = new NuGetManager(); + + using (var reader = await manager.OpenPackageAsync("Newtonsoft.Json", "13.0.1")) + { + Assert.NotNull(reader); + var identity = await reader.GetIdentityAsync(default); + Assert.Equal("Newtonsoft.Json", identity.Id); + Assert.Equal(NuGetVersion.Parse("13.0.1"), identity.Version); + } + } + + [Fact] + public async Task TestNuGetManagerCanExtractPackage() + { + var manager = new NuGetManager(); + var outputDir = Path.Combine(Path.GetTempPath(), "Mono.ApiTools.NuGetDiff", Path.GetRandomFileName()); + + try + { + await manager.ExtractPackageToDirectoryAsync("Newtonsoft.Json", "13.0.1", outputDir); + + Assert.True(Directory.Exists(outputDir)); + Assert.NotEmpty(Directory.GetFiles(outputDir, "*.nuspec", SearchOption.AllDirectories)); + } + finally + { + if (Directory.Exists(outputDir)) + { + try + { + Directory.Delete(outputDir, true); + } + catch + { + // Ignore cleanup errors + } + } + } + } + + [Fact] + public async Task TestNuGetManagerCanExtractCachedPackage() + { + var manager = new NuGetManager(); + manager.PackageCache = Path.Combine(Path.GetTempPath(), "Mono.ApiTools.NuGetDiff", "cache", Path.GetRandomFileName()); + + try + { + var dir = await manager.ExtractCachedPackageAsync("Newtonsoft.Json", "13.0.1"); + + Assert.True(Directory.Exists(dir)); + Assert.NotEmpty(Directory.GetFiles(dir, "*.nuspec", SearchOption.AllDirectories)); + } + finally + { + if (Directory.Exists(manager.PackageCache)) + { + try + { + Directory.Delete(manager.PackageCache, true); + } + catch + { + // Ignore cleanup errors + } + } + } + } + + [Fact] + public void TestNuGetManagerGetCachedPackagePath() + { + var manager = new NuGetManager(); + manager.PackageCache = "packages"; + + var path = manager.GetCachedPackagePath("Newtonsoft.Json", "13.0.1"); + + Assert.Equal("packages/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg", path.Replace('\\', '/')); + } + + [Fact] + public void TestNuGetManagerGetCachedPackageDirectory() + { + var manager = new NuGetManager(); + manager.PackageCache = "packages"; + + var dir = manager.GetCachedPackageDirectory("Newtonsoft.Json", "13.0.1"); + + Assert.Equal("packages/newtonsoft.json/13.0.1", dir.Replace('\\', '/')); + } + } +} diff --git a/Mono.ApiTools.NuGetDiff/NuGetDiff.cs b/Mono.ApiTools.NuGetDiff/NuGetDiff.cs index 17ca43d..370051c 100644 --- a/Mono.ApiTools.NuGetDiff/NuGetDiff.cs +++ b/Mono.ApiTools.NuGetDiff/NuGetDiff.cs @@ -17,10 +17,8 @@ namespace Mono.ApiTools { - public class NuGetDiff + public class NuGetDiff : NuGetManager { - internal const string NuGetSourceUrl = "https://api.nuget.org/v3/index.json"; - private const int DefaultSaveBufferSize = 1024; private const int DefaultCopyBufferSize = 81920; private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false, true); @@ -29,20 +27,15 @@ public class NuGetDiff private const string DefaultHtmlDiffFileExtension = ".diff.html"; private const string DefaultMarkdownDiffFileExtension = ".diff.md"; private const string DefaultApiInfoFileExtension = ".info.xml"; - private readonly SourceRepository source; - private readonly SourceCacheContext cache; - private readonly ILogger logger; public NuGetDiff() - : this(NuGetSourceUrl) + : base() { } public NuGetDiff(string sourceUrl) + : base(sourceUrl) { - source = Repository.Factory.GetCoreV3(sourceUrl); - cache = new SourceCacheContext(); - logger = NullLogger.Instance; } @@ -52,8 +45,6 @@ public NuGetDiff(string sourceUrl) public List IgnoreMemberRegex { get; set; } = new List(); - public string PackageCache { get; set; } = "packages"; - public bool IgnoreResolutionErrors { get; set; } = false; public bool IgnoreInheritedInterfaces { get; set; } = false; @@ -584,109 +575,6 @@ public async Task SaveCompleteDiffToDirectoryAsync(PackageArchiveReader oldReade } - // OpenPackageAsync - - public Task OpenPackageAsync(string id, string version, CancellationToken cancellationToken = default) - { - var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); - return OpenPackageAsync(identity, cancellationToken); - } - - public Task OpenPackageAsync(string id, NuGetVersion version, CancellationToken cancellationToken = default) - { - var identity = new PackageIdentity(id, version); - return OpenPackageAsync(identity, cancellationToken); - } - - public async Task OpenPackageAsync(PackageIdentity identity, CancellationToken cancellationToken = default) - { - var nupkgPath = await GetPackagePathAsync(identity, cancellationToken).ConfigureAwait(false); - return new PackageArchiveReader(nupkgPath); - } - - - // ExtractPackageToDirectoryAsync - - public Task ExtractPackageToDirectoryAsync(string id, string version, string outputDirectory, CancellationToken cancellationToken = default) - { - var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); - return ExtractPackageToDirectoryAsync(identity, outputDirectory, cancellationToken); - } - - public Task ExtractPackageToDirectoryAsync(string id, NuGetVersion version, string outputDirectory, CancellationToken cancellationToken = default) - { - var identity = new PackageIdentity(id, version); - return ExtractPackageToDirectoryAsync(identity, outputDirectory, cancellationToken); - } - - public async Task ExtractPackageToDirectoryAsync(PackageIdentity identity, string outputDirectory, CancellationToken cancellationToken = default) - { - var nupkgPath = await GetPackagePathAsync(identity, cancellationToken).ConfigureAwait(false); - - using (var reader = await OpenPackageAsync(identity, cancellationToken).ConfigureAwait(false)) - { - var files = await reader.GetFilesAsync(cancellationToken); - foreach (var file in files.ToArray()) - { - var dest = Path.Combine(outputDirectory, file); - await reader.CopyFilesAsync(outputDirectory, files, ExtractFile, logger, cancellationToken); - } - } - - string ExtractFile(string source, string target, Stream stream) - { - var extractDirectory = Path.GetDirectoryName(target); - if (!Directory.Exists(extractDirectory)) - Directory.CreateDirectory(extractDirectory); - - // the main .nuspec should be all lowercase to make things easy to find and match the clientz - if (Path.GetFileName(source) == source && Path.GetExtension(source).ToLowerInvariant() == ".nuspec") - target = Path.Combine(Path.GetDirectoryName(target), Path.GetFileName(target).ToLower()); - - // copying files stream-to-stream is less efficient - // attempt to copy using File.Copy if we the source is a file on disk - if (Path.IsPathRooted(source)) - File.Copy(source, target, true); - else - stream.CopyToFile(target); - - return target; - } - } - - - // ExtractCachedPackageAsync - - public Task ExtractCachedPackageAsync(string id, string version, CancellationToken cancellationToken = default) - { - var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); - return ExtractCachedPackageAsync(identity, cancellationToken); - } - - public Task ExtractCachedPackageAsync(string id, NuGetVersion version, CancellationToken cancellationToken = default) - { - var identity = new PackageIdentity(id, version); - return ExtractCachedPackageAsync(identity, cancellationToken); - } - - public async Task ExtractCachedPackageAsync(PackageIdentity identity, CancellationToken cancellationToken = default) - { - var dir = GetCachedPackageDirectory(identity); - - // a quick check to make sure the package has not already beed extracted - var nupkg = GetCachedPackagePath(identity); - var extractedFlag = $"{nupkg}.extracted"; - if (File.Exists(nupkg) && File.Exists(extractedFlag)) - return dir; - - await ExtractPackageToDirectoryAsync(identity, dir, cancellationToken); - - File.WriteAllText(extractedFlag, ""); - - return dir; - } - - // OpenAssemblyAsync public async Task OpenAssemblyAsync(PackageArchiveReader reader, string assemblyPath, CancellationToken cancellationToken = default) @@ -703,47 +591,6 @@ public async Task OpenAssemblyAsync(PackageArchiveReader reader, string } - // GetPackagePath - - public string GetCachedPackagePath(string id, string version) - { - var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); - return GetCachedPackagePath(identity); - } - - public string GetCachedPackagePath(string id, NuGetVersion version) - { - var identity = new PackageIdentity(id, version); - return GetCachedPackagePath(identity); - } - - public string GetCachedPackagePath(PackageIdentity ident) - { - var nupkgDir = GetCachedPackageDirectory(ident); - return Path.Combine(nupkgDir, $"{ident.Id.ToLowerInvariant()}.{ident.Version.ToNormalizedString()}.nupkg"); - } - - - // GetPackageRootDirectory - - public string GetCachedPackageDirectory(string id, string version) - { - var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); - return GetCachedPackageDirectory(identity); - } - - public string GetCachedPackageDirectory(string id, NuGetVersion version) - { - var identity = new PackageIdentity(id, version); - return GetCachedPackageDirectory(identity); - } - - public string GetCachedPackageDirectory(PackageIdentity ident) - { - return Path.Combine(PackageCache, GetPackageDirectoryBase(ident)); - } - - // Private members internal static NuGetFramework TryMatchFramework(NuGetFramework added, NuGetFramework[] choices) @@ -976,39 +823,6 @@ XAttribute CreateAssemblyPresence(string assembly) } } - private async Task GetPackagePathAsync(PackageIdentity identity, CancellationToken cancellationToken) - { - var metadataResource = await source.GetResourceAsync(); - - var metadata = await metadataResource.GetMetadataAsync(identity, cache, logger, cancellationToken).ConfigureAwait(false); - - if (metadata == null) - throw new ArgumentException($"Package identity is not valid: {identity}", nameof(identity)); - - var ident = metadata.Identity; - - var nupkgDir = GetCachedPackageDirectory(ident); - if (!Directory.Exists(nupkgDir)) - Directory.CreateDirectory(nupkgDir); - - var byId = await source.GetResourceAsync(cancellationToken).ConfigureAwait(false); - - var nupkgPath = GetCachedPackagePath(ident); - var nupkgHashPath = $"{nupkgPath}.sha512"; - if (!File.Exists(nupkgPath) || !File.Exists(nupkgHashPath)) - { - using (var downloader = await byId.GetPackageDownloaderAsync(ident, cache, logger, cancellationToken).ConfigureAwait(false)) - { - await downloader.CopyNupkgFileToAsync(nupkgPath, cancellationToken).ConfigureAwait(false); - - var sha512 = await downloader.GetPackageHashAsync("SHA512", cancellationToken).ConfigureAwait(false); - File.WriteAllText(nupkgHashPath, sha512); - } - } - - return nupkgPath; - } - private Task GenerateAssemblyXmlDiffAsync(Stream oldInfo, Stream newInfo, CancellationToken cancellationToken) { return Task.Run(() => @@ -1077,11 +891,6 @@ private string GetOutputFilenameBase(string assembly, bool includePlatform) return string.Join(Path.DirectorySeparatorChar.ToString(), parts.Skip(skip)); } - private string GetPackageDirectoryBase(PackageIdentity ident) - { - return Path.Combine(ident.Id.ToLowerInvariant(), ident.Version.ToNormalizedString()); - } - private async Task SaveToFileAsync(Stream stream, string path, CancellationToken cancellationToken) { var directory = Path.GetDirectoryName(path); diff --git a/Mono.ApiTools.NuGetDiff/NuGetManager.cs b/Mono.ApiTools.NuGetDiff/NuGetManager.cs new file mode 100644 index 0000000..3c9b3e5 --- /dev/null +++ b/Mono.ApiTools.NuGetDiff/NuGetManager.cs @@ -0,0 +1,284 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NuGet.Common; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace Mono.ApiTools +{ + public class NuGetManager + { + internal const string NuGetSourceUrl = "https://api.nuget.org/v3/index.json"; + + private const int DefaultCopyBufferSize = 81920; + + protected readonly SourceRepository source; + protected readonly SourceCacheContext cache; + protected readonly ILogger logger; + + public NuGetManager() + : this(NuGetSourceUrl) + { + } + + public NuGetManager(string sourceUrl) + { + source = Repository.Factory.GetCoreV3(sourceUrl); + cache = new SourceCacheContext(); + logger = NullLogger.Instance; + } + + + // Properties + + public string PackageCache { get; set; } = "packages"; + + + // OpenPackageAsync + + public Task OpenPackageAsync(string id, string version, CancellationToken cancellationToken = default) + { + var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); + return OpenPackageAsync(identity, cancellationToken); + } + + public Task OpenPackageAsync(string id, NuGetVersion version, CancellationToken cancellationToken = default) + { + var identity = new PackageIdentity(id, version); + return OpenPackageAsync(identity, cancellationToken); + } + + public async Task OpenPackageAsync(PackageIdentity identity, CancellationToken cancellationToken = default) + { + var nupkgPath = await GetPackagePathAsync(identity, cancellationToken).ConfigureAwait(false); + return new PackageArchiveReader(nupkgPath); + } + + + // ExtractPackageToDirectoryAsync + + public Task ExtractPackageToDirectoryAsync(string id, string version, string outputDirectory, CancellationToken cancellationToken = default) + { + return ExtractPackageToDirectoryAsync(id, version, outputDirectory, false, cancellationToken); + } + + public Task ExtractPackageToDirectoryAsync(string id, NuGetVersion version, string outputDirectory, CancellationToken cancellationToken = default) + { + return ExtractPackageToDirectoryAsync(id, version, outputDirectory, false, cancellationToken); + } + + public Task ExtractPackageToDirectoryAsync(PackageIdentity identity, string outputDirectory, CancellationToken cancellationToken = default) + { + return ExtractPackageToDirectoryAsync(identity, outputDirectory, false, cancellationToken); + } + + public Task ExtractPackageToDirectoryAsync(string id, string version, string outputDirectory, bool includeDependencies, CancellationToken cancellationToken = default) + { + var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); + return ExtractPackageToDirectoryAsync(identity, outputDirectory, includeDependencies, cancellationToken); + } + + public Task ExtractPackageToDirectoryAsync(string id, NuGetVersion version, string outputDirectory, bool includeDependencies, CancellationToken cancellationToken = default) + { + var identity = new PackageIdentity(id, version); + return ExtractPackageToDirectoryAsync(identity, outputDirectory, includeDependencies, cancellationToken); + } + + public async Task ExtractPackageToDirectoryAsync(PackageIdentity identity, string outputDirectory, bool includeDependencies, CancellationToken cancellationToken = default) + { + var nupkgPath = await GetPackagePathAsync(identity, cancellationToken).ConfigureAwait(false); + + using (var reader = await OpenPackageAsync(identity, cancellationToken).ConfigureAwait(false)) + { + var files = await reader.GetFilesAsync(cancellationToken); + foreach (var file in files.ToArray()) + { + var dest = Path.Combine(outputDirectory, file); + await reader.CopyFilesAsync(outputDirectory, files, ExtractFile, logger, cancellationToken); + } + + // Extract dependencies recursively if requested + if (includeDependencies) + { + var dependencySets = await reader.GetPackageDependenciesAsync(cancellationToken).ConfigureAwait(false); + foreach (var dependencySet in dependencySets) + { + foreach (var dependency in dependencySet.Packages) + { + // Try to use MinVersion if available, otherwise try MaxVersion + var version = dependency.VersionRange?.MinVersion ?? dependency.VersionRange?.MaxVersion; + if (version != null) + { + var depIdentity = new PackageIdentity(dependency.Id, version); + try + { + await ExtractPackageToDirectoryAsync(depIdentity, outputDirectory, includeDependencies, cancellationToken).ConfigureAwait(false); + } + catch + { + // Silently ignore errors extracting dependencies + // This prevents failures when dependencies are not available + } + } + } + } + } + } + + string ExtractFile(string source, string target, Stream stream) + { + var extractDirectory = Path.GetDirectoryName(target); + if (!Directory.Exists(extractDirectory)) + Directory.CreateDirectory(extractDirectory); + + // the main .nuspec should be all lowercase to make things easy to find and match the clientz + if (Path.GetFileName(source) == source && Path.GetExtension(source).ToLowerInvariant() == ".nuspec") + target = Path.Combine(Path.GetDirectoryName(target), Path.GetFileName(target).ToLower()); + + // copying files stream-to-stream is less efficient + // attempt to copy using File.Copy if we the source is a file on disk + if (Path.IsPathRooted(source)) + File.Copy(source, target, true); + else + stream.CopyToFile(target); + + return target; + } + } + + + // ExtractCachedPackageAsync + + public Task ExtractCachedPackageAsync(string id, string version, CancellationToken cancellationToken = default) + { + return ExtractCachedPackageAsync(id, version, false, cancellationToken); + } + + public Task ExtractCachedPackageAsync(string id, NuGetVersion version, CancellationToken cancellationToken = default) + { + return ExtractCachedPackageAsync(id, version, false, cancellationToken); + } + + public Task ExtractCachedPackageAsync(PackageIdentity identity, CancellationToken cancellationToken = default) + { + return ExtractCachedPackageAsync(identity, false, cancellationToken); + } + + public Task ExtractCachedPackageAsync(string id, string version, bool includeDependencies, CancellationToken cancellationToken = default) + { + var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); + return ExtractCachedPackageAsync(identity, includeDependencies, cancellationToken); + } + + public Task ExtractCachedPackageAsync(string id, NuGetVersion version, bool includeDependencies, CancellationToken cancellationToken = default) + { + var identity = new PackageIdentity(id, version); + return ExtractCachedPackageAsync(identity, includeDependencies, cancellationToken); + } + + public async Task ExtractCachedPackageAsync(PackageIdentity identity, bool includeDependencies, CancellationToken cancellationToken = default) + { + var dir = GetCachedPackageDirectory(identity); + + // a quick check to make sure the package has not already beed extracted + var nupkg = GetCachedPackagePath(identity); + var extractedFlag = $"{nupkg}.extracted"; + if (File.Exists(nupkg) && File.Exists(extractedFlag)) + return dir; + + await ExtractPackageToDirectoryAsync(identity, dir, includeDependencies, cancellationToken); + + File.WriteAllText(extractedFlag, ""); + + return dir; + } + + + // GetPackagePath + + public string GetCachedPackagePath(string id, string version) + { + var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); + return GetCachedPackagePath(identity); + } + + public string GetCachedPackagePath(string id, NuGetVersion version) + { + var identity = new PackageIdentity(id, version); + return GetCachedPackagePath(identity); + } + + public string GetCachedPackagePath(PackageIdentity ident) + { + var nupkgDir = GetCachedPackageDirectory(ident); + return Path.Combine(nupkgDir, $"{ident.Id.ToLowerInvariant()}.{ident.Version.ToNormalizedString()}.nupkg"); + } + + + // GetPackageRootDirectory + + public string GetCachedPackageDirectory(string id, string version) + { + var identity = new PackageIdentity(id, NuGetVersion.Parse(version)); + return GetCachedPackageDirectory(identity); + } + + public string GetCachedPackageDirectory(string id, NuGetVersion version) + { + var identity = new PackageIdentity(id, version); + return GetCachedPackageDirectory(identity); + } + + public string GetCachedPackageDirectory(PackageIdentity ident) + { + return Path.Combine(PackageCache, GetPackageDirectoryBase(ident)); + } + + + // Private members + + private async Task GetPackagePathAsync(PackageIdentity identity, CancellationToken cancellationToken) + { + var metadataResource = await source.GetResourceAsync(); + + var metadata = await metadataResource.GetMetadataAsync(identity, cache, logger, cancellationToken).ConfigureAwait(false); + + if (metadata == null) + throw new ArgumentException($"Package identity is not valid: {identity}", nameof(identity)); + + var ident = metadata.Identity; + + var nupkgDir = GetCachedPackageDirectory(ident); + if (!Directory.Exists(nupkgDir)) + Directory.CreateDirectory(nupkgDir); + + var byId = await source.GetResourceAsync(cancellationToken).ConfigureAwait(false); + + var nupkgPath = GetCachedPackagePath(ident); + var nupkgHashPath = $"{nupkgPath}.sha512"; + if (!File.Exists(nupkgPath) || !File.Exists(nupkgHashPath)) + { + using (var downloader = await byId.GetPackageDownloaderAsync(ident, cache, logger, cancellationToken).ConfigureAwait(false)) + { + await downloader.CopyNupkgFileToAsync(nupkgPath, cancellationToken).ConfigureAwait(false); + + var sha512 = await downloader.GetPackageHashAsync("SHA512", cancellationToken).ConfigureAwait(false); + File.WriteAllText(nupkgHashPath, sha512); + } + } + + return nupkgPath; + } + + private string GetPackageDirectoryBase(PackageIdentity ident) + { + return Path.Combine(ident.Id.ToLowerInvariant(), ident.Version.ToNormalizedString()); + } + } +} diff --git a/api-tools/ApiCompatCommand.cs b/api-tools/AssemblyCommands/ApiCompatCommand.cs similarity index 100% rename from api-tools/ApiCompatCommand.cs rename to api-tools/AssemblyCommands/ApiCompatCommand.cs diff --git a/api-tools/ApiInfoCommand.cs b/api-tools/AssemblyCommands/ApiInfoCommand.cs similarity index 95% rename from api-tools/ApiInfoCommand.cs rename to api-tools/AssemblyCommands/ApiInfoCommand.cs index 9126b78..7deea57 100644 --- a/api-tools/ApiInfoCommand.cs +++ b/api-tools/AssemblyCommands/ApiInfoCommand.cs @@ -9,8 +9,8 @@ namespace Mono.ApiTools { public class ApiInfoCommand : BaseCommand { - public ApiInfoCommand() - : base("api-info", "ASSEMBLY ...", "Generate API info XML for assemblies.") + public ApiInfoCommand(string name = "api-info") + : base(name, "ASSEMBLY ...", "Generate API info XML for assemblies.") { } diff --git a/api-tools/DiffCommand.cs b/api-tools/AssemblyCommands/DiffCommand.cs similarity index 100% rename from api-tools/DiffCommand.cs rename to api-tools/AssemblyCommands/DiffCommand.cs diff --git a/api-tools/MergeCommand.cs b/api-tools/AssemblyCommands/MergeCommand.cs similarity index 100% rename from api-tools/MergeCommand.cs rename to api-tools/AssemblyCommands/MergeCommand.cs diff --git a/api-tools/NuGetCommands/NuGetBaseCommand.cs b/api-tools/NuGetCommands/NuGetBaseCommand.cs new file mode 100644 index 0000000..516353b --- /dev/null +++ b/api-tools/NuGetCommands/NuGetBaseCommand.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.IO; +using Mono.Options; + +namespace Mono.ApiTools; + +public abstract class NuGetBaseCommand : BaseCommand +{ + protected NuGetBaseCommand(string name, string help) + : this(name, null, help) + { + } + + protected NuGetBaseCommand(string name, string extras, string help) + : base(name, extras, help) + { + } + + public string PackageCache { get; set; } + + public bool PrePrelease { get; set; } + + public bool PreferRelease { get; set; } + + public string SourceUrl { get; set; } = "https://api.nuget.org/v3/index.json"; + + protected override OptionSet OnCreateOptions() => new OptionSet + { + { "cache=", "The package cache directory", v => PackageCache = v }, + { "prerelease", "Include preprelease packages", v => PrePrelease = true }, + { "prefer-release", "Prefer release packages over prerelease packages", v => PreferRelease = true }, + { "source=", "The NuGet URL source", v => SourceUrl = v }, + }; + + protected override bool OnValidateArguments(IEnumerable extras) + { + var hasError = false; + + if (string.IsNullOrEmpty(PackageCache)) + PackageCache = Path.Combine(Directory.GetCurrentDirectory(), "packages"); + + return !hasError; + } +} diff --git a/api-tools/NuGetDiffCommand.cs b/api-tools/NuGetCommands/NuGetDiffCommand.cs similarity index 79% rename from api-tools/NuGetDiffCommand.cs rename to api-tools/NuGetCommands/NuGetDiffCommand.cs index de4c8e6..e3faa1e 100644 --- a/api-tools/NuGetDiffCommand.cs +++ b/api-tools/NuGetCommands/NuGetDiffCommand.cs @@ -9,17 +9,15 @@ namespace Mono.ApiTools { - public class NuGetDiffCommand : BaseCommand + public class NuGetDiffCommand : NuGetBaseCommand { - public NuGetDiffCommand() - : base("nuget-diff", "[PACKAGES | DIRECTORIES]", "Compare two NuGet packages.") + public NuGetDiffCommand(string name = "nuget-diff") + : base(name, "[PACKAGES | DIRECTORIES]", "Compare two NuGet packages.") { } public List Packages { get; set; } = new List(); - public string PackageCache { get; set; } - public List SearchPaths { get; set; } = new List(); public bool GroupByPackageId { get; set; } @@ -30,39 +28,31 @@ public NuGetDiffCommand() public bool Latest { get; set; } - public bool PrePrelease { get; set; } - - public bool PreferRelease { get; set; } - public bool IgnoreUnchanged { get; set; } public string OutputDirectory { get; set; } - public string SourceUrl { get; set; } = "https://api.nuget.org/v3/index.json"; - public bool CompareNuGetStructure { get; set; } - protected override OptionSet OnCreateOptions() => new OptionSet + protected override OptionSet OnCreateOptions() { - { "cache=", "The package cache directory", v => PackageCache = v }, - { "group-ids", "Group the output by package ID", v => GroupByPackageId = true }, - { "group-versions", "Group the output by version", v => GroupByVersion = true }, - { "latest", "Compare against the latest", v => Latest = true }, - { "output=", "The output directory", v => OutputDirectory = v }, - { "prerelease", "Include preprelease packages", v => PrePrelease = true }, - { "prefer-release", "Prefer release packages over prerelease packages", v => PreferRelease = true }, - { "ignore-unchanged", "Ignore unchanged packages and assemblies", v => IgnoreUnchanged = true }, - { "search-path=", "A search path directory", v => SearchPaths.Add(v) }, - { "s|search=", "A search path directory", v => SearchPaths.Add(v) }, - { "source=", "The NuGet URL source", v => SourceUrl = v }, - { "version=", "The version of the package to compare", v => Version = v }, - { "compare-nuget-structure", "Compare NuGet metadata and file contents", v => CompareNuGetStructure = true }, - { "include-structure", "Compare NuGet metadata and file contents", v => CompareNuGetStructure = true }, - }; - - protected override bool OnValidateArguments(IEnumerable extras) + var options = base.OnCreateOptions(); + options.Add("group-ids", "Group the output by package ID", v => GroupByPackageId = true); + options.Add("group-versions", "Group the output by version", v => GroupByVersion = true); + options.Add("latest", "Compare against the latest", v => Latest = true); + options.Add("output=", "The output directory", v => OutputDirectory = v); + options.Add("ignore-unchanged", "Ignore unchanged packages and assemblies", v => IgnoreUnchanged = true); + options.Add("search-path=", "A search path directory", v => SearchPaths.Add(v)); + options.Add("s|search=", "A search path directory", v => SearchPaths.Add(v)); + options.Add("version=", "The version of the package to compare", v => Version = v); + options.Add("compare-nuget-structure", "Compare NuGet metadata and file contents", v => CompareNuGetStructure = true); + options.Add("include-structure", "Compare NuGet metadata and file contents", v => CompareNuGetStructure = true); + return options; + } + + protected override bool OnValidateArguments(IEnumerable extras) { - var hasError = false; + var hasError = !base.OnValidateArguments(extras); var packages = extras.Where(p => !string.IsNullOrEmpty(p)).ToArray(); @@ -110,9 +100,6 @@ protected override bool OnValidateArguments(IEnumerable extras) if (string.IsNullOrEmpty(OutputDirectory)) OutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "api-diff"); - if (string.IsNullOrEmpty(PackageCache)) - PackageCache = Path.Combine(Directory.GetCurrentDirectory(), "packages"); - return !hasError; } diff --git a/api-tools/NuGetCommands/NuGetDownloadCommand.cs b/api-tools/NuGetCommands/NuGetDownloadCommand.cs new file mode 100644 index 0000000..076ef07 --- /dev/null +++ b/api-tools/NuGetCommands/NuGetDownloadCommand.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Mono.Options; +using NuGet.Versioning; + +namespace Mono.ApiTools; + +public class NuGetDownloadCommand : NuGetBaseCommand +{ + public NuGetDownloadCommand() + : base("download", "PACKAGE ID", "Download a NuGet package.") + { + } + + public string PackageId { get; set; } + + public string Version { get; set; } + + public bool Latest { get; set; } + + protected override OptionSet OnCreateOptions() + { + var options = base.OnCreateOptions(); + options.Add("latest", "Compare against the latest", v => Latest = true); + options.Add("version=", "The version of the package to compare", v => Version = v); + return options; + } + + protected override bool OnValidateArguments(IEnumerable extras) + { + var hasError = !base.OnValidateArguments(extras); + + var packages = extras.Where(p => !string.IsNullOrEmpty(p)).ToArray(); + + if (packages.Length != 1) + { + Console.Error.WriteLine($"{Program.Name}: Exactly one package ID is required."); + hasError = true; + } + else + { + PackageId = packages[0]; + } + + if (!string.IsNullOrEmpty(Version) && Latest) + { + Console.Error.WriteLine($"{Program.Name}: Both `--latest` and `--version=` cannot be provided at the same time."); + hasError = true; + } + + if (string.IsNullOrEmpty(Version) && !Latest) + { + Console.Error.WriteLine($"{Program.Name}: Either `--latest` or `--version=` must be specified."); + hasError = true; + } + + if (!string.IsNullOrEmpty(Version) && !NuGetVersion.TryParse(Version, out _)) + { + Console.Error.WriteLine($"{Program.Name}: An invalid version was provided."); + hasError = true; + } + + return !hasError; + } + + protected override bool OnInvoke(IEnumerable extras) + { + var manager = new NuGetManager(SourceUrl); + manager.PackageCache = PackageCache; + + var success = DownloadPackageAsync(manager).Result; + + return success; + } + + private async Task DownloadPackageAsync(NuGetManager manager) + { + string latest; + if (Latest) + { + // get the latest version of this package - if any + if (Program.Verbose) + Console.WriteLine($"Determining the latest version of '{PackageId}'..."); + var filter = new NuGetVersions.Filter + { + IncludePrerelease = PrePrelease, + PreferRelease = PreferRelease, + SourceUrl = SourceUrl, + }; + latest = (await NuGetVersions.GetLatestAsync(PackageId, filter))?.ToNormalizedString(); + } + else + { + latest = Version; + } + + + if (string.IsNullOrEmpty(latest)) + { + if (Program.Verbose) + Console.WriteLine($"No package found for '{PackageId}'..."); + return false; + } + + if (Program.Verbose) + Console.WriteLine($"Downloading version '{latest}' of '{PackageId}'..."); + + var dest = await manager.ExtractCachedPackageAsync(PackageId, latest); + + if (Program.Verbose) + Console.WriteLine($"Package downloaded to '{dest}'."); + + return true; + } +} diff --git a/api-tools/Program.cs b/api-tools/Program.cs index ee555d5..5c319d7 100644 --- a/api-tools/Program.cs +++ b/api-tools/Program.cs @@ -19,7 +19,23 @@ static int Main(string[] args) "Global options:", { "v|verbose", "Use a more verbose output", _ => Verbose = true }, "", - "Available commands:", + "Assembly commands:", + new CommandSet("assembly") + { + new ApiInfoCommand("info"), + new ApiCompatCommand(), + new DiffCommand(), + new MergeCommand(), + }, + "", + "NuGet commands:", + new CommandSet("nuget") + { + new NuGetDiffCommand("diff"), + new NuGetDownloadCommand() + }, + "", + "Obsolete commands:", new ApiInfoCommand(), new ApiCompatCommand(), new DiffCommand(),