diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs index 09dcd9324cdc..46b6aa5fa2e8 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/SdkDirectoryWorkloadManifestProvider.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using Microsoft.DotNet.Cli; +using Microsoft.NET.Sdk.Localization; namespace Microsoft.NET.Sdk.WorkloadManifestReader { @@ -16,6 +17,7 @@ public class SdkDirectoryWorkloadManifestProvider : IWorkloadManifestProvider private readonly string [] _manifestDirectories; private static HashSet _outdatedManifestIds = new HashSet(StringComparer.OrdinalIgnoreCase) { "microsoft.net.workload.android", "microsoft.net.workload.blazorwebassembly", "microsoft.net.workload.ios", "microsoft.net.workload.maccatalyst", "microsoft.net.workload.macos", "microsoft.net.workload.tvos" }; + private readonly HashSet? _knownManifestIds; public SdkDirectoryWorkloadManifestProvider(string sdkRootPath, string sdkVersion) : this(sdkRootPath, sdkVersion, Environment.GetEnvironmentVariable) @@ -52,6 +54,12 @@ static int Last2DigitsTo0(int versionBuild) _sdkRootPath = sdkRootPath; _sdkVersionBand = sdkVersionBand; + var knownManifestIdsFilePath = Path.Combine(_sdkRootPath, "sdk", sdkVersion, "IncludedWorkloadManifests.txt"); + if (File.Exists(knownManifestIdsFilePath)) + { + _knownManifestIds = File.ReadAllLines(knownManifestIdsFilePath).Where(l => !string.IsNullOrEmpty(l)).ToHashSet(); + } + var manifestDirectory = Path.Combine(_sdkRootPath, "sdk-manifests", _sdkVersionBand); var manifestDirectoryEnvironmentVariable = getEnvironmentVariable(EnvironmentVariableNames.WORKLOAD_MANIFEST_ROOTS); @@ -81,6 +89,7 @@ static int Last2DigitsTo0(int versionBuild) public IEnumerable GetManifestDirectories() { + var manifestIdsToDirectories = new Dictionary(); if (_manifestDirectories.Length == 1) { // Optimization for common case where test hook to add additional directories isn't being used @@ -90,7 +99,7 @@ public IEnumerable GetManifestDirectories() { if (!IsManifestIdOutdated(workloadManifestDirectory)) { - yield return workloadManifestDirectory; + manifestIdsToDirectories.Add(Path.GetFileName(workloadManifestDirectory), workloadManifestDirectory); } } } @@ -114,10 +123,42 @@ public IEnumerable GetManifestDirectories() { if (!IsManifestIdOutdated(workloadManifestDirectory)) { - yield return workloadManifestDirectory; + manifestIdsToDirectories.Add(Path.GetFileName(workloadManifestDirectory), workloadManifestDirectory); } } } + + if (_knownManifestIds != null && _knownManifestIds.Any(id => !manifestIdsToDirectories.ContainsKey(id))) + { + var missingManifestIds = _knownManifestIds.Where(id => !manifestIdsToDirectories.ContainsKey(id)); + foreach (var missingManifestId in missingManifestIds) + { + var manifestDir = FallbackForMissingManifest(missingManifestId); + manifestIdsToDirectories.Add(missingManifestId, manifestDir); + } + } + + return manifestIdsToDirectories.Values; + } + + private string FallbackForMissingManifest(string manifestId) + { + var candidateFeatureBands = Directory.GetDirectories(Path.Combine(_sdkRootPath, "sdk-manifests")) + .Select(dir => Path.GetFileName(dir)) + .Where(featureBand => Version.TryParse(featureBand, out _)) + .Select(featureBand => Version.Parse(featureBand)) + .Where(featureBand => featureBand < Version.Parse(_sdkVersionBand)); + var matchingManifestFatureBands = candidateFeatureBands + .Where(featureBand => Directory.Exists(Path.Combine(_sdkRootPath, "sdk-manifests", featureBand.ToString(), manifestId))); + if (matchingManifestFatureBands.Any()) + { + return Path.Combine(_sdkRootPath, "sdk-manifests", matchingManifestFatureBands.Max()!.ToString(), manifestId); + } + else + { + // Manifest does not exist + return string.Empty; + } } private bool IsManifestIdOutdated(string workloadManifestDir) diff --git a/src/Tests/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/SdkDirectoryWorkloadManifestProviderTests.cs b/src/Tests/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/SdkDirectoryWorkloadManifestProviderTests.cs index aee64247adb1..31eda221580e 100644 --- a/src/Tests/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/SdkDirectoryWorkloadManifestProviderTests.cs +++ b/src/Tests/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/SdkDirectoryWorkloadManifestProviderTests.cs @@ -7,7 +7,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; - +using System.Text.Json; using FluentAssertions; using Microsoft.DotNet.Cli; using Microsoft.NET.Sdk.WorkloadManifestReader; @@ -247,6 +247,52 @@ var sdkDirectoryWorkloadManifestProvider .BeEquivalentTo("iOSContent"); } + [Fact] + public void ItShouldFallbackWhenFeatureBandHasNoManifests() + { + var testDirectory = _testAssetsManager.CreateTestDirectory().Path; + var fakeDotnetRootDirectory = Path.Combine(testDirectory, "dotnet"); + + // Write 4.0.100 manifests-> android only + var manifestDirectory4 = Path.Combine(fakeDotnetRootDirectory, "sdk-manifests", "4.0.100"); + Directory.CreateDirectory(manifestDirectory4); + Directory.CreateDirectory(Path.Combine(manifestDirectory4, "Android")); + File.WriteAllText(Path.Combine(manifestDirectory4, "Android", "WorkloadManifest.json"), "4.0.100"); + + // Write 5.0.100 manifests-> ios and android + var manifestDirectory5 = Path.Combine(fakeDotnetRootDirectory, "sdk-manifests", "5.0.100"); + Directory.CreateDirectory(manifestDirectory5); + Directory.CreateDirectory(Path.Combine(manifestDirectory5, "Android")); + File.WriteAllText(Path.Combine(manifestDirectory5, "Android", "WorkloadManifest.json"), "5.0.100"); + Directory.CreateDirectory(Path.Combine(manifestDirectory5, "iOS")); + File.WriteAllText(Path.Combine(manifestDirectory5, "iOS", "WorkloadManifest.json"), "5.0.100"); + + // Write 6.0.100 manifests-> ios only + var manifestDirectory6 = Path.Combine(fakeDotnetRootDirectory, "sdk-manifests", "6.0.100"); + Directory.CreateDirectory(manifestDirectory6); + Directory.CreateDirectory(Path.Combine(manifestDirectory6, "iOS")); + File.WriteAllText(Path.Combine(manifestDirectory6, "iOS", "WorkloadManifest.json"), "6.0.100"); + + // Write 7.0.100 manifests-> ios and android + var manifestDirectory7 = Path.Combine(fakeDotnetRootDirectory, "sdk-manifests", "7.0.100"); + Directory.CreateDirectory(manifestDirectory7); + Directory.CreateDirectory(Path.Combine(manifestDirectory7, "Android")); + File.WriteAllText(Path.Combine(manifestDirectory7, "Android", "WorkloadManifest.json"), "7.0.100"); + Directory.CreateDirectory(Path.Combine(manifestDirectory7, "iOS")); + File.WriteAllText(Path.Combine(manifestDirectory7, "iOS", "WorkloadManifest.json"), "7.0.100"); + + var knownWorkloadsFilePath = Path.Combine(fakeDotnetRootDirectory, "sdk", "6.0.100", "IncludedWorkloadManifests.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(knownWorkloadsFilePath)!); + File.WriteAllText(knownWorkloadsFilePath, "Android\niOS"); + + var sdkDirectoryWorkloadManifestProvider + = new SdkDirectoryWorkloadManifestProvider(sdkRootPath: fakeDotnetRootDirectory, sdkVersion: "6.0.100"); + + GetManifestContents(sdkDirectoryWorkloadManifestProvider) + .Should() + .BeEquivalentTo("6.0.100", "5.0.100"); + } + private IEnumerable GetManifestContents(SdkDirectoryWorkloadManifestProvider manifestProvider) { return manifestProvider.GetManifests().Select(manifest => new StreamReader(manifest.openManifestStream()).ReadToEnd()); diff --git a/src/Tests/dotnet-workload-install.Tests/GivenDotnetWorkloadInstall.cs b/src/Tests/dotnet-workload-install.Tests/GivenDotnetWorkloadInstall.cs index 20da47dc3dcd..43c1b9e5ab4c 100644 --- a/src/Tests/dotnet-workload-install.Tests/GivenDotnetWorkloadInstall.cs +++ b/src/Tests/dotnet-workload-install.Tests/GivenDotnetWorkloadInstall.cs @@ -21,6 +21,7 @@ using Microsoft.NET.TestFramework.Assertions; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.DotNet.Cli.Utils; +using System.Text.Json; namespace Microsoft.DotNet.Cli.Workload.Install.Tests {