Skip to content

Fix Framework-Dependent, RuntimeIdentifier-specific tool execution #49521

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 1 commit into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ NOTE: This file is imported from the following contexts, so be aware when writin
</PropertyGroup>

<PropertyGroup Condition="'$(ToolCommandRunner)' == ''">
<ToolCommandRunner>dotnet</ToolCommandRunner>
<ToolCommandRunner Condition="'$(SelfContained)' == 'true'">executable</ToolCommandRunner>
<ToolCommandRunner Condition="!$(UseAppHost)">dotnet</ToolCommandRunner>
<ToolCommandRunner Condition="$(UseAppHost)">executable</ToolCommandRunner>
</PropertyGroup>

<Target Name="_GenerateToolsSettingsFileInputCache">
Expand Down
113 changes: 89 additions & 24 deletions test/Microsoft.DotNet.PackageInstall.Tests/EndToEndToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void InstallAndRunNativeAotGlobalTool()
{
NativeAOT = true
};
string toolPackagesPath = ToolBuilder.CreateTestTool(Log, toolSettings);
string toolPackagesPath = ToolBuilder.CreateTestTool(Log, toolSettings, collectBinlogs: true);

var testDirectory = _testAssetsManager.CreateTestDirectory();

Expand Down Expand Up @@ -138,10 +138,9 @@ public void InstallAndRunNativeAotLocalTool()
[Fact]
public void PackagesMultipleToolsWithASingleInvocation()
{

var toolSettings = new TestToolBuilder.TestToolSettings()
{
SelfContained = true
RidSpecific = true
};
string toolPackagesPath = ToolBuilder.CreateTestTool(Log, toolSettings);

Expand All @@ -154,21 +153,15 @@ public void PackagesMultipleToolsWithASingleInvocation()
{
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");
package.Should()
.NotBeNull($"Package {packageName} should be present in the tool packages directory")
.And.Satisfy<string>(EnsurePackageIsAnExecutable);
}

// top-level package should declare all of the rids
var topLevelPackage = packages.First(p => p.EndsWith($"{packageIdentifier}.{toolSettings.ToolPackageVersion}.nupkg"));
using var zipArchive = ZipFile.OpenRead(topLevelPackage);
var nuspecEntry = zipArchive.GetEntry($"tools/{ToolsetInfo.CurrentTargetFramework}/any/DotnetToolSettings.xml")!;
var stream = nuspecEntry.Open();
var xml = XDocument.Load(stream, LoadOptions.None);
var packageNodes =
(xml.Root!.Nodes()
.First(n => n is XElement e && e.Name == "RuntimeIdentifierPackages") as XElement)!.Nodes()
.Where(n => (n as XElement)!.Name == "RuntimeIdentifierPackage")
.Select(e => (e as XElement)!.Attributes().First(a => a.Name == "RuntimeIdentifier").Value);
packageNodes.Should().BeEquivalentTo(expectedRids, "The top-level package should declare all of the RIDs for the tools it contains");
var foundRids = GetRidsInSettingsFile(topLevelPackage);
foundRids.Should().BeEquivalentTo(expectedRids, "The top-level package should declare all of the RIDs for the tools it contains");
}

[Fact]
Expand All @@ -189,22 +182,94 @@ public void PackagesMultipleTrimmedToolsWithASingleInvocation()
{
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");
EnsurePackageLacksTrimmedDependency(package!, "System.Xml.dll");
package.Should()
.NotBeNull($"Package {packageName} should be present in the tool packages directory")
.And.Satisfy<string>(EnsurePackageIsAnExecutable)
.And.Satisfy((string package) => EnsurePackageLacksTrimmedDependency(package, "System.Xml.dll"));
}

