Skip to content
Merged
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 @@ -76,37 +76,49 @@ internal static class TargetFrameworkParser

/// <summary>
/// Resolves the short target framework moniker of <paramref name="entryAssembly"/>, including the
/// OS-platform component (e.g. <c>net8.0-windows10.0.18362.0</c>) when the assembly was built for an
/// OS-specific TFM.
/// platform component (e.g. <c>net8.0-windows10.0.18362.0</c>) when the assembly was built for a
/// platform-specific TFM, including non-OS / custom platform identifiers such as Uno's <c>browserwasm</c>.
/// </summary>
/// <remarks>
/// A plain <c>net8.0</c> build and a <c>net8.0-windows10.0.18362.0</c> build carry the exact same
/// <see cref="TargetFrameworkAttribute"/> (<c>.NETCoreApp,Version=v8.0</c>) and produce the same
/// <see cref="RuntimeInformation.FrameworkDescription"/>, so the short TFM alone cannot tell them apart.
/// The only runtime-visible signal is <c>System.Runtime.Versioning.TargetPlatformAttribute</c>, which the
/// SDK emits for OS-specific TFMs only. Appending it here keeps report file names unique per build so two
/// modules of the same assembly no longer overwrite each other's report.
/// SDK emits for any platform-specific TFM (including non-OS / custom platform identifiers such as Uno's
/// <c>browserwasm</c>). Appending it here keeps report file names unique per build so two modules of the
/// same assembly no longer overwrite each other's report.
/// </remarks>
public static string? GetShortTargetFrameworkIncludingPlatform(Assembly? entryAssembly)
{
string? shortTargetFramework = GetShortTargetFramework(entryAssembly?.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkDisplayName)
?? GetShortTargetFramework(RuntimeInformation.FrameworkDescription);
string? shortTargetFramework = GetShortTargetFramework(entryAssembly?.GetCustomAttribute<TargetFrameworkAttribute>()?.FrameworkDisplayName);

// FrameworkDisplayName can be missing, empty, or whitespace for some assemblies (e.g. when the SDK
// emits a TargetFrameworkAttribute without a display name for a custom TargetFrameworkIdentifier such
// as Uno's net8.0-browserwasm). In that case GetShortTargetFramework echoes the empty value back rather
// than returning null, so a plain null-coalesce would not fall back. Treat null/empty/whitespace alike
// and fall back to the runtime description so the base moniker stays meaningful (e.g. net8.0) and we
// never produce a dangling "-platform" name.
if (RoslynString.IsNullOrWhiteSpace(shortTargetFramework))
{
shortTargetFramework = GetShortTargetFramework(RuntimeInformation.FrameworkDescription);
}

return BuildTargetFrameworkMoniker(shortTargetFramework, GetTargetPlatformName(entryAssembly));
}

/// <summary>
/// Combines a short target framework (e.g. <c>net8.0</c>) with an optional OS-platform name
/// Combines a short target framework (e.g. <c>net8.0</c>) with an optional platform name
/// (e.g. <c>Windows10.0.18362.0</c>) into a full moniker (e.g. <c>net8.0-windows10.0.18362.0</c>).
/// </summary>
internal static string? BuildTargetFrameworkMoniker(string? shortTargetFramework, string? targetPlatformName)
=> shortTargetFramework is null || RoslynString.IsNullOrEmpty(targetPlatformName)
=> RoslynString.IsNullOrWhiteSpace(shortTargetFramework) || RoslynString.IsNullOrWhiteSpace(targetPlatformName)
? shortTargetFramework
: $"{shortTargetFramework}-{targetPlatformName!.ToLowerInvariant()}";
: $"{shortTargetFramework}-{targetPlatformName.ToLowerInvariant()}";
Comment thread
Copilot marked this conversation as resolved.

