Skip to content

Commit c1c82cf

Browse files
authored
Apply dotnetup code review feedback (#51711)
2 parents 181e526 + 1254344 commit c1c82cf

19 files changed

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

0 commit comments

Comments
 (0)