diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 02e5a2f2fee..02a99bf24b4 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -29,6 +29,9 @@ Change wave checks around features will be removed in the release that accompani ## Current Rotation of Change Waves +### 18.10 +- [Restore passes ExcludeRestorePackageImports=true as a global property so NuGet's restore no longer triggers a second evaluation of every project.](https://github.com/dotnet/msbuild/issues/14273) + ### 18.9 - [GenerateResource: typed ResX data/metadata entries in Mark-of-the-Web files are now treated as untrusted and blocked with MSB3821; unblock the file (or set MSBUILDDISABLEFEATURESFROMVERSION=18.9) to restore prior behavior. ResXFileRef entries are always blocked regardless of this wave.](https://github.com/dotnet/msbuild/pull/14015) - [TaskHost named-pipe buffers default to 1 MB (was 128 KB), reducing send backpressure for large TaskHostConfiguration packets. Tunable via MSBUILDNODECONNECTIONBUFFERSIZE](https://github.com/dotnet/msbuild/pull/14094) diff --git a/src/Framework/ChangeWaves.cs b/src/Framework/ChangeWaves.cs index bd25ffd1b86..808af2d69a6 100644 --- a/src/Framework/ChangeWaves.cs +++ b/src/Framework/ChangeWaves.cs @@ -37,7 +37,8 @@ internal static class ChangeWaves internal static readonly Version Wave18_7 = new Version(18, 7); internal static readonly Version Wave18_8 = new Version(18, 8); internal static readonly Version Wave18_9 = new Version(18, 9); - internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7, Wave18_8, Wave18_9]; + internal static readonly Version Wave18_10 = new Version(18, 10); + internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7, Wave18_8, Wave18_9, Wave18_10]; /// /// Special value indicating that all features behind all Change Waves should be enabled. diff --git a/src/Framework/MSBuildConstants.cs b/src/Framework/MSBuildConstants.cs index 4e8e8fd29d8..9ab783960d6 100644 --- a/src/Framework/MSBuildConstants.cs +++ b/src/Framework/MSBuildConstants.cs @@ -80,6 +80,20 @@ internal static class MSBuildConstants /// internal const string MSBuildIsRestoring = nameof(MSBuildIsRestoring); + /// + /// A property set during an implicit restore (/restore) or explicit restore (/t:restore) to instruct projects to exclude imports that are + /// provided by NuGet packages. NuGet passes this same global property when it re-invokes the build, so setting it up front ensures the + /// initial evaluation is reused rather than being discarded and re-evaluated. + /// + internal const string ExcludeRestorePackageImports = nameof(ExcludeRestorePackageImports); + + /// + /// The value MSBuild assigns to during restore. It must match the literal value NuGet passes + /// (lowercase "true" from NuGet.targets) because global property values are compared case-sensitively when matching build configurations + /// for evaluation reuse. + /// + internal const string ExcludeRestorePackageImportsValue = "true"; + /// /// The most current VSGeneralAssemblyVersion known to this version of MSBuild. /// diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index d4bce53e21c..7285bd81853 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -2615,6 +2615,49 @@ public void RestoreRunsInitialTargets() logContents.ShouldContain("Restore target ran"); } + /// + /// Verifies restore sets the ExcludeRestorePackageImports global property so that NuGet's restore reuses the initial + /// evaluation instead of forcing a second evaluation of every project. + /// + [Fact] + public void RestoreSetsExcludeRestorePackageImports() + { + string projectContents = ObjectModelHelpers.CleanupFileContents( + @" + + + +"); + + string logContents = ExecuteMSBuildExeExpectSuccess(projectContents, arguments: "/t:restore"); + + // The value must be the literal lowercase "true" to exactly match the global property NuGet's restore passes + // (NuGet.targets sets ExcludeRestorePackageImports=true), otherwise evaluation reuse would not occur. + logContents.ShouldContain($"ExcludeRestorePackageImports=[{MSBuildConstants.ExcludeRestorePackageImportsValue}]"); + } + + /// + /// Verifies restore does not set the ExcludeRestorePackageImports global property when change wave 18.10 is disabled, + /// preserving the prior behavior for repositories that opt out. + /// + [Fact] + public void RestoreDoesNotSetExcludeRestorePackageImportsWhenWaveDisabled() + { + string projectContents = ObjectModelHelpers.CleanupFileContents( + @" + + + +"); + + Dictionary envVars = new() { { "MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_10.ToString() } }; + + string logContents = ExecuteMSBuildExeExpectSuccess(projectContents, envsToCreate: envVars, arguments: "/t:restore"); + + // When the wave is disabled, the property must not be injected. + logContents.ShouldContain("ExcludeRestorePackageImports=[]"); + } + /// /// We check if there is only one target name specified and this logic caused a regression: https://github.com/dotnet/msbuild/issues/3317 /// diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 6f29cbac80b..6111509324c 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -2064,6 +2064,19 @@ private static BuildResult ExecuteRestore(string projectFile, string toolsVersio // Add a property to indicate that a Restore is executing restoreGlobalProperties[MSBuildConstants.MSBuildIsRestoring] = bool.TrueString; + // Add the property that NuGet passes when it re-invokes the build during restore. NuGet's restore targets pass + // ExcludeRestorePackageImports=true as a global property, which normally forces a second evaluation of every project. + // Setting it up front here means the initial evaluation already matches, so it is reused instead of being discarded. + // The value must match NuGet's exactly (the literal lowercase "true" from NuGet.targets) because global property values + // are compared case-sensitively when matching build configurations for evaluation reuse. + // Only set it when the user has not explicitly supplied a value so that an explicit opt-out (e.g. + // /p:ExcludeRestorePackageImports=false) is respected instead of being silently overwritten. + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_10) + && !globalProperties.ContainsKey(MSBuildConstants.ExcludeRestorePackageImports)) + { + restoreGlobalProperties[MSBuildConstants.ExcludeRestorePackageImports] = MSBuildConstants.ExcludeRestorePackageImportsValue; + } + // Create a new request with a Restore target only and specify: // - BuildRequestDataFlags.ClearCachesAfterBuild to ensure the projects will be reloaded from disk for subsequent builds // - BuildRequestDataFlags.SkipNonexistentTargets to ignore missing targets since Restore does not require that all targets exist