Skip to content

Commit c9c2ea5

Browse files
committed
Compose NuGet configuration for integration restores
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d38799a7-a99c-48c5-b45b-9d6594dd1830
1 parent 80781cd commit c9c2ea5

5 files changed

Lines changed: 778 additions & 47 deletions

File tree

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Xml;
5+
using System.Xml.Linq;
6+
7+
namespace Aspire.Cli.Packaging;
8+
9+
internal static class NuGetConfigComposer
10+
{
11+
private static readonly HashSet<string> s_knownItemNames = new(StringComparer.OrdinalIgnoreCase)
12+
{
13+
"add",
14+
"author",
15+
"certificate",
16+
"owners",
17+
"package",
18+
"repository",
19+
"fileCert",
20+
"storeCert"
21+
};
22+
23+
/// <summary>
24+
/// Composes NuGet configuration files ordered from highest to lowest precedence.
25+
/// </summary>
26+
public static async Task<XDocument> ComposeAsync(
27+
IReadOnlyList<string> configPaths,
28+
CancellationToken cancellationToken)
29+
{
30+
var result = new XDocument(new XElement("configuration"));
31+
32+
foreach (var configPath in configPaths.Reverse())
33+
{
34+
if (string.IsNullOrWhiteSpace(configPath) || !File.Exists(configPath))
35+
{
36+
continue;
37+
}
38+
39+
var fullConfigPath = Path.GetFullPath(configPath);
40+
var document = await LoadAsync(fullConfigPath, cancellationToken).ConfigureAwait(false);
41+
if (document.Root is not { } configuration ||
42+
!string.Equals(configuration.Name.LocalName, "configuration", StringComparison.OrdinalIgnoreCase))
43+
{
44+
throw new InvalidDataException($"NuGet configuration '{fullConfigPath}' does not have a <configuration> root element.");
45+
}
46+
47+
foreach (var section in configuration.Elements())
48+
{
49+
MergeSection(result.Root!, section, Path.GetDirectoryName(fullConfigPath)!);
50+
}
51+
}
52+
53+
return result;
54+
}
55+
56+
private static async Task<XDocument> LoadAsync(string path, CancellationToken cancellationToken)
57+
{
58+
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
59+
using var reader = XmlReader.Create(stream, new XmlReaderSettings
60+
{
61+
Async = true,
62+
DtdProcessing = DtdProcessing.Prohibit,
63+
XmlResolver = null
64+
});
65+
66+
return await XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken).ConfigureAwait(false);
67+
}
68+
69+
private static void MergeSection(XElement configuration, XElement incomingSection, string originDirectory)
70+
{
71+
// NuGet applies configuration files from lowest to highest precedence. A <clear /> removes
72+
// inherited items and keyed items replace the inherited item with the same key.
73+
// https://learn.microsoft.com/nuget/consume-packages/configuring-nuget-behavior#how-settings-are-applied
74+
var section = configuration.Elements()
75+
.FirstOrDefault(element => string.Equals(element.Name.LocalName, incomingSection.Name.LocalName, StringComparison.OrdinalIgnoreCase));
76+
if (section is null)
77+
{
78+
section = new XElement(incomingSection.Name);
79+
configuration.Add(section);
80+
}
81+
82+
foreach (var attribute in incomingSection.Attributes())
83+
{
84+
section.SetAttributeValue(attribute.Name, attribute.Value);
85+
}
86+
87+
if (string.Equals(incomingSection.Name.LocalName, "minPublishAgeExceptions", StringComparison.OrdinalIgnoreCase) &&
88+
!incomingSection.Elements().Any(element => string.Equals(element.Name.LocalName, "clear", StringComparison.OrdinalIgnoreCase)))
89+
{
90+
section.RemoveNodes();
91+
}
92+
93+
foreach (var incomingItem in incomingSection.Elements())
94+
{
95+
if (string.Equals(incomingItem.Name.LocalName, "clear", StringComparison.OrdinalIgnoreCase))
96+
{
97+
section.RemoveNodes();
98+
section.Add(new XElement(incomingItem.Name));
99+
continue;
100+
}
101+
102+
var item = new XElement(incomingItem);
103+
ApplyEnvironmentTransforms(item);
104+
ResolveRelativePaths(incomingSection.Name.LocalName, item, originDirectory);
105+
106+
var existingItem = section.Elements()
107+
.FirstOrDefault(candidate => AreEquivalentItems(incomingSection.Name.LocalName, candidate, item));
108+
if (existingItem is null)
109+
{
110+
section.Add(item);
111+
}
112+
else if (IsUnknownItem(incomingSection.Name.LocalName, item))
113+
{
114+
MergeUnknownItem(existingItem, item);
115+
}
116+
else
117+
{
118+
existingItem.ReplaceWith(item);
119+
}
120+
}
121+
}
122+
123+
private static bool AreEquivalentItems(string sectionName, XElement first, XElement second)
124+
{
125+
if (!string.Equals(first.Name.LocalName, second.Name.LocalName, StringComparison.OrdinalIgnoreCase))
126+
{
127+
return false;
128+
}
129+
130+
var itemName = first.Name.LocalName;
131+
if (string.Equals(itemName, "add", StringComparison.OrdinalIgnoreCase))
132+
{
133+
return string.Equals(GetAttributeValue(first, "key"), GetAttributeValue(second, "key"), StringComparison.Ordinal);
134+
}
135+
136+
if (string.Equals(sectionName, "packageSourceCredentials", StringComparison.OrdinalIgnoreCase))
137+
{
138+
return string.Equals(itemName, second.Name.LocalName, StringComparison.Ordinal);
139+
}
140+
141+
if (string.Equals(sectionName, "packageSourceMapping", StringComparison.OrdinalIgnoreCase) &&
142+
string.Equals(itemName, "packageSource", StringComparison.OrdinalIgnoreCase))
143+
{
144+
return string.Equals(GetAttributeValue(first, "key"), GetAttributeValue(second, "key"), StringComparison.OrdinalIgnoreCase);
145+
}
146+
147+
if (string.Equals(sectionName, "minPublishAgeExceptions", StringComparison.OrdinalIgnoreCase) &&
148+
string.Equals(itemName, "package", StringComparison.OrdinalIgnoreCase))
149+
{
150+
return string.Equals(GetAttributeValue(first, "pattern"), GetAttributeValue(second, "pattern"), StringComparison.OrdinalIgnoreCase);
151+
}
152+
153+
var identityAttribute =
154+
string.Equals(itemName, "author", StringComparison.OrdinalIgnoreCase) ||
155+
string.Equals(itemName, "repository", StringComparison.OrdinalIgnoreCase)
156+
? "name"
157+
: string.Equals(itemName, "certificate", StringComparison.OrdinalIgnoreCase)
158+
? "fingerprint"
159+
: string.Equals(itemName, "fileCert", StringComparison.OrdinalIgnoreCase) ||
160+
string.Equals(itemName, "storeCert", StringComparison.OrdinalIgnoreCase)
161+
? "packageSource"
162+
: null;
163+
164+
return identityAttribute is null ||
165+
string.Equals(GetAttributeValue(first, identityAttribute), GetAttributeValue(second, identityAttribute), StringComparison.OrdinalIgnoreCase);
166+
}
167+
168+
private static bool IsUnknownItem(string sectionName, XElement item)
169+
{
170+
var itemName = item.Name.LocalName;
171+
if (string.Equals(sectionName, "packageSourceCredentials", StringComparison.OrdinalIgnoreCase) ||
172+
string.Equals(sectionName, "packageSources", StringComparison.OrdinalIgnoreCase) ||
173+
string.Equals(sectionName, "auditSources", StringComparison.OrdinalIgnoreCase) ||
174+
string.Equals(sectionName, "packageSourceMapping", StringComparison.OrdinalIgnoreCase))
175+
{
176+
return false;
177+
}
178+
179+
return !s_knownItemNames.Contains(itemName);
180+
}
181+
182+
private static void MergeUnknownItem(XElement existingItem, XElement incomingItem)
183+
{
184+
foreach (var attribute in incomingItem.Attributes())
185+
{
186+
existingItem.SetAttributeValue(attribute.Name, attribute.Value);
187+
}
188+
189+
foreach (var incomingChild in incomingItem.Elements())
190+
{
191+
var existingChild = existingItem.Elements()
192+
.FirstOrDefault(candidate => AreEquivalentItems(existingItem.Name.LocalName, candidate, incomingChild));
193+
if (existingChild is null)
194+
{
195+
existingItem.Add(new XElement(incomingChild));
196+
}
197+
else
198+
{
199+
existingChild.ReplaceWith(new XElement(incomingChild));
200+
}
201+
}
202+
}
203+
204+
private static void ResolveRelativePaths(string sectionName, XElement item, string originDirectory)
205+
{
206+
if (string.Equals(item.Name.LocalName, "add", StringComparison.OrdinalIgnoreCase))
207+
{
208+
var key = GetAttributeValue(item, "key");
209+
var isPathSetting =
210+
string.Equals(sectionName, "packageSources", StringComparison.OrdinalIgnoreCase) ||
211+
string.Equals(sectionName, "auditSources", StringComparison.OrdinalIgnoreCase) ||
212+
string.Equals(sectionName, "fallbackPackageFolders", StringComparison.OrdinalIgnoreCase) ||
213+
string.Equals(sectionName, "config", StringComparison.OrdinalIgnoreCase) &&
214+
(string.Equals(key, "globalPackagesFolder", StringComparison.OrdinalIgnoreCase) ||
215+
string.Equals(key, "repositoryPath", StringComparison.OrdinalIgnoreCase));
216+
if (isPathSetting)
217+
{
218+
ResolveRelativePathAttribute(item, "value", originDirectory);
219+
}
220+
}
221+
222+
else if (string.Equals(sectionName, "clientCertificates", StringComparison.OrdinalIgnoreCase) &&
223+
string.Equals(item.Name.LocalName, "fileCert", StringComparison.OrdinalIgnoreCase))
224+
{
225+
ResolveRelativePathAttribute(item, "path", originDirectory);
226+
}
227+
}
228+
229+
private static void ApplyEnvironmentTransforms(XElement item)
230+
{
231+
foreach (var attribute in item.DescendantsAndSelf().Attributes())
232+
{
233+
attribute.Value = Environment.ExpandEnvironmentVariables(attribute.Value);
234+
}
235+
}
236+
237+
private static void ResolveRelativePathAttribute(XElement element, string attributeName, string originDirectory)
238+
{
239+
var attribute = element.Attributes()
240+
.FirstOrDefault(candidate => string.Equals(candidate.Name.LocalName, attributeName, StringComparison.OrdinalIgnoreCase));
241+
if (attribute is null)
242+
{
243+
return;
244+
}
245+
246+
var expandedValue = Environment.ExpandEnvironmentVariables(attribute.Value);
247+
if (Uri.TryCreate(expandedValue, UriKind.Relative, out _))
248+
{
249+
attribute.Value = Path.GetFullPath(Path.Combine(originDirectory, expandedValue));
250+
}
251+
}
252+
253+
private static string? GetAttributeValue(XElement element, string name)
254+
=> element.Attributes()
255+
.FirstOrDefault(attribute => string.Equals(attribute.Name.LocalName, name, StringComparison.OrdinalIgnoreCase))
256+
?.Value;
257+
}

0 commit comments

Comments
 (0)