Skip to content

Commit 9b76d50

Browse files
committed
Add package source mapping
1 parent aac5448 commit 9b76d50

File tree

3 files changed

+247
-17
lines changed

3 files changed

+247
-17
lines changed

src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Package/Download/PackageDownloadRunner.cs

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,18 @@ public static async Task<int> RunAsync(PackageDownloadArgs args, CancellationTok
4848

4949
public static async Task<int> RunAsync(PackageDownloadArgs args, ILoggerWithColor logger, IReadOnlyList<PackageSource> packageSources, ISettings settings, CancellationToken token)
5050
{
51-
// Check for insecure sources
52-
if (DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger))
51+
// If --source is explicitly provided, validate those sources up front.
52+
if (args.Sources != null && args.Sources.Count > 0 && DetectAndReportInsecureSources(args.AllowInsecureConnections, packageSources, logger))
5353
{
5454
return ExitCodeError;
5555
}
5656

5757
string outputDirectory = args.OutputDirectory ?? Directory.GetCurrentDirectory();
5858
var cache = new SourceCacheContext();
59-
IReadOnlyList<SourceRepository> sourceRepositories = GetSourceRepositories(packageSources);
59+
60+
IReadOnlyDictionary<string, SourceRepository> sourceRepositoriesMap = GetSourceRepositories(packageSources);
61+
var packageSourceMapping = PackageSourceMapping.GetPackageSourceMapping(settings);
62+
6063
bool downloadedAllSuccessfully = true;
6164

6265
foreach (var package in args.Packages)
@@ -67,6 +70,19 @@ public static async Task<int> RunAsync(PackageDownloadArgs args, ILoggerWithColo
6770
package.Id,
6871
string.IsNullOrEmpty(package.NuGetVersion?.ToNormalizedString()) ? Strings.PackageDownloadCommand_LatestVersion : package.NuGetVersion.ToNormalizedString()));
6972

73+
// Resolve which repositories to use for this package
74+
if (!TryGetRepositoriesForPackage(
75+
package.Id,
76+
args,
77+
packageSources,
78+
packageSourceMapping,
79+
sourceRepositoriesMap,
80+
logger,
81+
out List<SourceRepository> sourceRepositories))
82+
{
83+
return ExitCodeError;
84+
}
85+
7086
try
7187
{
7288
(NuGetVersion version, SourceRepository downloadRepository) =
@@ -86,14 +102,14 @@ await ResolvePackageDownloadVersion(
86102
}
87103

88104
bool success = await DownloadPackageAsync(
89-
package.Id,
90-
version,
91-
downloadRepository,
92-
cache,
93-
settings,
94-
outputDirectory,
95-
logger,
96-
token);
105+
package.Id,
106+
version,
107+
downloadRepository,
108+
cache,
109+
settings,
110+
outputDirectory,
111+
logger,
112+
token);
97113

