From 6d3aa388c8da214fa8ff8286d5db30f23a64e2c6 Mon Sep 17 00:00:00 2001 From: Nigusu Solomon Yenework <59111203+Nigusu-Allehu@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:58:57 -0700 Subject: [PATCH 1/2] [Feature]Add `package download` command (#6804) --- .../Package/Download/PackageDownloadArgs.cs | 21 + .../Download/PackageDownloadCommand.cs | 97 ++++ .../Package/Download/PackageDownloadRunner.cs | 286 ++++++++++++ .../AddPackageReferenceCommand.cs | 2 +- .../NuGet.CommandLine.XPlat/NuGetCommands.cs | 2 + .../NuGet.CommandLine.XPlat/Program.cs | 8 +- .../Strings.Designer.cs | 126 ++++- .../NuGet.CommandLine.XPlat/Strings.resx | 51 ++- .../xlf/Strings.cs.xlf | 78 +++- .../xlf/Strings.de.xlf | 78 +++- .../xlf/Strings.es.xlf | 78 +++- .../xlf/Strings.fr.xlf | 78 +++- .../xlf/Strings.it.xlf | 78 +++- .../xlf/Strings.ja.xlf | 78 +++- .../xlf/Strings.ko.xlf | 78 +++- .../xlf/Strings.pl.xlf | 78 +++- .../xlf/Strings.pt-BR.xlf | 78 +++- .../xlf/Strings.ru.xlf | 78 +++- .../xlf/Strings.tr.xlf | 78 +++- .../xlf/Strings.zh-Hans.xlf | 78 +++- .../xlf/Strings.zh-Hant.xlf | 78 +++- .../Download/PackageDownloadRunnerTests.cs | 429 ++++++++++++++++++ .../Download/PackageDownloadCommandTests.cs | 288 ++++++++++++ .../Download/PackageDownloadRunnerTests.cs | 204 +++++++++ 24 files changed, 2451 insertions(+), 77 deletions(-) create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadArgs.cs create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadCommand.cs create mode 100644 src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs create mode 100644 test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs create mode 100644 test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadCommandTests.cs create mode 100644 test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadArgs.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadArgs.cs new file mode 100644 index 00000000000..7a17fde3209 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadArgs.cs @@ -0,0 +1,21 @@ +// 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. + +#nullable enable + +using System.Collections.Generic; +using NuGet.Common; + +namespace NuGet.CommandLine.XPlat.Commands.Package.PackageDownload; + +internal record PackageDownloadArgs +{ + public IReadOnlyList? Packages { get; set; } + public IList? Sources { get; set; } + public string? OutputDirectory { get; set; } + public string? ConfigFile { get; set; } + public bool IncludePrerelease { get; set; } + public bool AllowInsecureConnections { get; set; } + public bool Interactive { get; set; } + public LogLevel LogLevel { get; set; } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadCommand.cs new file mode 100644 index 00000000000..b76da4b49b5 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadCommand.cs @@ -0,0 +1,97 @@ +// 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.Generic; +using System.CommandLine; +using System.Threading; +using System.Threading.Tasks; + +namespace NuGet.CommandLine.XPlat.Commands.Package.PackageDownload +{ + internal class PackageDownloadCommand + { + internal static void Register(Command packageCommand, Option interactiveOption) + { + Register(packageCommand, interactiveOption, PackageDownloadRunner.RunAsync); + } + + public static void Register(Command packageCommand, Option interactiveOption, Func> action) + { + var downloadCommand = new DocumentedCommand( + "download", + Strings.PackageDownloadCommand_Description, + "https://aka.ms/dotnet/package/download"); + + // Arguments + var packagesArguments = new Argument>("packages") + { + Description = Strings.PackageUpdate_PackageArgumentDescription, + Arity = ArgumentArity.OneOrMore, + CustomParser = PackageWithNuGetVersion.Parse + }; + + // Options + var allowInsecureConnections = new Option("--allow-insecure-connections") + { + Description = Strings.PackageDownloadCommand_AllowInsecureConnectionsDescription, + Arity = ArgumentArity.Zero + }; + + var configFile = new Option("--configfile") + { + Description = Strings.Option_ConfigFile, + Arity = ArgumentArity.ExactlyOne + }; + + var outputDirectory = new Option("--output", "-o") + { + Description = Strings.PackageDownloadCommand_OutputDirectoryDescription, + Arity = ArgumentArity.ExactlyOne + }; + + var prerelease = new Option("--prerelease") + { + Description = Strings.Prerelease_Description, + Arity = ArgumentArity.Zero + }; + + var sources = new Option>("--source", "-s") + { + Description = Strings.PackageDownloadCommand_SourcesDescription, + Arity = ArgumentArity.OneOrMore + }; + + var verbosity = CommonOptions.GetVerbosityOption(); + + downloadCommand.Arguments.Add(packagesArguments); + downloadCommand.Options.Add(allowInsecureConnections); + downloadCommand.Options.Add(configFile); + downloadCommand.Options.Add(interactiveOption); + downloadCommand.Options.Add(outputDirectory); + downloadCommand.Options.Add(prerelease); + downloadCommand.Options.Add(sources); + downloadCommand.Options.Add(verbosity); + + downloadCommand.SetAction(async (parserResult, cancellationToken) => + { + IReadOnlyList packages = parserResult.GetValue(packagesArguments) ?? []; + var args = new PackageDownloadArgs() + { + Packages = packages, + Sources = parserResult.GetValue(sources), + OutputDirectory = parserResult.GetValue(outputDirectory), + IncludePrerelease = parserResult.GetValue(prerelease), + AllowInsecureConnections = parserResult.GetValue(allowInsecureConnections), + Interactive = parserResult.GetValue(interactiveOption), + ConfigFile = parserResult.GetValue(configFile), + LogLevel = (parserResult.GetValue(verbosity) ?? VerbosityEnum.normal).ToLogLevel() + }; + + return await action(args, cancellationToken); + }); + + packageCommand.Subcommands.Add(downloadCommand); + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs new file mode 100644 index 00000000000..f6a93645940 --- /dev/null +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs @@ -0,0 +1,286 @@ +// 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.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NuGet.CommandLine.XPlat.Utility; +using NuGet.Commands; +using NuGet.Configuration; +using NuGet.Credentials; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Packaging.PackageExtraction; +using NuGet.Packaging.Signing; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Repositories; +using NuGet.Versioning; + +namespace NuGet.CommandLine.XPlat.Commands.Package.PackageDownload +{ + internal static class PackageDownloadRunner + { + internal const int ExitCodeError = 1; + internal const int ExitCodeSuccess = 0; + + public static async Task RunAsync(PackageDownloadArgs args, CancellationToken token) + { + ILoggerWithColor logger = new CommandOutputLogger(args.LogLevel) + { + HidePrefixForInfoAndMinimal = true + }; + + XPlatUtility.ConfigureProtocol(); + DefaultCredentialServiceUtility.SetupDefaultCredentialService(logger, !args.Interactive); + ISettings settings = Settings.LoadDefaultSettings( + Directory.GetCurrentDirectory(), + args.ConfigFile, + new XPlatMachineWideSetting()); + IReadOnlyList packageSources = GetPackageSources(args.Sources, new PackageSourceProvider(settings)); + + return await RunAsync(args, logger, packageSources, settings, token); + } + + public static async Task RunAsync(PackageDownloadArgs args, ILoggerWithColor logger, IReadOnlyList packageSources, ISettings settings, CancellationToken token) + { + // Check for insecure sources + if (DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger)) + { + return ExitCodeError; + } + + string outputDirectory = args.OutputDirectory ?? Directory.GetCurrentDirectory(); + var cache = new SourceCacheContext(); + IReadOnlyList sourceRepositories = GetSourceRepositories(packageSources); + bool downloadedAllSuccessfully = true; + + foreach (var package in args.Packages) + { + logger.LogMinimal(string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_Starting, + package.Id, + string.IsNullOrEmpty(package.NuGetVersion?.ToNormalizedString()) ? Strings.PackageDownloadCommand_LatestVersion : package.NuGetVersion.ToNormalizedString())); + + try + { + (NuGetVersion version, SourceRepository downloadRepository) = + await ResolvePackageDownloadVersion( + package, + sourceRepositories, + cache, + logger, + args.IncludePrerelease, + token); + + if (version == null) + { + // Unable to find a valid version + downloadedAllSuccessfully &= false; + continue; + } + + bool success = await DownloadPackageAsync( + package.Id, + version, + downloadRepository, + cache, + settings, + outputDirectory, + logger, + token); + + if (success) + { + logger.LogMinimal(string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_Succeeded, + package.Id, + version, + outputDirectory)); + } + else + { + logger.LogError(string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_Failed, + package.Id, + version)); + + downloadedAllSuccessfully &= false; + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) + { + logger.LogError(ex.ToString()); + downloadedAllSuccessfully &= false; + } +#pragma warning restore CA1031 // Do not catch general exception types + } + + return downloadedAllSuccessfully ? ExitCodeSuccess : ExitCodeError; + } + + internal static async Task<(NuGetVersion, SourceRepository)> ResolvePackageDownloadVersion( + PackageWithNuGetVersion packageWithNuGetVersion, + IEnumerable sourceRepositories, + SourceCacheContext cache, + ILoggerWithColor logger, + bool includePrerelease, + CancellationToken token) + { + NuGetVersion versionToDownload = null; + SourceRepository downloadSourceRepository = null; + bool versionSpecified = packageWithNuGetVersion.NuGetVersion != null; + + foreach (var repo in sourceRepositories) + { + var finder = await repo.GetResourceAsync(token); + var packages = await finder.GetMetadataAsync( + packageWithNuGetVersion.Id, + includePrerelease, + includeUnlisted: versionSpecified, // only load unlisted if an exact version is specified + sourceCacheContext: cache, + logger, + token); + + if (packages == null) + { + continue; + } + + if (versionSpecified) + { + // If an exact version is specified, check if it exists at this source + foreach (var package in packages) + { + if (package?.Identity?.Version == packageWithNuGetVersion.NuGetVersion) + { + return (packageWithNuGetVersion.NuGetVersion, repo); + } + } + + continue; + } + + foreach (var package in packages) + { + var version = package.Identity.Version; + if (versionToDownload == null || version > versionToDownload) + { + versionToDownload = version; + downloadSourceRepository = repo; + } + } + } + + if (versionToDownload == null) + { + logger.LogError(Strings.Error_PackageDownload_VersionNotFound); + } + + return (versionToDownload, downloadSourceRepository); + } + + private static async Task DownloadPackageAsync( + string id, + NuGetVersion version, + SourceRepository repo, + SourceCacheContext cache, + ISettings settings, + string outputDirectory, + Common.ILogger logger, + CancellationToken token) + { + var extractionContext = new PackageExtractionContext( + PackageSaveMode.Defaultv3, + PackageExtractionBehavior.XmlDocFileSaveMode, + ClientPolicyContext.GetClientPolicy(settings, logger), + logger); + + var resolver = new VersionFolderPathResolver(outputDirectory); + var userPackageFolder = new NuGetv3LocalRepository(outputDirectory); + + // no-op if already installed + if (userPackageFolder.Exists(id, version)) + { + logger.LogMinimal(string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_AlreadyInstalled, + id, + version.ToNormalizedString(), + outputDirectory)); + + return true; + } + + var packageIdentity = new PackageIdentity(id, version); + var provider = new SourceRepositoryDependencyProvider(sourceRepository: repo, logger: logger, cacheContext: cache, ignoreFailedSources: false, ignoreWarning: false); + using var downloader = await provider.GetPackageDownloaderAsync(packageIdentity, cache, logger, token); + bool success = await PackageExtractor.InstallFromSourceAsync(packageIdentity, downloader, resolver, extractionContext, token); + + if (!success) + { + logger.LogError(string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_UnableToDownload, + id, + version.ToNormalizedString(), + repo.PackageSource.Source)); + return false; + } + + return success; + } + + private static IReadOnlyList GetPackageSources(IList sources, IPackageSourceProvider sourceProvider) + { + IEnumerable configuredSources = sourceProvider.LoadPackageSources() + .Where(s => s.IsEnabled); + + if (sources != null && sources.Count > 0) + { + // Use sources specified on command line + return [.. sources.Select(s => PackageSourceProviderExtensions.ResolveSource(configuredSources, s))]; + } + + return [.. configuredSources]; + } + + private static bool DetectAndReportInsecureSources( + bool allowInsecureConnections, + IEnumerable packageSources, + ILoggerWithColor logger) + { + if (!allowInsecureConnections) + { + var insecureSources = HttpSourcesUtility.GetDisallowedInsecureHttpSources([.. packageSources]); + if (insecureSources.Any()) + { + logger.LogError(HttpSourcesUtility.BuildHttpSourceErrorMessage(insecureSources, "package download")); + return true; + } + } + + return false; + } + + private static IReadOnlyList GetSourceRepositories(IReadOnlyList packageSources) + { + IEnumerable> providers = Repository.Provider.GetCoreV3(); + List sourceRepositories = []; + foreach (var source in packageSources) + { + sourceRepositories.Add(Repository.CreateSource(providers, source, FeedType.Undefined)); + } + + return sourceRepositories; + } + } +} diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommand.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommand.cs index 41d770b98db..e42ac09ccdf 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommand.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/AddPackageReferenceCommand.cs @@ -72,7 +72,7 @@ public static void Register(CommandLineApplication app, Func getLogger, var prerelease = addpkg.Option( "--prerelease", - Strings.AddPkg_PackagePrerelease, + Strings.Prerelease_Description, CommandOptionType.NoValue); addpkg.OnExecute(() => diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs index f9a6a037c49..62c00c79bd9 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/NuGetCommands.cs @@ -4,6 +4,7 @@ using System; using System.CommandLine; using System.Linq; +using NuGet.CommandLine.XPlat.Commands.Package.PackageDownload; using NuGet.CommandLine.XPlat.Commands.Package.Update; namespace NuGet.CommandLine.XPlat; @@ -30,6 +31,7 @@ public static void Add(RootCommand rootCommand, Option interactiveOption) } PackageUpdateCommand.Register(packageCommand, interactiveOption); + PackageDownloadCommand.Register(packageCommand, interactiveOption); } // To delete once the SDK starts using the other overload. Joys of public APIs. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs index f625524ad53..bf1af280fa6 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Program.cs @@ -11,6 +11,9 @@ using NuGet.CommandLine.XPlat.Commands.Package.Update; using NuGet.Commands; using NuGet.Common; +#if DEBUG +using NuGet.CommandLine.XPlat.Commands.Package.PackageDownload; +#endif namespace NuGet.CommandLine.XPlat { @@ -100,6 +103,9 @@ public static int MainInternal(string[] args, CommandOutputLogger log, IEnvironm PackageSearchCommand.Register(packageCommand, getHidePrefixLogger); PackageUpdateCommand.Register(packageCommand, interactiveOption); +#if DEBUG + PackageDownloadCommand.Register(packageCommand, interactiveOption); +#endif } else { @@ -246,7 +252,7 @@ private static bool IsSystemCommandLineParsedCommand(string[] args) if (args.Length >= 2 && arg0 == "package") { string arg1 = args[1]; - if (arg1 == "search" || arg1 == "update") + if (arg1 == "search" || arg1 == "update" || arg1 == "download") { return true; } diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 467fe9b47b9..33be7a2c176 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -150,15 +150,6 @@ internal static string AddPkg_PackageIdDescription { } } - /// - /// Looks up a localized string similar to Allows prerelease packages to be installed.. - /// - internal static string AddPkg_PackagePrerelease { - get { - return ResourceManager.GetString("AddPkg_PackagePrerelease", resourceCulture); - } - } - /// /// Looks up a localized string similar to Version of the package to be added.. /// @@ -763,6 +754,15 @@ internal static string Error_NoVersionsAvailable { } } + /// + /// Looks up a localized string similar to Unable to find a valid package version. + /// + internal static string Error_PackageDownload_VersionNotFound { + get { + return ResourceManager.GetString("Error_PackageDownload_VersionNotFound", resourceCulture); + } + } + /// /// Looks up a localized string similar to Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information.. /// @@ -1598,6 +1598,105 @@ internal static string OutputNuGetVersion { } } + /// + /// Looks up a localized string similar to Allows downloading from HTTP (non-HTTPS) package sources.. + /// + internal static string PackageDownloadCommand_AllowInsecureConnectionsDescription { + get { + return ResourceManager.GetString("PackageDownloadCommand_AllowInsecureConnectionsDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Skipping download. Package '{0}' version {1} already exists at '{2}'.. + /// + internal static string PackageDownloadCommand_AlreadyInstalled { + get { + return ResourceManager.GetString("PackageDownloadCommand_AlreadyInstalled", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Downloads a NuGet package to a local folder without requiring a project file.. + /// + internal static string PackageDownloadCommand_Description { + get { + return ResourceManager.GetString("PackageDownloadCommand_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Package '{0}' ({1}) failed to download.. + /// + internal static string PackageDownloadCommand_Failed { + get { + return ResourceManager.GetString("PackageDownloadCommand_Failed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to latest version. + /// + internal static string PackageDownloadCommand_LatestVersion { + get { + return ResourceManager.GetString("PackageDownloadCommand_LatestVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Directory where the package will be placed. Defaults to the current working directory.. + /// + internal static string PackageDownloadCommand_OutputDirectoryDescription { + get { + return ResourceManager.GetString("PackageDownloadCommand_OutputDirectoryDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Package identifier (e.g. 'Newtonsoft.Json').. + /// + internal static string PackageDownloadCommand_PackageIdDescription { + get { + return ResourceManager.GetString("PackageDownloadCommand_PackageIdDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Specifies one or more NuGet package sources to use.. + /// + internal static string PackageDownloadCommand_SourcesDescription { + get { + return ResourceManager.GetString("PackageDownloadCommand_SourcesDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Downloading package {0}, version {1}.. + /// + internal static string PackageDownloadCommand_Starting { + get { + return ResourceManager.GetString("PackageDownloadCommand_Starting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Package '{0}' ({1}) successfully downloaded to '{2}'.. + /// + internal static string PackageDownloadCommand_Succeeded { + get { + return ResourceManager.GetString("PackageDownloadCommand_Succeeded", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Package download failed for `{0} {1}` from source `{2}`.. + /// + internal static string PackageDownloadCommand_UnableToDownload { + get { + return ResourceManager.GetString("PackageDownloadCommand_UnableToDownload", resourceCulture); + } + } + /// /// Looks up a localized string similar to All packages are already up to date.. /// @@ -1850,6 +1949,15 @@ internal static string pkgSearch_VerbosityDescription { } } + /// + /// Looks up a localized string similar to Allows prerelease packages to be installed.. + /// + internal static string Prerelease_Description { + get { + return ResourceManager.GetString("Prerelease_Description", resourceCulture); + } + } + /// /// Looks up a localized string similar to There are no stable versions available, {0} is the best available. Consider adding the --prerelease option. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 9065e0a4da7..76c0f91d27b 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -586,7 +586,7 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Set the verbosity level of the command. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. Please don't localize "q[uiet]", "m[inimal]", "n[ormal]", "d[etailed]", or "diag[nostic]" - + Allows prerelease packages to be installed. @@ -1108,4 +1108,53 @@ Do not translate "PackageVersion" Invalid version value '{0}'. 0 - package version + + Downloads a NuGet package to a local folder without requiring a project file. + + + Package identifier (e.g. 'Newtonsoft.Json'). + + + Allows downloading from HTTP (non-HTTPS) package sources. + + + Directory where the package will be placed. Defaults to the current working directory. + + + Specifies one or more NuGet package sources to use. + + + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source + + + latest version + + + Unable to find a valid package version + \ No newline at end of file diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf index 4ab265ee684..7a54623b1e4 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf @@ -47,11 +47,6 @@ ID balíčku, který se má přidat - - Allows prerelease packages to be installed. - Umožňuje instalaci předběžných verzí balíčků. - - Version of the package to be added. Verze balíčku, který se má přidat @@ -403,6 +398,11 @@ NuGet vyžaduje zdroje HTTPS. Pokud chcete používat zdroje HTTP, musíte v sou Projekt {0} používá pro balíčky NuGet package.config, ale daný příkaz funguje jen s projekty odkazů na balíčky. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. Mapování zdroje balíčku je povolené, ale pro balíček {0} nebylo nalezeno žádné mapování. Další informace najdete na https://aka.ms/nuget/audit. @@ -890,6 +890,69 @@ Další informace najdete tady: https://docs.nuget.org/docs/reference/command-li {0} verze: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ Další informace najdete tady: https://docs.nuget.org/docs/reference/command-li Nejsou k dispozici žádné stabilní verze, nejlepší dostupná verze je {0}. Zvažte možnost přidat --prerelease. {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Jestliže balíček a verze už existuje, přeskočí se a bude se pokračovat dalším balíčkem k publikování, pokud nějaký existuje. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf index 694d7433b88..4df698a6e77 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf @@ -47,11 +47,6 @@ ID des hinzuzufügenden Pakets. - - Allows prerelease packages to be installed. - Ermöglicht die Installation von Paketen mit Vorabversionen. - - Version of the package to be added. Version des hinzuzufügenden Pakets. @@ -403,6 +398,11 @@ NuGet erfordert HTTPS-Quellen. Um HTTP-Quellen zu verwenden, müssen Sie „allo Das Projekt "{0}" verwendet "package.config" für NuGet-Pakete, der Befehl funktioniert jedoch nur mit Projekten mit Paketverweis. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. Die Paketquellzuordnung ist aktiviert, aber es wurde keine Zuordnung für das Paket {0} gefunden. Weitere Informationen finden Sie unter https://aka.ms/nuget/psm. @@ -890,6 +890,69 @@ Weitere Informationen finden Sie unter: https://docs.nuget.org/docs/reference/co {0} Version: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ Weitere Informationen finden Sie unter: https://docs.nuget.org/docs/reference/co Es sind keine stabilen Versionen verfügbar. Beste verfügbare Version: {0}. Erwägen Sie, die Option "--prerelease" hinzuzufügen. {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Wenn ein Paket und eine Version bereits vorhanden sind, überspringen Sie das Paket, und fahren Sie ggf. mit dem nächsten Paket im Pushvorgang fort. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf index df23ef140b8..3f8b89bf311 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf @@ -47,11 +47,6 @@ Id. del paquete que se va a agregar. - - Allows prerelease packages to be installed. - Permite que se instalen paquetes de versión preliminar. - - Version of the package to be added. Versión del paquete que se agregará. @@ -403,6 +398,11 @@ NuGet requiere orígenes HTTPS. Para usar orígenes HTTP, es necesario establece El proyecto "{0}" usa package.config para los paquetes NuGet, aunque el comando solo funciona con proyectos de referencia de paquetes. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. La asignación de origen del paquete está habilitada, pero no se encontró ninguna asignación para el paquete {0}. Consulte https://aka.ms/nuget/psm para obtener más información. @@ -890,6 +890,69 @@ Para obtener más información, visite https://docs.nuget.org/docs/reference/com {0} versión: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ Para obtener más información, visite https://docs.nuget.org/docs/reference/com No hay ninguna versión estable que se pueda seleccionar; {0} es la mejor opción disponible. Puede agregar la opción --prerelease. {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Si un paquete y una versión ya existen, omítalos y continúe con el siguiente en la inserción "push", en caso de haberlo. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf index 82f8fb778b1..8708500291d 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf @@ -47,11 +47,6 @@ ID du package à ajouter. - - Allows prerelease packages to be installed. - Permet d'installer les packages de préversion. - - Version of the package to be added. Version du package à ajouter. @@ -403,6 +398,11 @@ NuGet nécessite des sources HTTPS. Pour utiliser des sources HTTP, vous devez d Le projet '{0}' utilise package.config pour les packages NuGet, alors que la commande fonctionne uniquement avec les projets de référence de package. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. Le mappage de source du package est activé, mais aucun mappage pour le package {0} n’a été trouvé. Pour découvrir plus d’informations, consultez https://aka.ms/nuget/psm. @@ -890,6 +890,69 @@ Pour plus d'informations, visitez https://docs.nuget.org/docs/reference/command- {0} Version : {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ Pour plus d'informations, visitez https://docs.nuget.org/docs/reference/command- Aucune version stable n'est disponible, {0} est la meilleure version disponible. Ajoutez l'option --prerelease {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Si un package et une version existent déjà, ignorez-les et passez au package suivant dans l'envoi (push), le cas échéant. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf index e4eafe258ea..2a30afecf31 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf @@ -47,11 +47,6 @@ ID del pacchetto da aggiungere. - - Allows prerelease packages to be installed. - Consente l'installazione di pacchetti non definitivi. - - Version of the package to be added. Versione del pacchetto da aggiungere. @@ -403,6 +398,11 @@ NuGet richiede origini HTTPS. Per utilizzare origini HTTP, è necessario imposta Il progetto `{0}` usa package.config per i pacchetti NuGet, mentre il comando funziona solo con progetti di riferimento al pacchetto. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. Il mapping dell'origine del pacchetto è abilitato, ma non è stato trovato alcun mapping per il pacchetto {0}. Per altre informazioni, vedere https://aka.ms/nuget/psm. @@ -890,6 +890,69 @@ Per altre informazioni, vedere https://docs.nuget.org/docs/reference/command-lin Versione di {0}: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ Per altre informazioni, vedere https://docs.nuget.org/docs/reference/command-lin Non sono disponibili versioni stabili e {0} è la migliore versione disponibile. Provare ad aggiungere l'opzione --prerelease {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Se un pacchetto e una versione esistono già, ignorarli e continuare con il pacchetto successivo del push, se presente. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf index b59fae6677c..d8aa3b3fbef 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf @@ -47,11 +47,6 @@ 追加されるパッケージの ID。 - - Allows prerelease packages to be installed. - プレリリース パッケージのインストールを許可します。 - - Version of the package to be added. 追加されるパッケージのバージョン。 @@ -403,6 +398,11 @@ NuGet には HTTPS ソースが必要です。HTTP ソースを使用するに プロジェクト '{0}' では NuGet パッケージに package.config を使用しますが、コマンドはパッケージ参照プロジェクトでのみ動作します。 + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. パッケージ ソース マッピングは有効ですが、パッケージ {0} のマッピングが見つかりませんでした。詳細については、https://aka.ms/nuget/psm を参照してください。 @@ -890,6 +890,69 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r {0} バージョン: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r 利用可能な安定バージョンがありません。利用可能な中では {0} が最善です。--prerelease オプションを追加することをご検討ください {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. パッケージとバージョンが既に存在する場合は、それをスキップし、プッシュに次のパッケージがある場合は、それを続行します。 diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf index 49021d131a1..6ba03ee2e91 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf @@ -47,11 +47,6 @@ 추가할 패키지의 ID입니다. - - Allows prerelease packages to be installed. - 시험판 패키지를 설치할 수 있습니다. - - Version of the package to be added. 추가할 패키지의 버전입니다. @@ -403,6 +398,11 @@ NuGet에는 HTTPS 원본이 필요합니다. HTTP 원본을 사용하려면 NuGe `{0}` 프로젝트는 NuGet 패키지에 대한 package.config를 사용합니다. 명령은 패키지 참조 프로젝트에서만 작동합니다. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. 패키지 원본 매핑이 사용하도록 설정되어 있지만 패키지 {0}에 대한 매핑을 찾을 수 없습니다. 자세한 내용은 https://aka.ms/nuget/psm을 참조하세요. @@ -890,6 +890,69 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r {0} 버전: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r 사용할 수 있는 안정적인 버전이 없습니다. 사용할 수 있는 최상의 버전은 {0}입니다. --prerelease 옵션을 추가하는 것이 좋습니다. {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. 패키지 및 버전이 이미 있는 경우 건너뛴 후 푸시에서 다음 패키지(있는 경우)를 계속합니다. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf index 3973ba7aced..7f31ead7118 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf @@ -47,11 +47,6 @@ Identyfikator pakietu do dodania. - - Allows prerelease packages to be installed. - Zezwala na instalowanie pakietów wersji wstępnych. - - Version of the package to be added. Wersja pakietu do dodania. @@ -403,6 +398,11 @@ Menedżer NuGet wymaga źródeł HTTPS. Aby użyć źródeł HTTP, musisz wyraź Projekt „{0}” używa pliku package.config dla pakietów NuGet, podczas gdy polecenie działa tylko w przypadku projektów odwołania do pakietu. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. Mapowanie źródła pakietu jest włączone, ale nie znaleziono mapowania dla pakietu {0}. Aby uzyskać więcej informacji, zobacz https://aka.ms/nuget/psm. @@ -890,6 +890,69 @@ Aby uzyskać więcej informacji, odwiedź stronę https://docs.nuget.org/docs/re wersja{0} : {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ Aby uzyskać więcej informacji, odwiedź stronę https://docs.nuget.org/docs/re Brak dostępnych stabilnych wersji. Najlepsza dostępna opcja to {0}. Rozważ dodanie opcji --prerelease {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Jeśli pakiet i wersja już istnieje, pomiń to i kontynuuj przy użyciu następnego pakietu w ramach wypchnięcia, jeśli istnieje. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf index d39fd35c62e..2140037fd02 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf @@ -47,11 +47,6 @@ ID do pacote a ser adicionado. - - Allows prerelease packages to be installed. - Permite a instalação de pacotes de pré-lançamento. - - Version of the package to be added. Versão do pacote a ser adicionado. @@ -403,6 +398,11 @@ O NuGet requer fontes HTTPS. Para usar fontes HTTP, você deve definir explicita O projeto '{0}' usa package.config para pacotes do NuGet, enquanto o comando funciona somente com projetos de referência do pacote. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. O mapeamento de origem do pacote está habilitado, mas nenhum mapeamento para {0} foi encontrado. Consulte https://aka.ms/nuget/psm para obter mais informações. @@ -890,6 +890,69 @@ Para obter mais informações, acesse https://docs.nuget.org/docs/reference/comm {0} Versão: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ Para obter mais informações, acesse https://docs.nuget.org/docs/reference/comm Não há versões estáveis disponíveis. {0} é a melhor disponível. Considere a adição da opção --prerelease {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Se já existir um pacote e uma versão, ignore-os e continue com o próximo pacote no push, se houver. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf index 0c2ceb27912..65f1b305575 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf @@ -47,11 +47,6 @@ Идентификатор добавляемого пакета. - - Allows prerelease packages to be installed. - Разрешает установку пакетов предварительного выпуска. - - Version of the package to be added. Версия добавляемого пакета. @@ -403,6 +398,11 @@ NuGet requires HTTPS sources. To use HTTP sources, you must explicitly set 'allo В проекте "{0}" используется файл package.config для пакетов NuGet, а команда работает только с проектами, содержащими ссылки на пакеты. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. Сопоставление источника пакета включено, но сопоставление для пакета {0} не найдено. Дополнительные сведения см. на странице https://aka.ms/nuget/psm. @@ -890,6 +890,69 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r {0}, версия: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Нет доступных стабильных версий. Наилучшей из доступных является {0}. Попробуйте добавить параметр --prerelease. {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Если пакет и версия уже существуют, они пропускаются и осуществляется переход к следующему отправляемому пакету, если он есть. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf index 3fc1b6b3dc4..01e3e4eb894 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf @@ -47,11 +47,6 @@ Eklenecek paketin kimliği. - - Allows prerelease packages to be installed. - Ön sürüm paketlerinin yüklenmesini sağlar. - - Version of the package to be added. Eklenecek paket sürümü. @@ -404,6 +399,11 @@ NuGet için HTTPS kaynakları gereklidir. HTTP kaynaklarını kullanmak için Nu `{0}` projesi, NuGuet paketleri için package.config dosyasını kullanıyor fakat komut yalnızca paket başvurusu projeleriyle çalışır. + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. Paket kaynağı eşlemesi etkinleştirildi, ancak paket {0} için eşleme bulunamadı. Daha fazla bilgi için https://aka.ms/nuget/psm adresini ziyaret edin. @@ -891,6 +891,69 @@ Daha fazla bilgi için bkz. https://docs.nuget.org/docs/reference/command-line-r {0} Sürümü: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -990,6 +1053,11 @@ Daha fazla bilgi için bkz. https://docs.nuget.org/docs/reference/command-line-r Kullanılabilir kararlı bir sürüm olmadığından {0} en iyi seçenektir. --prerelease seçeneğini eklemeyi göz önünde bulundurun {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. Paket ve sürüm zaten mevcutsa bu paketi atlayın ve varsa gönderimde bulunan bir sonraki paketle devam edin. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf index d72727b90e9..b62044171b7 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf @@ -47,11 +47,6 @@ 要添加的包 ID。 - - Allows prerelease packages to be installed. - 允许安装预发行包。 - - Version of the package to be added. 要添加的包版本。 @@ -403,6 +398,11 @@ NuGet 需要 HTTPS 源。要使用 HTTP 源,必须在 NuGet.Config 文件中 项目“{0}”对 NuGet 包使用 package.config,但该命令仅适用于包引用项目。 + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. 已启用包源映射,但未找到包 {0} 的映射。有关详细信息,请参阅 https://aka.ms/nuget/psm。 @@ -890,6 +890,69 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r {0} 版本: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r 没有可用的稳定版本,{0} 是可用的最佳版本。请考虑添加 --prerelease 选项 {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. 如果包和版本已存在,则跳过它并继续推送中的下一个包(若有)。 diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf index 1e8ca73ed77..5dd4fe8bc5c 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf @@ -47,11 +47,6 @@ 要新增的套件之識別碼。 - - Allows prerelease packages to be installed. - 允許安裝發行前版本套件。 - - Version of the package to be added. 要新增的套件版本。 @@ -403,6 +398,11 @@ NuGet 需要 HTTPS 來源。您必須在 NuGet.Config 檔案中將 'allowInsecur 專案 `{0}` 會使用 NuGet 套件的 package.config,且命令僅能搭配套件參考專案使用。 + + Unable to find a valid package version + Unable to find a valid package version + + Package source mapping is enabled, but no mapping for package {0} was found. See https://aka.ms/nuget/psm for more information. 已啟用套件來源對應,但找不到套件 {0} 的對應。如需詳細資訊,請參閱 https://aka.ms/nuget/psm。 @@ -890,6 +890,69 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r {0} 版本: {1} {0}: App full name {1}: App version + + + Allows downloading from HTTP (non-HTTPS) package sources. + Allows downloading from HTTP (non-HTTPS) package sources. + + + + Skipping download. Package '{0}' version {1} already exists at '{2}'. + Skipping download. Package '{0}' version {1} already exists at '{2}'. + 0 - package id +1 - version +2 - directory path where the package was downloaded + + + Downloads a NuGet package to a local folder without requiring a project file. + Downloads a NuGet package to a local folder without requiring a project file. + + + + Package '{0}' ({1}) failed to download. + Package '{0}' ({1}) failed to download. + 0 - package id +1 - package version + + + latest version + latest version + + + + Directory where the package will be placed. Defaults to the current working directory. + Directory where the package will be placed. Defaults to the current working directory. + + + + Package identifier (e.g. 'Newtonsoft.Json'). + Package identifier (e.g. 'Newtonsoft.Json'). + + + + Specifies one or more NuGet package sources to use. + Specifies one or more NuGet package sources to use. + + + + Downloading package {0}, version {1}. + Downloading package {0}, version {1}. + 0- package id +1- package version + + + Package '{0}' ({1}) successfully downloaded to '{2}'. + Package '{0}' ({1}) successfully downloaded to '{2}'. + 0 - package id +1 - package version +2 - package download location + + + Package download failed for `{0} {1}` from source `{2}`. + Package download failed for `{0} {1}` from source `{2}`. + 0 package id +1 package version +2 package source Update referenced packages in a project or solution. @@ -989,6 +1052,11 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r 沒有可用的穩定版本,{0} 是最適用的版本。請考慮新增 --prerelease 選項 {0} - Package Version + + Allows prerelease packages to be installed. + Allows prerelease packages to be installed. + + If a package and version already exists, skip it and continue with the next package in the push, if any. 如果套件和版本已經存在,請予以跳過並繼續進行推送中的下一個套件 (如果有)。 diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs new file mode 100644 index 00000000000..c8ceeb8d334 --- /dev/null +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs @@ -0,0 +1,429 @@ +// 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.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NuGet.CommandLine.XPlat; +using NuGet.CommandLine.XPlat.Commands.Package; +using NuGet.CommandLine.XPlat.Commands.Package.PackageDownload; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Test.Utility; +using NuGet.Versioning; +using Xunit; + +namespace NuGet.CommandLine.Xplat.Tests; + +public class PackageDownloadRunnerTests +{ + public static IEnumerable PackageTestData() + { + // Basic stable explicit versions + yield return new object[] + { + new List<(string, string)> + { + ("Contoso.Core", "1.0.0"), + ("Contoso.Core", "1.1.0"), + ("Contoso.Core", "2.0.0-beta") + }, // source packages + new List<(string, string)> + { + ("Contoso.Core", "1.1.0"), + ("Contoso.Core", "1.0.0"), + }, // argument packages + false, // enablePrerelease + "myOutput", // output directory subpath + new List<(string, string)> + { + ("Contoso.Core", "1.1.0"), + ("Contoso.Core", "1.0.0") + } // expected + }; + + // Basic stable explicit versions with different ids + yield return new object[] + { + new List<(string, string)> + { + ("Contoso.Core", "1.0.0"), + ("Contoso.Core.Utils", "1.1.0"), + ("Contoso.Core", "2.0.0-beta") + }, // source packages + new List<(string, string)> + { + ("Contoso.Core.Utils", "1.1.0"), + ("Contoso.Core", "1.0.0"), + }, // argument packages + false, // enablePrerelease + "myOutput", // output directory subpath + new List<(string, string)> + { + ("Contoso.Core.Utils", "1.1.0"), + ("Contoso.Core", "1.0.0") + } // expected + }; + + // Mixed casing on the ID in the *download* argument + yield return new object[] + { + new List<(string, string)> + { + ("Contoso.Core", "1.1.0") + }, // source packages + new List<(string, string)> + { + ("contoso.core", "1.1.0") + }, // download argument + false, // enablePrerelease + "", // output directory subpath + new List<(string, string)> + { + ("Contoso.Core", "1.1.0") + }, // expected + }; + + // prerelease with IncludePrerelease == true + yield return new object[] + { + new List<(string, string)> + { + ("Contoso.Preview", "1.3.0"), + ("Contoso.Preview", "2.0.0-beta.2") + }, // source packages + new List<(string, string)> + { + ("Contoso.Preview", null), + }, // download argument + true, // enablePrerelease + "AnotherSubPath", // output directory subpath + new List<(string, string)> + { + ("Contoso.Preview", "2.0.0-beta.2") + }, // expected + }; + + // chose stable with IncludePrerelease == false + yield return new object[] + { + new List<(string, string)> + { + ("Contoso.Preview", "1.3.2"), + ("Contoso.Preview", "1.3.0"), + ("Contoso.Preview", "2.0.0-beta.2") + }, // source packages + new List<(string, string)> + { + ("Contoso.Preview", null) + }, // download argument + false, // enablePrerelease + "SubPath", // output directory subpath + new List <(string, string) > { + ("Contoso.Preview", "1.3.2") + } // expected + }; + } + + [Theory] + [MemberData(nameof(PackageTestData))] + public async Task RunAsync_ExplicitVersionFromLocalFolderSource_SucceedsAsync( + IReadOnlyList<(string, string)> sourcePackages, + IReadOnlyList<(string, string)> argumentPackages, + bool enablePrerelease, + string outputDirectorySubPath, + IReadOnlyList<(string, string)> expectedPackages) + { + // Arrange + using var context = new SimpleTestPathContext(); + var sourceDir = context.PackageSource; + var outputDir = Path.Combine(context.WorkingDirectory, outputDirectorySubPath); + + foreach (var (id, version) in sourcePackages) + { + await SimpleTestPackageUtility.CreateFullPackageAsync(sourceDir, id, version); + } + + var logger = new Mock(MockBehavior.Loose); + var settings = new Mock(MockBehavior.Loose); + List packages = []; + + foreach (var (id, version) in argumentPackages) + { + packages.Add(new PackageWithNuGetVersion + { + Id = id, + NuGetVersion = version == null ? null : NuGetVersion.Parse(version) + }); + } + + var args = new PackageDownloadArgs() + { + Packages = packages, + OutputDirectory = outputDir, + IncludePrerelease = enablePrerelease, + }; + + // Act + var result = await PackageDownloadRunner.RunAsync( + args, + logger.Object, + [new(sourceDir)], + settings.Object, + CancellationToken.None); + + // Assert + foreach (var (expectedId, expectedVersion) in expectedPackages) + { + result.Should().Be(PackageDownloadRunner.ExitCodeSuccess); + var installDir = Path.Combine(outputDir, expectedId.ToLowerInvariant(), expectedVersion); + Directory.Exists(installDir).Should().BeTrue(); + Directory.EnumerateFiles(installDir, "*.nupkg").Any().Should().BeTrue(); + File.Exists(Path.Combine(installDir, $"{expectedId.ToLowerInvariant()}.{expectedVersion}.nupkg")).Should().BeTrue(); + } + } + + [Fact] + public async Task RunAsync_NoVersion_PicksHighestAcrossMultipleSources() + { + // Arrange + using var contextA = new SimpleTestPathContext(); + using var contextB = new SimpleTestPathContext(); + var srcA = contextA.PackageSource; + var srcB = contextB.PackageSource; + var outputDir = contextA.WorkingDirectory; + + var id = "Contoso.Toolkit"; + await SimpleTestPackageUtility.CreateFullPackageAsync(srcA, id, "1.1.0"); + await SimpleTestPackageUtility.CreateFullPackageAsync(srcB, id, "1.2.0"); + + var logger = new Mock(MockBehavior.Loose); + var settings = new Mock(MockBehavior.Loose); + + var args = new PackageDownloadArgs() + { + Packages = [new PackageWithNuGetVersion { Id = id, NuGetVersion = null }], + OutputDirectory = outputDir, + }; + + // Act + var result = await PackageDownloadRunner.RunAsync( + args, + logger.Object, + [new PackageSource(srcA), new PackageSource(srcB)], + settings.Object, + CancellationToken.None); + + // Assert + result.Should().Be(PackageDownloadRunner.ExitCodeSuccess); + + var chosen = Path.Combine(outputDir, id.ToLowerInvariant(), "1.2.0"); + Directory.Exists(chosen).Should().BeTrue("should choose the highest version found across all sources"); + File.Exists(Path.Combine(chosen, $"{id.ToLowerInvariant()}.1.2.0.nupkg")).Should().BeTrue(); + } + + public static IEnumerable ShortCircuitsPackageData => + [ + // single package already installed + [ + new [] + { + ("Contoso.Utils", "3.4.5") // source packages + }, + new[] + { + new PackageWithNuGetVersion // argument packages + { + Id = "Contoso.Utils", + NuGetVersion = NuGetVersion.Parse("3.4.5") + } + }, + new [] + { + ("Contoso.Utils", "3.4.5") // expected packages + } + ], + + // multiple packages already installed + [ + new [] + { + ("Contoso.Utils", "3.4.5"), // source packages + ("Contoso.Core", "3.0.5") + }, + new [] + { + new PackageWithNuGetVersion // argument packages + { + Id = "Contoso.Utils", + NuGetVersion = NuGetVersion.Parse("3.4.5") + }, + new PackageWithNuGetVersion + { + Id = "Contoso.Core", + NuGetVersion = NuGetVersion.Parse("3.0.5") + } + }, + new [] + { + ("Contoso.Utils", "3.4.5"), // expected packages + ("Contoso.Core", "3.0.5") + } + ], + + // no version specified, but latest version already installed + [ + new [] + { + ("Contoso.Utils", "3.4.5"), // source packages + ("Contoso.Core", "3.0.5") + }, + new [] + { + new PackageWithNuGetVersion // argument packages + { + Id = "Contoso.Utils", + NuGetVersion = null + }, + new PackageWithNuGetVersion + { + Id = "Contoso.Core", + NuGetVersion = null + } + }, + new [] + { + ("Contoso.Utils", "3.4.5"), // expected packages + ("Contoso.Core", "3.0.5") + } + ]]; + + [Theory] + [MemberData(nameof(ShortCircuitsPackageData))] + internal async Task RunAsync_VersionAlreadyInstalled_ShortCircuitsAndSucceeds( + IReadOnlyList<(string, string)> sourcePackages, + PackageWithNuGetVersion[] packageDownloadArgs, + IReadOnlyList<(string, string)> expectedPackages) + { + // Arrange + using var context = new SimpleTestPathContext(); + var sourceDir = context.PackageSource; + var outputDir = context.WorkingDirectory; + + List packagesToInstall = []; + + foreach (var (id, version) in sourcePackages) + { + await SimpleTestPackageUtility.CreateFullPackageAsync(sourceDir, id, version); + } + + var logger = new Mock(MockBehavior.Loose); + var settings = new Mock(MockBehavior.Loose); + + // First run: install explicit version + var args1 = new PackageDownloadArgs() + { + Packages = packageDownloadArgs, + LogLevel = LogLevel.Verbose, + OutputDirectory = outputDir, + }; + + var first = await PackageDownloadRunner.RunAsync( + args1, + logger.Object, + [new PackageSource(sourceDir)], + settings.Object, + CancellationToken.None); + first.Should().Be(ExitCodes.Success); + + // Second run: should short-circuit because already installed + var args2 = new PackageDownloadArgs() + { + Packages = packageDownloadArgs, + OutputDirectory = outputDir, + }; + + // Act + var second = await PackageDownloadRunner.RunAsync( + args2, + logger.Object, + [new PackageSource(sourceDir)], + settings.Object, + CancellationToken.None); + + // Assert + foreach (var (id, version) in expectedPackages) + { + second.Should().Be(PackageDownloadRunner.ExitCodeSuccess); + var installDir = Path.Combine(outputDir, id.ToLowerInvariant(), version); + Directory.Exists(installDir).Should().BeTrue(); + File.Exists(Path.Combine(installDir, $"{id.ToLowerInvariant()}.{version}.nupkg")).Should().BeTrue(); + } + + logger.Verify(l => l.LogMinimal(It.Is(s => s.Contains("Skipping", StringComparison.OrdinalIgnoreCase))), Times.AtLeastOnce); + } + + [Fact] + public async Task RunAsync_WhenAllowInsecureConnectionsFalse_RejectsHttpSource() + { + // Arrange + using var context = new SimpleTestPathContext(); + var httpSource = "http://contoso/v3/index.json"; + var logger = new Mock(MockBehavior.Loose); + var settings = new Mock(MockBehavior.Loose); + + var args = new PackageDownloadArgs() + { + Packages = [new PackageWithNuGetVersion { Id = "Contoso.Lib", NuGetVersion = null }], + }; + + // Act + var result = await PackageDownloadRunner.RunAsync( + args, + logger.Object, + [new PackageSource(httpSource)], + settings.Object, + CancellationToken.None); + + // Assert + result.Should().Be(PackageDownloadRunner.ExitCodeError); + logger.Verify(l => l.LogError(It.Is(s => s.Contains(httpSource, StringComparison.OrdinalIgnoreCase))), Times.AtLeastOnce); + } + + [Fact] + public async Task RunAsync_PackageDoesNotExist_ReturnsError() + { + // Arrange + using var context = new SimpleTestPathContext(); + var id = "Missing.Package"; + var v = "9.9.9"; + var logger = new Mock(MockBehavior.Loose); + var settings = new Mock(MockBehavior.Loose); + + var args = new PackageDownloadArgs() + { + Packages = [new PackageWithNuGetVersion { Id = id, NuGetVersion = NuGetVersion.Parse(v) }], + OutputDirectory = context.WorkingDirectory, + }; + + // Act + var result = await PackageDownloadRunner.RunAsync( + args, + logger.Object, + [new PackageSource(context.PackageSource)], + settings.Object, + CancellationToken.None); + + // Assert + result.Should().Be(PackageDownloadRunner.ExitCodeError); + logger.Verify(l => l.LogError(It.IsAny()), Times.AtLeastOnce); + + File.Exists(Path.Combine(context.WorkingDirectory, $"{id.ToLowerInvariant()}.{v}.nupkg")) + .Should().BeFalse("Package does not exist in sources"); + } +} diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadCommandTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadCommandTests.cs new file mode 100644 index 00000000000..02075ea0132 --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadCommandTests.cs @@ -0,0 +1,288 @@ +// 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.CommandLine; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NuGet.CommandLine.XPlat.Commands.Package.PackageDownload; +using NuGet.Common; +using NuGet.Test.Utility; +using Xunit; + +namespace NuGet.CommandLine.Xplat.Tests.Commands.Package.Download +{ + public class PackageDownloadCommandArgsTest + { + [Fact] + public async Task NoArguments_ThrowsAnExceptionForMissingArg() + { + // Arrange + string args = "package download"; + + // Act & Assert + await Assert.ThrowsAsync(() => RunAsync(args)); + } + + [Fact] + public async Task WithSinglePackage_ShouldParsePackageId() + { + // Arrange + string args = "package download Contoso.Utils"; + + // Act + var result = await RunAsync(args); + + // Assert + result.Should().NotBeNull(); + result.Packages.Should().HaveCount(1); + result.Packages[0].Id.Should().Be("Contoso.Utils"); + result.Packages[0].NuGetVersion.Should().BeNull(); + } + + [Fact] + public async Task WithMultiplePackages_ShouldParseAllPackageIds() + { + // Arrange + string args = "package download Contoso.Utils Contoso.Framework"; + + // Act + var result = await RunAsync(args); + + // Assert + result.Should().NotBeNull(); + result.Packages.Should().HaveCount(2); + result.Packages[0].Id.Should().Be("Contoso.Utils"); + result.Packages[0].NuGetVersion.Should().BeNull(); + result.Packages[1].Id.Should().Be("Contoso.Framework"); + result.Packages[1].NuGetVersion.Should().BeNull(); + } + + [Fact] + public async Task WithPackageAndVersion_ShouldParsePackageWithVersion() + { + // Arrange + string args = "package download Contoso.Utils@2.1.0"; + + // Act + var result = await RunAsync(args); + + // Assert + result.Should().NotBeNull(); + result.Packages.Should().HaveCount(1); + result.Packages[0].Id.Should().Be("Contoso.Utils"); + result.Packages[0].NuGetVersion.Should().NotBeNull(); + result.Packages[0].NuGetVersion!.ToString().Should().Be("2.1.0"); + } + + [Fact] + public async Task WithPackageAndVersionRange_ShouldFail() + { + // Arrange + string args = "package download Contoso.Utils@[2.0.0,3.0.0)"; + + // Act & Assert + await Assert.ThrowsAsync(() => RunAsync(args)); + } + + [Fact] + public async Task WithMixedPackages_ShouldParseMixedPackagesCorrectly() + { + // Arrange + string args = "package download Contoso.Utils@2.1.0 Contoso.Framework"; + + // Act + var result = await RunAsync(args); + + // Assert + result.Should().NotBeNull(); + result.Packages.Should().HaveCount(2); + result.Packages[0].Id.Should().Be("Contoso.Utils"); + result.Packages[0].NuGetVersion.ToString().Should().Be("2.1.0"); + result.Packages[1].Id.Should().Be("Contoso.Framework"); + result.Packages[1].NuGetVersion.Should().BeNull(); + } + + [Fact] + public async Task WithOutputAndConfig_ShouldBindPaths() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + string outDir = Path.Combine(pathContext.WorkingDirectory, "out"); + string cfg = Path.Combine(pathContext.WorkingDirectory, "nuget.config"); + + string args = $"package download Contoso --output \"{outDir}\" --configfile \"{cfg}\""; + + // Act + var result = await RunAsync(args); + + // Assert + result.Should().NotBeNull(); + result.OutputDirectory.Should().Be(outDir); + result.ConfigFile.Should().Be(cfg); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WithInteractiveOption_ShouldSetCorrectInteractiveValue(bool value) + { + // Arrange + string args = $"package download Contoso --interactive:{value}"; + + // Act + var result = await RunAsync(args); + + // Assert + result.Should().NotBeNull(); + result.Interactive.Should().Be(value); + } + + [Fact] + public async Task WithBooleanFlags_ShouldSetAllFlagsTrue() + { + // Arrange + string args = "package download contoso --prerelease --allow-insecure-connections --interactive"; + + // Act + var result = await RunAsync(args); + + // Assert + result.IncludePrerelease.Should().BeTrue(); + result.AllowInsecureConnections.Should().BeTrue(); + result.Interactive.Should().BeTrue(); + } + + [Theory] + [InlineData("--verbosity quiet", LogLevel.Warning)] + [InlineData("--verbosity q", LogLevel.Warning)] + [InlineData("--verbosity minimal", LogLevel.Minimal)] + [InlineData("--verbosity m", LogLevel.Minimal)] + [InlineData("--verbosity normal", LogLevel.Information)] + [InlineData("--verbosity n", LogLevel.Information)] + [InlineData("--verbosity detailed", LogLevel.Verbose)] + [InlineData("--verbosity d", LogLevel.Verbose)] + [InlineData("--verbosity diagnostic", LogLevel.Debug)] + [InlineData("--verbosity diag", LogLevel.Debug)] + [InlineData("-v quiet", LogLevel.Warning)] + [InlineData("-v minimal", LogLevel.Minimal)] + [InlineData("-v normal", LogLevel.Information)] + [InlineData("-v detailed", LogLevel.Verbose)] + [InlineData("-v diagnostic", LogLevel.Debug)] + public async Task WithVerbosityOption_ShouldSetCorrectLogLevel(string verbosityArgs, LogLevel expectedLogLevel) + { + // Arrange + string args = $"package download contoso {verbosityArgs}"; + + // Act + var result = await RunAsync(args); + + // Assert + result.Should().NotBeNull(); + result.LogLevel.Should().Be(expectedLogLevel); + } + + [Fact] + public void WithInvalidVersion_ShouldHaveParseErrors() + { + // Arrange + string args = "package download Contoso.Utils@invalid-version"; + + // Act + var result = Parse(args); + + // Assert + result.Errors.Count.Should().BeGreaterThan(0); + } + + [Fact] + public void WithEmptyVersionAfterAt_ShouldHaveParseErrors() + { + // Arrange + string args = "package download Contoso.Utils@"; + + // Act + var result = Parse(args); + + // Assert + result.Errors.Count.Should().BeGreaterThan(0); + } + + [Fact] + public async Task WithAllOptions_ShouldParseAllOptionsCorrectly() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + string outDir = Path.Combine(pathContext.WorkingDirectory, "out"); + string cfg = Path.Combine(pathContext.WorkingDirectory, "nuget.config"); + + string args = $"package download Contoso.Utils@2.1.0 --output \"{outDir}\" --configfile \"{cfg}\" --prerelease --allow-insecure-connections --source s1 --source s2 --verbosity detailed --interactive"; + + // Act + var result = await RunAsync(args); + + // Assert + result.Should().NotBeNull(); + result.Packages.Should().HaveCount(1); + result.Packages[0].Id.Should().Be("Contoso.Utils"); + result.Packages[0].NuGetVersion.ToString().Should().Be("2.1.0"); + result.OutputDirectory.Should().Be(outDir); + result.ConfigFile.Should().Be(cfg); + result.IncludePrerelease.Should().BeTrue(); + result.AllowInsecureConnections.Should().BeTrue(); + result.Sources.Should().ContainInOrder("s1", "s2"); + result.LogLevel.Should().Be(LogLevel.Verbose); + result.Interactive.Should().BeTrue(); + } + + private ParseResult Parse(string commandLine, Func> action = null) + { + RootCommand rootCommand = new RootCommand(); + + var packageCommand = new Command("package"); + rootCommand.Subcommands.Add(packageCommand); + + // Simulate SDK-provided interactive option + var interactiveOption = new Option("--interactive"); + + if (action == null) + { + action = (_, _) => throw new NotImplementedException("No action provided for command execution."); + } + + PackageDownloadCommand.Register(packageCommand, interactiveOption, action); + + var parser = rootCommand.Parse(commandLine); + return parser; + } + + private async Task RunAsync(string commandLine) + { + PackageDownloadArgs commandArgs = null; + + var parseResult = Parse(commandLine, (args, cancellationToken) => + { + commandArgs = args; + return Task.FromResult(0); + }); + + using var output = new StringWriter(); + var commandLineConfiguration = new InvocationConfiguration + { + Output = output, + Error = output, + }; + + await parseResult.InvokeAsync(commandLineConfiguration); + + if (commandArgs is null) + { + throw new InvalidOperationException("Command arguments were not set during command execution. Output:" + output.ToString()); + } + + return commandArgs; + } + } +} diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs new file mode 100644 index 00000000000..c4426e176ed --- /dev/null +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs @@ -0,0 +1,204 @@ +// 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.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NuGet.CommandLine.XPlat; +using NuGet.CommandLine.XPlat.Commands.Package; +using NuGet.CommandLine.XPlat.Commands.Package.PackageDownload; +using NuGet.Configuration; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Test.Utility; +using NuGet.Versioning; +using Test.Utility; +using Xunit; + +namespace NuGet.CommandLine.Xplat.Tests; + +public class PackageDownloadRunnerTests +{ + public sealed record SourceSpec(string Name, params string[] Versions); + + public sealed record ResolveScenario( + string Id, + IReadOnlyList Sources, + string RequestedVersion, + bool IncludePrerelease, + string ExpectedSource, // null => expect no resolution + string ExpectedVersion); // null => expect no resolution + + public sealed class ResolveVersionData : TheoryData + { + public ResolveVersionData() + { + // Exact version at first source -> returns early + Add(new ResolveScenario( + Id: "Contoso", + Sources: [new SourceSpec("src", "1.2.3")], + RequestedVersion: "1.2.3", + IncludePrerelease: false, + ExpectedSource: "src", + ExpectedVersion: "1.2.3")); + + // Exact version missing in first, present in second -> pick second + Add(new ResolveScenario( + "Contoso", + [new SourceSpec("srcA", "1.0.0"), new SourceSpec("srcB", "1.2.3")], + "1.2.3", + false, + "srcB", + "1.2.3")); + + // No version; exclude prerelease -> highest stable across sources + Add(new ResolveScenario( + "Contoso", + [new SourceSpec("srcA", "1.0.0", "1.5.0-alpha"), new SourceSpec("srcB", "2.0.0")], + null, + false, + "srcB", + "2.0.0")); + + // No version; include prerelease -> highest including prerelease + Add(new ResolveScenario( + "Contoso", + [new SourceSpec("srcA", "2.0.0"), new SourceSpec("srcB", "2.1.0-rc.1")], + null, + true, + "srcB", + "2.1.0-rc.1")); + + // No matches anywhere -> null + Add(new ResolveScenario( + "Contoso", + [new SourceSpec("emptyA"), new SourceSpec("emptyB")], + null, + false, + ExpectedSource: null, + ExpectedVersion: null)); + + // Exact version requested, but not found anywhere -> null + Add(new ResolveScenario( + "Contoso", + [new SourceSpec("src", "1.0.0")], + "1.2.3", + false, + null, + null)); + } + } + + [Theory] + [ClassData(typeof(ResolveVersionData))] + public async Task ResolvePackageDownloadVersion_Scenarios(ResolveScenario scenario) + { + // Arrange + using var context = new SimpleTestPathContext(); + + // Create source folders and packages + var repos = new List(); + foreach (var src in scenario.Sources) + { + var folder = Path.Combine(context.WorkingDirectory, src.Name); + Directory.CreateDirectory(folder); + + foreach (var version in src.Versions) + { + await SimpleTestPackageUtility.CreateFullPackageAsync(folder, scenario.Id, version); + } + + repos.Add(Repository.Factory.GetCoreV3(new PackageSource(folder))); + } + + var logger = new Mock(MockBehavior.Loose); + var package = new PackageWithNuGetVersion + { + Id = scenario.Id, + NuGetVersion = scenario.RequestedVersion is null ? null : NuGetVersion.Parse(scenario.RequestedVersion) + }; + + // Act + (NuGetVersion resolved, SourceRepository resolvedRepo) = await PackageDownloadRunner.ResolvePackageDownloadVersion( + package, + [.. repos], + new SourceCacheContext(), + logger.Object, + includePrerelease: scenario.IncludePrerelease, + CancellationToken.None); + + // Assert + if (scenario.ExpectedVersion is null) + { + Assert.Null(resolved); + Assert.Null(resolvedRepo); + return; + } + + Assert.Equal(new NuGetVersion(scenario.ExpectedVersion), resolved); + Assert.NotNull(resolvedRepo); + + var expectedPath = Path.Combine(context.WorkingDirectory, scenario.ExpectedSource!); + Assert.Equal(expectedPath, resolvedRepo.PackageSource.Source); + } + + [Theory] + [InlineData("1.0.0", true)] // exact version specified -> should find even if unlisted + [InlineData(null, false)] // no exact version -> should return null when unlisted + public async Task ResolvePackageDownloadVersion_UnlistedPackage_BehavesAsExpected(string requestedVersion, bool expectFound) + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource); + + var cache = new SourceCacheContext(); + var logger = new Mock(); + var token = CancellationToken.None; + + var sourceRepo = Repository.Factory.GetCoreV3(mockServer.ServiceIndexUri); + + // Create the package and unlist it on the mock server. + var id = "Contoso.unlisted"; + var createdVersion = "1.0.0"; + var pkg = new SimpleTestPackageContext(id, createdVersion); + await SimpleTestPackageUtility.CreatePackagesAsync(pathContext.PackageSource, pkg); + mockServer.UnlistedPackages.Add(pkg.Identity); + + mockServer.Start(); + + var packageWithNuGetVersion = new PackageWithNuGetVersion + { + Id = id, + NuGetVersion = requestedVersion is null ? null : NuGetVersion.Parse(requestedVersion) + }; + + // Act + (NuGetVersion foundVersion, SourceRepository foundRepo) = await PackageDownloadRunner.ResolvePackageDownloadVersion( + packageWithNuGetVersion, + [sourceRepo], + cache, + logger.Object, + includePrerelease: false, + token); + + mockServer.Stop(); + + // Assert + if (expectFound) + { + foundVersion.Should().NotBeNull(); + foundVersion.OriginalVersion.Should().Be(requestedVersion); + foundRepo.Should().NotBeNull(); + foundRepo.PackageSource.Name.Should().Be(sourceRepo.PackageSource.Name); + } + else + { + foundVersion.Should().BeNull(); + foundRepo.Should().BeNull(); + } + } + +} From 2d404a2bf1f6f9095dd2442be78e941b366951c8 Mon Sep 17 00:00:00 2001 From: Nigusu Solomon Yenework <59111203+Nigusu-Allehu@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:59:43 -0700 Subject: [PATCH 2/2] [Feature] Add package source mapping to `package download` (#6885) --- .../Package/Download/PackageDownloadRunner.cs | 121 ++++++++- .../Strings.Designer.cs | 9 + .../NuGet.CommandLine.XPlat/Strings.resx | 5 + .../xlf/Strings.cs.xlf | 6 + .../xlf/Strings.de.xlf | 6 + .../xlf/Strings.es.xlf | 6 + .../xlf/Strings.fr.xlf | 6 + .../xlf/Strings.it.xlf | 6 + .../xlf/Strings.ja.xlf | 6 + .../xlf/Strings.ko.xlf | 6 + .../xlf/Strings.pl.xlf | 6 + .../xlf/Strings.pt-BR.xlf | 6 + .../xlf/Strings.ru.xlf | 6 + .../xlf/Strings.tr.xlf | 6 + .../xlf/Strings.zh-Hans.xlf | 6 + .../xlf/Strings.zh-Hant.xlf | 6 + .../Download/PackageDownloadRunnerTests.cs | 232 ++++++++++++++++++ .../Download/PackageDownloadRunnerTests.cs | 1 - .../FileSystemBackedV3MockServer.cs | 16 +- 19 files changed, 448 insertions(+), 14 deletions(-) diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs index f6a93645940..940821c4ad3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs @@ -1,6 +1,8 @@ // 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. +#nullable enable + using System; using System.Collections.Generic; using System.Globalization; @@ -48,18 +50,32 @@ public static async Task RunAsync(PackageDownloadArgs args, CancellationTok public static async Task RunAsync(PackageDownloadArgs args, ILoggerWithColor logger, IReadOnlyList packageSources, ISettings settings, CancellationToken token) { - // Check for insecure sources - if (DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger)) + bool hasSourcesArg = args.Sources?.Count > 0; + PackageSourceMapping? packageSourceMapping = null; + if (!hasSourcesArg) + { + packageSourceMapping = PackageSourceMapping.GetPackageSourceMapping(settings); + } + + bool ignorePackageSourceMapping = + hasSourcesArg + || packageSourceMapping is null + || !packageSourceMapping.IsEnabled; + + // When package source mapping is disabled, validate all configured sources upfront. + // When mapping is enabled, source validation is deferred to the per-package resolution step, + // since each package may map to a different subset of sources. + if (ignorePackageSourceMapping && DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger)) { return ExitCodeError; } string outputDirectory = args.OutputDirectory ?? Directory.GetCurrentDirectory(); var cache = new SourceCacheContext(); - IReadOnlyList sourceRepositories = GetSourceRepositories(packageSources); + IReadOnlyList allRepositories = GetSourceRepositories(packageSources); bool downloadedAllSuccessfully = true; - foreach (var package in args.Packages) + foreach (var package in args.Packages ?? []) { logger.LogMinimal(string.Format( CultureInfo.CurrentCulture, @@ -67,9 +83,29 @@ public static async Task RunAsync(PackageDownloadArgs args, ILoggerWithColo package.Id, string.IsNullOrEmpty(package.NuGetVersion?.ToNormalizedString()) ? Strings.PackageDownloadCommand_LatestVersion : package.NuGetVersion.ToNormalizedString())); + // Resolve which repositories to use for this package + IReadOnlyList sourceRepositories; + if (ignorePackageSourceMapping) + { + sourceRepositories = allRepositories; + } + else + { + if (!TryGetRepositoriesForPackage( + package.Id, + args, + packageSourceMapping!, + allRepositories, + logger, + out sourceRepositories)) + { + return ExitCodeError; + } + } + try { - (NuGetVersion version, SourceRepository downloadRepository) = + (NuGetVersion? version, SourceRepository? downloadRepository) = await ResolvePackageDownloadVersion( package, sourceRepositories, @@ -88,7 +124,7 @@ await ResolvePackageDownloadVersion( bool success = await DownloadPackageAsync( package.Id, version, - downloadRepository, + downloadRepository!, cache, settings, outputDirectory, @@ -127,16 +163,16 @@ await ResolvePackageDownloadVersion( return downloadedAllSuccessfully ? ExitCodeSuccess : ExitCodeError; } - internal static async Task<(NuGetVersion, SourceRepository)> ResolvePackageDownloadVersion( + internal static async Task<(NuGetVersion?, SourceRepository?)> ResolvePackageDownloadVersion( PackageWithNuGetVersion packageWithNuGetVersion, - IEnumerable sourceRepositories, + IReadOnlyList sourceRepositories, SourceCacheContext cache, ILoggerWithColor logger, bool includePrerelease, CancellationToken token) { - NuGetVersion versionToDownload = null; - SourceRepository downloadSourceRepository = null; + NuGetVersion? versionToDownload = null; + SourceRepository? downloadSourceRepository = null; bool versionSpecified = packageWithNuGetVersion.NuGetVersion != null; foreach (var repo in sourceRepositories) @@ -188,6 +224,69 @@ await ResolvePackageDownloadVersion( return (versionToDownload, downloadSourceRepository); } + /// + /// Builds the set of SourceRepository objects to use for a given package, + /// applying package source mapping + /// validating HTTP usage only on the *effective* sources. + /// + private static bool TryGetRepositoriesForPackage( + string packageId, + PackageDownloadArgs args, + PackageSourceMapping packageSourceMapping, + IReadOnlyList allRepos, + ILoggerWithColor logger, + out IReadOnlyList repositories) + { + var mappedNames = packageSourceMapping.GetConfiguredPackageSources(packageId); + + // Only validate insecure sources when mapping produced something + if (mappedNames.Count > 0) + { + var mappedRepos = new List(mappedNames.Count); + foreach (var mappedName in mappedNames) + { + SourceRepository? repo = null; + for (int i = 0; i < allRepos.Count; i++) + { + if (string.Equals(allRepos[i].PackageSource.Name, mappedName, StringComparison.OrdinalIgnoreCase)) + { + repo = allRepos[i]; + break; + } + } + + if (repo != null) + { + mappedRepos.Add(repo); + } + else + { + logger.LogVerbose( + string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_PackageSourceMapping_NoSuchSource, + mappedName, + packageId)); + } + } + + if (DetectAndReportInsecureSources(args.AllowInsecureConnections, mappedRepos.Select(repo => repo.PackageSource), logger)) + { + repositories = []; + return false; + } + + repositories = mappedRepos; + return true; + } + else + { + // No mapping for this package: fall back to all sources + repositories = allRepos; + return true; + } + } + private static async Task DownloadPackageAsync( string id, NuGetVersion version, @@ -239,7 +338,7 @@ private static async Task DownloadPackageAsync( return success; } - private static IReadOnlyList GetPackageSources(IList sources, IPackageSourceProvider sourceProvider) + private static IReadOnlyList GetPackageSources(IList? sources, IPackageSourceProvider sourceProvider) { IEnumerable configuredSources = sourceProvider.LoadPackageSources() .Where(s => s.IsEnabled); diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs index 33be7a2c176..a34243fa494 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.Designer.cs @@ -1661,6 +1661,15 @@ internal static string PackageDownloadCommand_PackageIdDescription { } } + /// + /// Looks up a localized string similar to The mapped source '{0}' for package '{1}' was not found among the configured sources.. + /// + internal static string PackageDownloadCommand_PackageSourceMapping_NoSuchSource { + get { + return ResourceManager.GetString("PackageDownloadCommand_PackageSourceMapping_NoSuchSource", resourceCulture); + } + } + /// /// Looks up a localized string similar to Specifies one or more NuGet package sources to use.. /// diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx index 76c0f91d27b..3a9b3a4c269 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/Strings.resx @@ -1157,4 +1157,9 @@ Do not translate "PackageVersion" Unable to find a valid package version + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + \ No newline at end of file diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf index 7a54623b1e4..9c8611810b4 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.cs.xlf @@ -929,6 +929,12 @@ Další informace najdete tady: https://docs.nuget.org/docs/reference/command-li Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf index 4df698a6e77..1561190af34 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.de.xlf @@ -929,6 +929,12 @@ Weitere Informationen finden Sie unter: https://docs.nuget.org/docs/reference/co Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf index 3f8b89bf311..10590d156e3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.es.xlf @@ -929,6 +929,12 @@ Para obtener más información, visite https://docs.nuget.org/docs/reference/com Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf index 8708500291d..e9ac4d4a071 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.fr.xlf @@ -929,6 +929,12 @@ Pour plus d'informations, visitez https://docs.nuget.org/docs/reference/command- Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf index 2a30afecf31..5a4c20f00a3 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.it.xlf @@ -929,6 +929,12 @@ Per altre informazioni, vedere https://docs.nuget.org/docs/reference/command-lin Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf index d8aa3b3fbef..2b2dfa9f833 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ja.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf index 6ba03ee2e91..809170e5b82 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ko.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf index 7f31ead7118..1ba1ff39581 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pl.xlf @@ -929,6 +929,12 @@ Aby uzyskać więcej informacji, odwiedź stronę https://docs.nuget.org/docs/re Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf index 2140037fd02..3ac34709fcf 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.pt-BR.xlf @@ -929,6 +929,12 @@ Para obter mais informações, acesse https://docs.nuget.org/docs/reference/comm Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf index 65f1b305575..62e0a674d4d 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.ru.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf index 01e3e4eb894..7e53ce27174 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.tr.xlf @@ -930,6 +930,12 @@ Daha fazla bilgi için bkz. https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf index b62044171b7..6a6b143d1e5 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hans.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf index 5dd4fe8bc5c..41c7e5b851b 100644 --- a/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf +++ b/src/NuGet.Core/NuGet.CommandLine.XPlat/xlf/Strings.zh-Hant.xlf @@ -929,6 +929,12 @@ For more information, visit https://docs.nuget.org/docs/reference/command-line-r Package identifier (e.g. 'Newtonsoft.Json'). + + The mapped source '{0}' for package '{1}' was not found among the configured sources. + The mapped source '{0}' for package '{1}' was not found among the configured sources. + 0 - package source name +1 - package name + Specifies one or more NuGet package sources to use. Specifies one or more NuGet package sources to use. diff --git a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs index c8ceeb8d334..aa3186220db 100644 --- a/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -16,6 +17,7 @@ using NuGet.Configuration; using NuGet.Test.Utility; using NuGet.Versioning; +using Test.Utility; using Xunit; namespace NuGet.CommandLine.Xplat.Tests; @@ -426,4 +428,234 @@ [new PackageSource(context.PackageSource)], File.Exists(Path.Combine(context.WorkingDirectory, $"{id.ToLowerInvariant()}.{v}.nupkg")) .Should().BeFalse("Package does not exist in sources"); } + + public static IEnumerable Cases() + { + // Parameters: + // A-packages, B-packages, sourceMappings, sourcesArgs, downloadId, downloadVersion, + // allowInsecureConnections, expectSuccess, expectedInstalled + + // --source specified, mapping ignored, package only in A -> success + yield return new object[] + { + new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A + new List<(string,string)>(), // B + new List<(string,string)> { ("B", "Contoso.*") }, // mapping ignored + new List { "A" }, // --source A + "Contoso.Lib", "1.0.0", // downloadId, downloadVersion + true, // allow insecure + true, // expect success + ("Contoso.Lib", "1.0.0") // expectedInstalled + }; + + // no --source, mapping -> B, package only in B -> success + yield return new object[] + { + new List<(string,string)>(), // A + new List<(string,string)> { ("Contoso.Mapped", "2.0.0") }, // B + new List<(string,string)> { ("B", "Contoso.*") }, // mapping -> B + null, // no --source + "Contoso.Mapped", "2.0.0", // downloadId, downloadVersion + true, // allow insecure + true, // expect success + ("Contoso.Mapped", "2.0.0") // expectedInstalled + }; + + // no --source, mapping -> A, package only in B -> fail + yield return new object[] + { + new List<(string,string)>(), // A + new List<(string,string)> { ("Contoso.Mapped", "2.0.0") }, + new List<(string,string)> { ("A", "Contoso.*") }, // mapped to A + null, + "Contoso.Mapped", "2.0.0", + true, + false, + null! + }; + + // --source specified, no source mapping with an insecure source + yield return new object[] + { + new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A + new List<(string,string)>(), + new List<(string,string)> { ("A", "Contoso.*") }, + new List { "A" }, // --source + "Contoso.Lib", "1.0.0", + false, // allow insecure connections false / not set to true + false, + null! + }; + + // no --source, mapping -> B, allow insecure not enabled -> fail + yield return new object[] + { + new List<(string,string)>(), // A + new List<(string,string)> { ("Contoso.Mapped", "1.0.0") }, + new List<(string,string)> { ("B", "Contoso.*") }, + null, + "Contoso.Mapped", "1.0.0", + false, // allow insecure connections false / not set to true + false, + null! + }; + } + + [Theory] + [MemberData(nameof(Cases))] + public async Task RunAsync_WithSourceMapping( + IReadOnlyList<(string id, string version)> sourceAPackages, + IReadOnlyList<(string id, string version)> sourceBPackages, + IReadOnlyList<(string source, string pattern)> sourceMappings, + IReadOnlyList sourcesArgs, + string downloadId, + string downloadVersion, + bool allowInsecureConnections, + bool expectSuccess, + (string id, string version)? expectedInstalled) + { + // Arrange + using var context = new SimpleTestPathContext(); + string srcADirectory = Path.Combine(context.PackageSource, "SourceA"); + string srcBDirectory = Path.Combine(context.PackageSource, "SourceB"); + + using var serverA = new FileSystemBackedV3MockServer(srcADirectory); + using var serverB = new FileSystemBackedV3MockServer(srcBDirectory); + + foreach (var (id, ver) in sourceAPackages) + { + await SimpleTestPackageUtility.CreateFullPackageAsync(srcADirectory, id, ver); + } + + foreach (var (id, ver) in sourceBPackages) + { + await SimpleTestPackageUtility.CreateFullPackageAsync(srcBDirectory, id, ver); + } + + serverA.Start(); + serverB.Start(); + + // sources + context.Settings.AddSource("A", serverA.ServiceIndexUri); + context.Settings.AddSource("B", serverB.ServiceIndexUri); + + // mapping + foreach (var (src, pattern) in sourceMappings) + { + context.Settings.AddPackageSourceMapping(src, pattern); + } + + var settings = Settings.LoadSettingsGivenConfigPaths([context.Settings.ConfigPath]); + + var packageSources = new List + { + new(serverA.ServiceIndexUri, "A"), + new(serverB.ServiceIndexUri, "B") + }; + + // args + var args = new PackageDownloadArgs + { + Packages = + [ + new PackageWithNuGetVersion + { + Id = downloadId, + NuGetVersion = downloadVersion is null ? null : NuGetVersion.Parse(downloadVersion) + } + ], + OutputDirectory = context.WorkingDirectory, + AllowInsecureConnections = allowInsecureConnections, + Sources = sourcesArgs == null ? [] : sourcesArgs.ToList() + }; + + string capturedLogs = string.Empty; + var logger = new Mock(MockBehavior.Loose); + logger + .Setup(l => l.LogError(It.IsAny())) + .Callback(msg => capturedLogs += msg + Environment.NewLine); + + // Act + var exit = await PackageDownloadRunner.RunAsync( + args, + logger.Object, + packageSources, + settings, + CancellationToken.None); + + serverA.Stop(); + serverB.Stop(); + + // Assert + if (expectSuccess) + { + exit.Should().Be(PackageDownloadRunner.ExitCodeSuccess, because: capturedLogs); + expectedInstalled.Should().NotBeNull(); + + var (expId, expVer) = expectedInstalled!.Value; + var installDir = Path.Combine(context.WorkingDirectory, expId.ToLowerInvariant(), expVer); + Directory.Exists(installDir).Should().BeTrue(); + File.Exists(Path.Combine(installDir, $"{expId.ToLowerInvariant()}.{expVer}.nupkg")).Should().BeTrue(); + } + else + { + exit.Should().Be(PackageDownloadRunner.ExitCodeError); + } + } + + [Fact] + public async Task RunAsync_WhenMappedSourceMissing_LogsVerbose() + { + // Arrange + using var context = new SimpleTestPathContext(); + string srcADirectory = Path.Combine(context.PackageSource, "SourceA"); + using var serverA = new FileSystemBackedV3MockServer(srcADirectory); + + await SimpleTestPackageUtility.CreateFullPackageAsync(srcADirectory, "Contoso.Utils", "1.0.0"); + context.Settings.AddSource("A", serverA.ServiceIndexUri); + + // Map the package to a NON-EXISTING source name "MissingSource" + context.Settings.AddPackageSourceMapping("MissingSource", "Contoso.*"); + var settings = Settings.LoadSettingsGivenConfigPaths([context.Settings.ConfigPath]); + + var args = new PackageDownloadArgs + { + Packages = + [ + new PackageWithNuGetVersion + { + Id = "Contoso.Utils", + NuGetVersion = NuGetVersion.Parse("1.0.0") + } + ], + OutputDirectory = context.WorkingDirectory, + AllowInsecureConnections = true, + Sources = [] + }; + string capturedVerbose = string.Empty; + var logger = new Mock(MockBehavior.Loose); + logger.Setup(l => l.LogVerbose(It.IsAny())) + .Callback(msg => capturedVerbose += msg + Environment.NewLine); + + // Act + serverA.Start(); + + var exit = await PackageDownloadRunner.RunAsync( + args, + logger.Object, + [new(serverA.ServiceIndexUri, "A")], + settings, + CancellationToken.None); + + serverA.Stop(); + + // Assert + var expected = string.Format( + CultureInfo.CurrentCulture, + Strings.PackageDownloadCommand_PackageSourceMapping_NoSuchSource, + "MissingSource", + "Contoso.Utils"); + + capturedVerbose.Should().Contain(expected, because: "a package mapped to a non-existing source name must log a verbose diagnostic"); + } } diff --git a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs index c4426e176ed..4348aa6bcf0 100644 --- a/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs +++ b/test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs @@ -200,5 +200,4 @@ public async Task ResolvePackageDownloadVersion_UnlistedPackage_BehavesAsExpecte foundRepo.Should().BeNull(); } } - } diff --git a/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs b/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs index 125ab70933e..ae0a128b4fc 100644 --- a/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs +++ b/test/TestUtilities/Test.Utility/FileSystemBackedV3MockServer.cs @@ -99,9 +99,21 @@ private Action ServerHandlerV3(HttpListenerRequest request } else if (path.StartsWith("/flat/") && path.EndsWith(".nupkg")) { - var file = new FileInfo(Path.Combine(_packageDirectory, parts.Last())); + var requestedFileName = parts.Last(); + var directory = new DirectoryInfo(_packageDirectory); + FileInfo file = null; - if (file.Exists) + // Scan for file and ignore case to make sure this works in linux too + foreach (var candidate in directory.EnumerateFiles("*.nupkg", SearchOption.TopDirectoryOnly)) + { + if (string.Equals(candidate.Name, requestedFileName, StringComparison.OrdinalIgnoreCase)) + { + file = candidate; + break; + } + } + + if (file != null && file.Exists) { return new Action(response => {