-
Notifications
You must be signed in to change notification settings - Fork 851
Expand file tree
/
Copy pathRestoreCommand.cs
More file actions
313 lines (270 loc) · 11.5 KB
/
RestoreCommand.cs
File metadata and controls
313 lines (270 loc) · 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
using System.CommandLine;
using NuGet.Commands;
using NuGet.Configuration;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.ProjectModel;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
namespace Aspire.Managed.NuGet.Commands;
/// <summary>
/// Restore command - restores NuGet packages without requiring a .csproj file.
/// Uses NuGet's RestoreRunner to produce a project.assets.json file.
/// </summary>
public static class RestoreCommand
{
/// <summary>
/// Creates the restore command.
/// </summary>
public static Command Create()
{
var command = new Command("restore", "Restore NuGet packages");
var packageOption = new Option<string[]>("--package", "-p")
{
Description = "Package reference as 'PackageId,Version' (can specify multiple)",
Required = true,
AllowMultipleArgumentsPerToken = true
};
command.Options.Add(packageOption);
var frameworkOption = new Option<string>("--framework", "-f")
{
Description = "Target framework (default: net10.0)",
DefaultValueFactory = _ => "net10.0"
};
command.Options.Add(frameworkOption);
var outputOption = new Option<string>("--output", "-o")
{
Description = "Output directory for project.assets.json",
DefaultValueFactory = _ => "./obj"
};
command.Options.Add(outputOption);
var packagesDirOption = new Option<string?>("--packages-dir")
{
Description = "NuGet packages directory"
};
command.Options.Add(packagesDirOption);
var sourceOption = new Option<string[]>("--source")
{
Description = "NuGet feed URL (can specify multiple)",
DefaultValueFactory = _ => Array.Empty<string>(),
AllowMultipleArgumentsPerToken = true
};
command.Options.Add(sourceOption);
var configOption = new Option<string?>("--nuget-config")
{
Description = "Path to nuget.config file"
};
command.Options.Add(configOption);
var workingDirOption = new Option<string?>("--working-dir", "-w")
{
Description = "Working directory for nuget.config discovery"
};
command.Options.Add(workingDirOption);
var noNugetOrgOption = new Option<bool>("--no-nuget-org")
{
Description = "Don't add nuget.org as fallback source"
};
command.Options.Add(noNugetOrgOption);
var verboseOption = new Option<bool>("--verbose")
{
Description = "Enable verbose output"
};
command.Options.Add(verboseOption);
command.SetAction(async (parseResult, ct) =>
{
// Note: ?? is used for null-safety even with DefaultValueFactory because GetValue returns T?
var packageArgs = parseResult.GetValue(packageOption) ?? [];
var framework = parseResult.GetValue(frameworkOption)!;
var output = parseResult.GetValue(outputOption)!;
var packagesDir = parseResult.GetValue(packagesDirOption);
var sources = parseResult.GetValue(sourceOption) ?? [];
var nugetConfigPath = parseResult.GetValue(configOption);
var workingDir = parseResult.GetValue(workingDirOption);
var noNugetOrg = parseResult.GetValue(noNugetOrgOption);
var verbose = parseResult.GetValue(verboseOption);
// Parse packages (format: PackageId,Version)
var packages = new List<(string Id, string Version)>();
foreach (var pkgArg in packageArgs)
{
if (verbose)
{
Console.WriteLine($"Parsing package argument: {pkgArg}");
}
var parts = pkgArg.Split(',', 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
Console.Error.WriteLine($"Error: Package argument '{pkgArg}' must be in format 'PackageId,Version'");
return 1;
}
packages.Add((parts[0], parts[1]));
}
return await ExecuteRestoreAsync(packages, framework, output, packagesDir, sources, nugetConfigPath, workingDir, noNugetOrg, verbose).ConfigureAwait(false);
});
return command;
}
private static async Task<int> ExecuteRestoreAsync(
List<(string Id, string Version)> packages,
string framework,
string output,
string? packagesDir,
string[] cliSources,
string? nugetConfigPath,
string? workingDir,
bool noNugetOrg,
bool verbose)
{
var outputPath = Path.GetFullPath(output);
Directory.CreateDirectory(outputPath);
var logger = new NuGetLogger(verbose);
try
{
// Load NuGet settings once — handles working dir, config file, and machine-wide settings.
var settings = Settings.LoadDefaultSettings(workingDir, nugetConfigPath, new XPlatMachineWideSetting());
if (verbose)
{
Console.WriteLine($"Restoring {packages.Count} packages for {framework}");
Console.WriteLine($"Output: {outputPath}");
Console.WriteLine($"Packages: {packagesDir}");
if (workingDir is not null)
{
Console.WriteLine($"Working dir: {workingDir}");
}
if (nugetConfigPath is not null)
{
Console.WriteLine($"NuGet config: {nugetConfigPath}");
}
}
// Resolve the default packages path from settings (env var, config, or ~/.nuget/packages).
// If --packages-dir is provided, RestoreArgs.GlobalPackagesFolder overrides this.
var defaultPackagesPath = SettingsUtility.GetGlobalPackagesFolder(settings);
// Resolve package sources using NuGet's PackageSourceProvider
var packageSources = ResolvePackageSources(settings, cliSources, noNugetOrg);
var nugetFramework = NuGetFramework.Parse(framework);
// Build PackageSpec and DependencyGraphSpec
var packageSpec = BuildPackageSpec(packages, nugetFramework, outputPath, defaultPackagesPath, packageSources, settings);
var dgSpec = new DependencyGraphSpec();
dgSpec.AddProject(packageSpec);
dgSpec.AddRestore(packageSpec.RestoreMetadata.ProjectUniqueName);
// Pass settings to the provider so it reuses our pre-loaded settings
var providerCache = new RestoreCommandProvidersCache();
var dgProvider = new DependencyGraphSpecRequestProvider(providerCache, dgSpec, settings);
// Run restore — let NuGet handle source credentials, parallel execution, etc.
using var cacheContext = new SourceCacheContext();
var restoreArgs = new RestoreArgs
{
CacheContext = cacheContext,
Log = logger,
PreLoadedRequestProviders = [dgProvider],
DisableParallel = Environment.ProcessorCount == 1,
AllowNoOp = false,
GlobalPackagesFolder = packagesDir,
MachineWideSettings = new XPlatMachineWideSetting(),
};
var results = await RestoreRunner.RunAsync(restoreArgs).ConfigureAwait(false);
var summary = results.Count > 0 ? results[0] : null;
if (summary is null)
{
Console.Error.WriteLine("Error: Restore returned no results");
return 1;
}
if (!summary.Success)
{
var errors = string.Join(Environment.NewLine,
summary.Errors?.Select(e => e.Message) ?? ["Unknown error"]);
Console.Error.WriteLine($"Error: Restore failed: {errors}");
return 1;
}
var assetsPath = Path.Combine(outputPath, "project.assets.json");
Console.WriteLine($"Restore completed successfully");
Console.WriteLine($"Assets file: {assetsPath}");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
if (verbose)
{
Console.Error.WriteLine(ex.ToString());
}
return 1;
}
}
private static List<PackageSource> ResolvePackageSources(ISettings settings, string[] cliSources, bool noNugetOrg)
{
// Load enabled sources from NuGet config
var provider = new PackageSourceProvider(settings);
var sources = provider.LoadPackageSources().Where(s => s.IsEnabled).ToList();
// Append CLI --source values (matching NuGet's behavior of merging, not replacing)
foreach (var cliSource in cliSources)
{
if (!sources.Any(s => s.Source.Equals(cliSource, StringComparison.OrdinalIgnoreCase)))
{
sources.Add(new PackageSource(cliSource));
}
}
// Add nuget.org as a fallback source unless opted out
if (!noNugetOrg)
{
const string nugetOrgUrl = "https://api.nuget.org/v3/index.json";
if (!sources.Any(s => s.Source.Equals(nugetOrgUrl, StringComparison.OrdinalIgnoreCase)))
{
Console.WriteLine("Note: Adding nuget.org as fallback package source. Use --no-nuget-org to disable.");
sources.Add(new PackageSource(nugetOrgUrl, "nuget.org"));
}
}
return sources;
}
private static PackageSpec BuildPackageSpec(
List<(string Id, string Version)> packages,
NuGetFramework framework,
string outputPath,
string packagesPath,
List<PackageSource> sources,
ISettings settings)
{
var projectName = "AspireRestore";
var projectPath = Path.Combine(outputPath, "project.json");
var tfmShort = framework.GetShortFolderName();
var dependencies = packages.Select(p => new LibraryDependency
{
LibraryRange = new LibraryRange(
p.Id,
VersionRange.Parse(p.Version),
LibraryDependencyTarget.Package)
}).ToImmutableArray();
var tfInfo = new TargetFrameworkInformation
{
FrameworkName = framework,
TargetAlias = tfmShort,
Dependencies = dependencies
};
var restoreMetadata = new ProjectRestoreMetadata
{
ProjectUniqueName = projectName,
ProjectName = projectName,
ProjectPath = projectPath,
ProjectStyle = ProjectStyle.PackageReference,
OutputPath = outputPath,
PackagesPath = packagesPath,
OriginalTargetFrameworks = [tfmShort],
ConfigFilePaths = settings.GetConfigFilePaths().ToList(),
};
foreach (var source in sources)
{
restoreMetadata.Sources.Add(source);
}
restoreMetadata.TargetFrameworks.Add(new ProjectRestoreMetadataFrameworkInfo(framework)
{
TargetAlias = tfmShort
});
return new PackageSpec([tfInfo])
{
Name = projectName,
FilePath = projectPath,
RestoreMetadata = restoreMetadata,
};
}
}