// top-level package should declare all of the rids
var topLevelPackage = packages.First(p => p.EndsWith($"{packageIdentifier}.{toolSettings.ToolPackageVersion}.nupkg"));
using var zipArchive = ZipFile.OpenRead(topLevelPackage);
var nuspecEntry = zipArchive.GetEntry($"tools/{ToolsetInfo.CurrentTargetFramework}/any/DotnetToolSettings.xml")!;
var stream = nuspecEntry.Open();
var xml = XDocument.Load(stream, LoadOptions.None);
var packageNodes =
(xml.Root!.Nodes()
var foundRids = GetRidsInSettingsFile(topLevelPackage);
foundRids.Should().BeEquivalentTo(expectedRids, "The top-level package should declare all of the RIDs for the tools it contains");
}

[Fact]
public void PackagesFrameworkDependentRidSpecificPackagesCorrectly()
{
var toolSettings = new TestToolBuilder.TestToolSettings()
{
RidSpecific = true,
};
string toolPackagesPath = ToolBuilder.CreateTestTool(Log, toolSettings, collectBinlogs: true);

var packages = Directory.GetFiles(toolPackagesPath, "*.nupkg");
var packageIdentifier = toolSettings.ToolPackageId;
var expectedRids = ToolsetInfo.LatestRuntimeIdentifiers.Split(';');

packages.Length.Should().Be(expectedRids.Length + 1, "There should be one package for the tool-wrapper and one for each RID");
foreach (string rid in expectedRids)
{
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<string>(EnsurePackageIsAnExecutable);
}

// top-level package should declare all of the rids
var topLevelPackage = packages.First(p => p.EndsWith($"{packageIdentifier}.{toolSettings.ToolPackageVersion}.nupkg"));
var foundRids = GetRidsInSettingsFile(topLevelPackage);
foundRids.Should().BeEquivalentTo(expectedRids, "The top-level package should declare all of the RIDs for the tools it contains");
}


private void EnsurePackageIsFdd(string packagePath)
{
var settingsXml = GetToolSettingsFile(packagePath);
var runner = GetRunnerFromSettingsFile(settingsXml);
runner.Should().Be("dotnet", "The tool should be packaged as a framework-dependent executable (FDD) with a 'dotnet' runner.");
}

private void EnsurePackageIsAnExecutable(string packagePath)
{
var settingsXml = GetToolSettingsFile(packagePath);
var runner = GetRunnerFromSettingsFile(settingsXml);
runner.Should().Be("executable", "The tool should be packaged as a executable with an 'executable' runner.");
}

private object GetRunnerFromSettingsFile(XElement settingsXml)
{
return settingsXml.Elements("Commands").First().Elements("Command").First().Attribute("Runner")?.Value
?? throw new InvalidOperationException("The tool settings file does not contain a 'Runner' attribute.");
}

private string[] GetRidsInSettingsFile(string packagePath)
{
var settingsXml = GetToolSettingsFile(packagePath);
var rids = GetRidsInSettingsFile(settingsXml);
rids.Should().NotBeEmpty("The tool settings file should contain at least one RuntimeIdentifierPackage element.");
return rids;
}

private string[] GetRidsInSettingsFile(XElement settingsXml)
{
var nodes = (settingsXml.Nodes()
.First(n => n is XElement e && e.Name == "RuntimeIdentifierPackages") as XElement)!.Nodes()
.Where(n => (n as XElement)!.Name == "RuntimeIdentifierPackage")
.Select(e => (e as XElement)!.Attributes().First(a => a.Name == "RuntimeIdentifier").Value);
packageNodes.Should().BeEquivalentTo(expectedRids, "The top-level package should declare all of the RIDs for the tools it contains");
.Select(e => (e as XElement)!.Attributes().First(a => a.Name == "RuntimeIdentifier").Value)
.ToArray();
return nodes;
}

private XElement GetToolSettingsFile(string packagePath)
{
using var zipArchive = ZipFile.OpenRead(packagePath);
var nuspecEntry = zipArchive.Entries.First(e => e.Name == "DotnetToolSettings.xml")!;
var stream = nuspecEntry.Open();
var xml = XDocument.Load(stream, LoadOptions.None);
return xml.Root!;

}