98114
if (success)
99115
{
@@ -188,6 +204,50 @@ await ResolvePackageDownloadVersion(
188204
return (versionToDownload, downloadSourceRepository);
189205
}
190206

207+
/// <summary>
208+
/// Builds the set of SourceRepository objects to use for a given package,
209+
/// applying package source mapping (when --source is not provided) and
210+
/// validating HTTP usage only on the *effective* sources.
211+
/// </summary>
212+
private static bool TryGetRepositoriesForPackage(
213+
string packageId,
214+
PackageDownloadArgs args,
215+
IReadOnlyList<PackageSource> allPackageSources,
216+
PackageSourceMapping packageSourceMapping,
217+
IReadOnlyDictionary<string, SourceRepository> sourceRepositoriesMap,
218+
ILoggerWithColor logger,
219+
out List<SourceRepository> repositories)
220+
{
221+
repositories = new List<SourceRepository>();
222+
223+
if (args.Sources != null && args.Sources.Count > 0)
224+
{
225+
// --source specified: use the provided sources
226+
foreach (var source in allPackageSources)
227+
{
228+
repositories.Add(sourceRepositoriesMap[source.Name]);
229+
}
230+
231+
return true;
232+
}
233+
234+
// No --source: use package source mapping
235+
var mappedSourceNames = packageSourceMapping.GetConfiguredPackageSources(packageId);
236+
var mappedSources = allPackageSources.Where(ps => mappedSourceNames.Contains(ps.Name, StringComparer.OrdinalIgnoreCase)).ToList();
237+
if (DetectAndReportInsecureSources(args.AllowInsecureConnections, mappedSources, logger))
238+
{
239+
return false;
240+
}
241+
242+
foreach (var name in mappedSourceNames)
243+
{
244+
repositories.Add(sourceRepositoriesMap[name]);
245+
}
246+
247+
return true;
248+
}
249+
250+
191251
private static async Task<bool> DownloadPackageAsync(
192252
string id,
193253
NuGetVersion version,
@@ -270,13 +330,13 @@ private static bool DetectAndReportInsecureSources(
270330
return false;
271331
}
272332

273-
private static IReadOnlyList<SourceRepository> GetSourceRepositories(IReadOnlyList<PackageSource> packageSources)
333+
private static IReadOnlyDictionary<string, SourceRepository> GetSourceRepositories(IReadOnlyList<PackageSource> packageSources)
274334
{
275335
IEnumerable<Lazy<INuGetResourceProvider>> providers = Repository.Provider.GetCoreV3();
276-
List<SourceRepository> sourceRepositories = [];
336+
Dictionary<string, SourceRepository> sourceRepositories = [];
277337
foreach (var source in packageSources)
278338
{
279-
sourceRepositories.Add(Repository.CreateSource(providers, source, FeedType.Undefined));
339+
sourceRepositories[source.Name] = Repository.CreateSource(providers, source, FeedType.Undefined);
280340
}
281341

282342
return sourceRepositories;

test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/Package/Download/PackageDownloadRunnerTests.cs

Lines changed: 173 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
namespace NuGet.CommandLine.Xplat.Tests;
5-
64
using System;
75
using System.Collections.Generic;
86
using System.IO;
97
using System.Linq;
108
using System.Threading;
119
using System.Threading.Tasks;
1210
using FluentAssertions;
11+
using global::Test.Utility;
1312
using Moq;
1413
using NuGet.CommandLine.XPlat;
1514
using NuGet.CommandLine.XPlat.Commands.Package;
@@ -20,6 +19,8 @@ namespace NuGet.CommandLine.Xplat.Tests;
2019
using NuGet.Versioning;
2120
using Xunit;
2221

22+
namespace NuGet.CommandLine.Xplat.Tests;
23+
2324
public class PackageDownloadRunnerTests
2425
{
2526
public static IEnumerable<object[]> PackageTestData()
@@ -280,4 +281,174 @@ [new PackageSource(context.PackageSource)],
280281
File.Exists(Path.Combine(context.WorkingDirectory, $"{id.ToLowerInvariant()}.{v}.nupkg"))
281282
.Should().BeFalse("Package does not exist in sources");
282283
}
284+
285+
public static IEnumerable<object[]> Cases()
286+
{
287+
// Parameters:
288+
// A-packages, B-packages, sourceMappings, sourcesArgs, downloadId, downloadVersion,
289+
// allowInsecureConnections, expectSuccess, expectedInstalled
290+
291+
// --source specified, mapping ignored, package only in A -> success
292+
yield return new object[]
293+
{
294+
new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A
295+
new List<(string,string)>(), // B
296+
new List<(string,string)> { ("B", "Contoso.*") }, // mapping ignored
297+
new List<string> { "A" }, // --source A
298+
"Contoso.Lib", "1.0.0", // downloadId, downloadVersion
299+
true, // allow insecure
300+
true, // expect success
301+
("Contoso.Lib", "1.0.0") // expectedInstalled
302+
};
303+
304+
// no --source, mapping -> B, package only in B -> success
305+
yield return new object[]
306+
{
307+
new List<(string,string)>(), // A
308+
new List<(string,string)> { ("Contoso.Mapped", "2.0.0") }, // B
309+
new List<(string,string)> { ("B", "Contoso.*") }, // mapping -> B
310+
null, // no --source
311+
"Contoso.Mapped", "2.0.0", // downloadId, downloadVersion
312+
true, // allow insecure
313+
true, // expect success
314+
("Contoso.Mapped", "2.0.0") // expectedInstalled
315+
};
316+
317+
// no --source, mapping -> A, package only in B -> fail
318+
yield return new object[]
319+
{
320+
new List<(string,string)>(), // A
321+
new List<(string,string)> { ("Contoso.Mapped", "2.0.0") },
322+
new List<(string,string)> { ("A", "Contoso.*") }, // mapped to A
323+
null,
324+
"Contoso.Mapped", "2.0.0",
325+
true,
326+
false,
327+
null!
328+
};
329+
330+
// --source specified, no source mapping with an insecure source
331+
yield return new object[]
332+
{
333+
new List<(string,string)> { ("Contoso.Lib", "1.0.0") }, // A
334+
new List<(string,string)>(),
335+
new List<(string,string)> { ("A", "Contoso.*") },
336+
new List<string> { "A" }, // --source
337+
"Contoso.Lib", "1.0.0",
338+
false, // allow insecure connections false / not set to true
339+
false,
340+
null!
341+
};
342+
343+
// no --source, mapping -> B, allow insecure not enabled -> fail
344+
yield return new object[]
345+
{
346+
new List<(string,string)>(), // A
347+
new List<(string,string)> { ("Contoso.Mapped", "1.0.0") },
348+
new List<(string,string)> { ("B", "Contoso.*") },
349+
null,
350+
"Contoso.Mapped", "1.0.0",
351+
false, // allow insecure connections false / not set to true
352+
false,
353+
null!
354+
};
355+
}
356+
357+
[Theory]
358+
[MemberData(nameof(Cases))]
359+
public async Task RunAsync_WithSourceMapping_ListDriven_UsingCleanSetup(
360+
IReadOnlyList<(string id, string version)> sourceAPackages,
361+
IReadOnlyList<(string id, string version)> sourceBPackages,
362+
IReadOnlyList<(string source, string pattern)> sourceMappings,
363+
IReadOnlyList<string> sourcesArgs,
364+
string downloadId,
365+
string downloadVersion,
366+
bool allowInsecureConnections,
367+
bool expectSuccess,
368+
(string id, string version)? expectedInstalled)
369+
{
370+
// Arrange
371+
using var context = new SimpleTestPathContext();
372+
string srcADirectory = Path.Combine(context.PackageSource, "SourceA");
373+
string srcBDirectory = Path.Combine(context.PackageSource, "SourceB");
374+
375+
using var serverA = new FileSystemBackedV3MockServer(srcADirectory);
376+
using var serverB = new FileSystemBackedV3MockServer(srcBDirectory);
377+
378+
foreach (var (id, ver) in sourceAPackages)
379+
{
380+
await SimpleTestPackageUtility.CreateFullPackageAsync(srcADirectory, id, ver);
381+
}
382+
383+
foreach (var (id, ver) in sourceBPackages)
384+
{
385+
await SimpleTestPackageUtility.CreateFullPackageAsync(srcBDirectory, id, ver);
386+
}
387+
388+
serverA.Start();
389+
serverB.Start();
390+
391+
// sources
392+
context.Settings.AddSource("A", serverA.ServiceIndexUri);
393+
context.Settings.AddSource("B", serverB.ServiceIndexUri);
394+
395+
// mapping
396+
foreach (var (src, pattern) in sourceMappings)
397+
{
398+
context.Settings.AddPackageSourceMapping(src, pattern);
399+
}
400+
401+
var settings = Settings.LoadSettingsGivenConfigPaths([context.Settings.ConfigPath]);
402+
403+
var packageSources = new List<PackageSource>
404+
{
405+
new(serverA.ServiceIndexUri, "A"),
406+
new(serverB.ServiceIndexUri, "B")
407+
};
408+
409+
// args
410+
var args = new PackageDownloadArgs
411+
{
412+
Packages =
413+
[
414+
new PackageWithNuGetVersion
415+
{
416+
Id = downloadId,
417+
NuGetVersion = downloadVersion is null ? null : NuGetVersion.Parse(downloadVersion)
418+
}
419+
],
420+
OutputDirectory = context.WorkingDirectory,
421+
AllowInsecureConnections = allowInsecureConnections,
422+
Sources = sourcesArgs == null ? [] : sourcesArgs.ToList()
423+
};
424+
425+
var logger = new Mock<ILoggerWithColor>(MockBehavior.Loose);
426+
427+
// Act
428+
var exit = await PackageDownloadRunner.RunAsync(
429+
args,
430+
logger.Object,
431+
packageSources,
432+
settings,
433+
CancellationToken.None);
434+
435+
serverA.Stop();
436+
serverB.Stop();
437+
438+
// Assert
439+
if (expectSuccess)
440+
{
441+
exit.Should().Be(PackageDownloadRunner.ExitCodeSuccess);
442+
expectedInstalled.Should().NotBeNull();
443+
444+
var (expId, expVer) = expectedInstalled!.Value;
445+
var installDir = Path.Combine(context.WorkingDirectory, expId.ToLowerInvariant(), expVer);
446+
Directory.Exists(installDir).Should().BeTrue();
447+
File.Exists(Path.Combine(installDir, $"{expId.ToLowerInvariant()}.{expVer}.nupkg")).Should().BeTrue();
448+
}
449+
else
450+
{
451+
exit.Should().Be(PackageDownloadRunner.ExitCodeError);
452+
}
453+
}
283454
}

test/NuGet.Core.Tests/NuGet.CommandLine.Xplat.Tests/Commands/Package/Download/PackageDownloadRunnerTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,4 @@ public async Task ResolvePackageDownloadVersion_UnlistedPackage_BehavesAsExpecte
200200
foundRepo.Should().BeNull();
201201
}
202202
}
203-
204203
}

0 commit comments

Comments
 (0)