diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md
index c09ef8f9bf9..66693a74667 100644
--- a/documentation/wiki/ChangeWaves.md
+++ b/documentation/wiki/ChangeWaves.md
@@ -24,6 +24,9 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t
## Current Rotation of Change Waves
+### 18.4
+- [Start throwing on null or empty paths in MultiProcess and MultiThreaded Task Environment Drivers.](https://github.com/dotnet/msbuild/pull/12914)
+
### 18.3
- [Replace Transactional property with ChangeWave control, implement atomic file replacement with retry logic, and update tests.](https://github.com/dotnet/msbuild/pull/12627)
diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
index f794bf64293..bec24b034f7 100644
--- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
@@ -4,11 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-#if NETFRAMEWORK
-using Microsoft.IO;
-#else
-using System.IO;
-#endif
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
@@ -45,10 +40,13 @@ public AbsolutePath ProjectDirectory
///
public AbsolutePath GetAbsolutePath(string path)
{
- // This function should not throw when path has illegal characters.
- // For .NET Framework, Microsoft.IO.Path.Combine should be used instead of System.IO.Path.Combine to achieve it.
- // For .NET Core, System.IO.Path.Combine already does not throw in this case.
- return new AbsolutePath(Path.Combine(NativeMethodsShared.GetCurrentDirectory(), path), ignoreRootedCheck: true);
+ // Opt-out for null path when Wave18_4 is disabled - return null as-is.
+ if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && path is null)
+ {
+ return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
+ }
+
+ return new AbsolutePath(path, basePath: ProjectDirectory);
}
///
diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
index cf54c430d0d..1bf9009054a 100644
--- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
@@ -71,6 +71,12 @@ public AbsolutePath ProjectDirectory
///
public AbsolutePath GetAbsolutePath(string path)
{
+ // Opt-out for null path when Wave18_4 is disabled - return null as-is.
+ if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && path is null)
+ {
+ return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
+ }
+
return new AbsolutePath(path, ProjectDirectory);
}
diff --git a/src/Framework/ChangeWaves.cs b/src/Framework/ChangeWaves.cs
index 66ff885a352..22049fdfc65 100644
--- a/src/Framework/ChangeWaves.cs
+++ b/src/Framework/ChangeWaves.cs
@@ -31,7 +31,8 @@ internal static class ChangeWaves
internal static readonly Version Wave17_12 = new Version(17, 12);
internal static readonly Version Wave17_14 = new Version(17, 14);
internal static readonly Version Wave18_3 = new Version(18, 3);
- internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3];
+ internal static readonly Version Wave18_4 = new Version(18, 4);
+ internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4];
///
/// Special value indicating that all features behind all Change Waves should be enabled.
diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs
index 1c5cc585e5b..7fb31c57232 100644
--- a/src/Framework/PathHelpers/AbsolutePath.cs
+++ b/src/Framework/PathHelpers/AbsolutePath.cs
@@ -19,7 +19,7 @@ namespace Microsoft.Build.Framework
/// file system conventions (case-sensitive on Linux, case-insensitive on Windows and macOS).
/// Does not perform any normalization beyond validating the path is fully qualified.
/// A default instance (created via default(AbsolutePath)) has a null Value
- /// and should not be used. Two default instances are considered equal.
+ /// and represents an issue in path handling. Two default instances are considered equal.
///
public readonly struct AbsolutePath : IEquatable
{
@@ -32,6 +32,11 @@ namespace Microsoft.Build.Framework
/// The normalized string representation of this path.
///
public string Value { get; }
+
+ ///
+ /// The original string used to create this path.
+ ///
+ public string OriginalValue { get; }
///
/// Initializes a new instance of the struct.
@@ -41,6 +46,7 @@ public AbsolutePath(string path)
{
ValidatePath(path);
Value = path;
+ OriginalValue = path;
}
///
@@ -50,12 +56,24 @@ public AbsolutePath(string path)
/// If true, skips checking whether the path is rooted.
/// For internal and testing use, when we want to force bypassing the rooted check.
internal AbsolutePath(string path, bool ignoreRootedCheck)
+ : this(path, path, ignoreRootedCheck)
{
- if (!ignoreRootedCheck)
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The absolute path string.
+ /// The original string used to create this path.
+ /// If true, skips checking whether the path is rooted.
+ internal AbsolutePath(string path, string original, bool ignoreRootedCheck)
+ {
+ if (!ignoreRootedCheck)
{
ValidatePath(path);
}
Value = path;
+ OriginalValue = original;
}
///
@@ -85,7 +103,14 @@ private static void ValidatePath(string path)
///
/// The path to combine with the base path.
/// The base path to combine with.
- public AbsolutePath(string path, AbsolutePath basePath) => Value = Path.Combine(basePath.Value, path);
+ public AbsolutePath(string path, AbsolutePath basePath)
+ {
+ // This function should not throw when path has illegal characters.
+ // For .NET Framework, Microsoft.IO.Path.Combine should be used instead of System.IO.Path.Combine to achieve it.
+ // For .NET Core, System.IO.Path.Combine already does not throw in this case.
+ Value = Path.Combine(basePath.Value, path);
+ OriginalValue = path;
+ }
///
/// Implicitly converts an AbsolutePath to a string.