Skip to content

Multi-RID tool packages usability tweaks #49288

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Jun 13, 2025

Conversation

baronfel
Copy link
Member

@baronfel baronfel commented Jun 7, 2025

This PR:

  • Migrates from an Item-based usage mode (<ToolPackageRuntimeIdentifier Include="linux-x64" Version="A.B.C" />) to a property-based usage mode (<ToolPackageRuntimeIdentifier>linux-x64</ToolPackageRuntimeIdentifier>).
    • This implies the removal of the Version from the RID-specific tool package configuration entries (the versions must always be aligned with the 'parent' RID-less package)
  • Simplifies some of the packaging of the Tool packages
  • Makes it so packaging the 'parent' package via dotnet pack triggers the packaging of the 'inner' per-RID packages as well
    • Caveat - this won't ever work if we're in AOT mode, so we don't do that for PublishAOT packages.

@Copilot Copilot AI review requested due to automatic review settings June 7, 2025 01:00
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adjusts the packaging mechanism for RID-specific tool packages by migrating from an item‐based to a property‐based usage mode, streamlining package creation for both parent and RID‐specific packages.

  • Migrates from using an Item-based RID specification with a Version attribute to a property-based configuration without a version.
  • Updates various build targets to correctly trigger and define inner and outer builds, including adjustments to packaging tasks and property conditions.
  • Removes legacy code (such as the version handling in tool package resolution and associated unit tests) to simplify the overall tool package design.

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.targets Updates PackTool import condition to respect PackAsTool usage.
src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.Common.targets Adjusts condition to reference the new property ToolPackageRuntimeIdentifiers.
src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.PackTool.targets Refactors packaging targets to differentiate inner and outer builds and update package paths.
src/Tasks/Microsoft.NET.Build.Tasks/GenerateToolsSettingsFile.cs Removes version attribute assignment in RID package nodes.
src/Cli/dotnet/ToolPackage/ToolPackageDownloaderBase.cs Updates package version sourcing for installation and asset file creation.
src/Cli/dotnet/ToolPackage/ToolConfigurationDeserializer.cs Eliminates version information when creating PackageIdentity from RID-specific data.
src/Cli/dotnet/ToolPackage/ToolConfigurationDeserialization/DotNetCliToolRuntimeIdentifierPackage.cs Removes the Version attribute in favor of property-based configuration.
(Other files) Removal of legacy test and resolution files in-line with the new design.
Comments suppressed due to low confidence (3)

src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.Common.targets:57

  • Verify that the new property 'ToolPackageRuntimeIdentifiers' is correctly defined and consistently propagated across the targets to avoid mismatches with legacy item names.
<_ToolPackageType Condition="'$(RuntimeIdentifier)' != '' And '$(ToolPackageRuntimeIdentifiers)' != ''">DotnetToolRidPackage</_ToolPackageType>

src/Cli/dotnet/ToolPackage/ToolConfigurationDeserializer.cs:70

  • Ensure that all consumers of PackageIdentity are updated to handle a null version, as the version is now inherited from the parent package rather than being stored per RID-specific package.
var ridSpecificPackages = dotNetCliTool.RuntimeIdentifierPackages?.ToDictionary(p => p.RuntimeIdentifier, p => new PackageIdentity(p.Id, null))

src/Cli/dotnet/ToolPackage/ToolPackageDownloaderBase.cs:291

  • Confirm that the 'packageVersion' variable is correctly assigned before this check, as replacing resolvedPackage.Version could lead to unintended behavior if packageVersion is not properly derived.
if (!IsPackageInstalled(new PackageId(resolvedPackage.Id), packageVersion, packageDownloadDir.Value))

