Skip to content

[release/9.0.1xx] Workloads: Support VS component IDs #44198

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: release/9.0.1xx
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
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",
};

/// <summary>
/// Default prefix to use for Visual Studio component and component group IDs.
/// </summary>
private static readonly string s_visualStudioComponentPrefix = "Microsoft.NET.Component";

/// <summary>
/// Well-known prefixes used by some workloads that can be replaced when generating component IDs.
/// </summary>
private static readonly string[] s_wellKnownWorkloadPrefixes = { "Microsoft.NET.", "Microsoft." };

/// <summary>
/// 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".
Expand All @@ -67,22 +57,9 @@ internal static Dictionary<string, string> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ namespace Microsoft.NET.Sdk.WorkloadManifestReader
/// </summary>
public readonly struct WorkloadId : IComparable<WorkloadId>, IEquatable<WorkloadId>
{
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)
Expand All @@ -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);
Expand Down
12 changes: 4 additions & 8 deletions src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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; }

Expand Down Expand Up @@ -70,22 +70,18 @@ out ISet<WorkloadPackId> 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();
}
}
}

internal static string ToSafeId(string id)
{
return id.Replace("-", ".").Replace(" ", ".").Replace("_", ".");
}

private static IEnumerable<string> 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;
Expand Down
5 changes: 3 additions & 2 deletions test/Microsoft.NET.Build.Tests/WorkloadTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
9 changes: 9 additions & 0 deletions test/Microsoft.NET.TestFramework/Commands/GetValuesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -81,6 +83,13 @@ protected override SdkCommandSpec CreateCommand(IEnumerable<string> 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();
Expand Down
Loading