diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs b/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs index 5b4cecbcfd88..7dbc792028a2 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/DependencyContextBuilder.cs @@ -112,7 +112,7 @@ public DependencyContextBuilder( runtimeIdentifier, string.IsNullOrWhiteSpace(platformLibraryName)); - _isPortable = _isFrameworkDependent && string.IsNullOrEmpty(_runtimeIdentifier); + _isPortable = _isFrameworkDependent && (string.IsNullOrEmpty(_runtimeIdentifier) || _runtimeIdentifier == "any"); if (_isFrameworkDependent != true || _isPortable != true) { @@ -317,7 +317,7 @@ public DependencyContext Build(string[] userRuntimeAssemblies = null) * 1. If runtimeAssemblyGroups, nativeLibraryGroups, dependencies, and resourceAssemblies are all empty, remove this runtimeLibrary as well as any dependencies on it. * 2. Add all runtimeLibraries to a list of to-be-processed libraries called libraryCandidatesForRemoval * 3. libraryCandidatesForRemoval.Pop() --> if there are no runtimeAssemblyGroups, nativeLibraryGroups, or resourceAssemblies, and either dependencies is empty or all - * dependencies have something else that depends on them, remove it (and from libraryCandidatesForRemoval), adding everything that depends on this to + * dependencies have something else that depends on them, remove it (and from libraryCandidatesForRemoval), adding everything that depends on this to * libraryCandidatesForRemoval if it isn't already there * Repeat 3 until libraryCandidatesForRemoval is empty */ @@ -483,8 +483,8 @@ public DependencyContext Build(string[] userRuntimeAssemblies = null) runtimeSignature: string.Empty, _isPortable); - // Compute the runtime fallback graph - // + // Compute the runtime fallback graph + // // If the input RuntimeGraph is empty, or we're not compiling // for a specific RID, then an runtime fallback graph is empty // diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index 8b89525c9fd1..e3245a4f5103 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -31,6 +31,11 @@ public class GenerateDepsFile : TaskBase public string RuntimeIdentifier { get; set; } + /// + /// Strips the RID if it's any, because that's not reasonable + /// + private string EffectiveRuntimeIdentifier => string.IsNullOrEmpty(RuntimeIdentifier) ? null : RuntimeIdentifier == "any" ? null : RuntimeIdentifier; + public string PlatformLibraryName { get; set; } public ITaskItem[] RuntimeFrameworks { get; set; } @@ -95,7 +100,7 @@ public class GenerateDepsFile : TaskBase public bool IncludeProjectsNotInAssetsFile { get; set; } - // List of runtime identifer (platform part only) to validate for runtime assets + // List of runtime identifier (platform part only) to validate for runtime assets // If set, the task will warn on any RIDs that aren't in the list public string[] ValidRuntimeIdentifierPlatformsForAssets { get; set; } @@ -137,7 +142,7 @@ private void WriteDepsFile(string depsFilePath) LockFile lockFile = new LockFileCache(this).GetLockFile(AssetsFilePath); projectContext = lockFile.CreateProjectContext( TargetFramework, - RuntimeIdentifier, + EffectiveRuntimeIdentifier, PlatformLibraryName, RuntimeFrameworks, IsSelfContained); @@ -226,7 +231,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) RuntimeFrameworks, isSelfContained: IsSelfContained, platformLibraryName: PlatformLibraryName, - runtimeIdentifier: RuntimeIdentifier, + runtimeIdentifier: EffectiveRuntimeIdentifier, targetFramework: TargetFramework); } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/LockFileExtensions.cs b/src/Tasks/Microsoft.NET.Build.Tasks/LockFileExtensions.cs index 5c03457ffc3a..6da136843d6b 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/LockFileExtensions.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/LockFileExtensions.cs @@ -94,7 +94,7 @@ public static ProjectContext CreateProjectContext( public static bool IsFrameworkDependent(ITaskItem[] runtimeFrameworks, bool isSelfContained, string runtimeIdentifier, bool hasPlatformLibrary) { return (hasPlatformLibrary || runtimeFrameworks?.Any() == true) && - (!isSelfContained || string.IsNullOrEmpty(runtimeIdentifier)); + (!isSelfContained || (string.IsNullOrEmpty(runtimeIdentifier) || runtimeIdentifier == "any")); } public static LockFileTargetLibrary GetLibrary(this LockFileTarget lockFileTarget, string libraryName) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs index db3c6905fc12..5569f32a4cde 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs @@ -335,6 +335,7 @@ out List knownRuntimePacksForTargetFramework var runtimeRequiredByDeployment = (SelfContained || ReadyToRunEnabled) && !string.IsNullOrEmpty(RuntimeIdentifier) && + RuntimeIdentifier != "any" && selectedRuntimePack != null && !string.IsNullOrEmpty(selectedRuntimePack.Value.RuntimePackNamePatterns); @@ -372,6 +373,13 @@ var runtimeRequiredByDeployment continue; } + if (runtimeIdentifier == "any") + { + // The `any` RID represents a platform-agnostic target. As such, it has no + // platform-specific runtime pack associated with it. + continue; + } + // Pass in null for the runtimePacks list, as for these runtime identifiers we only want to // download the runtime packs, but not use the assets from them ProcessRuntimeIdentifier(runtimeIdentifier, runtimePackForRuntimeIDProcessing, runtimePackVersion, additionalFrameworkReferencesForRuntimePack: null, diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveAppHosts.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveAppHosts.cs index ae5308daad31..00c6bf6fdcc3 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveAppHosts.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveAppHosts.cs @@ -188,6 +188,13 @@ protected override void ExecuteCore() { foreach (var otherRuntimeIdentifier in OtherRuntimeIdentifiers) { + // The 'any' RID represents a platform-agnostic platform. As such, it has no + // apphost pack associated with it. + if (otherRuntimeIdentifier == "any") + { + continue; + } + // Download any apphost packages for other runtime identifiers. // This allows you to specify the list of RIDs in RuntimeIdentifiers and only restore once, // and then build for each RuntimeIdentifier without restoring separately. diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs b/test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs index d9da99103c5c..fdf93944da13 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs @@ -260,11 +260,9 @@ public void PackageToolWithAnyRid() .And.Satisfy(EnsurePackageIsFdd); // top-level package should declare all of the rids - var topLevelPackage = packages.First(p => p.EndsWith($"{packageIdentifier}.{toolSettings.ToolPackageVersion}.nupkg")); - var settingsXml = GetToolSettingsFile(topLevelPackage); - var packageNodes = GetRidsInSettingsFile(settingsXml); - - packageNodes.Should().BeEquivalentTo([.. expectedRids, "any"], "The top-level package should declare all of the RIDs for the tools it contains"); + var topLevelPackage = packages.FirstOrDefault(p => p.EndsWith($"{packageIdentifier}.{toolSettings.ToolPackageVersion}.nupkg")); + topLevelPackage.Should().NotBeNull($"Package {packageIdentifier}.{toolSettings.ToolPackageVersion}.nupkg should be present in the tool packages directory") + .And.Satisfy(SupportAllOfTheseRuntimes([.. expectedRids, "any"])); } [Fact] @@ -353,6 +351,45 @@ public void StripsPackageTypesFromInnerToolPackages() foundRids.Should().BeEquivalentTo(expectedRids, "The top-level package should declare all of the RIDs for the tools it contains"); } + [Fact] + public void MixedPackageTypesBuildInASingleBatchSuccessfully() + { + var toolSettings = new TestToolBuilder.TestToolSettings() + { + RidSpecific = true, + IncludeAnyRid = true, + SelfContained = true // ensure that the RID-specific packages get runtime packs/assets - but the any RID package does not! + }; + string toolPackagesPath = ToolBuilder.CreateTestTool(Log, toolSettings, collectBinlogs: true); + + var packages = Directory.GetFiles(toolPackagesPath, "*.nupkg"); + var packageIdentifier = toolSettings.ToolPackageId; + var ridSpecificPackages = ToolsetInfo.LatestRuntimeIdentifiers.Split(';'); + packages.Length.Should().Be(ridSpecificPackages.Length + 1 + 1, "There should be one package for the tool-wrapper and one for each RID, and one for the any rid"); + foreach (string rid in ridSpecificPackages) + { + var packageName = $"{toolSettings.ToolPackageId}.{rid}.{toolSettings.ToolPackageVersion}"; + var package = packages.FirstOrDefault(p => p.EndsWith(packageName + ".nupkg")); + package.Should() + .NotBeNull($"Package {packageName} should be present in the tool packages directory") + .And.Satisfy(EnsurePackageIsAnExecutable) + .And.Satisfy(EnsurePackageOnlyHasToolRidPackageType); + } + + var agnosticFallbackPackageId = $"{toolSettings.ToolPackageId}.any.{toolSettings.ToolPackageVersion}"; + var agnosticFallbackPackage = packages.FirstOrDefault(p => p.EndsWith(agnosticFallbackPackageId + ".nupkg")); + agnosticFallbackPackage.Should() + .NotBeNull($"Package {agnosticFallbackPackageId} should be present in the tool packages directory") + .And.Satisfy(EnsurePackageIsFdd) + .And.Satisfy(EnsurePackageOnlyHasToolRidPackageType); + + // top-level package should declare all of the rids + var topLevelPackage = packages.First(p => p.EndsWith($"{packageIdentifier}.{toolSettings.ToolPackageVersion}.nupkg")); + topLevelPackage.Should().NotBeNull($"Package {packageIdentifier}.{toolSettings.ToolPackageVersion}.nupkg should be present in the tool packages directory") + .And.Satisfy(EnsurePackageHasNoRunner) + .And.Satisfy(SupportAllOfTheseRuntimes([..ridSpecificPackages, "any"])); + } + private Action EnsurePackageHasToolPackageTypeAnd(string[] additionalPackageTypes) => (string packagePath) => { var nuspec = GetPackageNuspec(packagePath); @@ -363,6 +400,13 @@ private Action EnsurePackageHasToolPackageTypeAnd(string[] additionalPac .And.BeEquivalentTo(expectedPackageTypes, "The PackageType should be 'DotnetTool'."); }; + private Action SupportAllOfTheseRuntimes(string[] runtimes) => (string packagePath) => + { + var settingsXml = GetToolSettingsFile(packagePath); + var rids = GetRidsInSettingsFile(settingsXml); + rids.Should().BeEquivalentTo(runtimes, "The tool settings file should contain all of the specified RuntimeIdentifierPackage elements."); + }; + static void EnsurePackageOnlyHasToolRidPackageType(string packagePath) { var nuspec = GetPackageNuspec(packagePath); diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/TestToolBuilder.cs b/test/Microsoft.DotNet.PackageInstall.Tests/TestToolBuilder.cs index 45aba2344727..40136f094758 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/TestToolBuilder.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/TestToolBuilder.cs @@ -28,12 +28,28 @@ public class TestToolSettings public string ToolPackageVersion { get; set; } = "1.0.0"; public string ToolCommandName { get; set; } = "TestTool"; public string[]? AdditionalPackageTypes { get; set; } = null; - public bool NativeAOT { get; set { field = value; this.RidSpecific = value; } } = false; public bool SelfContained { get; set { field = value; this.RidSpecific = value; } } = false; public bool Trimmed { get; set { field = value; this.RidSpecific = value; } } = false; + + /// + /// If set, the generated tool will include the any RID in the list of RIDs to target. + /// This will cause a framework-dependent, platform-agnostic package to be created. + /// public bool IncludeAnyRid { get; set { field = value; } } = false; + + /// + /// If set, the generated tool will target all of the RIDs specified in . + /// Defaults to . + /// public bool RidSpecific { get; set; } = false; + + /// + /// If set, the generated tool will include the current executing platform's RID in the list of RIDs to target + /// (which is otherwise made of .) If set to , + /// the current RID will be stripped from that set. + /// Defaults to . + /// public bool IncludeCurrentRid { get; set; } = true; public string GetIdentifier() {