Skip to content

Conversation

@dsplaisted
Copy link
Member

Work in progress, currently supports RID-specific packages for global tools.

This comment was marked as outdated.

Copy link
Member

@baronfel baronfel left a comment

Choose a reason for hiding this comment

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

Left a few notes. I think this is good to go into preview as it is. There are a few follow-up questions and scenarios I want to cover:

  • I think we need to be able to support both RID-agnostic and RID-specific tools at install-time (and when packaging) to make sure that older SDKs can continue to install tools that start opting in to RID-specific mode
  • I don't want users to have to make MSBuild Items - we already know that users don't understand/think about Items, and they're used to dealing with RIDs as properties
  • I really want to get to a single-command invocation when possible (i.e. when NativeAOT isn't happening) instead of having to do N+1 invocations, where N is the number of RIDs to publish for.

Comment on lines +279 to +297
if (toolConfiguration.RidSpecificPackages?.Any() == true)
{
var runtimeGraph = JsonRuntimeFormat.ReadRuntimeGraph(_runtimeJsonPath);
var bestRuntimeIdentifier = Microsoft.NET.Build.Tasks.NuGetUtils.GetBestMatchingRid(runtimeGraph, RuntimeInformation.RuntimeIdentifier, toolConfiguration.RidSpecificPackages.Keys, out bool wasInGraph);
if (bestRuntimeIdentifier == null)
{
throw new ToolPackageException(string.Format(CliStrings.ToolUnsupportedRuntimeIdentifier, RuntimeInformation.RuntimeIdentifier,
string.Join(" ", toolConfiguration.RidSpecificPackages.Keys)));
}

var resolvedPackage = toolConfiguration.RidSpecificPackages[bestRuntimeIdentifier];

if (!IsPackageInstalled(new PackageId(resolvedPackage.Id), resolvedPackage.Version, packageDownloadDir.Value))
{
DownloadAndExtractPackage(new PackageId(resolvedPackage.Id), nugetPackageDownloader, packageDownloadDir.Value, resolvedPackage.Version, packageSourceLocation, includeUnlisted: true);
}

CreateAssetFile(new PackageId(resolvedPackage.Id), resolvedPackage.Version, packageDownloadDir, Path.Combine(assetFileDirectory.Value, ToolPackageInstance.RidSpecificPackageAssetsFileName), _runtimeJsonPath, targetFramework);
}
Copy link
Member

Choose a reason for hiding this comment

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

Effectively, this means that as soon as you have any RID-specific packages, you must have RID-specific packages for all platforms you intend to support. This seems limiting - ideally we'd be able to support a RID-agnostic package and a number of RID-specific packages, and in this way older SDKs can use the RID-agnostic package, and newer SDKs can use a RID-specific package when available, and then fallback to the RID-agnostic package if there's no best-fit in the RID-specific packages.

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 it's going to be tricky to support old SDKs. Older SDKs aren't going to be able to understand a newer package / tool configuration XML format. So I think the only way that would work is if the primary tool package still contained the RID-agnostic tool implementation. But then for newer SDKs you're downloading the whole RID-agnostic implementation just to figure out that you'd prefer a RID-specific package and then go download that.

I think it's probably OK if this is a new feature and if tools want to take advantage of it they won't work with older SDKs. There is a format version in the tool configuration XML, so older SDKs will get a warning message saying they don't support the package. (I would have made it an error probably, but for older SDKs we're stuck with existing behavior.)

'@(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.
Copy link
Member

Choose a reason for hiding this comment

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

After talking with @nkolev92, we can potentially skip the need for this RemoveTargetFromList Task entirely if we set IncludeBuildOutput to false for the outer RID-specific tool package (see here). If we do this then even if the Build target is called no content from it will be auto-included in the package, which seems cleaner to me than modifying the Target list in this way.

Copy link
Member Author

Choose a reason for hiding this comment

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

IncludeBuildOutput is already set to false since the tool targets explicitly add the publish output to the package. I had to disable build from running because otherwise either PublishAot would cause a default RuntimeIdentifier to be set, and if I disabled that then the build would error out because PublishAot needs a RuntimeIdentifier.

//
// The main thing this class can't handle is if the way the .NET SDK builds packages changes. In CI runs, we should use a clean test execution folder each time (I think!), so this shouldn't be an issue.
// For local testing, you may need to delete the artifacts\tmp\Debug\TestTools folder if the SDK changes in a way that affects the built package.
public class TestToolBuilder
Copy link
Member

Choose a reason for hiding this comment

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

This is great, thanks :)