/// <summary>
/// Reads the OS-platform name from <c>System.Runtime.Versioning.TargetPlatformAttribute</c> on
/// <paramref name="entryAssembly"/>, or <see langword="null"/> when the assembly targets no specific OS.
/// Reads the platform name from <c>System.Runtime.Versioning.TargetPlatformAttribute</c> on
/// <paramref name="entryAssembly"/>, or <see langword="null"/> when the assembly targets no specific
/// platform (or only carries an empty/whitespace platform value).
/// </summary>
internal static string? GetTargetPlatformName(Assembly? entryAssembly)
{
Expand All @@ -122,7 +134,7 @@ internal static class TargetFrameworkParser
if (string.Equals(attribute.AttributeType.FullName, "System.Runtime.Versioning.TargetPlatformAttribute", StringComparison.Ordinal)
&& attribute.ConstructorArguments.Count == 1
&& attribute.ConstructorArguments[0].Value is string platformName
&& platformName.Length > 0)
&& !RoslynString.IsNullOrWhiteSpace(platformName))
{
return platformName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,18 @@ public void BuildTargetFrameworkMoniker_WithEmptyPlatform_ReturnsShortTargetFram
public void BuildTargetFrameworkMoniker_WithPlatform_AppendsLowercasedPlatform()
=> Assert.AreEqual("net8.0-windows10.0.18362.0", TargetFrameworkParser.BuildTargetFrameworkMoniker("net8.0", "Windows10.0.18362.0"));

[TestMethod]
public void BuildTargetFrameworkMoniker_WithWhitespacePlatform_ReturnsShortTargetFramework()
=> Assert.AreEqual("net8.0", TargetFrameworkParser.BuildTargetFrameworkMoniker("net8.0", " "));

[TestMethod]
public void BuildTargetFrameworkMoniker_WithNullShortTargetFramework_ReturnsNull()
=> Assert.IsNull(TargetFrameworkParser.BuildTargetFrameworkMoniker(null, "Windows10.0.18362.0"));

[TestMethod]
public void BuildTargetFrameworkMoniker_WithEmptyShortTargetFramework_DoesNotEmitDanglingPlatform()
=> Assert.AreEqual(string.Empty, TargetFrameworkParser.BuildTargetFrameworkMoniker(string.Empty, "BrowserWasm1.0"));

[TestMethod]
public void GetTargetPlatformName_WithNullAssembly_ReturnsNull()
=> Assert.IsNull(TargetFrameworkParser.GetTargetPlatformName(null));
Expand All @@ -83,6 +91,13 @@ public void GetTargetPlatformName_WithEmptyPlatformValue_ReturnsNull()
Assert.IsNull(TargetFrameworkParser.GetTargetPlatformName(dynamicAssembly));
}

[TestMethod]
public void GetTargetPlatformName_WithWhitespacePlatformValue_ReturnsNull()
{
Assembly dynamicAssembly = CreateAssemblyWithTargetPlatform(" ");
Assert.IsNull(TargetFrameworkParser.GetTargetPlatformName(dynamicAssembly));
}

[TestMethod]
public void GetShortTargetFrameworkIncludingPlatform_WithTargetPlatformAttribute_AppendsLowercasedPlatform()
{
Expand All @@ -108,6 +123,21 @@ public void GetShortTargetFrameworkIncludingPlatform_WithoutTargetPlatformAttrib
Assert.DoesNotContain("-", result);
}

[TestMethod]
public void GetShortTargetFrameworkIncludingPlatform_WithEmptyFrameworkDisplayName_FallsBackToRuntimeBase()
{
// Simulates a build whose TargetFrameworkAttribute carries an empty FrameworkDisplayName (as can happen
// for a custom TargetFrameworkIdentifier such as Uno's net8.0-browserwasm). The base must fall back to
// the runtime description (e.g. net8.0) instead of leaving a dangling "-browserwasm1.0" name.
Assembly dynamicAssembly = CreateAssemblyWithFrameworkDisplayNameAndTargetPlatform(string.Empty, "BrowserWasm1.0");

string? result = TargetFrameworkParser.GetShortTargetFrameworkIncludingPlatform(dynamicAssembly);

Assert.IsNotNull(result);
Assert.StartsWith("net", result);
Assert.EndsWith("-browserwasm1.0", result);
}

private static Assembly CreateAssemblyWithTargetPlatform(string platformName)
{
var name = new AssemblyName($"TestAssembly_{Guid.NewGuid():N}");
Expand All @@ -118,6 +148,20 @@ private static Assembly CreateAssemblyWithTargetPlatform(string platformName)
return builder;
}

private static Assembly CreateAssemblyWithFrameworkDisplayNameAndTargetPlatform(string frameworkDisplayName, string platformName)
{
var name = new AssemblyName($"TestAssembly_{Guid.NewGuid():N}");
var builder = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndCollect);

ConstructorInfo frameworkCtor = typeof(System.Runtime.Versioning.TargetFrameworkAttribute).GetConstructor([typeof(string)])!;
PropertyInfo displayNameProperty = typeof(System.Runtime.Versioning.TargetFrameworkAttribute).GetProperty(nameof(System.Runtime.Versioning.TargetFrameworkAttribute.FrameworkDisplayName))!;
builder.SetCustomAttribute(new CustomAttributeBuilder(frameworkCtor, [".NETCoreApp,Version=v8.0"], [displayNameProperty], [frameworkDisplayName]));

ConstructorInfo platformCtor = typeof(System.Runtime.Versioning.TargetPlatformAttribute).GetConstructor([typeof(string)])!;
builder.SetCustomAttribute(new CustomAttributeBuilder(platformCtor, [platformName]));
return builder;
}

private static Assembly CreateAssemblyWithoutTargetPlatform()
{
var name = new AssemblyName($"TestAssembly_{Guid.NewGuid():N}");
Expand Down
Loading