diff --git a/documentation/specs/ProcessFrameworkReferences-outputs.md b/documentation/specs/ProcessFrameworkReferences-outputs.md new file mode 100644 index 000000000000..ee849f5e6307 --- /dev/null +++ b/documentation/specs/ProcessFrameworkReferences-outputs.md @@ -0,0 +1,345 @@ +# ProcessFrameworkReferences Task Output Specification + +## Overview + +The `ProcessFrameworkReferences` MSBuild task is responsible for resolving framework references and determining which NuGet packages need to be downloaded to support compilation and publishing scenarios. This task produces several output item groups that represent different types of packages required for the build. + +## Output Item Groups + +The task produces the following output collections: + +- **`TargetingPacks`**: Reference assemblies used during compilation +- **`RuntimePacks`**: Runtime-specific implementations of framework libraries +- **`PackagesToDownload`**: Packages that need to be restored via direct PackageDownload mechanism +- **`RuntimeFrameworks`**: Framework dependencies to be written to runtimeconfig.json +- **`ImplicitPackageReferences`**: Build-time tool packages that need to be restored via PackageReference mechanism (ILLink, Crossgen2, ILCompiler) +- **`Crossgen2Packs`**: Ready-to-Run compilation tools +- **`HostILCompilerPacks`**: Native AOT compiler tools for the host platform +- **`TargetILCompilerPacks`**: Native AOT compiler tools for the target platform +- **`UnavailableRuntimePacks`**: Framework packs not available for the specified RID + +## Expected Outputs by Scenario + +### Base Scenario: Framework-Dependent Projects + +For all projects that reference frameworks (via `` items): + +**Expected Outputs:** +- ✅ `TargetingPacks`: One entry per framework reference per target framework + - Contains the targeting pack name and version + - Used for compile-time reference resolution +- ✅ `RuntimeFrameworks`: One entry per framework reference per target framework + - Written to runtimeconfig.json to specify runtime dependencies + +**Example:** +```xml + + +``` + +Results in: +- `TargetingPacks`: `Microsoft.NETCore.App.Ref`, `Microsoft.AspNetCore.App.Ref` +- `RuntimeFrameworks`: `Microsoft.NETCore.App`, `Microsoft.AspNetCore.App` + +--- + +### Scenario 1: Self-Contained Deployment (`SelfContained=true`, `PublishSelfContained=true`) + +When `RuntimeIdentifier` is specified explicitly: + +**Expected Outputs:** +- ✅ `TargetingPacks`: One per framework reference per TFM +- ✅ `RuntimePacks`: One per framework reference per TFM for the specified RID + - Contains RID-specific runtime libraries + - Example: `Microsoft.NETCore.App.Runtime.linux-x64` +- ✅ `PackagesToDownload`: Includes both targeting packs and runtime packs +- ✅ `RuntimeFrameworks`: One per framework reference per TFM + +**Key Properties:** +```xml +true +linux-x64 +``` + +**Behavior:** +- Runtime packs are resolved for the primary `RuntimeIdentifier` +- RID-specific assets are included in the publish output + +--- + +### Scenario 2: Self-Contained with multiple RuntimeIdentifiers (`RuntimeIdentifiers` property) + +When multiple RIDs are specified via `RuntimeIdentifiers`: + +**Expected Outputs:** +- ✅ `TargetingPacks`: One per framework reference per TFM +- ✅ `RuntimePacks`: + - For the primary RID (if `RuntimeIdentifier` is set): Full runtime pack metadata + - For additional RIDs: Runtime packs are downloaded but not added to `RuntimePacks` output +- ✅ `PackagesToDownload`: Runtime packs for ALL RIDs in `RuntimeIdentifiers` +- ✅ `RuntimeFrameworks`: One per framework reference per TFM + +**Key Properties:** +```xml +true +linux-x64 +linux-x64;linux-arm64;win-x64 +``` + +**Behavior:** +- All runtime packs for all RIDs are downloaded to support multi-RID publishing +- Only the primary RID's runtime packs appear in the `RuntimePacks` output for consumption + +--- + +### Scenario 3: Ready-to-Run Compilation (`ReadyToRunEnabled=true`, `ReadyToRunUseCrossgen2=true`) + +For projects using Ready-to-Run (R2R) compilation: + +**Expected Outputs:** +- ✅ `TargetingPacks`: One per framework reference per TFM +- ✅ `RuntimePacks`: One per framework reference per TFM for the specified RID +- ✅ `Crossgen2Packs`: RID-specific Crossgen2 compiler for the host platform + - Example: `Microsoft.NETCore.App.Crossgen2.linux-x64` +- ✅ `PackagesToDownload`: Includes targeting packs, runtime packs (if applicable), and Crossgen2 pack +- ✅ `RuntimeFrameworks`: One per framework reference per TFM + +**Key Properties:** +```xml +true +linux-x64 +true +true +``` + +**Behavior:** +- Crossgen2 pack is resolved based on the SDK's runtime identifier (host RID) +- Enables ahead-of-time compilation of IL to native code for faster startup + +--- + +### Scenario 4: Publish with Trimming (`PublishTrimmed=true`, `RequiresILLinkPack=true`) + +For projects that trim unused code during publish: + +**Expected Outputs:** +- ✅ `TargetingPacks`: One per framework reference per TFM +- ✅ `RuntimePacks`: One per framework reference per TFM for the specified RID + - **Required even when `SelfContained=false`** because trimming requires RID-specific analysis +- ✅ `ImplicitPackageReferences`: `Microsoft.NET.ILLink.Tasks` + - Contains MSBuild targets and tasks for trimming +- ✅ `PackagesToDownload`: Includes targeting packs, runtime packs, and ILLink pack +- ✅ `RuntimeFrameworks`: One per framework reference per TFM + +**Key Properties:** +```xml +linux-x64 +true +true +true +``` + +**Behavior:** +- Runtime packs are required for trimming analysis even in framework-dependent deployments +- ILLink pack provides the trimming engine and MSBuild integration +- For .NET 6+, trim analyzer warnings are enabled by default + +--- + +### Scenario 5: Trim and AOT Analysis Support (`IsTrimmable=true`, `EnableTrimAnalyzer=true`, `IsAotCompatible=true`, `EnableAotAnalyzer=true`) + +For libraries that want to support trimming and AOT: + +**Expected Outputs:** +- ✅ `TargetingPacks`: One per framework reference per TFM +- ✅ `ImplicitPackageReferences`: `Microsoft.NET.ILLink.Tasks` + - Provides analyzers for trim and AOT compatibility +- ✅ `PackagesToDownload`: Includes targeting packs and ILLink pack +- ✅ `RuntimeFrameworks`: One per framework reference per TFM + +**Key Properties:** +```xml +true +true +true +true +true +``` + +**Behavior:** +- No RID specified, so no runtime packs are downloaded +- ILLink pack provides compile-time analyzers for library authors +- Warnings/errors guide developers to make code trim/AOT-compatible + +--- + +### Scenario 6: Native AOT Publishing (`PublishAot=true`) + +For projects using Native AOT compilation: + +**Expected Outputs:** +- ✅ `TargetingPacks`: One per framework reference per TFM +- ✅ `RuntimePacks`: One per framework reference per TFM for the specified RID + - **Required even when `SelfContained=false`** because AOT requires RID-specific compilation +- ✅ `HostILCompilerPacks`: ILCompiler pack for the host/SDK RID + - Example: `runtime.linux-x64.Microsoft.DotNet.ILCompiler` + - Used to run the AOT compiler +- ✅ `TargetILCompilerPacks`: ILCompiler pack for the target RID (if different from host) + - Example: `runtime.linux-arm64.Microsoft.DotNet.ILCompiler` + - Contains target-specific compilation assets +- ✅ `ImplicitPackageReferences`: `Microsoft.NET.ILLink.Tasks` + - ILLink is used as part of the AOT compilation pipeline +- ✅ `PackagesToDownload`: Includes targeting packs, runtime packs, and ILCompiler packs +- ✅ `RuntimeFrameworks`: One per framework reference per TFM + +**Key Properties:** +```xml +linux-arm64 +true +true +``` + +**Behavior:** +- Runtime packs are required for AOT compilation regardless of `SelfContained` setting +- Host ILCompiler pack must match the SDK's RID to run the compiler +- Target ILCompiler pack must match the `RuntimeIdentifier` for cross-compilation scenarios +- Native AOT produces a single native executable with no runtime dependencies + +--- + +### Scenario 7: Combined Scenarios + +Projects can combine multiple publish features: + +#### Self-Contained + Ready-to-Run + Trimming + +**Key Properties:** +```xml +true +linux-x64 +true +true +true +true +``` + +**Expected Outputs:** +- ✅ `TargetingPacks` +- ✅ `RuntimePacks` (for the specified RID) +- ✅ `Crossgen2Packs` (for R2R compilation) +- ✅ `ImplicitPackageReferences` (ILLink for trimming) +- ✅ `PackagesToDownload` (all of the above) + +#### Multi-RID + Ready-to-Run + Trimming + +**Key Properties:** +```xml +true +linux-x64 +linux-x64;linux-arm64;win-x64 +true +true +true +true +``` + +**Expected Outputs:** +- ✅ `TargetingPacks` +- ✅ `RuntimePacks` (for the primary RID only) +- ✅ `Crossgen2Packs` (for the host RID) +- ✅ `ImplicitPackageReferences` (ILLink for trimming) +- ✅ `PackagesToDownload` (runtime packs for ALL RIDs, plus Crossgen2 and ILLink) + +--- + +## Current Issues and Expected Fixes + +### Issue #51667: Runtime Packs Not Resolved for PublishTrimmed/PublishAot Without SelfContained + +**Problem:** +Currently, when `PublishTrimmed=true` or `PublishAot=true` is set without `SelfContained=true`, the task does not resolve runtime packs. This causes publish failures because trimming and AOT require runtime-specific assets. + +**Current Behavior:** +```csharp +var runtimeRequiredByDeployment + = (SelfContained || ReadyToRunEnabled) && + !string.IsNullOrEmpty(EffectiveRuntimeIdentifier) && + !string.IsNullOrEmpty(selectedRuntimePack?.RuntimePackNamePatterns); +``` + +**Expected Behavior:** +Runtime packs should be resolved when: +- `SelfContained=true`, OR +- `ReadyToRunEnabled=true`, OR +- `PublishTrimmed=true`, OR +- `PublishAot=true` + +**AND** a `RuntimeIdentifier` is specified. + +**Proposed Fix:** +```csharp +var runtimeRequiredByDeployment + = (SelfContained || ReadyToRunEnabled || PublishAot || RequiresILLinkPack) && + !string.IsNullOrEmpty(EffectiveRuntimeIdentifier) && + !string.IsNullOrEmpty(selectedRuntimePack?.RuntimePackNamePatterns); +--- + +## Implementation Details + +### Key Decision Points + +The task uses the following logic to determine which packs to include: + +1. **Targeting Packs**: Always included for all framework references matching the target framework +2. **Runtime Packs**: Included when `runtimeRequiredByDeployment` is true OR `RuntimePackAlwaysCopyLocal` is set +3. **Tool Packs**: Included based on specific properties: + - Crossgen2: When `ReadyToRunEnabled && ReadyToRunUseCrossgen2` + - ILCompiler: When `PublishAot` + - ILLink: When `RequiresILLinkPack` + +### RID Resolution + +The task uses the runtime graph (`RuntimeGraphPath`) to find the best matching RID from the available runtime packs: + +1. For the primary `RuntimeIdentifier`, full runtime pack metadata is generated +2. For additional `RuntimeIdentifiers`, only download entries are created +3. Portable RIDs are preferred over non-portable RIDs when appropriate for tool packs + +### Version Selection + +Runtime framework versions follow this precedence: +1. `RuntimeFrameworkVersion` metadata on `FrameworkReference` item +2. `RuntimeFrameworkVersion` MSBuild property +3. `LatestRuntimeFrameworkVersion` (if `TargetLatestRuntimePatch=true`) +4. `DefaultRuntimeFrameworkVersion` (if `TargetLatestRuntimePatch=false`) + +--- + +## Testing Scenarios + +The following test scenarios validate the expected outputs: + +1. ✅ **Self-contained deployment** resolves runtime packs for the specified RID +2. ❌ **PublishTrimmed without SelfContained** should resolve runtime packs (currently fails) +3. ❌ **PublishAot without SelfContained** should resolve runtime packs (currently fails) +4. ✅ **Multiple RuntimeIdentifiers** downloads runtime packs for all RIDs +5. ✅ **Ready-to-Run** includes Crossgen2 packs +6. ✅ **Combined scenarios** include all necessary packs + +Tests are located in: `src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs` + +--- + +## Related Documentation + +- [Runtime Identifier Catalog](https://learn.microsoft.com/dotnet/core/rid-catalog) +- [Framework-dependent vs Self-contained deployment](https://learn.microsoft.com/dotnet/core/deploying/) +- [Trim self-contained deployments](https://learn.microsoft.com/dotnet/core/deploying/trimming/trim-self-contained) +- [Native AOT deployment](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) +- [ReadyToRun compilation](https://learn.microsoft.com/dotnet/core/deploying/ready-to-run) + +--- + +## Revision History + +- **2025-01-14**: Initial specification documenting expected outputs and identifying issue #51667 diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs index 50b1b9c8df00..34286cbb468a 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs @@ -60,9 +60,15 @@ public class ProcessFrameworkReferencesTests "win-x86": { "#import": ["win"] }, + "win-arm64": { + "#import": ["win"] + }, "linux-x64": { "#import": ["any"] }, + "linux-arm64": { + "#import": ["any"] + }, "osx": { "#import": ["any"] }, @@ -167,6 +173,8 @@ private static ProcessFrameworkReferences CreateTask(TaskConfiguration config) if (config.EnableAotAnalyzer.HasValue) task.EnableAotAnalyzer = config.EnableAotAnalyzer.Value; if (config.EnableTrimAnalyzer.HasValue) task.EnableTrimAnalyzer = config.EnableTrimAnalyzer.Value; if (config.EnableSingleFileAnalyzer.HasValue) task.EnableSingleFileAnalyzer = config.EnableSingleFileAnalyzer.Value; + if (config.ReadyToRunEnabled.HasValue) task.ReadyToRunEnabled = config.ReadyToRunEnabled.Value; + if (config.ReadyToRunUseCrossgen2.HasValue) task.ReadyToRunUseCrossgen2 = config.ReadyToRunUseCrossgen2.Value; if (!string.IsNullOrEmpty(config.NetCoreRoot)) task.NetCoreRoot = config.NetCoreRoot; if (!string.IsNullOrEmpty(config.NETCoreSdkVersion)) task.NETCoreSdkVersion = config.NETCoreSdkVersion; @@ -205,6 +213,8 @@ private class TaskConfiguration public bool? EnableAotAnalyzer { get; set; } public bool? EnableTrimAnalyzer { get; set; } public bool? EnableSingleFileAnalyzer { get; set; } + public bool? ReadyToRunEnabled { get; set; } + public bool? ReadyToRunUseCrossgen2 { get; set; } public string? NetCoreRoot { get; set; } public string? NETCoreSdkVersion { get; set; } public string? NETCoreSdkPortableRuntimeIdentifier { get; set; } @@ -419,10 +429,6 @@ public void It_processes_various_RuntimeIdentifier_scenarios(string? runtimeIden } } - // This test is now covered by the theory above - - // This test is now covered by the theory above - [Fact] public void It_processes_RuntimeIdentifiers_with_AlwaysCopyLocal_and_no_RuntimeIdentifier() { @@ -452,10 +458,6 @@ public void It_processes_RuntimeIdentifiers_with_AlwaysCopyLocal_and_no_RuntimeI task.RuntimePacks[0].ItemSpec.Should().Be("Microsoft.Windows.SDK.NET.Ref"); } - // This test is now covered by the theory above - - // This test is now covered by the theory above - [Fact] public void It_handles_real_world_ridless_scenario_with_aot_and_trimming() { @@ -733,6 +735,205 @@ public void It_handles_different_target_platforms(string platformId, string plat task.TargetingPacks.Should().NotBeNull(); } + [Fact] + public void It_resolves_runtime_packs_for_SelfContained_deployment() + { + // This test validates the "good" behavior from good-processframeworkreferences-selfcontained.txt + // When SelfContained=true with a RuntimeIdentifier, runtime packs should be resolved + + var netCoreAppRef = CreateKnownFrameworkReference("Microsoft.NETCore.App", "net10.0", "10.0.0", + "Microsoft.NETCore.App.Runtime.**RID**", + "linux-arm;linux-arm64;linux-musl-arm64;linux-musl-x64;linux-x64;osx-x64;tizen.4.0.0-armel;tizen.5.0.0-armel;win-arm64;win-x64;win-x86;linux-musl-arm;osx-arm64;linux-s390x;linux-loongarch64;linux-bionic-arm;linux-bionic-arm64;linux-bionic-x64;linux-bionic-x86;linux-ppc64le;freebsd-x64;freebsd-arm64;linux-riscv64;linux-musl-riscv64;linux-musl-loongarch64;android-arm64;android-x64"); + + var aspNetCoreRef = CreateKnownFrameworkReference("Microsoft.AspNetCore.App", "net10.0", "10.0.0", + "Microsoft.AspNetCore.App.Runtime.**RID**", + "win-x64;win-x86;osx-x64;linux-musl-x64;linux-musl-arm64;linux-x64;linux-arm;linux-arm64;linux-musl-arm;win-arm64;osx-arm64;linux-s390x;linux-loongarch64;linux-ppc64le;freebsd-x64;freebsd-arm64;linux-riscv64;linux-musl-riscv64;linux-loongarch64;linux-musl-loongarch64"); + + var ilLinkPack = new MockTaskItem("Microsoft.NET.ILLink.Tasks", new Dictionary + { + ["TargetFramework"] = "net10.0", + ["ILLinkPackVersion"] = "10.0.0" + }); + + var config = new TaskConfiguration + { + TargetFrameworkVersion = "10.0", + EnableRuntimePackDownload = true, + EnableTargetingPackDownload = true, + SelfContained = true, + RuntimeIdentifier = "linux-arm64", + PublishTrimmed = true, + RequiresILLinkPack = true, + TargetLatestRuntimePatch = true, + TargetLatestRuntimePatchIsDefault = true, + RuntimeGraphPath = CreateRuntimeGraphFile(MultiPlatformRuntimeGraph), + FrameworkReferences = new[] { + new MockTaskItem("Microsoft.NETCore.App", new Dictionary()), + new MockTaskItem("Microsoft.AspNetCore.App", new Dictionary { ["IsImplicitlyDefined"] = "true" }) + }, + KnownFrameworkReferences = new[] { netCoreAppRef, aspNetCoreRef }, + KnownILLinkPacks = new[] { ilLinkPack } + }; + + var task = CreateTask(config); + task.Execute().Should().BeTrue("SelfContained deployment should succeed"); + + // Validate PackagesToDownload contains runtime packs + task.PackagesToDownload.Should().NotBeNull(); + task.PackagesToDownload.Should().Contain(p => p.ItemSpec == "Microsoft.NETCore.App.Runtime.linux-arm64", + "Should download NETCore runtime pack for target RID"); + task.PackagesToDownload.Should().Contain(p => p.ItemSpec == "Microsoft.AspNetCore.App.Runtime.linux-arm64", + "Should download AspNetCore runtime pack for target RID"); + + // Validate RuntimePacks output + task.RuntimePacks.Should().NotBeNull().And.HaveCount(2, "Should have runtime packs for both frameworks"); + task.RuntimePacks.Should().Contain(p => p.ItemSpec == "Microsoft.NETCore.App.Runtime.linux-arm64"); + task.RuntimePacks.Should().Contain(p => p.ItemSpec == "Microsoft.AspNetCore.App.Runtime.linux-arm64"); + + // Validate RuntimeFrameworks + task.RuntimeFrameworks.Should().NotBeNull().And.HaveCount(2); + task.RuntimeFrameworks.Should().Contain(p => p.ItemSpec == "Microsoft.NETCore.App"); + task.RuntimeFrameworks.Should().Contain(p => p.ItemSpec == "Microsoft.AspNetCore.App"); + + // Validate TargetingPacks + task.TargetingPacks.Should().NotBeNull().And.HaveCount(2); + task.TargetingPacks.Should().Contain(p => p.GetMetadata(MetadataKeys.NuGetPackageId) == "Microsoft.NETCore.App.Ref"); + task.TargetingPacks.Should().Contain(p => p.GetMetadata(MetadataKeys.NuGetPackageId) == "Microsoft.AspNetCore.App.Ref"); + + // Validate ILLink pack is included + task.ImplicitPackageReferences.Should().NotBeNull(); + task.ImplicitPackageReferences.Should().Contain(p => p.ItemSpec == "Microsoft.NET.ILLink.Tasks"); + } + + [Fact] + public void It_resolves_runtime_packs_for_PublishTrimmed_without_SelfContained() + { + // This test validates that PublishTrimmed should trigger runtime pack resolution + // even when SelfContained=false (addresses the issue in bad-processframeworkreferences-publishtrimmed) + // PublishTrimmed requires runtime-specific assets, similar to SelfContained + + var netCoreAppRef = CreateKnownFrameworkReference("Microsoft.NETCore.App", "net10.0", "10.0.0", + "Microsoft.NETCore.App.Runtime.**RID**", + "linux-arm;linux-arm64;linux-musl-arm64;linux-musl-x64;linux-x64;osx-x64;tizen.4.0.0-armel;tizen.5.0.0-armel;win-arm64;win-x64;win-x86;linux-musl-arm;osx-arm64;linux-s390x;linux-loongarch64;linux-bionic-arm;linux-bionic-arm64;linux-bionic-x64;linux-bionic-x86;linux-ppc64le;freebsd-x64;freebsd-arm64;linux-riscv64;linux-musl-riscv64;linux-musl-loongarch64;android-arm64;android-x64"); + + var aspNetCoreRef = CreateKnownFrameworkReference("Microsoft.AspNetCore.App", "net10.0", "10.0.0", + "Microsoft.AspNetCore.App.Runtime.**RID**", + "win-x64;win-x86;osx-x64;linux-musl-x64;linux-musl-arm64;linux-x64;linux-arm;linux-arm64;linux-musl-arm;win-arm64;osx-arm64;linux-s390x;linux-loongarch64;linux-ppc64le;freebsd-x64;freebsd-arm64;linux-riscv64;linux-musl-riscv64;linux-loongarch64;linux-musl-loongarch64"); + + var ilLinkPack = new MockTaskItem("Microsoft.NET.ILLink.Tasks", new Dictionary + { + ["TargetFramework"] = "net10.0", + ["ILLinkPackVersion"] = "10.0.0" + }); + + var config = new TaskConfiguration + { + TargetFrameworkVersion = "10.0", + EnableRuntimePackDownload = true, + EnableTargetingPackDownload = true, + SelfContained = false, // This is the key difference from the previous test + RuntimeIdentifier = "linux-arm64", + PublishTrimmed = true, + RequiresILLinkPack = true, + EnableTrimAnalyzer = true, + TargetLatestRuntimePatch = false, + TargetLatestRuntimePatchIsDefault = true, + RuntimeGraphPath = CreateRuntimeGraphFile(MultiPlatformRuntimeGraph), + FrameworkReferences = new[] { + new MockTaskItem("Microsoft.NETCore.App", new Dictionary()), + new MockTaskItem("Microsoft.AspNetCore.App", new Dictionary { ["IsImplicitlyDefined"] = "true" }) + }, + KnownFrameworkReferences = new[] { netCoreAppRef, aspNetCoreRef }, + KnownILLinkPacks = new[] { ilLinkPack } + }; + + var task = CreateTask(config); + task.Execute().Should().BeTrue("PublishTrimmed deployment should succeed"); + + // The key assertion: runtime packs should be resolved even when SelfContained=false + // because PublishTrimmed requires runtime-specific assets + task.PackagesToDownload.Should().NotBeNull(); + task.PackagesToDownload.Should().Contain(p => p.ItemSpec == "Microsoft.NETCore.App.Runtime.linux-arm64", + "Should download NETCore runtime pack for PublishTrimmed even when SelfContained=false"); + task.PackagesToDownload.Should().Contain(p => p.ItemSpec == "Microsoft.AspNetCore.App.Runtime.linux-arm64", + "Should download AspNetCore runtime pack for PublishTrimmed even when SelfContained=false"); + + // Validate RuntimePacks output + task.RuntimePacks.Should().NotBeNull().And.HaveCount(2, + "Should have runtime packs for both frameworks when PublishTrimmed=true"); + task.RuntimePacks.Should().Contain(p => p.ItemSpec == "Microsoft.NETCore.App.Runtime.linux-arm64"); + task.RuntimePacks.Should().Contain(p => p.ItemSpec == "Microsoft.AspNetCore.App.Runtime.linux-arm64"); + + // Validate RuntimeFrameworks + task.RuntimeFrameworks.Should().NotBeNull().And.HaveCount(2); + + // Validate TargetingPacks + task.TargetingPacks.Should().NotBeNull().And.HaveCount(2); + + // Validate ILLink pack is included for trimming + task.ImplicitPackageReferences.Should().NotBeNull(); + task.ImplicitPackageReferences.Should().Contain(p => p.ItemSpec == "Microsoft.NET.ILLink.Tasks", + "ILLink pack should be included for PublishTrimmed"); + } + + [Theory] + [InlineData(true, false, false, false, "SelfContained only")] + [InlineData(false, true, false, false, "PublishTrimmed only")] + [InlineData(false, false, true, false, "PublishReadyToRun only")] + [InlineData(false, false, false, true, "PublishAot only")] + [InlineData(true, true, false, false, "SelfContained + PublishTrimmed")] + [InlineData(true, false, true, false, "SelfContained + PublishReadyToRun")] + [InlineData(false, true, true, false, "PublishTrimmed + PublishReadyToRun")] + public void It_resolves_runtime_packs_for_various_publish_scenarios( + bool selfContained, bool publishTrimmed, bool publishReadyToRun, bool publishAot, string scenario) + { + // This test validates that runtime packs are resolved for any scenario requiring runtime-specific assets + + var netCoreAppRef = CreateKnownFrameworkReference("Microsoft.NETCore.App", "net10.0", "10.0.0", + "Microsoft.NETCore.App.Runtime.**RID**", + "linux-x64;linux-arm64;win-x64;osx-x64;osx-arm64"); + + var ilLinkPack = new MockTaskItem("Microsoft.NET.ILLink.Tasks", new Dictionary + { + ["TargetFramework"] = "net10.0", + ["ILLinkPackVersion"] = "10.0.0" + }); + + var ilCompilerPack = CreateKnownILCompilerPack("net10.0", "10.0.0", "linux-x64;linux-arm64;win-x64;osx-x64;osx-arm64"); + + var config = new TaskConfiguration + { + TargetFrameworkVersion = "10.0", + EnableRuntimePackDownload = true, + EnableTargetingPackDownload = true, + SelfContained = selfContained, + RuntimeIdentifier = "linux-x64", + PublishTrimmed = publishTrimmed, + PublishAot = publishAot, + ReadyToRunEnabled = publishReadyToRun, + RequiresILLinkPack = publishTrimmed || publishAot, + TargetLatestRuntimePatch = true, + TargetLatestRuntimePatchIsDefault = true, + RuntimeGraphPath = CreateRuntimeGraphFile(MultiPlatformRuntimeGraph), + FrameworkReferences = new[] { new MockTaskItem("Microsoft.NETCore.App", new Dictionary()) }, + KnownFrameworkReferences = new[] { netCoreAppRef }, + KnownILLinkPacks = new[] { ilLinkPack }, + KnownILCompilerPacks = new[] { ilCompilerPack } + }; + + var task = CreateTask(config); + task.Execute().Should().BeTrue($"Task should succeed for scenario: {scenario}"); + + // All of these scenarios require runtime-specific assets, so runtime packs should be resolved + task.PackagesToDownload.Should().NotBeNull($"PackagesToDownload should not be null for scenario: {scenario}"); + task.PackagesToDownload.Should().Contain(p => p.ItemSpec == "Microsoft.NETCore.App.Runtime.linux-x64", + $"Should download runtime pack for scenario: {scenario}"); + + task.RuntimePacks.Should().NotBeNull($"RuntimePacks should not be null for scenario: {scenario}"); + task.RuntimePacks.Should().Contain(p => p.ItemSpec == "Microsoft.NETCore.App.Runtime.linux-x64", + $"Should have runtime pack in output for scenario: {scenario}"); + } + [Fact] public void It_handles_complex_cross_compilation_RuntimeIdentifiers() diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs index 7b5b7bc0da15..910452e75b1d 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.DotNet.Cli; @@ -77,12 +78,6 @@ public class ProcessFrameworkReferences : TaskBase public string? RuntimeIdentifier { get; set; } - /// - /// Since this Target is mostly focused on managing RID-specific assets, we massage the 'any' RID (which is platform-agnostic) into a 'null' - /// value to make processing simpler. - /// - public string? EffectiveRuntimeIdentifier => RuntimeIdentifier == "any" ? null : RuntimeIdentifier; - public string[]? RuntimeIdentifiers { get; set; } public string? RuntimeFrameworkVersion { get; set; } @@ -161,8 +156,33 @@ public class ProcessFrameworkReferences : TaskBase [Output] public string[]? KnownRuntimeIdentifierPlatforms { get; set; } + /// + /// The runtime identifier used to represent platform-agnostic components - 'any' runtime will suffice! + /// + private const string RuntimeIdentifierForPlatformAgnosticComponents = "any"; + private Version? _normalizedTargetFrameworkVersion; + /// + /// Since this Target is mostly focused on managing RID-specific assets, we massage the 'any' RID (which is platform-agnostic) into a 'null' + /// value to make processing simpler. + /// + private string? EffectiveRuntimeIdentifier => RuntimeIdentifier == RuntimeIdentifierForPlatformAgnosticComponents ? null : RuntimeIdentifier; + + /// + /// If the current project is specifically targeting a platform, as opposed to being platform-agnostic. + /// + private bool ProjectIsPlatformSpecific => + !string.IsNullOrEmpty(EffectiveRuntimeIdentifier); + + /// + /// We have several deployment models that require the use of runtime assets of various kinds. + /// This member helps identify when any of those models are in use, because we've had bugs in the past + /// where we didn't properly account for all of them. + /// + private bool DeploymentModelRequiresRuntimeComponents => + SelfContained || ReadyToRunEnabled || RequiresILLinkPack; // RequiresILLinkPack indicates trimming/AOT scenarios, see the _ComputeToolPackInputsToProcessFrameworkReferences Target. + void AddPacksForFrameworkReferences( List packagesToDownload, List runtimeFrameworks, @@ -348,9 +368,9 @@ out List knownRuntimePacksForTargetFramework var hasRuntimePackAlwaysCopyLocal = selectedRuntimePack != null && selectedRuntimePack.Value.RuntimePackAlwaysCopyLocal; var runtimeRequiredByDeployment - = (SelfContained || ReadyToRunEnabled) && - !string.IsNullOrEmpty(EffectiveRuntimeIdentifier) && - !string.IsNullOrEmpty(selectedRuntimePack?.RuntimePackNamePatterns); + = DeploymentModelRequiresRuntimeComponents && + ProjectIsPlatformSpecific && + selectedRuntimePack?.HasRuntimePackages == true; if (hasRuntimePackAlwaysCopyLocal || runtimeRequiredByDeployment) { @@ -367,7 +387,7 @@ var runtimeRequiredByDeployment } // Process primary runtime identifier - ProcessRuntimeIdentifier(EffectiveRuntimeIdentifier ?? "any", runtimePackForRuntimeIDProcessing, runtimePackVersion, additionalFrameworkReferencesForRuntimePack, + ProcessRuntimeIdentifier(EffectiveRuntimeIdentifier ?? RuntimeIdentifierForPlatformAgnosticComponents, runtimePackForRuntimeIDProcessing, runtimePackVersion, additionalFrameworkReferencesForRuntimePack, unrecognizedRuntimeIdentifiers, unavailableRuntimePacks, runtimePacks, packagesToDownload, isTrimmable, useRuntimePackAndDownloadIfNecessary, wasReferencedDirectly: frameworkReference != null); @@ -384,7 +404,7 @@ var runtimeRequiredByDeployment continue; } - if (runtimeIdentifier == "any") + if (runtimeIdentifier == RuntimeIdentifierForPlatformAgnosticComponents) { // The `any` RID represents a platform-agnostic target. As such, it has no // platform-specific runtime pack associated with it. @@ -830,7 +850,7 @@ private ToolPackSupport AddToolPack( // This makes non-portable SDKs behave the same as portable SDKs except for the specific cases added to "supported", such as targeting the non-portable RID. // This also ensures that targeting common RIDs doesn't require any non-portable assets that aren't packaged in the SDK by default. // Due to size concerns, the non-portable ILCompiler and Crossgen2 aren't included by default in non-portable SDK distributions. - var runtimeIdentifier = RuntimeIdentifier ?? "any"; + var runtimeIdentifier = RuntimeIdentifier ?? RuntimeIdentifierForPlatformAgnosticComponents; string? supportedTargetRid = NuGetUtils.GetBestMatchingRid(runtimeGraph, runtimeIdentifier, packSupportedRuntimeIdentifiers, out _); string? supportedPortableTargetRid = NuGetUtils.GetBestMatchingRid(runtimeGraph, runtimeIdentifier, packSupportedPortableRuntimeIdentifiers, out _); @@ -1252,47 +1272,34 @@ public KnownRuntimePack ToKnownRuntimePack() } } - private struct KnownRuntimePack + [DebuggerDisplay("{Name}@{LatestRuntimeFrameworkVersion} for {TargetFramework}")] + private struct KnownRuntimePack(ITaskItem item) { - ITaskItem _item; - - public KnownRuntimePack(ITaskItem item) - { - _item = item; - TargetFramework = NuGetFramework.Parse(item.GetMetadata("TargetFramework")); - string runtimePackLabels = item.GetMetadata(MetadataKeys.RuntimePackLabels); - if (string.IsNullOrEmpty(runtimePackLabels)) - { - RuntimePackLabels = Array.Empty(); - } - else - { - RuntimePackLabels = runtimePackLabels.Split(';'); - } - } // The name / itemspec of the FrameworkReference used in the project - public string Name => _item.ItemSpec; + public readonly string Name => item.ItemSpec; - //// The framework name to write to the runtimeconfig file (and the name of the folder under dotnet/shared) - public string LatestRuntimeFrameworkVersion => _item.GetMetadata("LatestRuntimeFrameworkVersion"); + // The framework name to write to the runtimeconfig file (and the name of the folder under dotnet/shared) + public readonly string LatestRuntimeFrameworkVersion => item.GetMetadata("LatestRuntimeFrameworkVersion"); - public string RuntimePackNamePatterns => _item.GetMetadata("RuntimePackNamePatterns"); + public readonly string RuntimePackNamePatterns => item.GetMetadata("RuntimePackNamePatterns"); - public string RuntimePackRuntimeIdentifiers => _item.GetMetadata(MetadataKeys.RuntimePackRuntimeIdentifiers); + public readonly bool HasRuntimePackages => !string.IsNullOrEmpty(RuntimePackNamePatterns); - public string RuntimePackExcludedRuntimeIdentifiers => _item.GetMetadata(MetadataKeys.RuntimePackExcludedRuntimeIdentifiers); + public readonly string RuntimePackRuntimeIdentifiers => item.GetMetadata(MetadataKeys.RuntimePackRuntimeIdentifiers); - public string IsTrimmable => _item.GetMetadata(MetadataKeys.IsTrimmable); + public readonly string RuntimePackExcludedRuntimeIdentifiers => item.GetMetadata(MetadataKeys.RuntimePackExcludedRuntimeIdentifiers); - public bool IsWindowsOnly => _item.HasMetadataValue("IsWindowsOnly", "true"); + public readonly string IsTrimmable => item.GetMetadata(MetadataKeys.IsTrimmable); - public bool RuntimePackAlwaysCopyLocal => - _item.HasMetadataValue(MetadataKeys.RuntimePackAlwaysCopyLocal, "true"); + public readonly bool IsWindowsOnly => item.HasMetadataValue("IsWindowsOnly", "true"); - public string[] RuntimePackLabels { get; } + public readonly bool RuntimePackAlwaysCopyLocal => + item.HasMetadataValue(MetadataKeys.RuntimePackAlwaysCopyLocal, "true"); - public NuGetFramework TargetFramework { get; } + public readonly string[] RuntimePackLabels => item.GetMetadata(MetadataKeys.RuntimePackLabels) is string s ? s.Split([';'], StringSplitOptions.RemoveEmptyEntries) : Array.Empty(); + + public readonly NuGetFramework TargetFramework => NuGetFramework.Parse(item.GetMetadata("TargetFramework")); } } } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets index 728f17b43132..6cfb6576c10f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets @@ -184,6 +184,26 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + true + + + $(NetCoreTargetingPackRoot) false - + - + - + $(InterceptorsPreviewNamespaces);Microsoft.Extensions.Validation.Generated - + - + diff --git a/tasks.slnf b/tasks.slnf new file mode 100644 index 000000000000..6039d0ed8a12 --- /dev/null +++ b/tasks.slnf @@ -0,0 +1,11 @@ +{ + "solution": { + "path": "sdk.slnx", + "projects": [ + "src\\Tasks\\Microsoft.NET.Build.Tasks\\Microsoft.NET.Build.Tasks.csproj", + "src\\Compatibility\\ApiCompat\\Microsoft.DotNet.ApiCompat.Task\\Microsoft.DotNet.ApiCompat.Task.csproj", + "src\\Tasks\\Microsoft.NET.Build.Tasks.UnitTests\\Microsoft.NET.Build.Tasks.UnitTests.csproj", + "test\\Microsoft.NET.TestFramework\\Microsoft.NET.TestFramework.csproj" + ] + } +} diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishASingleFileApp.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishASingleFileApp.cs index d8a821397e42..cbdc699a46d4 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishASingleFileApp.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishASingleFileApp.cs @@ -932,6 +932,7 @@ public void It_errors_when_enabling_compression_without_selfcontained() IsExe = true, }; + // SingleFile implies self-contained so we need to explicitly disable it to test this case. testProject.AdditionalProperties.Add("SelfContained", "false"); testProject.AdditionalProperties.Add("EnableCompressionInSingleFile", "true"); diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs index 2a038cb9a679..411cc003edfc 100644 --- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs +++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs @@ -1662,28 +1662,6 @@ public void ILLink_can_treat_warnings_as_errors_independently(string targetFrame .And.NotHaveStdOutContaining("error IL2075"); } - /// - /// The reason we test this on net7 and below is because in net8 _IsPublishing was added which changes - /// the RID-defaulting behavior such that 8+ apps are not 'portable apps' when published for configurations that - /// require a RID (self-contained, or trimmed). - /// - /// - [RequiresMSBuildVersionTheory("17.0.0.32901")] - [MemberData(nameof(TFMsThatDoNotInferPublishSelfContained), MemberType = typeof(PublishTestUtils))] - public void ILLink_error_on_portable_app(string targetFramework) - { - var projectName = "HelloWorld"; - var referenceProjectName = "ClassLibForILLink"; - - var testProject = CreateTestProjectForILLinkTesting(_testAssetsManager, targetFramework, projectName, referenceProjectName, setSelfContained: false); - var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: targetFramework); - - var publishCommand = new PublishCommand(testAsset); - publishCommand.Execute("/p:PublishTrimmed=true") - .Should().Fail() - .And.HaveStdOutContaining(Strings.ILLinkNotSupportedError); - } - // https://github.com/dotnet/sdk/issues/49665 [PlatformSpecificTheory(TestPlatforms.Any & ~TestPlatforms.OSX)] [InlineData("net5.0")] diff --git a/test/Microsoft.NET.Publish.Tests/PublishTestUtils.cs b/test/Microsoft.NET.Publish.Tests/PublishTestUtils.cs index cadaff949c57..ee64555eb7c3 100644 --- a/test/Microsoft.NET.Publish.Tests/PublishTestUtils.cs +++ b/test/Microsoft.NET.Publish.Tests/PublishTestUtils.cs @@ -95,20 +95,6 @@ internal static class PublishTestUtils // new object[] { ToolsetInfo.NextTargetFramework }, }; - /// - /// Starting in 8 we introduced made Publish* properties that imply SelfContained actually set SelfContained, - /// and that means RIDs are inferred when publishing these. This list should contain all TFMs that do not infer SelfContained - /// when PublishSelfContained or PublishSingleFile are set without an explicit SelfContained value. - /// - /// - /// Tried to be fancy here and compute this by stripping the NET8Plus items from the SupportedTfms list, - /// but that broke test explorer integration in devkit. - /// - public static IEnumerable TFMsThatDoNotInferPublishSelfContained => [ - ["net5.0"], - ["net6.0"], - ["net7.0"], - ]; #else #error If building for a newer TFM, please update the values above to include both the old and new TFMs. #endif diff --git a/test/Microsoft.NET.Publish.Tests/RuntimeIdentifiersTests.cs b/test/Microsoft.NET.Publish.Tests/RuntimeIdentifiersTests.cs index 8fc36944a03f..63058af9aa56 100644 --- a/test/Microsoft.NET.Publish.Tests/RuntimeIdentifiersTests.cs +++ b/test/Microsoft.NET.Publish.Tests/RuntimeIdentifiersTests.cs @@ -259,55 +259,37 @@ public void PublishRuntimeIdentifierOverridesUseCurrentRuntime() } [Theory] - [InlineData("PublishReadyToRun", true)] - [InlineData("PublishSingleFile", true)] - [InlineData("PublishTrimmed", true)] - [InlineData("PublishAot", true)] - [InlineData("PublishReadyToRun", false)] - [InlineData("PublishSingleFile", false)] - [InlineData("PublishTrimmed", false)] - public void SomePublishPropertiesInferSelfContained(string property, bool useFrameworkDependentDefaultTargetFramework) + [InlineData("PublishReadyToRun", ToolsetInfo.CurrentTargetFramework, false)] // R2R doesn't imply self-contained in 8 and above + [InlineData("PublishSingleFile", ToolsetInfo.CurrentTargetFramework, true)] // single-file implies self-contained + [InlineData("PublishTrimmed", ToolsetInfo.CurrentTargetFramework, true)] // trimming implies self-contained + [InlineData("PublishAot", ToolsetInfo.CurrentTargetFramework, true)] // AOT implies self-contained + [InlineData("PublishReadyToRun", "net7.0", true)] // R2R implies self-contained in .NET 7 and below + [InlineData("PublishSingleFile", "net7.0", true)] // single-file implies self-contained + [InlineData("PublishTrimmed", "net7.0", true)] // trimming implies self-contained + public void SomePublishPropertiesInferSelfContained(string property, string targetFramework, bool expectedSelfContainedValue) { // Note: there is a bug with PublishAot I think where this test will fail for Aot if the testname is too long. Do not make it longer. - var tfm = useFrameworkDependentDefaultTargetFramework ? ToolsetInfo.CurrentTargetFramework : "net7.0"; // net 7 is the last non FDD default TFM at the time of this PR. var testProject = new TestProject() { IsExe = true, - TargetFrameworks = tfm, + TargetFrameworks = targetFramework, }; testProject.AdditionalProperties[property] = "true"; testProject.RecordProperties("SelfContained"); - var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: $"{property}-{useFrameworkDependentDefaultTargetFramework}"); + testProject.RecordPropertiesBeforeTarget("Publish"); + var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: $"{property}-{targetFramework}"); var publishCommand = new DotnetPublishCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name)); - if (property == "PublishTrimmed" && !useFrameworkDependentDefaultTargetFramework) - { - publishCommand - .Execute() - .Should() - .Fail(); - } - else - { - publishCommand - .Execute() - .Should() - .Pass(); - } - - var properties = testProject.GetPropertyValues(testAsset.TestRoot, targetFramework: tfm, configuration: useFrameworkDependentDefaultTargetFramework ? "Release" : "Debug"); - - var expectedSelfContainedValue = "true"; - if ( - (property == "PublishReadyToRun" && useFrameworkDependentDefaultTargetFramework) || // This property should no longer infer SelfContained in net 8 - (property == "PublishTrimmed" && !useFrameworkDependentDefaultTargetFramework) // This property did not infer SelfContained until net 8 - ) - { - expectedSelfContainedValue = "false"; - } + publishCommand + .WithWorkingDirectory(Path.Combine(testAsset.TestRoot, testProject.Name)) + .Execute($"-bl:{testAsset.Name}-{{}}.binlog") + .Should() + .Pass(); - properties["SelfContained"].Should().Be(expectedSelfContainedValue); + // default configuration for publish in <7 is Debug, Release for 7+ + var properties = testProject.GetPropertyValues(testAsset.TestRoot, targetFramework: targetFramework, configuration: targetFramework == "net7.0" ? "Debug" : "Release"); + bool.Parse(properties["SelfContained"]).Should().Be(expectedSelfContainedValue); } [Fact] diff --git a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj index 7829d0829246..bf76fed9924f 100644 --- a/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj +++ b/test/Microsoft.NET.TestFramework/Microsoft.NET.TestFramework.csproj @@ -84,4 +84,8 @@ + + + + diff --git a/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs b/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs index b2260f81205c..576475008c79 100644 --- a/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs +++ b/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs @@ -87,6 +87,12 @@ public TestProject([CallerMemberName] string? name = null) /// public List PropertiesToRecord { get; } = new List(); + /// + /// The target before which to record properties specified in . + /// Defaults to "AfterBuild". + /// + public string TargetToRecordPropertiesBefore { get; private set; } = "AfterBuild"; + public IEnumerable TargetFrameworkIdentifiers { get @@ -252,7 +258,7 @@ internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder) if(importGroup?.Attribute("Project") is not null) { importGroup.Attribute("Project")!.Value = "$(VSINSTALLDIR)\\MSBuild\\Microsoft\\Portable\\$(TargetFrameworkVersion)\\Microsoft.Portable.CSharp.targets"; - } + } } if(TargetFrameworkVersion is not null) @@ -375,12 +381,12 @@ internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder) propertyGroup?.Add(new XElement(ns + "CustomAfterDirectoryBuildTargets", $"$(CustomAfterDirectoryBuildTargets);{customAfterDirectoryBuildTargetsPath.FullName}")); propertyGroup?.Add(new XElement(ns + "CustomAfterMicrosoftCommonCrossTargetingTargets", $"$(CustomAfterMicrosoftCommonCrossTargetingTargets);{customAfterDirectoryBuildTargetsPath.FullName}")); - + var customAfterDirectoryBuildTargets = new XDocument(new XElement(ns + "Project")); var target = new XElement(ns + "Target", new XAttribute("Name", "WritePropertyValues"), - new XAttribute("BeforeTargets", "AfterBuild")); + new XAttribute("BeforeTargets", TargetToRecordPropertiesBefore)); customAfterDirectoryBuildTargets.Root?.Add(target); @@ -491,6 +497,15 @@ public void RecordProperties(params string[] propertyNames) PropertiesToRecord.AddRange(propertyNames); } + /// + /// Tells this TestProject to record properties specified in before the specified target. + /// By default properties are recorded before the "AfterBuild" target (so after the actual compile+copy targets have run). + /// + public void RecordPropertiesBeforeTarget(string targetName) + { + TargetToRecordPropertiesBefore = targetName; + } + /// /// A dictionary of property keys to property value strings, case sensitive. /// Only properties added to the member will be observed.