diff --git a/Aspire.sln b/Aspire.sln
index ad1e0dd2696..fdd5827becf 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -583,6 +583,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Dapr.Tests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.AWS.Tests", "tests\Aspire.Hosting.AWS.Tests\Aspire.Hosting.AWS.Tests.csproj", "{6F71BC73-B703-4E64-98E0-801781302E7A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Sdk.Tests", "tests\Aspire.Hosting.Sdk.Tests\Aspire.Hosting.Sdk.Tests.csproj", "{ABDC7DF8-7ACA-430C-9C91-983693DBBCB9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.RuntimeIdentifier.Tool", "src\Aspire.Hosting.Sdk\Aspire.RuntimeIdentifier.Tool\Aspire.RuntimeIdentifier.Tool.csproj", "{100A7A22-2517-41AD-8649-68E3E289F537}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1537,6 +1541,14 @@ Global
{6F71BC73-B703-4E64-98E0-801781302E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F71BC73-B703-4E64-98E0-801781302E7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F71BC73-B703-4E64-98E0-801781302E7A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ABDC7DF8-7ACA-430C-9C91-983693DBBCB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ABDC7DF8-7ACA-430C-9C91-983693DBBCB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ABDC7DF8-7ACA-430C-9C91-983693DBBCB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ABDC7DF8-7ACA-430C-9C91-983693DBBCB9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {100A7A22-2517-41AD-8649-68E3E289F537}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {100A7A22-2517-41AD-8649-68E3E289F537}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {100A7A22-2517-41AD-8649-68E3E289F537}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {100A7A22-2517-41AD-8649-68E3E289F537}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1818,6 +1830,8 @@ Global
{091EA540-355B-4763-9980-5F83F0BB6F11} = {15966C27-17FA-4A46-A172-55985411540A}
{C60C5CFA-5B6D-4432-BFCD-54D1BEEC7DBE} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
{6F71BC73-B703-4E64-98E0-801781302E7A} = {830A89EC-4029-4753-B25A-068BAE37DEC7}
+ {ABDC7DF8-7ACA-430C-9C91-983693DBBCB9} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
+ {100A7A22-2517-41AD-8649-68E3E289F537} = {F534D4F8-5E3A-42FC-BCD7-4C2D6060F9C8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b8d53a4d66e..005f7f7f351 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -92,6 +92,8 @@
+
+
diff --git a/eng/dashboardpack/Common.projitems b/eng/dashboardpack/Common.projitems
index dcefee7e58e..89b793bb47a 100644
--- a/eng/dashboardpack/Common.projitems
+++ b/eng/dashboardpack/Common.projitems
@@ -10,6 +10,7 @@
true
true
+ false
false
$(ArtifactsShippingPackagesDir)
@@ -24,7 +25,7 @@
-
+
@@ -56,8 +57,7 @@
-
+
diff --git a/eng/dashboardpack/Sdk.in.targets b/eng/dashboardpack/Sdk.targets
similarity index 84%
rename from eng/dashboardpack/Sdk.in.targets
rename to eng/dashboardpack/Sdk.targets
index 4af9f8fb1ce..f903ca475d9 100644
--- a/eng/dashboardpack/Sdk.in.targets
+++ b/eng/dashboardpack/Sdk.targets
@@ -2,7 +2,7 @@
-
+
$([MSBuild]::NormalizeDirectory($(MSBuildThisFileDirectory), '..', 'tools'))
$([MSBuild]::EnsureTrailingSlash('$(AspireDashboardDir)'))
$([MSBuild]::NormalizePath($(AspireDashboardDir), 'Aspire.Dashboard'))
@@ -10,7 +10,7 @@
$(AspireDashboardPath).dll
-
+
diff --git a/eng/dcppack/Aspire.Hosting.Orchestration.in.targets b/eng/dcppack/Aspire.Hosting.Orchestration.targets
similarity index 91%
rename from eng/dcppack/Aspire.Hosting.Orchestration.in.targets
rename to eng/dcppack/Aspire.Hosting.Orchestration.targets
index 23524cc8d5c..ad698ca042d 100644
--- a/eng/dcppack/Aspire.Hosting.Orchestration.in.targets
+++ b/eng/dcppack/Aspire.Hosting.Orchestration.targets
@@ -1,6 +1,6 @@
-
+
$([MSBuild]::NormalizeDirectory($(MSBuildThisFileDirectory), '..', 'tools'))
$([MSBuild]::EnsureTrailingSlash('$(DcpDir)'))
$([MSBuild]::NormalizeDirectory($(DcpDir), 'ext'))
diff --git a/eng/dcppack/Common.projitems b/eng/dcppack/Common.projitems
index 2aa23ec83d4..5754e5eead2 100644
--- a/eng/dcppack/Common.projitems
+++ b/eng/dcppack/Common.projitems
@@ -11,6 +11,7 @@
true
true
false
+ false
$(ArtifactsShippingPackagesDir)
$(TargetsForTfmSpecificContentInPackage);AddPackageFiles
@@ -30,7 +31,7 @@
-
+
@@ -75,7 +76,7 @@
-
+
diff --git a/src/Aspire.Hosting.Sdk/Aspire.Hosting.Sdk.csproj b/src/Aspire.Hosting.Sdk/Aspire.Hosting.Sdk.csproj
index f785b101c2d..4be1db33cbe 100644
--- a/src/Aspire.Hosting.Sdk/Aspire.Hosting.Sdk.csproj
+++ b/src/Aspire.Hosting.Sdk/Aspire.Hosting.Sdk.csproj
@@ -5,17 +5,45 @@
aspire hosting sdk
.NET Aspire Hosting SDK. Enabled via <IsAspireHost>true</IsAspireHost>.
+ _PublishAndPackRIDTool;$(TargetsForTfmSpecificContentInPackage)
100
+
+
+
+
+
+
+
+
+
+
+
+ <_publishContentToPackage Include="$(DotNetOutputPath)Aspire.RuntimeIdentifier.Tool/$(Configuration)/$(NetCurrent)/publish/**/*" />
+
+
+
+
+
+
+
+
diff --git a/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/Aspire.RuntimeIdentifier.Tool.csproj b/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/Aspire.RuntimeIdentifier.Tool.csproj
new file mode 100644
index 00000000000..ccf58834294
--- /dev/null
+++ b/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/Aspire.RuntimeIdentifier.Tool.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ $(NetCurrent)
+ Major
+ false
+ false
+ false
+
+
+
+
+
+
+
+
diff --git a/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/NuGetUtils.cs b/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/NuGetUtils.cs
new file mode 100644
index 00000000000..00b5782d461
--- /dev/null
+++ b/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/NuGetUtils.cs
@@ -0,0 +1,50 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using NuGet.RuntimeModel;
+
+namespace Aspire.Hosting.Sdk;
+
+/*
+ * These utility methods were copied from the sdk repository to mimic the behavior used when selecting the best matching RID
+ * for a given runtime identifier. For more information, please see the original source code at:
+ * https://github.com/dotnet/sdk/blob/e6da8ca6de3ec8f392dc87b8529415e1ef59b7ea/src/Tasks/Microsoft.NET.Build.Tasks/NuGetUtils.NuGet.cs#L76-L109
+ */
+
+internal static class NuGetUtils
+{
+ public static string? GetBestMatchingRid(RuntimeGraph runtimeGraph, string runtimeIdentifier,
+ IEnumerable availableRuntimeIdentifiers, out bool wasInGraph)
+ {
+ return GetBestMatchingRidWithExclusion(runtimeGraph, runtimeIdentifier,
+ runtimeIdentifiersToExclude: null,
+ availableRuntimeIdentifiers, out wasInGraph);
+ }
+
+ public static string? GetBestMatchingRidWithExclusion(RuntimeGraph runtimeGraph, string runtimeIdentifier,
+ IEnumerable? runtimeIdentifiersToExclude,
+ IEnumerable availableRuntimeIdentifiers, out bool wasInGraph)
+ {
+ wasInGraph = runtimeGraph.Runtimes.ContainsKey(runtimeIdentifier);
+
+ string? bestMatch = null;
+
+ HashSet availableRids = new(availableRuntimeIdentifiers, StringComparer.Ordinal);
+ HashSet? excludedRids = runtimeIdentifiersToExclude switch { null => null, _ => new HashSet(runtimeIdentifiersToExclude, StringComparer.Ordinal) };
+ foreach (var candidateRuntimeIdentifier in runtimeGraph.ExpandRuntime(runtimeIdentifier))
+ {
+ if (bestMatch == null && availableRids.Contains(candidateRuntimeIdentifier))
+ {
+ bestMatch = candidateRuntimeIdentifier;
+ }
+
+ if (excludedRids != null && excludedRids.Contains(candidateRuntimeIdentifier))
+ {
+ // Don't treat this as a match
+ return null;
+ }
+ }
+
+ return bestMatch;
+ }
+}
diff --git a/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/Program.cs b/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/Program.cs
new file mode 100644
index 00000000000..d8215c49b7d
--- /dev/null
+++ b/src/Aspire.Hosting.Sdk/Aspire.RuntimeIdentifier.Tool/Program.cs
@@ -0,0 +1,87 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.CommandLine;
+using System.CommandLine.Parsing;
+using System.Diagnostics;
+using System.Reflection;
+using Aspire.Hosting.Sdk;
+using NuGet.RuntimeModel;
+
+namespace Aspire.RuntimeIdentifier.Tool;
+
+sealed class Program
+{
+ static int Main(string[] args)
+ {
+ CliRootCommand rootCommand = new("Aspire.RuntimeIdentifier.Tool v" + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion)
+ {
+ TreatUnmatchedTokensAsErrors = true
+ };
+
+ CliOption runtimeGraphPathOption = new("--runtimeGraphPath")
+ {
+ Description = "Path to runtime graph path to use for RID mapping.",
+ Required = true
+ };
+
+ CliOption netcoreSdkRuntimeIdentifierOption = new("--netcoreSdkRuntimeIdentifier")
+ {
+ Description = "RID to use for finding the best applicable RID from mapping.",
+ Required = true
+ };
+
+ CliOption supportedRidsOption = new("--supportedRids")
+ {
+ Description = "List of RIDs that are supported. Comma-separated.",
+ Required = true,
+ Arity = ArgumentArity.OneOrMore,
+ CustomParser = ParseSupportedRidsArgument
+ };
+
+ rootCommand.Options.Add(runtimeGraphPathOption);
+ rootCommand.Options.Add(netcoreSdkRuntimeIdentifierOption);
+ rootCommand.Options.Add(supportedRidsOption);
+ rootCommand.SetAction((ParseResult parseResult) =>
+ {
+ string rgp = parseResult.GetValue(runtimeGraphPathOption) ?? throw new InvalidOperationException("The --runtimeGraphPath argument is required.");
+
+ if (!File.Exists(rgp))
+ {
+ Console.WriteLine($"File {rgp} does not exist. Please ensure the runtime graph path exists.");
+ return -1;
+ }
+
+ RuntimeGraph graph = JsonRuntimeFormat.ReadRuntimeGraph(rgp);
+
+ var ridToUse = parseResult.GetValue(netcoreSdkRuntimeIdentifierOption);
+
+ var supportedRids = parseResult.GetValue(supportedRidsOption);
+
+ string? bestRidForPlatform = NuGetUtils.GetBestMatchingRid(graph, ridToUse!, supportedRids!, out bool wasInGraph);
+
+ if (!wasInGraph)
+ {
+ Console.WriteLine("Unable to find the best rid to use");
+ return -1;
+ }
+
+ Console.WriteLine(bestRidForPlatform);
+ return 0;
+ });
+
+ return rootCommand.Parse(args).Invoke();
+ }
+
+ private static string[]? ParseSupportedRidsArgument(ArgumentResult result)
+ {
+ List args = new();
+
+ foreach (var token in result.Tokens)
+ {
+ args.AddRange(token.Value.Split(','));
+ }
+
+ return args.ToArray();
+ }
+}
diff --git a/src/Aspire.Hosting.Sdk/SDK/Sdk.in.targets b/src/Aspire.Hosting.Sdk/SDK/Sdk.in.targets
index 3987bf67320..1ce3b25dd79 100644
--- a/src/Aspire.Hosting.Sdk/SDK/Sdk.in.targets
+++ b/src/Aspire.Hosting.Sdk/SDK/Sdk.in.targets
@@ -43,6 +43,12 @@
+
+ $(MSBuildThisFileDirectory)..\tools\@DefaultTargetFramework@\
+ $([MSBuild]::NormalizePath('$(AspireRidToolRoot)\'))
+ $(AspireRidToolDirectory)Aspire.RuntimeIdentifier.Tool.dll
+
+
+
+ <_DashboardAndDCPSupportedTargetingRIDs Include="win-x64" />
+ <_DashboardAndDCPSupportedTargetingRIDs Include="win-x86" />
+ <_DashboardAndDCPSupportedTargetingRIDs Include="win-arm64" />
+ <_DashboardAndDCPSupportedTargetingRIDs Include="linux-x64" />
+ <_DashboardAndDCPSupportedTargetingRIDs Include="linux-arm64" />
+ <_DashboardAndDCPSupportedTargetingRIDs Include="osx-x64" />
+ <_DashboardAndDCPSupportedTargetingRIDs Include="osx-arm64" />
+
+
+
+
+ <_DotNetHostPath>$(DOTNET_HOST_PATH)
+
+
+
+
+ <_DotNetHostDirectory>$(NetCoreRoot)
+ <_DotNetHostFileName>dotnet
+ <_DotNetHostFileName Condition="'$(OS)' == 'Windows_NT'">dotnet.exe
+
+ <_DotNetHostPath>$(_DotNetHostDirectory)\$(_DotNetHostFileName)
+
+
+
+
+
+
+
+
+
+
+
+ <_DashboardAndDcpRID>@(_AspireRidToolOutput)
+
+
+
-
-
+
+
diff --git a/tests/Aspire.Hosting.Sdk.Tests/Aspire.Hosting.Sdk.Tests.csproj b/tests/Aspire.Hosting.Sdk.Tests/Aspire.Hosting.Sdk.Tests.csproj
new file mode 100644
index 00000000000..c1d17f2fa6f
--- /dev/null
+++ b/tests/Aspire.Hosting.Sdk.Tests/Aspire.Hosting.Sdk.Tests.csproj
@@ -0,0 +1,16 @@
+
+
+
+ $(NetCurrent)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Aspire.Hosting.Sdk.Tests/NuGetUtils.Tests.cs b/tests/Aspire.Hosting.Sdk.Tests/NuGetUtils.Tests.cs
new file mode 100644
index 00000000000..46ba5483885
--- /dev/null
+++ b/tests/Aspire.Hosting.Sdk.Tests/NuGetUtils.Tests.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using NuGet.RuntimeModel;
+using Xunit;
+
+namespace Aspire.Hosting.Sdk.Tests;
+
+public class NuGetUtilsTests
+{
+ [Theory]
+ // Matching RID cases
+ [InlineData("win-x64", "win-x64")]
+ [InlineData("win-x86", "win-x86")]
+ [InlineData("win-arm64", "win-arm64")]
+ [InlineData("linux-x64", "linux-x64")]
+ [InlineData("linux-arm64", "linux-arm64")]
+ [InlineData("osx-x64", "osx-x64")]
+ [InlineData("osx-arm64", "osx-arm64")]
+
+ //Compatible RID cases
+ [InlineData("rhel.8-x64", "linux-x64")] // https://github.com/dotnet/aspire/issues/5486
+ [InlineData("ubuntu.23.04-x64", "linux-x64")]
+ [InlineData("fedora.39-x64", "linux-x64")]
+ [InlineData("linux-musl-x64", "linux-x64")]
+ public void RightRIDIsSelected(string inputRID, string expectedRID)
+ {
+ RuntimeGraph graph = JsonRuntimeFormat.ReadRuntimeGraph("RuntimeIdentifierGraph.json");
+
+ var result = NuGetUtils.GetBestMatchingRid(graph, inputRID, new[] { "win-x64", "win-arm64", "win-x86",
+ "linux-x64", "linux-arm64",
+ "osx-x64", "osx-arm64"}, out bool wasInGraph);
+
+ Assert.Equal(expectedRID, result);
+ Assert.True(wasInGraph);
+ }
+}