Skip to content

Commit

Permalink
[9.0] Generate proper Visual Studio component IDs (#15103)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeloff authored Sep 27, 2024
1 parent 1230437 commit b2d3a3a
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ public static void ItCanCreateWorkloads()
ShortNames = shortNames,
WixToolsetPath = TestBase.WixToolsetPath,
WorkloadManifestPackageFiles = manifestsPackages,
IsOutOfSupportInVisualStudio = true
IsOutOfSupportInVisualStudio = true,
UseVisualStudioComponentPrefix = true,
};

bool result = createWorkloadTask.Execute();
Expand All @@ -94,13 +95,13 @@ public static void ItCanCreateWorkloads()
string componentSwr = File.ReadAllText(
Path.Combine(Path.GetDirectoryName(
createWorkloadTask.SwixProjects.FirstOrDefault(
i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr"));
Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr);
i => i.ItemSpec.Contains("Microsoft.NET.Component.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr"));
Assert.Contains("package name=Microsoft.NET.Component.sdk.emscripten", componentSwr);
string previewComponentSwr = File.ReadAllText(
Path.Combine(Path.GetDirectoryName(
createWorkloadTask.SwixProjects.FirstOrDefault(
i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.pre.5.6.swixproj")).ItemSpec), "component.swr"));
Assert.Contains("package name=microsoft.net.sdk.emscripten.pre", previewComponentSwr);
i => i.ItemSpec.Contains("Microsoft.NET.Component.sdk.emscripten.pre.5.6.swixproj")).ItemSpec), "component.swr"));
Assert.Contains("package name=Microsoft.NET.Component.sdk.emscripten.pre", previewComponentSwr);

// Emscripten is an abstract workload so it should be a component group.
Assert.Contains("vs.package.type=component", componentSwr);
Expand Down Expand Up @@ -204,6 +205,7 @@ public static void ItCanCreateWorkloadsThatSupportArm64InVisualStudio()
ShortNames = shortNames,
WixToolsetPath = TestBase.WixToolsetPath,
WorkloadManifestPackageFiles = manifestsPackages,
UseVisualStudioComponentPrefix = true,
};

bool result = createWorkloadTask.Execute();
Expand All @@ -229,8 +231,8 @@ public static void ItCanCreateWorkloadsThatSupportArm64InVisualStudio()
string componentSwr = File.ReadAllText(
Path.Combine(Path.GetDirectoryName(
createWorkloadTask.SwixProjects.FirstOrDefault(
i => i.ItemSpec.Contains("microsoft.net.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr"));
Assert.Contains("package name=microsoft.net.sdk.emscripten", componentSwr);
i => i.ItemSpec.Contains("Microsoft.NET.Component.sdk.emscripten.5.6.swixproj")).ItemSpec), "component.swr"));
Assert.Contains("package name=Microsoft.NET.Component.sdk.emscripten", componentSwr);

// Emscripten is an abstract workload so it should be a component group.
Assert.Contains("vs.package.type=component", componentSwr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ public void ItAssignsDefaultValues()
{
WorkloadManifest manifest = Create("WorkloadManifest.json");
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null);
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null,
componentPrefix: DefaultValues.VisualStudioComponentPrefix);

ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
string swixProj = project.Create();

string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
Assert.Contains("package name=microsoft.net.sdk.blazorwebassembly.aot", componentSwr);
Assert.Contains("package name=Microsoft.NET.Component.sdk.blazorwebassembly.aot", componentSwr);
Assert.Contains("version=1.0.0", componentSwr);

string componentResSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.res.swr"));
Expand All @@ -52,13 +53,13 @@ public void ItCanAdvertiseComponents()
WorkloadManifest manifest = Create("WorkloadManifest.json");
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null,
componentResources);
componentResources, componentPrefix: DefaultValues.VisualStudioComponentPrefix);

ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
string swixProj = project.Create();