try
{
// NOTE: The manifest file may or may not be under a .config folder. If it is, we will use
// that directory as the root config directory. This should be OK, as usually there won't be
Copy link
Member

Choose a reason for hiding this comment

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

nit: Extra space in the comment

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean after the period? I often put two spaces after the end of a sentence as that's what I learned when I was learning to type (on a mechanical typewriter!).

if (OperatingSystem.IsWindows())
{
return _shimsDirectory.WithFile(commandName.Value + ".exe");
if (toolCommand.Runner == "dotnet")
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to support Powershell at all?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, but testing just now in powershell it seems like it will resolve .cmd files that are in the path:

PS C:\Users\Daniel> ToolTest
Hello, Tool!
PS C:\Users\Daniel> (Get-Command ToolTest).Path
C:\Users\Daniel\.dotnet\tools\ToolTest.cmd

So it looks like we don't need to do anything more.

bool isGlobalTool = false,
RestoreActionConfig restoreActionConfig = null)

// The following methods are copied from the LockFileUtils class in Nuget.Client
Copy link
Member

Choose a reason for hiding this comment

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

Does this risk missing out on changes to the Nuget.Client?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it's possible. In practice it's not a whole lot of code. Also it's code that was already copied into the SDK repo when we switched from running Restore on a temporary project to using the NuGet package download APIs. This PR just refactors and moves the code around.


if (commandRunner != "dotnet")
{
formatVersion = 2;
Copy link
Member

Choose a reason for hiding this comment

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

Can this be linked back to the other formatVersion in the CLI or does that not make sense, something like const int LatestToolFormatVersion = 2; ? Also, should we check for specific command runners

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 didn't do something like that since the version written depends on the features you use, so if you don't use new features we'll use the old format version and be backwards compatible.

I have now just added an error if you set the runner to something besides "dotnet" or "executable".

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.

Thanks for responding to my changes. I would like to do more testing, but I know that we'd like to get this in quickly. I agree with @baronfel's suggestions about a future improvement PR. In the future, it'd be nice to have more telem to understand how users are interfacing with/using the feature. Excited for this change and it's a big feature! 🙌

@dsplaisted
Copy link
Member Author

I believe @dsplaisted wanted @rainersigwald to look over these MSBuild workarounds

Yes, I had forgotten to ping Rainer about that.

Also @nkolev92 @dotnet/nuget-team this is a pretty big PR, but you might want to look at the build logic parts of it.

@dsplaisted
Copy link
Member Author

/azp run dotnet-sdk-public-ci

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

return packageOutputPath;
}

public void RemovePackageFromGlobalPackages(ITestOutputHelper log, string packageId, string version)
Copy link
Member

@zivkan zivkan Jun 6, 2025

Choose a reason for hiding this comment

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

In case you're not already aware, the global packages folder can be changed via nuget.config, with the globalPackagesFolder config. NuGet does this in the NuGet.Client repo for restore tests, and deletes the entire directory after each test. But considering this PR is about tools, I'm not sure if dotnet tools loads nuget.config files, and if so, if it takes into account the CWD, or if it looks at the user nuget.config only.

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 believe we redirect the global packages folder to a local folder, but we may not clean it each time for perf reasons. So this method uses the NuGet locals command to resolve the correct global packages folder in the current context (and tools do take into account NuGet.config configuration from the current working directory).

if (!Uri.IsWellFormedUriString(feed, UriKind.Absolute) && !Path.IsPathRooted(feed))
{
localFeedsThatIsRooted[index] = Path.GetFullPath(feed);
localFeedsThatIsRooted[index] = Path.GetFullPath(feed, basePath);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this order correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, the second parameter is what replaces the current directory if you didn't pass it in.

@dsplaisted dsplaisted merged commit 47d0bba into dotnet:main Jun 6, 2025
21 of 30 checks passed
@baronfel baronfel added breaking-change Using this label will notify dotnet/compat and trigger a request to file a compat bug needs-breaking-change-doc-created labels Aug 11, 2025
@dotnet-policy-service
Copy link
Contributor

dotnet-policy-service bot commented Aug 11, 2025

Added needs-breaking-change-doc-created label because this PR has the breaking-change label.

When you commit this breaking change:

  1. Create and link to this PR and the issue a matching issue in the dotnet/docs repo using the breaking change documentation template, then remove this needs-breaking-change-doc-created label.
  2. Ask a committer to mail the .NET SDK Breaking Change Notification email list.

You can refer to the .NET SDK breaking change guidelines

@baronfel
Copy link
Member

Added the breaking change label based on feedback from #49840 - we need to have a breaking change notice to the .NET 10 release notes that dotnet pack of tools changed from the Build target to the Publish target regardless of multi-RID support or not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Tools breaking-change Using this label will notify dotnet/compat and trigger a request to file a compat bug untriaged Request triage from a team member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants