Skip to content

Commit bc57e9f

Browse files
nagilsondsplaisted
authored andcommitted
Refactor our channel to release version parsing logic
Splits up `ReleaseManifest` into 3 pieces: - A. Downloading of .NET Archives - B. Accessing the release manifest & release manifest data structures - C. Parsing a 'channel' into a ReleaseVersion This is a better separation of responsibility and makes each class a far more reasonable size. # Conflicts: # src/Installer/Microsoft.Dotnet.Installation/Internal/ArchiveDotnetExtractor.cs # src/Installer/Microsoft.Dotnet.Installation/Internal/ReleaseManifest.cs # src/Installer/dnup/Commands/Sdk/Install/SdkInstallCommand.cs
1 parent 201d538 commit bc57e9f

File tree

12 files changed

+539
-523
lines changed

12 files changed

+539
-523
lines changed

src/Installer/Microsoft.Dotnet.Installation/Internal/ArchiveDotnetExtractor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@ public void Prepare()
3333
{
3434
using var activity = InstallationActivitySource.ActivitySource.StartActivity("DotnetInstaller.Prepare");
3535

36-
using var releaseManifest = new ReleaseManifest();
36+
using var archiveDownloader = new DotnetArchiveDownloader();
3737
var archiveName = $"dotnet-{Guid.NewGuid()}";
3838
_archivePath = Path.Combine(scratchDownloadDirectory, archiveName + DnupUtilities.GetArchiveFileExtensionForPlatform());
3939

4040
using (var progressReporter = _progressTarget.CreateProgressReporter())
4141
{
4242
var downloadTask = progressReporter.AddTask($"Downloading .NET SDK {_resolvedVersion}", 100);
4343
var reporter = new DownloadProgressReporter(downloadTask, $"Downloading .NET SDK {_resolvedVersion}");
44-
var downloadSuccess = releaseManifest.DownloadArchiveWithVerification(_request, _resolvedVersion, _archivePath, reporter);
44+
var downloadSuccess = archiveDownloader.DownloadArchiveWithVerification(_request, _resolvedVersion, _archivePath, reporter);
4545
if (!downloadSuccess)
4646
{
4747
throw new InvalidOperationException($"Failed to download .NET archive for version {_resolvedVersion}");
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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.Collections.Generic;
5+
using System.Linq;
6+
using Microsoft.Deployment.DotNet.Releases;
7+
8+
9+
namespace Microsoft.Dotnet.Installation.Internal;
10+
11+
internal class ChannelVersionResolver
12+
{
13+
public IEnumerable<string> GetSupportedChannels()
14+
{
15+
// TODO: Share this with other methods from this class
16+
var productIndex = ProductCollection.GetAsync().GetAwaiter().GetResult();
17+
18+
return ["latest", "preview", "lts", "sts",
19+
..productIndex
20+
.Where(p => p.IsSupported)
21+
.OrderByDescending(p => p.LatestReleaseVersion)
22+
.SelectMany(GetChannelsForProduct)
23+
];
24+
25+
static IEnumerable<string> GetChannelsForProduct(Product product)
26+
{
27+
return [product.ProductVersion,
28+
..product.GetReleasesAsync().GetAwaiter().GetResult()
29+
.SelectMany(r => r.Sdks)
30+
.Select(sdk => sdk.Version)
31+
.OrderByDescending(v => v)
32+
.Select(v => $"{v.Major}.{v.Minor}.{(v.Patch / 100)}xx")
33+
.Distinct()
34+
.ToList()
35+
];
36+
}
37+
38+
}
39+
40+
public ReleaseVersion? Resolve(DotnetInstallRequest installRequest)
41+
{
42+
return GetLatestVersionForChannel(installRequest.Channel, installRequest.Component);
43+
}
44+
45+
/// <summary>
46+
/// Parses a version channel string into its components.
47+
/// </summary>
48+
/// <param name="channel">Channel string to parse (e.g., "9", "9.0", "9.0.1xx", "9.0.103")</param>
49+
/// <returns>Tuple containing (major, minor, featureBand, isFullySpecified)</returns>
50+
private (int Major, int Minor, string? FeatureBand, bool IsFullySpecified) ParseVersionChannel(UpdateChannel channel)
51+
{
52+
var parts = channel.Name.Split('.');
53+
int major = parts.Length > 0 && int.TryParse(parts[0], out var m) ? m : -1;
54+
int minor = parts.Length > 1 && int.TryParse(parts[1], out var n) ? n : -1;
55+
56+
// Check if we have a feature band (like 1xx) or a fully specified patch
57+
string? featureBand = null;
58+
bool isFullySpecified = false;
59+
60+
if (parts.Length >= 3)
61+
{
62+
if (parts[2].EndsWith("xx"))
63+
{
64+
// Feature band pattern (e.g., "1xx")
65+
featureBand = parts[2].Substring(0, parts[2].Length - 2);
66+
}
67+
else if (int.TryParse(parts[2], out _))
68+
{
69+
// Fully specified version (e.g., "9.0.103")
70+
isFullySpecified = true;
71+
}
72+
}
73+
74+
return (major, minor, featureBand, isFullySpecified);
75+
}
76+
77+
/// <summary>
78+
/// Finds the latest fully specified version for a given channel string (major, major.minor, or feature band).
79+
/// </summary>
80+
/// <param name="channel">Channel string (e.g., "9", "9.0", "9.0.1xx", "9.0.103", "lts", "sts", "preview")</param>
81+
/// <param name="mode">InstallMode.SDK or InstallMode.Runtime</param>
82+
/// <returns>Latest fully specified version string, or null if not found</returns>
83+
public ReleaseVersion? GetLatestVersionForChannel(UpdateChannel channel, InstallComponent component)
84+
{
85+
if (string.Equals(channel.Name, "lts", StringComparison.OrdinalIgnoreCase) || string.Equals(channel.Name, "sts", StringComparison.OrdinalIgnoreCase))
86+
{
87+
var releaseType = string.Equals(channel.Name, "lts", StringComparison.OrdinalIgnoreCase) ? ReleaseType.LTS : ReleaseType.STS;
88+
var productIndex = ProductCollection.GetAsync().GetAwaiter().GetResult();
89+
return GetLatestVersionByReleaseType(productIndex, releaseType, component);
90+
}
91+
else if (string.Equals(channel.Name, "preview", StringComparison.OrdinalIgnoreCase))
92+
{
93+
var productIndex = ProductCollection.GetAsync().GetAwaiter().GetResult();
94+
return GetLatestPreviewVersion(productIndex, component);
95+
}
96+
else if (string.Equals(channel.Name, "latest", StringComparison.OrdinalIgnoreCase))
97+
{
98+
var productIndex = ProductCollection.GetAsync().GetAwaiter().GetResult();
99+
return GetLatestActiveVersion(productIndex, component);
100+
}
101+
102+
var (major, minor, featureBand, isFullySpecified) = ParseVersionChannel(channel);
103+
104+
// If major is invalid, return null
105+
if (major < 0)
106+
{
107+
return null;
108+
}
109+
110+
// If the version is already fully specified, just return it as-is
111+
if (isFullySpecified)
112+
{
113+
return new ReleaseVersion(channel.Name);
114+
}
115+
116+
// Load the index manifest
117+
var index = ProductCollection.GetAsync().GetAwaiter().GetResult();
118+
if (minor < 0)
119+
{
120+
return GetLatestVersionForMajorOrMajorMinor(index, major, component); // Major Only (e.g., "9")
121+
}
122+
else if (minor >= 0 && featureBand == null) // Major.Minor (e.g., "9.0")
123+
{
124+
return GetLatestVersionForMajorOrMajorMinor(index, major, component, minor);
125+
}
126+
else if (minor >= 0 && featureBand is not null) // Not Fully Qualified Feature band Version (e.g., "9.0.1xx")
127+
{
128+
return GetLatestVersionForFeatureBand(index, major, minor, featureBand, component);
129+
}
130+
131+
return null;
132+
}
133+
134+
private IEnumerable<Product> GetProductsInMajorOrMajorMinor(IEnumerable<Product> index, int major, int? minor = null)
135+
{
136+
var validProducts = index.Where(p => p.ProductVersion.StartsWith(minor is not null ? $"{major}.{minor}" : $"{major}."));
137+
return validProducts;
138+
}
139+
140+
/// <summary>
141+
/// Gets the latest version for a major-only channel (e.g., "9").
142+
/// </summary>
143+
private ReleaseVersion? GetLatestVersionForMajorOrMajorMinor(IEnumerable<Product> index, int major, InstallComponent component, int? minor = null)
144+
{
145+
// Assumption: The manifest is designed so that the first product for a major version will always be latest.
146+
Product? latestProductWithMajor = GetProductsInMajorOrMajorMinor(index, major, minor).FirstOrDefault();
147+
return GetLatestReleaseVersionInProduct(latestProductWithMajor, component);
148+
}
149+
150+
/// <summary>
151+
/// Gets the latest version based on support status (LTS or STS).
152+
/// </summary>
153+
/// <param name="index">The product collection to search</param>
154+
/// <param name="isLts">True for LTS (Long-Term Support), false for STS (Standard-Term Support)</param>
155+
/// <param name="mode">InstallComponent.SDK or InstallComponent.Runtime</param>
156+
/// <returns>Latest stable version string matching the support status, or null if none found</returns>
157+
private static ReleaseVersion? GetLatestVersionByReleaseType(IEnumerable<Product> index, ReleaseType releaseType, InstallComponent component)
158+
{
159+
var correctPhaseProducts = index?.Where(p => p.ReleaseType == releaseType) ?? Enumerable.Empty<Product>();
160+
return GetLatestActiveVersion(correctPhaseProducts, component);
161+
}
162+
163+
/// <summary>
164+
/// Gets the latest preview version available.
165+
/// </summary>
166+
/// <param name="index">The product collection to search</param>
167+
/// <param name="mode">InstallComponent.SDK or InstallComponent.Runtime</param>
168+
/// <returns>Latest preview or GoLive version string, or null if none found</returns>
169+
private ReleaseVersion? GetLatestPreviewVersion(IEnumerable<Product> index, InstallComponent component)
170+
{
171+
ReleaseVersion? latestPreviewVersion = GetLatestVersionBySupportPhase(index, component, [SupportPhase.Preview, SupportPhase.GoLive]);
172+
if (latestPreviewVersion is not null)
173+
{
174+
return latestPreviewVersion;
175+
}
176+
177+
return GetLatestVersionBySupportPhase(index, component, [SupportPhase.Active]);
178+
}
179+
180+
/// <summary>
181+
/// Gets the latest version across all available products that matches the support phase.
182+
/// </summary>
183+
private static ReleaseVersion? GetLatestActiveVersion(IEnumerable<Product> index, InstallComponent component)
184+
{
185+
return GetLatestVersionBySupportPhase(index, component, [SupportPhase.Active]);
186+
}
187+
/// <summary>
188+
/// Gets the latest version across all available products that matches the support phase.
189+
/// </summary>
190+
private static ReleaseVersion? GetLatestVersionBySupportPhase(IEnumerable<Product> index, InstallComponent component, SupportPhase[] acceptedSupportPhases)
191+
{
192+
// A version in preview/ga/rtm support is considered Go Live and not Active.
193+
var activeSupportProducts = index?.Where(p => acceptedSupportPhases.Contains(p.SupportPhase));
194+
195+
// The manifest is designed so that the first product will always be latest.
196+
Product? latestActiveSupportProduct = activeSupportProducts?.FirstOrDefault();
197+
198+
return GetLatestReleaseVersionInProduct(latestActiveSupportProduct, component);
199+
}
200+
201+
private static ReleaseVersion? GetLatestReleaseVersionInProduct(Product? product, InstallComponent component)
202+
{
203+
// Assumption: The latest runtime version will always be the same across runtime components.
204+
ReleaseVersion? latestVersion = component switch
205+
{
206+
InstallComponent.SDK => product?.LatestSdkVersion,
207+
_ => product?.LatestRuntimeVersion
208+
};
209+
210+
return latestVersion;
211+
}
212+
213+
/// <summary>
214+
/// Replaces user input feature band strings into the full feature band.
215+
/// This would convert '1xx' into '100'.
216+
/// 100 is not necessarily the latest but it is the feature band.
217+
/// The other number in the band is the patch.
218+
/// </summary>
219+
/// <param name="band"></param>
220+
/// <returns></returns>
221+
private static int NormalizeFeatureBandInput(string band)
222+
{
223+
var bandString = band
224+
.Replace("X", "x")
225+
.Replace("x", "0")
226+
.PadRight(3, '0')
227+
.Substring(0, 3);
228+
return int.Parse(bandString);
229+
}
230+
231+
232+
/// <summary>
233+
/// Gets the latest version for a feature band channel (e.g., "9.0.1xx").
234+
/// </summary>
235+
private ReleaseVersion? GetLatestVersionForFeatureBand(ProductCollection index, int major, int minor, string featureBand, InstallComponent component)
236+
{
237+
if (component != InstallComponent.SDK)
238+
{
239+
return null;
240+
}
241+
242+
var validProducts = GetProductsInMajorOrMajorMinor(index, major, minor);
243+
var latestProduct = validProducts.FirstOrDefault();
244+
var releases = latestProduct?.GetReleasesAsync().GetAwaiter().GetResult().ToList() ?? new List<ProductRelease>();
245+
var normalizedFeatureBand = NormalizeFeatureBandInput(featureBand);
246+
247+
foreach (var release in releases)
248+
{
249+
foreach (var sdk in release.Sdks)
250+
{
251+
if (sdk.Version.SdkFeatureBand == normalizedFeatureBand)
252+
{
253+
return sdk.Version;
254+
}
255+
}
256+
}
257+
258+
return null;
259+
}
260+
}

0 commit comments

Comments
 (0)