string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
Assert.Contains("package name=microsoft.net.sdk.blazorwebassembly.aot", componentSwr);
Assert.Contains("package name=Microsoft.NET.Component.sdk.blazorwebassembly.aot", componentSwr);
Assert.Contains("version=4.5.6", componentSwr);
Assert.Contains("isAdvertisedPackage=yes", componentSwr);

Expand All @@ -81,13 +82,13 @@ public void ItPrefersComponentResourcesOverDefaults()
WorkloadManifest manifest = Create("WorkloadManifest.json");
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null,
componentResources);
componentResources, componentPrefix: DefaultValues.VisualStudioComponentPrefix);

ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
string swixProj = project.Create();

string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
Assert.Contains("package name=microsoft.net.sdk.blazorwebassembly.aot", componentSwr);
Assert.Contains("package name=Microsoft.NET.Component.sdk.blazorwebassembly.aot", componentSwr);
Assert.Contains("version=4.5.6", componentSwr);
Assert.Contains("isAdvertisedPackage=no", componentSwr);

Expand All @@ -100,14 +101,16 @@ public void ItPrefersComponentResourcesOverDefaults()
[WindowsOnlyFact]
public void ItShortensComponentIds()
{
// Shortnames can trim out potential prefix values.
ITaskItem[] shortNames = new TaskItem[]
{
new TaskItem("Microsoft.NET.Runtime", new Dictionary<string, string> { { Metadata.Replacement, "MSFT" } })
};

WorkloadManifest manifest = Create("WorkloadManifest.json");
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, shortNames: shortNames);
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, shortNames: shortNames,
componentPrefix: DefaultValues.VisualStudioComponentPrefix);

ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
string swixProj = project.Create();
Expand All @@ -121,19 +124,20 @@ public void ItIgnoresNonApplicableDepedencies()
{
WorkloadManifest manifest = Create("AbstractWorkloadsNonWindowsPacks.json");
WorkloadDefinition workload = (WorkloadDefinition)manifest.Workloads.FirstOrDefault().Value;
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, null, null);
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null,
componentPrefix: DefaultValues.VisualStudioComponentPrefix);

ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
string swixProj = project.Create();

string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));
Assert.Contains(@"package name=microsoft.net.runtime.ios", componentSwr);
Assert.Contains(@"package name=Microsoft.NET.Component.runtime.ios", componentSwr);
Assert.DoesNotContain(@"vs.dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.ios-arm", componentSwr);
Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.ios-arm64", componentSwr);
Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-arm64", componentSwr);
Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-x64", componentSwr);
Assert.DoesNotContain(@"vs dependency id=Microsoft.NETCore.App.Runtime.AOT.Cross.iossimulator-x86", componentSwr);
Assert.Contains(@"vs.dependency id=runtimes.ios", componentSwr);
Assert.Contains(@"vs.dependency id=Microsoft.NET.Component.runtimes.ios", componentSwr);
}

[WindowsOnlyFact]
Expand All @@ -151,7 +155,8 @@ public void ItCanOverrideDefaultValues()
})
};

SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, componentResources);
SwixComponent component = SwixComponent.Create(new ReleaseVersion("6.0.300"), workload, manifest, packGroupId: null, componentResources,
componentPrefix: DefaultValues.VisualStudioComponentPrefix);
ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
string swixProj = project.Create();

Expand All @@ -170,8 +175,10 @@ public void ItCreatesComponentsWhenWorkloadsDoNotIncludePacks()
SwixComponent component = SwixComponent.Create(new ReleaseVersion("7.0.100"), workload, manifest, packGroupId: null);
ComponentSwixProject project = new(component, BaseIntermediateOutputPath, BaseOutputPath);
string swixProj = project.Create();

string componentSwr = File.ReadAllText(Path.Combine(Path.GetDirectoryName(swixProj), "component.swr"));

// Component prefix is defaults to null when creating a SWIX component, so these values remain
// unchanged.
Assert.Contains(@"vs.dependency id=maui.mobile", componentSwr);
Assert.Contains(@"vs.dependency id=maui.desktop", componentSwr);
}
Expand All @@ -193,6 +200,16 @@ public void ItCreatesDependenciesForPackGroup()
Assert.Contains($"vs.dependency id={packGroupId}", componentSwr);
}

