diff --git a/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs b/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs index 43553bb769e9..66ff2b935416 100644 --- a/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs +++ b/src/Cli/dotnet/commands/dotnet-workload/list/VisualStudioWorkloads.cs @@ -25,23 +25,13 @@ internal static class VisualStudioWorkloads /// Visual Studio product ID filters. We dont' want to query SKUs such as Server, TeamExplorer, TestAgent /// TestController and BuildTools. /// - private static readonly string[] s_visualStudioProducts = new string[] + private static readonly string[] s_visualStudioProducts = { "Microsoft.VisualStudio.Product.Community", "Microsoft.VisualStudio.Product.Professional", "Microsoft.VisualStudio.Product.Enterprise", }; - /// - /// Default prefix to use for Visual Studio component and component group IDs. - /// - private static readonly string s_visualStudioComponentPrefix = "Microsoft.NET.Component"; - - /// - /// Well-known prefixes used by some workloads that can be replaced when generating component IDs. - /// - private static readonly string[] s_wellKnownWorkloadPrefixes = { "Microsoft.NET.", "Microsoft." }; - /// /// The SWIX package ID wrapping the SDK installer in Visual Studio. The ID should contain /// the SDK version as a suffix, e.g., "Microsoft.NetCore.Toolset.5.0.403". @@ -67,22 +57,9 @@ internal static Dictionary GetAvailableVisualStudioWorkloads(IWo { string workloadId = workload.Id.ToString(); // Old style VS components simply replaced '-' with '.' in the workload ID. - string componentId = workload.Id.ToString().Replace('-', '.'); - - visualStudioComponentWorkloads.Add(componentId, workloadId); - + visualStudioComponentWorkloads.Add(workload.Id.ToSafeId(), workloadId); // Starting in .NET 9.0 and VS 17.12, workload components will follow the VS naming convention. - foreach (string wellKnownPrefix in s_wellKnownWorkloadPrefixes) - { - if (componentId.StartsWith(wellKnownPrefix, StringComparison.OrdinalIgnoreCase)) - { - componentId = componentId.Substring(wellKnownPrefix.Length); - break; - } - } - - componentId = s_visualStudioComponentPrefix + "." + componentId; - visualStudioComponentWorkloads.Add(componentId, workloadId); + visualStudioComponentWorkloads.Add(workload.Id.ToSafeId(includeVisualStudioPrefix: true), workloadId); } return visualStudioComponentWorkloads; diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadId.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadId.cs index c6adcff13fbc..3517870a3339 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadId.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadManifestReader/WorkloadId.cs @@ -9,6 +9,10 @@ namespace Microsoft.NET.Sdk.WorkloadManifestReader /// public readonly struct WorkloadId : IComparable, IEquatable { + private static readonly string s_visualStudioComponentPrefix = "Microsoft.NET.Component"; + + private static readonly string[] s_wellKnownWorkloadPrefixes = { "Microsoft.NET.", "Microsoft." }; + private readonly string _id; public WorkloadId(string id) @@ -31,6 +35,27 @@ public WorkloadId(string id) public override string ToString() => _id; + public string ToSafeId(bool includeVisualStudioPrefix = false) + { + string safeId = _id.Replace('-', '.').Replace(' ', '.').Replace('_', '.'); + + if (includeVisualStudioPrefix) + { + foreach (string wellKnownPrefix in s_wellKnownWorkloadPrefixes) + { + if (safeId.StartsWith(wellKnownPrefix, StringComparison.OrdinalIgnoreCase)) + { + safeId = safeId.Substring(wellKnownPrefix.Length); + break; + } + } + + safeId = s_visualStudioComponentPrefix + "." + safeId; + } + + return safeId; + } + public static implicit operator string(WorkloadId id) => id._id; public static bool operator ==(WorkloadId a, WorkloadId b) => a.Equals(b); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs index f0a32830d6fa..a48a21cd531e 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs @@ -19,7 +19,7 @@ public class ShowMissingWorkloads : TaskBase { "android", "android-aot", "ios", "maccatalyst", "macos", "maui", "maui-android", "maui-desktop", "maui-ios", "maui-maccatalyst", "maui-mobile", "maui-windows", "tvos" }; private static readonly HashSet WasmWorkloadIds = new(StringComparer.OrdinalIgnoreCase) - { "wasm-tools", "wasm-tools-net6", "wasm-tools-net7" }; + { "wasm-tools", "wasm-tools-net6", "wasm-tools-net7", "wasm-tools-net8" }; public ITaskItem[] MissingWorkloadPacks { get; set; } @@ -70,7 +70,7 @@ out ISet unsatisfiablePacks { var suggestedWorkloadsList = GetSuggestedWorkloadsList(suggestedWorkload); var taskItem = new TaskItem(suggestedWorkload.Id); - taskItem.SetMetadata("VisualStudioComponentId", ToSafeId(suggestedWorkload.Id)); + taskItem.SetMetadata("VisualStudioComponentId", suggestedWorkload.Id.ToSafeId(includeVisualStudioPrefix: true)); taskItem.SetMetadata("VisualStudioComponentIds", string.Join(";", suggestedWorkloadsList)); return taskItem; }).ToArray(); @@ -78,14 +78,10 @@ out ISet unsatisfiablePacks } } - internal static string ToSafeId(string id) - { - return id.Replace("-", ".").Replace(" ", ".").Replace("_", "."); - } - private static IEnumerable GetSuggestedWorkloadsList(WorkloadInfo workloadInfo) { - yield return ToSafeId(workloadInfo.Id); + yield return workloadInfo.Id.ToSafeId(); + yield return workloadInfo.Id.ToSafeId(includeVisualStudioPrefix: true); if (MauiWorkloadIds.Contains(workloadInfo.Id.ToString())) { yield return MauiCrossPlatTopLevelVSWorkloads; diff --git a/test/Microsoft.NET.Build.Tests/WorkloadTests.cs b/test/Microsoft.NET.Build.Tests/WorkloadTests.cs index 7526c08d0353..017ec9d6f61f 100644 --- a/test/Microsoft.NET.Build.Tests/WorkloadTests.cs +++ b/test/Microsoft.NET.Build.Tests/WorkloadTests.cs @@ -63,6 +63,7 @@ public void It_should_create_suggested_workload_items() var getValuesCommand = new GetValuesCommand(testAsset, "SuggestedWorkload", GetValuesCommand.ValueType.Item) { + ShouldEscapeItems = true, DependsOnTargets = "GetSuggestedWorkloads" }; getValuesCommand.MetadataNames.Add("VisualStudioComponentId"); @@ -75,11 +76,11 @@ public void It_should_create_suggested_workload_items() getValuesCommand.GetValuesWithMetadata().Select(valueAndMetadata => (valueAndMetadata.value, valueAndMetadata.metadata["VisualStudioComponentId"])) .Should() - .BeEquivalentTo(new[] { ("microsoft-net-sdk-missingtestworkload", "microsoft.net.sdk.missingtestworkload") }); + .BeEquivalentTo(new[] { ("microsoft-net-sdk-missingtestworkload", "Microsoft.NET.Component.sdk.missingtestworkload") }); getValuesCommand.GetValuesWithMetadata().Select(valueAndMetadata => (valueAndMetadata.value, valueAndMetadata.metadata["VisualStudioComponentIds"])) .Should() - .BeEquivalentTo(new[] { ("microsoft-net-sdk-missingtestworkload", "microsoft.net.sdk.missingtestworkload") }); + .BeEquivalentTo(new[] { ("microsoft-net-sdk-missingtestworkload", "microsoft.net.sdk.missingtestworkload;Microsoft.NET.Component.sdk.missingtestworkload") }); } [Fact] diff --git a/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/WorkloadIdTests.cs b/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/WorkloadIdTests.cs new file mode 100644 index 000000000000..d91b787d4400 --- /dev/null +++ b/test/Microsoft.NET.Sdk.WorkloadManifestReader.Tests/WorkloadIdTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.NET.Sdk.WorkloadManifestReader.Tests +{ + public class WorkloadIdTests + { + [Theory] + [InlineData("wasm-tools", "wasm.tools")] + [InlineData("something_something", "something.something")] + public void ItCanCreateSafeIds(string workloadId, string expectedSafeId) + { + var id = new WorkloadId(workloadId); + Assert.Equal(expectedSafeId, id.ToSafeId()); + } + + [Theory] + [InlineData("wasm-tools", "Microsoft.NET.Component.wasm.tools")] + [InlineData("microsoft-android-runtime", "Microsoft.NET.Component.android.runtime")] + public void ItCanCreateSafeIdsWithVisualStudioStudioPrefix(string workloadId, string expectedSafeId) + { + var id = new WorkloadId(workloadId); + Assert.Equal(expectedSafeId, id.ToSafeId(includeVisualStudioPrefix: true)); + } + } +} diff --git a/test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs b/test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs index 7288f7a5bc54..32711ced9469 100644 --- a/test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs +++ b/test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs @@ -16,6 +16,8 @@ public enum ValueType string _valueName; ValueType _valueType; + public bool ShouldEscapeItems { get; set; } = false; + public bool ShouldCompile { get; set; } = true; public string DependsOnTargets { get; set; } = "Compile"; @@ -81,6 +83,13 @@ protected override SdkCommandSpec CreateCommand(IEnumerable args) { linesAttribute += $"%09%({_valueName}.{metadataName})"; } + + if (ShouldEscapeItems) + { + // items with semi-colon delimited values need to be escaped to avoid creating separate items + // when the include attribute is evaluated. + linesAttribute = $"$([MSBuild]::Escape({linesAttribute}))"; + } } var propertyGroup = project.Root.Elements(ns + "PropertyGroup").FirstOrDefault();