/// <summary>
Expand Down
27 changes: 17 additions & 10 deletions test/Microsoft.DotNet.PackageInstall.Tests/TestToolBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ public class TestToolSettings
public string ToolPackageVersion { get; set; } = "1.0.0";
public string ToolCommandName { get; set; } = "TestTool";

public bool NativeAOT { get; set; } = false;
public bool SelfContained { get; set; } = false;
public bool Trimmed { get; set; } = false;
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;
public bool IncludeAnyRid { get; set { field = value; this.RidSpecific = value; } } = false;
public bool RidSpecific { get; set; } = false;

public string GetIdentifier() => $"{ToolPackageId}-{ToolPackageVersion}-{ToolCommandName}-{(NativeAOT ? "nativeaot" : SelfContained ? "selfcontained" : Trimmed ? "trimmed" : "managed")}";
public string GetIdentifier() => $"{ToolPackageId}-{ToolPackageVersion}-{ToolCommandName}-{(NativeAOT ? "nativeaot" : SelfContained ? "selfcontained" : Trimmed ? "trimmed" : "managed")}{(RidSpecific ? "-specific" : "")}{(IncludeAnyRid ? "-anyrid" : "")}";
}


public string CreateTestTool(ITestOutputHelper log, TestToolSettings toolSettings)
public string CreateTestTool(ITestOutputHelper log, TestToolSettings toolSettings, bool collectBinlogs = false)
{
var targetDirectory = Path.Combine(TestContext.Current.TestExecutionDirectory, "TestTools", toolSettings.GetIdentifier());

Expand All @@ -55,22 +57,27 @@ public string CreateTestTool(ITestOutputHelper log, TestToolSettings toolSetting
testProject.AdditionalProperties["ImplicitUsings"] = "enable";
testProject.AdditionalProperties["Version"] = toolSettings.ToolPackageVersion;

var singleRid = RuntimeInformation.RuntimeIdentifier;
var multiRid = toolSettings.IncludeAnyRid ? $"{ToolsetInfo.LatestRuntimeIdentifiers};any" : ToolsetInfo.LatestRuntimeIdentifiers;

if (toolSettings.RidSpecific)
{
testProject.AdditionalProperties["RuntimeIdentifiers"] = multiRid;
}

if (toolSettings.NativeAOT)
{
testProject.AdditionalProperties["PublishAot"] = "true";
testProject.AdditionalProperties["RuntimeIdentifiers"] = RuntimeInformation.RuntimeIdentifier;
}

if (toolSettings.SelfContained)
{
testProject.AdditionalProperties["SelfContained"] = "true";
testProject.AdditionalProperties["RuntimeIdentifiers"] = ToolsetInfo.LatestRuntimeIdentifiers;
}

if (toolSettings.Trimmed)
{
testProject.AdditionalProperties["PublishTrimmed"] = "true";
testProject.AdditionalProperties["RuntimeIdentifiers"] = ToolsetInfo.LatestRuntimeIdentifiers;
}

testProject.SourceFiles.Add("Program.cs", "Console.WriteLine(\"Hello Tool!\");");
Expand Down Expand Up @@ -101,15 +108,15 @@ public string CreateTestTool(ITestOutputHelper log, TestToolSettings toolSetting
{
new DotnetPackCommand(log)
.WithWorkingDirectory(targetDirectory)
.Execute()
.Execute(collectBinlogs ? $"--bl:{toolSettings.GetIdentifier()}-{{}}" : "")
.Should().Pass();

if (toolSettings.NativeAOT)
{
// For Native AOT tools, we need to repack the tool to include the runtime-specific files that were generated during publish
new DotnetPackCommand(log, "-r", RuntimeInformation.RuntimeIdentifier)
.WithWorkingDirectory(targetDirectory)
.Execute()
.Execute(collectBinlogs ? $"--bl:{toolSettings.GetIdentifier()}-{RuntimeInformation.RuntimeIdentifier}-{{}}" : "")
.Should().Pass();
}

Expand Down