Skip to content
3 changes: 3 additions & 0 deletions documentation/wiki/ChangeWaves.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/Framework/ChangeWaves.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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];

/// <summary>
/// Special value indicating that all features behind all Change Waves should be enabled.
Expand Down
14 changes: 14 additions & 0 deletions src/Framework/MSBuildConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ internal static class MSBuildConstants
/// </summary>
internal const string MSBuildIsRestoring = nameof(MSBuildIsRestoring);

/// <summary>
/// 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.
/// </summary>
internal const string ExcludeRestorePackageImports = nameof(ExcludeRestorePackageImports);

/// <summary>
/// The value MSBuild assigns to <see cref="ExcludeRestorePackageImports"/> 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.
/// </summary>
internal const string ExcludeRestorePackageImportsValue = "true";

/// <summary>
/// The most current VSGeneralAssemblyVersion known to this version of MSBuild.
/// </summary>
Expand Down
43 changes: 43 additions & 0 deletions src/MSBuild.UnitTests/XMake_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2615,6 +2615,49 @@ public void RestoreRunsInitialTargets()
logContents.ShouldContain("Restore target ran");
}

/// <summary>
/// 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.
/// </summary>
[Fact]
public void RestoreSetsExcludeRestorePackageImports()
{
string projectContents = ObjectModelHelpers.CleanupFileContents(
@"<Project>
<Target Name=""Restore"">
<Message Text=""ExcludeRestorePackageImports=[$(ExcludeRestorePackageImports)]"" Importance=""High"" />
</Target>
</Project>");

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}]");
Comment thread
ViktorHofer marked this conversation as resolved.
}

/// <summary>
/// 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.
/// </summary>
[Fact]
public void RestoreDoesNotSetExcludeRestorePackageImportsWhenWaveDisabled()
{
string projectContents = ObjectModelHelpers.CleanupFileContents(
@"<Project>
<Target Name=""Restore"">
<Message Text=""ExcludeRestorePackageImports=[$(ExcludeRestorePackageImports)]"" Importance=""High"" />
</Target>
</Project>");

Dictionary<string, string> 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=[]");
}

/// <summary>
/// We check if there is only one target name specified and this logic caused a regression: https://github.com/dotnet/msbuild/issues/3317
/// </summary>
Expand Down
13 changes: 13 additions & 0 deletions src/MSBuild/XMake.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
ViktorHofer marked this conversation as resolved.
}

// 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
Expand Down
Loading