[Theory]
[InlineData("wasm-tools", "Microsoft.NET.Component.wasm.tools", DefaultValues.VisualStudioComponentPrefix)]
[InlineData("wasm-tools", "wasm.tools")]
[InlineData("microsoft-net-runtime-android", "Microsoft.NET.Component.runtime.android", DefaultValues.VisualStudioComponentPrefix)]
[InlineData("microsoft-net-runtime-android", "Foo.runtime.android", "Foo")]
public void ItGeneratesValidComponentIds(string workloadId, string expectedComponentId, string prefix = null)
{
Assert.Equal(expectedComponentId, Utils.ToSafeComponentId(workloadId, null, prefix));
}

private static WorkloadManifest Create(string filename)
{
return WorkloadManifestReader.ReadWorkloadManifest(Path.GetFileNameWithoutExtension(filename),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ public ITaskItem[] WorkloadManifestPackageFiles
set;
}

/// <summary>
/// Overrides the default component prefix when generating component IDs.
/// </summary>
public string VisualStudioComponentPrefix
{
get;
set;
} = DefaultValues.VisualStudioComponentPrefix;

public bool CreateWorkloadPackGroups
{
get;
Expand Down Expand Up @@ -130,6 +139,15 @@ public bool AllowMissingPacks
set;
} = false;

/// <summary>
/// Generate components using a fixed prefix that follow the Visual Studio component naming convention.
/// </summary>
public bool UseVisualStudioComponentPrefix
{
get;
set;
} = false;

protected override bool ExecuteCore()
{
// TODO: trim out duplicate manifests.
Expand Down Expand Up @@ -307,12 +325,14 @@ protected override bool ExecuteCore()
}
}

string prefix = UseVisualStudioComponentPrefix ? VisualStudioComponentPrefix : null;

// Finally, add a component for the workload in Visual Studio.
SwixComponent component = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, packGroupId,
ComponentResources, ShortNames);
ComponentResources, ShortNames, null, prefix);
// Create an additional component for shipping previews
SwixComponent previewComponent = SwixComponent.Create(manifestPackage.SdkFeatureBand, workload, manifest, packGroupId,
ComponentResources, ShortNames, "pre");
ComponentResources, ShortNames, "pre", prefix);

