diff --git a/src/Cli/dotnet/NugetPackageInstaller/NuGetPackageInstaller.cs b/src/Cli/dotnet/NugetPackageInstaller/NuGetPackageInstaller.cs new file mode 100644 index 000000000000..c876ae8fb775 --- /dev/null +++ b/src/Cli/dotnet/NugetPackageInstaller/NuGetPackageInstaller.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.ToolPackage; +using NuGet.Common; +using NuGet.Packaging; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace Microsoft.DotNet.Cli.NuGetPackageInstaller +{ + internal class NuGetPackageInstaller + { + private static readonly string sourceUrl = "https://api.nuget.org/v3/index.json"; + private readonly ILogger _logger; + private readonly string _packageInstallDir; + + public NuGetPackageInstaller(string packageInstallDir, ILogger logger = null) + { + _packageInstallDir = packageInstallDir; + _logger = logger ?? new NullLogger(); + } + + public async Task InstallPackageAsync(PackageId packageId, NuGetVersion packageVersion) + { + var cancellationToken = CancellationToken.None; + var cache = new SourceCacheContext() { DirectDownload = true, NoCache = true }; + var source = Repository.Factory.GetCoreV3(sourceUrl); + var findPackageByIdResource = await source.GetResourceAsync(); + var nupkgPath = Path.Combine(_packageInstallDir, packageId.ToString(), packageVersion.ToNormalizedString(), $"{packageId}.{packageVersion.ToNormalizedString()}.nupkg"); + Directory.CreateDirectory(Path.GetDirectoryName(nupkgPath)); + using var destinationStream = File.Create(nupkgPath); + var success = await findPackageByIdResource.CopyNupkgToStreamAsync( + id: packageId.ToString(), + version: packageVersion, + destination: destinationStream, + cacheContext: cache, + logger: _logger, + cancellationToken: cancellationToken); + + if (!success) + { + throw new Exception($"Downloading {packageId} version {packageVersion.ToNormalizedString()} failed."); + } + + return nupkgPath; + } + + public async Task> ExtractPackageAsync(string packagePath, string targetFolder) + { + using var packageStream = File.OpenRead(packagePath); + var packageReader = new PackageFolderReader(targetFolder); + var packageExtractionContext = new PackageExtractionContext( + PackageSaveMode.Defaultv3, + XmlDocFileSaveMode.None, + clientPolicyContext: null, + logger: _logger); + var packagePathResolver = new PackagePathResolver(targetFolder); + var cancellationToken = CancellationToken.None; + + return await PackageExtractor.ExtractPackageAsync( + source: targetFolder, + packageStream: packageStream, + packagePathResolver: packagePathResolver, + packageExtractionContext: packageExtractionContext, + token: cancellationToken); + } + } +} diff --git a/src/Tests/dotnet.Tests/NugetPackageInstallerTests/NuGetPackageInstallerTests.cs b/src/Tests/dotnet.Tests/NugetPackageInstallerTests/NuGetPackageInstallerTests.cs new file mode 100644 index 000000000000..c70958d83969 --- /dev/null +++ b/src/Tests/dotnet.Tests/NugetPackageInstallerTests/NuGetPackageInstallerTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.DotNet.Cli.NuGetPackageInstaller; +using Microsoft.DotNet.ToolPackage; +using Microsoft.NET.TestFramework; +using NuGet.Versioning; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.Tests +{ + public class NuGetPackageInstallerTests : SdkTest + { + public NuGetPackageInstallerTests(ITestOutputHelper log) : base(log) + { + } + + [Fact] + public async Task It_installs_nuget_package() + { + var packageId = "Humanizer"; + var packageVersion = "2.6.2"; + var logger = new NuGetTestLogger(); + var installer = new NuGetPackageInstaller(Directory.GetCurrentDirectory(), logger); + var packagePath = await installer.InstallPackageAsync(new PackageId(packageId), new NuGetVersion(packageVersion)); + + logger.Errors.Should().Be(0); + logger.Warnings.Should().Be(0); + packagePath.Should().ContainEquivalentOf(packageId); + packagePath.Should().Contain(packageVersion); + File.Exists(packagePath).Should().BeTrue(); + } + + [Fact] + public async Task It_extracts_nuget_package() + { + var packageId = "Newtonsoft.Json"; + var packageVersion = "12.0.3"; + var logger = new NuGetTestLogger(); + var installer = new NuGetPackageInstaller(Directory.GetCurrentDirectory(), logger); + var packagePath = await installer.InstallPackageAsync(new PackageId(packageId), new NuGetVersion(packageVersion)); + var targetPath = Path.Combine(Directory.GetCurrentDirectory(), "ExtractedPackage"); + var result = await installer.ExtractPackageAsync(packagePath, targetPath); + + var resultPath = Path.Combine(targetPath, $"{packageId}.{packageVersion}"); + Directory.Exists(resultPath).Should().BeTrue(); + var extractedFiles = Directory.GetFiles(resultPath, "*", SearchOption.AllDirectories); + extractedFiles.Should().Contain(Path.Combine(resultPath, $"{packageId}.nuspec")); + extractedFiles.Should().BeEquivalentTo(result); + } + } +} diff --git a/src/Tests/dotnet.Tests/NugetPackageInstallerTests/NuGetTestLogger.cs b/src/Tests/dotnet.Tests/NugetPackageInstallerTests/NuGetTestLogger.cs new file mode 100644 index 000000000000..9bc16cc6f083 --- /dev/null +++ b/src/Tests/dotnet.Tests/NugetPackageInstallerTests/NuGetTestLogger.cs @@ -0,0 +1,185 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using NuGet.Common; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.Tests +{ + public class NuGetTestLogger : ILogger + { + private readonly ITestOutputHelper _output; + + public NuGetTestLogger() + { + } + + public NuGetTestLogger(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Logged messages + /// + public ConcurrentQueue Messages { get; } = new ConcurrentQueue(); + public ConcurrentQueue DebugMessages { get; } = new ConcurrentQueue(); + public ConcurrentQueue VerboseMessages { get; } = new ConcurrentQueue(); + public ConcurrentQueue MinimalMessages { get; } = new ConcurrentQueue(); + public ConcurrentQueue InformationMessages { get; } = new ConcurrentQueue(); + public ConcurrentQueue ErrorMessages { get; } = new ConcurrentQueue(); + public ConcurrentQueue WarningMessages { get; } = new ConcurrentQueue(); + + public ConcurrentQueue LogMessages { get; } = new ConcurrentQueue(); + + public int Errors { get; set; } + + public int Warnings { get; set; } + + public void LogDebug(string data) + { + Messages.Enqueue(data); + DebugMessages.Enqueue(data); + DumpMessage("DEBUG", data); + } + + public void LogError(string data) + { + Errors++; + Messages.Enqueue(data); + ErrorMessages.Enqueue(data); + DumpMessage("ERROR", data); + } + + public void LogInformation(string data) + { + Messages.Enqueue(data); + InformationMessages.Enqueue(data); + DumpMessage("INFO ", data); + } + + public void LogMinimal(string data) + { + Messages.Enqueue(data); + MinimalMessages.Enqueue(data); + DumpMessage("LOG ", data); + } + + public void LogVerbose(string data) + { + Messages.Enqueue(data); + VerboseMessages.Enqueue(data); + DumpMessage("TRACE", data); + } + + public void LogWarning(string data) + { + Warnings++; + Messages.Enqueue(data); + WarningMessages.Enqueue(data); + DumpMessage("WARN ", data); + } + + public void LogInformationSummary(string data) + { + Messages.Enqueue(data); + DumpMessage("ISMRY", data); + } + + private void DumpMessage(string level, string data) + { + // NOTE(anurse): Uncomment this to help when debugging tests + //Console.WriteLine($"{level}: {data}"); + _output?.WriteLine($"{level}: {data}"); + } + + public void Clear() + { + string msg; + while (Messages.TryDequeue(out msg)) + { + // do nothing + } + } + + public string ShowErrors() + { + return string.Join(Environment.NewLine, ErrorMessages); + } + + public string ShowWarnings() + { + return string.Join(Environment.NewLine, WarningMessages); + } + + public string ShowMessages() + { + return string.Join(Environment.NewLine, Messages); + } + + public void Log(LogLevel level, string data) + { + switch (level) + { + case LogLevel.Debug: + { + LogDebug(data); + break; + } + + case LogLevel.Error: + { + LogError(data); + break; + } + + case LogLevel.Information: + { + LogInformation(data); + break; + } + + case LogLevel.Minimal: + { + LogMinimal(data); + break; + } + + case LogLevel.Verbose: + { + LogVerbose(data); + break; + } + + case LogLevel.Warning: + { + LogWarning(data); + break; + } + } + } + + public Task LogAsync(LogLevel level, string data) + { + Log(level, data); + + return Task.FromResult(0); + } + + public void Log(ILogMessage message) + { + LogMessages.Enqueue(message); + + Log(message.Level, message.Message); + } + + public async Task LogAsync(ILogMessage message) + { + LogMessages.Enqueue(message); + await LogAsync(message.Level, message.FormatWithCode()); + } + } +}