From e8576178ee80520138950471b4ac8db18e0f66b9 Mon Sep 17 00:00:00 2001 From: David Federman Date: Wed, 13 May 2026 14:50:24 -0700 Subject: [PATCH] Guard NuGetRestoreTargets import in Microsoft.Common.CrossTargeting.targets Microsoft.Common.CurrentVersion.targets guards its NuGet restore targets import with Exists('$(NuGetRestoreTargets)') (and the cooperative IsRestoreTargetsFileLoaded != 'true' check added in PR #4969), but the parallel import in Microsoft.Common.CrossTargeting.targets was unguarded. This meant cross-targeting (multi-TFM) projects loaded in non-NuGet contexts -- focused unit tests, custom build systems, stripped-down project types -- failed with Imported project not found unless the caller manufactured a stub NuGet.targets file. Single-targeting projects in the same situation worked silently. Apply the canonical guard byte-for-byte from Microsoft.Common.CurrentVersion.targets:7046 to close the asymmetry. Behavior change: when NuGetRestoreTargets resolves to a missing file, evaluation now succeeds with the NuGet import as a no-op instead of throwing. Projects that already point NuGetRestoreTargets at a real file are unaffected. No ChangeWave is needed -- the canonical fix in CurrentVersion.targets was added in PR #4969 (2020) without one, and this change just closes the asymmetry. Discovered while writing tests for PR #13427, whose synthetic cross-targeting test had to inject an empty NuGet.targets stub purely to satisfy the unguarded import. That stub becomes dead code post-fix and will be cleaned up in a separate follow-up after both PRs merge. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CrossTargetingTargetsImportTests.cs | 97 +++++++++++++++++++ .../Microsoft.Common.CrossTargeting.targets | 2 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/Tasks.UnitTests/CrossTargetingTargetsImportTests.cs diff --git a/src/Tasks.UnitTests/CrossTargetingTargetsImportTests.cs b/src/Tasks.UnitTests/CrossTargetingTargetsImportTests.cs new file mode 100644 index 00000000000..025d46c0d61 --- /dev/null +++ b/src/Tasks.UnitTests/CrossTargetingTargetsImportTests.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +using Microsoft.Build.Evaluation; +using Microsoft.Build.UnitTests; + +using Shouldly; + +using Xunit; + +namespace Microsoft.Build.Tasks.UnitTests; + +/// +/// Tests for the NuGet restore targets import in Microsoft.Common.CrossTargeting.targets. +/// The parallel import in Microsoft.Common.CurrentVersion.targets is guarded with +/// Exists('$(NuGetRestoreTargets)'), but for years the cross-targeting variant was unguarded +/// — which meant cross-targeting (multi-TFM) projects loaded in non-NuGet contexts (custom build +/// systems, focused unit tests) failed with "Imported project not found" unless the caller +/// manufactured a stub NuGet.targets file. These tests cover the now-guarded import. +/// +public sealed class CrossTargetingTargetsImportTests +{ + private readonly ITestOutputHelper _output; + + public CrossTargetingTargetsImportTests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// When NuGetRestoreTargets points at a file that does not exist, evaluating a project + /// that imports Microsoft.Common.CrossTargeting.targets must succeed (the import is a + /// silent no-op), matching the long-standing behavior of Microsoft.Common.CurrentVersion.targets. + /// + [Fact] + public void ImportSucceedsWhenNuGetRestoreTargetsDoesNotExist() + { + using TestEnvironment env = TestEnvironment.Create(_output); + TransientTestFolder folder = env.CreateFolder(createFolder: true); + string nonExistentNuGetTargets = Path.Combine(folder.Path, "DoesNotExist.NuGet.targets"); + + string projectContents = $""" + + + {nonExistentNuGetTargets} + + + + """.Cleanup(); + + using ProjectFromString projectFromString = new(projectContents); + Project project = projectFromString.Project; + + project.GetPropertyValue("NuGetRestoreTargets").ShouldBe(nonExistentNuGetTargets); + } + + /// + /// When NuGetRestoreTargets points at a real file, the import still happens. This + /// guards against the new Exists() condition accidentally skipping a valid NuGet + /// targets file. The synthetic project does not set IsRestoreTargetsFileLoaded, and + /// nothing in the Microsoft.Common.CrossTargeting.targets import chain sets it before + /// the guarded import, so the canonical guard does not short-circuit here. + /// + [Fact] + public void ImportLoadsNuGetTargetsWhenItExists() + { + using TestEnvironment env = TestEnvironment.Create(_output); + TransientTestFolder folder = env.CreateFolder(createFolder: true); + + const string MarkerTargetName = "_CrossTargetingNuGetStubMarker"; + TransientTestFile nuGetStub = env.CreateFile( + folder, + "NuGet.targets", + $""" + + + + """.Cleanup()); + + string projectContents = $""" + + + {nuGetStub.Path} + + + + """.Cleanup(); + + using ProjectFromString projectFromString = new(projectContents); + Project project = projectFromString.Project; + + project.Targets.ShouldContainKey(MarkerTargetName); + } +} + diff --git a/src/Tasks/Microsoft.Common.CrossTargeting.targets b/src/Tasks/Microsoft.Common.CrossTargeting.targets index 811dcdc42cc..296912f5025 100644 --- a/src/Tasks/Microsoft.Common.CrossTargeting.targets +++ b/src/Tasks/Microsoft.Common.CrossTargeting.targets @@ -201,7 +201,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. $(MSBuildToolsPath)\NuGet.targets - +