// Check for duplicates, e.g. manifests that were copied without changing workload definition IDs and
// provide a more usable error message so users can track down the duplication.
Expand Down
11 changes: 11 additions & 0 deletions src/Microsoft.DotNet.Build.Tasks.Workloads/src/DefaultValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ namespace Microsoft.DotNet.Build.Tasks.Workloads
/// </summary>
internal static class DefaultValues
{
/// <summary>
/// Well-known prefixes used by some workloads that can be replaced when generating component IDs
/// using <see cref="VisualStudioComponentPrefix"/>.
/// </summary>
public static readonly string[] WellKnownWorkloadPrefixes = { "Microsoft.NET.", "Microsoft." };

/// <summary>
/// Default prefix to use for Visual Studio component and component group IDs.
/// </summary>
public const string VisualStudioComponentPrefix = "Microsoft.NET.Component";

/// <summary>
/// Prefix used in Visual Studio for SWIX based package group.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,13 @@ public override bool Equals(object? obj)
/// <param name="componentResources">Additional resources that can be used to override component attributes such
/// as the title, description, and category.</param>
/// <param name="shortNames">A set of items used to shorten the names of setup packages.</param>
/// <param name="componentSuffix">Optional suffix to use for component IDs.</param>
/// <param name="componentPrefix">Optional prefix to use for component IDs.</param>
/// <returns>A SWIX component.</returns>
public static SwixComponent Create(ReleaseVersion sdkFeatureBand, WorkloadDefinition workload, WorkloadManifest manifest,
string? packGroupId,
ITaskItem[]? componentResources = null, ITaskItem[]? shortNames = null, string? componentSuffix = null)
ITaskItem[]? componentResources = null, ITaskItem[]? shortNames = null, string? componentSuffix = null,
string? componentPrefix = null)
{
ITaskItem? resourceItem = componentResources?.Where(r => string.Equals(r.ItemSpec, workload.Id)).FirstOrDefault();

Expand All @@ -167,7 +170,7 @@ public static SwixComponent Create(ReleaseVersion sdkFeatureBand, WorkloadDefini

// Since workloads only define a description, if no custom resources were provided, both the title and description of
// the SWIX component will default to the workload description.
SwixComponent component = new(sdkFeatureBand, Utils.ToSafeId(workload.Id, componentSuffix),
SwixComponent component = new(sdkFeatureBand, Utils.ToSafeComponentId(workload.Id, componentSuffix, componentPrefix),
resourceItem != null && !string.IsNullOrEmpty(resourceItem.GetMetadata(Metadata.Title)) ? resourceItem.GetMetadata(Metadata.Title) : workload.Description ?? throw new Exception(Strings.ComponentTitleCannotBeNull),
resourceItem != null && !string.IsNullOrEmpty(resourceItem.GetMetadata(Metadata.Description)) ? resourceItem.GetMetadata(Metadata.Description) : workload.Description ?? throw new Exception(Strings.ComponentDescriptionCannotBeNull),
componentVersion, workload.IsAbstract,
Expand All @@ -179,7 +182,7 @@ public static SwixComponent Create(ReleaseVersion sdkFeatureBand, WorkloadDefini
// processing direct pack dependencies.
foreach (WorkloadId dependency in workload.Extends ?? Enumerable.Empty<WorkloadId>())
{
component.AddDependency(Utils.ToSafeId(dependency, componentSuffix), s_v1);
component.AddDependency(Utils.ToSafeComponentId(dependency, componentSuffix, componentPrefix), s_v1);
}

// TODO: Check for missing packs
Expand Down
32 changes: 32 additions & 0 deletions src/Microsoft.DotNet.Build.Tasks.Workloads/src/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,43 @@ internal static string EnsureTrailingSlash(string path)
/// Generates a safe SWIX ID by replacing "-", " ", and "_" with ".".
/// </summary>
/// <param name="id">The identifier to convert to a safe identifier</param>
/// <param name="suffix">Optional suffix to add to the identifier.</param>
/// <returns>The safe identifier.</returns>
internal static string ToSafeId(string id, string suffix = null) =>
id.Replace("-", ".").Replace(" ", ".").Replace("_", ".") +
(string.IsNullOrWhiteSpace(suffix) ? null : $".{suffix}");

/// <summary>
/// Generates a safe SWIX ID that conforms with the Visual Studio naming convention.
/// Well-known prefixes (Microsoft. and Microsoft.NET.) are automatically removed.
/// </summary>
/// <param name="id">The identifier to convert to a safe identifier</param>
/// <param name="suffix">Optional suffix to add to the identifier.</param>
/// <param name="prefix">The component prefix to add.</param>
/// <returns>The safe component identifier.</returns>
internal static string ToSafeComponentId(string id, string suffix = null, string prefix = null)
{
// The suffix is always included in the safe ID.
string safeId = ToSafeId(id, suffix);

if (string.IsNullOrWhiteSpace(prefix))
{
return safeId;
}

foreach (string wellKnownPrefix in DefaultValues.WellKnownWorkloadPrefixes)
{
if (safeId.StartsWith(wellKnownPrefix, StringComparison.OrdinalIgnoreCase))
{
safeId = safeId.Substring(wellKnownPrefix.Length);
break;
}
}

// Trim well-known prefixes manually since net472 doesn't support all the string.Replace overloads
return prefix + '.' + safeId;
}

/// <summary>
/// Replaces all the tokens in a file using the provided dictionary. The dictionary keys define the tokens and
/// their values the replacement strings.
Expand Down

0 comments on commit b2d3a3a

Please sign in to comment.