@@ -12,6 +12,5 @@ Copyright (c) .NET Foundation. All rights reserved.
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<PropertyGroup>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);PackTool</TargetsForTfmSpecificContentInPackage>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I added a PackAsTool condition to the PackTool.targets imports in Sdk.Common.targets, we can't unconditionally add this anymore. We don't know if PackAsTool has been set at this point (because we're pre-project-file-contents in the MSBuild evaluation ordering), so this should get pushed to the PackTool.targets instead. It doesn't effect ordering in anyway, so this is safe.

Comment on lines 29 to 55
<PublishSelfContained Condition="'$(RuntimeIdentifier)' == ''">false</PublishSelfContained>
<PublishTrimmed Condition="'$(RuntimeIdentifier)' == ''">false</PublishTrimmed>
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' == ''">false</PublishReadyToRun>
<PublishAot Condition="'$(RuntimeIdentifier)' == ''">false</PublishAot>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting these means that if Build is called accidentally for the RID-less build then things can still mostly work.

Comment on lines -46 to -67
<Target Name="PackAsToolSwitchToNoBuild"
BeforeTargets="_IntermediatePack"
Condition=" '$(PackAsTool)' == 'true' And
'@(ToolPackageRuntimeIdentifier)' != '' And
'$(RuntimeIdentifier)' == ''">

<!-- If we are building the primary package for a tool with RID-specific packages, we don't include any of the build output in the package, so we don't need to run Build.
We might not be able to build without a RuntimeIdentifier if PublishAOT or SelfContained is set to true, so we skip the build so it doesn't fail.

We use a task to remove the Build target from GenerateNuspecDependsOn. This is effectively what happens if NoBuild is set, but we can't use the same logic because
we need to look at items (ToolPackageRuntimeIdentifier) to determine if we are building the primary package for a tool with RID-specific packages. So we have to fix
up the dependency list afterwards.

We also can't use MSBuild property functions for string replacement to remove Build from the list, because MSBuild will escape the semicolons as %3B, and there
doesn't seem to be a way of avoiding that without using a task.
-->
<RemoveTargetFromList TargetList="$(GenerateNuspecDependsOn)"
TargetToRemove="Build">
<Output TaskParameter="UpdatedTargetList" PropertyName="GenerateNuspecDependsOn" />
</RemoveTargetFromList>

</Target>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't required anymore because we're preventing the errors that calling Build caused in the Property-sets above.

<Target Name="PackTool" DependsOnTargets="SetPackToolProperties;GenerateToolsSettingsFileFromBuildProperty;PackToPublishDependencyIndirection;_PackToolValidation;PackToolImplementation" Condition=" '$(PackAsTool)' == 'true' ">
<ItemGroup>
<_GeneratedFiles Include="$(_ToolsSettingsFilePath)"/>
<_PublishFilesRaw Condition="'$(RuntimeIdentifier)' != ''" Include="$(PublishDir)/**/*" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't rely on ResolvedFilesToPublish - it's not comprehensive enough. When working with container publish incrementality I found that there are tons of folks that just copy things to the PublishDir and expect that to work. So we have to glob across the publish dir and use that as the source of truth for the package content. The good news is that it makes deriving the Relative and Package paths trivial, so we can removee the entire Task invocation that used to do that.

<ItemGroup>
<TfmSpecificPackageFile Include="@(_GeneratedFiles)">
<PackagePath>tools/$(_ToolPackShortTargetFrameworkName)/any/%(_GeneratedFiles.RecursiveDir)%(_GeneratedFiles.Filename)%(_GeneratedFiles.Extension)</PackagePath>
<PackagePath>tools/$(_ToolPackShortTargetFrameworkName)/$(_ToolRidPath)/%(_GeneratedFiles.RecursiveDir)%(_GeneratedFiles.Filename)%(_GeneratedFiles.Extension)</PackagePath>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request from NuGet here that we maintain the RID in this path if we're doing RID-specific things

@@ -1431,7 +1431,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<Import Project="$(MSBuildThisFileDirectory)Microsoft.NET.ObsoleteReferences.targets" />
<Import Project="$(MSBuildThisFileDirectory)Microsoft.NET.Publish.targets" />
<Import Project="$(MSBuildThisFileDirectory)Microsoft.NET.PackStubs.targets" Condition="('$(Language)' == 'C++' and '$(_EnablePackageReferencesInVCProjects)' != 'true')"/>
<Import Project="$(MSBuildThisFileDirectory)Microsoft.NET.PackTool.targets" />
<Import Project="$(MSBuildThisFileDirectory)Microsoft.NET.PackTool.targets" Condition="'$(PackAsTool)' == 'true'" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this conditional means that we can modify things to our heart's content inside the PackTool.targets and not worry about corrupting non-Tool projects' state.

@baronfel baronfel requested a review from a team June 7, 2025 15:15
@baronfel baronfel requested a review from a team as a code owner June 7, 2025 15:28
@baronfel
Copy link
Member Author

baronfel commented Jun 8, 2025

All of the failing tests are around packaging the shims. After my changes the shims are getting the NuGetRecursiveDir metadata added onto them, which is causing them to be put into additional nested subfolders due to logic in the Pack targets:

NuGet.Build.Tasks.Pack.targets:

  <Target Name="_GetTfmSpecificContentForPackage"
          DependsOnTargets="$(TargetsForTfmSpecificContentInPackage)"
          Returns="@(TfmSpecificPackageFileWithRecursiveDir)">
    <!-- The below workaround needs to be done due to msbuild bug https://github.com/Microsoft/msbuild/issues/3121 -->
    <ItemGroup>
      <TfmSpecificPackageFileWithRecursiveDir Include="@(TfmSpecificPackageFile)">
        <NuGetRecursiveDir>%(TfmSpecificPackageFile.RecursiveDir)</NuGetRecursiveDir>
        <BuildAction>%(TfmSpecificPackageFile.BuildAction)</BuildAction>
      </TfmSpecificPackageFileWithRecursiveDir>
    </ItemGroup>
  </Target>

Bad item:
image

Leads to bad content:
image

@baronfel
Copy link
Member Author

baronfel commented Jun 8, 2025

I think I've got it - since NuGet forces the use of RecursiveDir if it's present, we just lean into that and don't try to be quite as precise with our PackagePath setting. I've pushed up a commit with comments and adjustments for that.

@baronfel
Copy link
Member Author

baronfel commented Jun 9, 2025

Alright - this is super close so is probably ok for review now. The only tests failing don't fail locally, and cover a pretty specific scenario: It_uses_customized_PackagedShimOutputRootDirectory.

@@ -157,7 +157,7 @@ public void It_uses_customized_PackagedShimOutputRootDirectory(bool multiTarget,

var packCommand = new PackCommand(Log, helloWorldAsset.TestRoot);

packCommand.Execute().Should().Pass();
packCommand.Execute("-bl:{}").Should().Pass();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this trying to create a binlog during test execution? As mentioned in our triage, even if it works, you cannot get at the file directly in Helix. We need a proper way to do this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can hard code the binlong path in the -bl command, and then dump the binary content into the test output, copy paste it into a file to read it. I like the idea of properly dumping the log from helix, but I don't want to block this PR to do so

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've figured it out, but now Helix is busted. In either case I'll be removing this before we merge this PR.

@baronfel
Copy link
Member Author

Ok, I have added a test to cover the new multi-packaging feature, and have verified via binlogs locally that the shim output path stuff works. I want helix to try one more time to give me a binlog so I can see what's going on in the CI system, but then I want to skip those tests so we can get the good syntax in for p6.

Comment on lines +33 to +54
<_InnerToolsPublishAot>false</_InnerToolsPublishAot>
<_InnerToolsPublishAot Condition="'$(RuntimeIdentifier)' == '' and '$(PublishAot)' == 'true'">true</_InnerToolsPublishAot>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to know if the inner tools are publishing AOT to know if we're allowed to dispatch to the per-RID inner packs below.

<_PackToolPublishDependency Condition=" '$(_ToolPackageShouldIncludeImplementation)' != '' and '$(GeneratePackageOnBuild)' == 'true' and $(IsPublishable) == 'true' ">$(_PublishNoBuildAlternativeDependsOn)</_PackToolPublishDependency>

<!-- Trigger RID-specific inner builds if RID-specific mode is enabled-->
<GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetDotnetToolPackageType;_CreateRIDSpecificToolPackages</GenerateNuspecDependsOn>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that these targets must be called from two different kinds of GenerateNuspec:

  • multi-tfm GenerateNuspec (from the crosstargeting targets)
  • single-tfm GenerateNuSpec (from the 'normal', single-TFM targets)

@@ -13,6 +13,7 @@ Copyright (c) .NET Foundation. All rights reserved.

<Import Project="Microsoft.NET.Sdk.Common.targets"/>
<Import Project="$(MSBuildThisFileDirectory)Microsoft.NET.Sdk.SourceLink.targets" Condition="'$(SuppressImplicitGitSourceLink)' != 'true'"/>
<Import Project="$(MSBuildThisFileDirectory)Microsoft.NET.PackTool.targets" Condition="'$(PackAsTool)' == 'true'" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big change here - we need to import the tool targets at the multi-tfm level so that we can ensure that the correct package types get set.

@baronfel baronfel force-pushed the multi-rid-packages-tweaks branch from 2c15ee9 to 7b701ca Compare June 12, 2025 02:30
@baronfel
Copy link
Member Author

Ok we're good to go now! The test failures are watch and container timeouts, everything else is good.

@baronfel baronfel force-pushed the multi-rid-packages-tweaks branch from bbe108a to 75b4ee6 Compare June 12, 2025 18:36
Copy link
Member

@nagilson nagilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another PR I wish I had more time to look over but I did what I could with the time we have :) I think using properties vs items is a great improvement and overall the change looks good. Kudos for the clever engineering here...

@baronfel
Copy link
Member Author

I've addressed @nagilson's feedback (thanks!) and this should be good to merge now.

@nagilson
Copy link
Member

mmmmm yes :shipit:

@baronfel
Copy link
Member Author

The only remaining failing test is the completions known issue. Let's merge!

@marcpopMSFT
Copy link
Member

Known completion test issue.

@marcpopMSFT marcpopMSFT merged commit d7a7d93 into dotnet:main Jun 13, 2025
27 of 30 checks passed
@baronfel baronfel deleted the multi-rid-packages-tweaks branch June 13, 2025 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants