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); + } +}