From 1c9ea4761f2916f2b299f79d79756afb87528d22 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Mon, 26 Jan 2026 12:18:06 +0100
Subject: [PATCH 01/13] absolutizing "" is error

---
 .../TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs      | 2 +-
 .../TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
index bec24b034f7..9e4895b4bb5 100644
--- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
@@ -41,7 +41,7 @@ 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)
+            if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path))
             {
                 return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
             }
diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
index 1bf9009054a..d356e75e996 100644
--- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
@@ -72,7 +72,7 @@ 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)
+            if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path))
             {
                 return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
             }

From 17bff5e129545f0337c7ba1932f49206457b688e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Mon, 26 Jan 2026 15:06:58 +0100
Subject: [PATCH 02/13] refactor and correct throwing

---
 .../MultiProcessTaskEnvironmentDriver.cs      |  8 +------
 .../MultiThreadedTaskEnvironmentDriver.cs     |  8 +------
 src/Framework/PathHelpers/AbsolutePath.cs     | 22 +++++++++++++++++++
 3 files changed, 24 insertions(+), 14 deletions(-)

diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
index 9e4895b4bb5..6544b57accf 100644
--- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
@@ -40,13 +40,7 @@ public AbsolutePath ProjectDirectory
         /// <inheritdoc/>
         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) && string.IsNullOrEmpty(path))
-            {
-                return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
-            }
-
-            return new AbsolutePath(path, basePath: ProjectDirectory);
+            return AbsolutePath.CreateFromRelative(path, ProjectDirectory);
         }
 
         /// <inheritdoc/>
diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
index d356e75e996..7f18d99d7fb 100644
--- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
@@ -71,13 +71,7 @@ public AbsolutePath ProjectDirectory
         /// <inheritdoc/>
         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) && string.IsNullOrEmpty(path))
-            {
-                return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
-            }
-
-            return new AbsolutePath(path, ProjectDirectory);
+            return AbsolutePath.CreateFromRelative(path, ProjectDirectory);
         }
 
         /// <inheritdoc/>
diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs
index 7fb31c57232..ebd8eb5d66b 100644
--- a/src/Framework/PathHelpers/AbsolutePath.cs
+++ b/src/Framework/PathHelpers/AbsolutePath.cs
@@ -97,6 +97,28 @@ private static void ValidatePath(string path)
             }
 #endif
         }
+        /// <summary>
+        /// Creates an <see cref="AbsolutePath"/> from a relative path combined with a base path.
+        /// </summary>
+        /// <param name="path">The path to combine with the base path.</param>
+        /// <param name="basePath">The base path to combine with.</param>
+        /// <returns>The combined absolute path.</returns>
+        /// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is empty and ChangeWave 18.4 is enabled.</exception>
+        internal static AbsolutePath CreateFromRelative(string path, AbsolutePath basePath)
+        {
+            // Opt-out for null/empty path when Wave18_4 is disabled - return as-is for legacy behavior.
+            if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path))
+            {
+                return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
+            }
+
+            if (path == "")
+            {
+                throw new ArgumentException("Path must not be empty.", nameof(path));
+            }
+
+            return new AbsolutePath(path, basePath);
+        }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AbsolutePath"/> struct by combining an absolute path with a relative path.

From 8371aa59be40ec00b219e997db3c803630321ffd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Mon, 26 Jan 2026 15:30:14 +0100
Subject: [PATCH 03/13] tests

---
 .../BackEnd/TaskEnvironment_Tests.cs          |  45 ++-
 src/Framework.UnitTests/AbsolutePath_Tests.cs | 312 ++++++++++++++++++
 2 files changed, 352 insertions(+), 5 deletions(-)

diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
index acb1bb6e832..35aa3770fff 100644
--- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
@@ -387,15 +387,50 @@ public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow(
 
         [Theory]
         [MemberData(nameof(EnvironmentTypes))]
-        public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_ReturnsProjectDirectory(string environmentType)
+        public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Disabled_ReturnsEmptyPath(string environmentType)
         {
+            using TestEnvironment testEnv = TestEnvironment.Create();
+            ChangeWaves.ResetStateForTests();
+            testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
+            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
             var taskEnvironment = CreateTaskEnvironment(environmentType);
 
-            // Empty path should absolutize to project directory (Path.Combine behavior)
-            var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty);
+            try
+            {
+                // When Wave18_4 is disabled, empty path returns as-is (legacy behavior)
+                var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty);
+
+                absolutePath.Value.ShouldBe(string.Empty);
+                absolutePath.OriginalValue.ShouldBe(string.Empty);
+            }
+            finally
+            {
+                DisposeTaskEnvironment(taskEnvironment);
+                ChangeWaves.ResetStateForTests();
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(EnvironmentTypes))]
+        public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Enabled_Throws(string environmentType)
+        {
+            using TestEnvironment testEnv = TestEnvironment.Create();
+            ChangeWaves.ResetStateForTests();
+            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
+            var taskEnvironment = CreateTaskEnvironment(environmentType);
 
-            absolutePath.Value.ShouldBe(taskEnvironment.ProjectDirectory.Value);
-            absolutePath.OriginalValue.ShouldBe(string.Empty);
+            try
+            {
+                // When Wave18_4 is enabled, empty path should throw
+                Should.Throw<ArgumentException>(() => taskEnvironment.GetAbsolutePath(string.Empty));
+            }
+            finally
+            {
+                DisposeTaskEnvironment(taskEnvironment);
+                ChangeWaves.ResetStateForTests();
+            }
         }
 
         [Theory]
diff --git a/src/Framework.UnitTests/AbsolutePath_Tests.cs b/src/Framework.UnitTests/AbsolutePath_Tests.cs
index 3a099ef36c5..bca6393b1c5 100644
--- a/src/Framework.UnitTests/AbsolutePath_Tests.cs
+++ b/src/Framework.UnitTests/AbsolutePath_Tests.cs
@@ -1,8 +1,10 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
+using System;
 using System.IO;
 using Microsoft.Build.Framework;
+using Microsoft.Build.Shared;
 using Shouldly;
 using Xunit;
 
@@ -10,6 +12,12 @@ namespace Microsoft.Build.UnitTests
 {
     public class AbsolutePath_Tests
     {
+        private static AbsolutePath GetTestBasePath()
+        {
+            string baseDirectory = Path.Combine(Path.GetTempPath(), "abspath_test_base");
+            return new AbsolutePath(baseDirectory, ignoreRootedCheck: false);
+        }
+
         private static void ValidatePathAcceptance(string path, bool shouldBeAccepted)
         {
             if (shouldBeAccepted)
@@ -189,5 +197,309 @@ public void AbsolutePath_UnixPathValidation_ShouldAcceptOnlyTrueAbsolutePaths(st
         {
             ValidatePathAcceptance(path, shouldBeAccepted);
         }
+
+        #region CreateFromRelative Tests
+
+        [Theory]
+        [InlineData("subfolder")]
+        [InlineData("deep/nested/path")]
+        [InlineData(".")]
+        [InlineData("..")]
+        [InlineData("./relative")]
+        [InlineData("../parent")]
+        [InlineData("file.txt")]
+        public void CreateFromRelative_WithValidRelativePath_ReturnsAbsolutePath(string relativePath)
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            string expectedPath = Path.Combine(basePath.Value, relativePath);
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(relativePath);
+            Path.IsPathRooted(result.Value).ShouldBeTrue();
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithEmptyPath_WhenWave18_4Enabled_ThrowsArgumentException()
+        {
+            // Arrange - Wave18_4 is enabled by default
+            using TestEnvironment testEnv = TestEnvironment.Create();
+            ChangeWaves.ResetStateForTests();
+            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
+            AbsolutePath basePath = GetTestBasePath();
+
+            // Act & Assert
+            var exception = Should.Throw<ArgumentException>(() => AbsolutePath.CreateFromRelative(string.Empty, basePath));
+            exception.ParamName.ShouldBe("path");
+            exception.Message.ShouldContain("must not be empty");
+
+            ChangeWaves.ResetStateForTests();
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithNullPath_WhenWave18_4Enabled_ThrowsArgumentNullException()
+        {
+            // Arrange - Wave18_4 is enabled by default
+            using TestEnvironment testEnv = TestEnvironment.Create();
+            ChangeWaves.ResetStateForTests();
+            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
+            AbsolutePath basePath = GetTestBasePath();
+
+            // Act & Assert - Path.Combine throws ArgumentNullException for null path
+            Should.Throw<ArgumentNullException>(() => AbsolutePath.CreateFromRelative(null!, basePath));
+
+            ChangeWaves.ResetStateForTests();
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithEmptyPath_WhenWave18_4Disabled_ReturnsEmptyPath()
+        {
+            // Arrange
+            using TestEnvironment testEnv = TestEnvironment.Create();
+            ChangeWaves.ResetStateForTests();
+            testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
+            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
+            AbsolutePath basePath = GetTestBasePath();
+
+            // Act - Legacy behavior returns empty path as-is
+            AbsolutePath result = AbsolutePath.CreateFromRelative(string.Empty, basePath);
+
+            // Assert
+            result.Value.ShouldBe(string.Empty);
+            result.OriginalValue.ShouldBe(string.Empty);
+
+            ChangeWaves.ResetStateForTests();
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath()
+        {
+            // Arrange
+            using TestEnvironment testEnv = TestEnvironment.Create();
+            ChangeWaves.ResetStateForTests();
+            testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
+            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
+            AbsolutePath basePath = GetTestBasePath();
+
+            // Act - Legacy behavior returns null path as-is
+            AbsolutePath result = AbsolutePath.CreateFromRelative(null!, basePath);
+
+            // Assert
+            result.Value.ShouldBeNull();
+            result.OriginalValue.ShouldBeNull();
+
+            ChangeWaves.ResetStateForTests();
+        }
+
+        [WindowsOnlyTheory]
+        [InlineData("C:\\absolute\\path")]
+        [InlineData("D:\\another\\absolute")]
+        public void CreateFromRelative_WithWindowsAbsolutePath_ReturnsAbsolutePath(string absolutePath)
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            // Path.Combine with an absolute second path returns the second path
+            string expectedPath = absolutePath;
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(absolutePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(absolutePath);
+        }
+
+        [UnixOnlyTheory]
+        [InlineData("/absolute/path")]
+        [InlineData("/another/absolute")]
+        public void CreateFromRelative_WithUnixAbsolutePath_ReturnsAbsolutePath(string absolutePath)
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            // Path.Combine with an absolute second path returns the second path
+            string expectedPath = absolutePath;
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(absolutePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(absolutePath);
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithNestedRelativePath_CombinesCorrectly()
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            string relativePath = Path.Combine("level1", "level2", "level3", "file.txt");
+            string expectedPath = Path.Combine(basePath.Value, relativePath);
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(relativePath);
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithCurrentDirectory_CombinesCorrectly()
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            string relativePath = ".";
+            string expectedPath = Path.Combine(basePath.Value, relativePath);
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(relativePath);
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithParentDirectory_CombinesCorrectly()
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            string relativePath = "..";
+            string expectedPath = Path.Combine(basePath.Value, relativePath);
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(relativePath);
+        }
+
+        [Theory]
+        [InlineData("../sibling")]
+        [InlineData("../../grandparent")]
+        [InlineData("../sibling/child")]
+        public void CreateFromRelative_WithParentTraversal_CombinesCorrectly(string relativePath)
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            string expectedPath = Path.Combine(basePath.Value, relativePath);
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(relativePath);
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithPathContainingSpaces_CombinesCorrectly()
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            string relativePath = "folder with spaces/file name.txt";
+            string expectedPath = Path.Combine(basePath.Value, relativePath);
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(relativePath);
+        }
+
+        [Fact]
+        public void CreateFromRelative_PreservesOriginalValue()
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            string relativePath = "some/relative/path";
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert - OriginalValue should be the relative path, not the combined path
+            result.OriginalValue.ShouldBe(relativePath);
+            result.Value.ShouldNotBe(relativePath);
+            result.Value.ShouldContain(relativePath);
+        }
+
+        [Fact]
+        public void CreateFromRelative_ResultIsRooted()
+        {
+            // Arrange
+            AbsolutePath basePath = GetTestBasePath();
+            string relativePath = "any/relative/path";
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert
+            Path.IsPathRooted(result.Value).ShouldBeTrue();
+        }
+
+        [Fact]
+        public void CreateFromRelative_WithWhitespaceOnlyPath_WhenWave18_4Enabled_CombinesCorrectly()
+        {
+            // Whitespace-only is not empty, so it should be processed by Path.Combine
+            using TestEnvironment testEnv = TestEnvironment.Create();
+            ChangeWaves.ResetStateForTests();
+            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
+            AbsolutePath basePath = GetTestBasePath();
+            string relativePath = "   ";
+            string expectedPath = Path.Combine(basePath.Value, relativePath);
+
+            // Act
+            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
+
+            // Assert
+            result.Value.ShouldBe(expectedPath);
+            result.OriginalValue.ShouldBe(relativePath);
+
+            ChangeWaves.ResetStateForTests();
+        }
+
+        [Fact]
+        public void CreateFromRelative_ChangeWaveTransition_BehaviorChangesCorrectly()
+        {
+            // This test verifies that the behavior changes when the wave is enabled/disabled
+            AbsolutePath basePath = GetTestBasePath();
+
+            // Test 1: Wave disabled - empty path returns as-is
+            using (TestEnvironment testEnv1 = TestEnvironment.Create())
+            {
+                ChangeWaves.ResetStateForTests();
+                testEnv1.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
+                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
+                AbsolutePath result = AbsolutePath.CreateFromRelative(string.Empty, basePath);
+                result.Value.ShouldBe(string.Empty);
+
+                ChangeWaves.ResetStateForTests();
+            }
+
+            // Test 2: Wave enabled - empty path throws
+            using (TestEnvironment testEnv2 = TestEnvironment.Create())
+            {
+                ChangeWaves.ResetStateForTests();
+                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
+
+                Should.Throw<ArgumentException>(() => AbsolutePath.CreateFromRelative(string.Empty, basePath));
+
+                ChangeWaves.ResetStateForTests();
+            }
+        }
+
+        #endregion
     }
 }

From 9bca29a4c16bc40de15877bf20104bf06c91d001 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 17:49:50 +0100
Subject: [PATCH 04/13] remove CreateFromRelative

---
 src/Framework.UnitTests/AbsolutePath_Tests.cs | 304 ------------------
 src/Framework/PathHelpers/AbsolutePath.cs     |  25 +-
 2 files changed, 4 insertions(+), 325 deletions(-)

diff --git a/src/Framework.UnitTests/AbsolutePath_Tests.cs b/src/Framework.UnitTests/AbsolutePath_Tests.cs
index bca6393b1c5..5b864549960 100644
--- a/src/Framework.UnitTests/AbsolutePath_Tests.cs
+++ b/src/Framework.UnitTests/AbsolutePath_Tests.cs
@@ -197,309 +197,5 @@ public void AbsolutePath_UnixPathValidation_ShouldAcceptOnlyTrueAbsolutePaths(st
         {
             ValidatePathAcceptance(path, shouldBeAccepted);
         }
-
-        #region CreateFromRelative Tests
-
-        [Theory]
-        [InlineData("subfolder")]
-        [InlineData("deep/nested/path")]
-        [InlineData(".")]
-        [InlineData("..")]
-        [InlineData("./relative")]
-        [InlineData("../parent")]
-        [InlineData("file.txt")]
-        public void CreateFromRelative_WithValidRelativePath_ReturnsAbsolutePath(string relativePath)
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            string expectedPath = Path.Combine(basePath.Value, relativePath);
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(relativePath);
-            Path.IsPathRooted(result.Value).ShouldBeTrue();
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithEmptyPath_WhenWave18_4Enabled_ThrowsArgumentException()
-        {
-            // Arrange - Wave18_4 is enabled by default
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            AbsolutePath basePath = GetTestBasePath();
-
-            // Act & Assert
-            var exception = Should.Throw<ArgumentException>(() => AbsolutePath.CreateFromRelative(string.Empty, basePath));
-            exception.ParamName.ShouldBe("path");
-            exception.Message.ShouldContain("must not be empty");
-
-            ChangeWaves.ResetStateForTests();
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithNullPath_WhenWave18_4Enabled_ThrowsArgumentNullException()
-        {
-            // Arrange - Wave18_4 is enabled by default
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            AbsolutePath basePath = GetTestBasePath();
-
-            // Act & Assert - Path.Combine throws ArgumentNullException for null path
-            Should.Throw<ArgumentNullException>(() => AbsolutePath.CreateFromRelative(null!, basePath));
-
-            ChangeWaves.ResetStateForTests();
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithEmptyPath_WhenWave18_4Disabled_ReturnsEmptyPath()
-        {
-            // Arrange
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            AbsolutePath basePath = GetTestBasePath();
-
-            // Act - Legacy behavior returns empty path as-is
-            AbsolutePath result = AbsolutePath.CreateFromRelative(string.Empty, basePath);
-
-            // Assert
-            result.Value.ShouldBe(string.Empty);
-            result.OriginalValue.ShouldBe(string.Empty);
-
-            ChangeWaves.ResetStateForTests();
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath()
-        {
-            // Arrange
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            AbsolutePath basePath = GetTestBasePath();
-
-            // Act - Legacy behavior returns null path as-is
-            AbsolutePath result = AbsolutePath.CreateFromRelative(null!, basePath);
-
-            // Assert
-            result.Value.ShouldBeNull();
-            result.OriginalValue.ShouldBeNull();
-
-            ChangeWaves.ResetStateForTests();
-        }
-
-        [WindowsOnlyTheory]
-        [InlineData("C:\\absolute\\path")]
-        [InlineData("D:\\another\\absolute")]
-        public void CreateFromRelative_WithWindowsAbsolutePath_ReturnsAbsolutePath(string absolutePath)
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            // Path.Combine with an absolute second path returns the second path
-            string expectedPath = absolutePath;
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(absolutePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(absolutePath);
-        }
-
-        [UnixOnlyTheory]
-        [InlineData("/absolute/path")]
-        [InlineData("/another/absolute")]
-        public void CreateFromRelative_WithUnixAbsolutePath_ReturnsAbsolutePath(string absolutePath)
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            // Path.Combine with an absolute second path returns the second path
-            string expectedPath = absolutePath;
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(absolutePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(absolutePath);
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithNestedRelativePath_CombinesCorrectly()
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            string relativePath = Path.Combine("level1", "level2", "level3", "file.txt");
-            string expectedPath = Path.Combine(basePath.Value, relativePath);
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(relativePath);
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithCurrentDirectory_CombinesCorrectly()
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            string relativePath = ".";
-            string expectedPath = Path.Combine(basePath.Value, relativePath);
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(relativePath);
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithParentDirectory_CombinesCorrectly()
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            string relativePath = "..";
-            string expectedPath = Path.Combine(basePath.Value, relativePath);
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(relativePath);
-        }
-
-        [Theory]
-        [InlineData("../sibling")]
-        [InlineData("../../grandparent")]
-        [InlineData("../sibling/child")]
-        public void CreateFromRelative_WithParentTraversal_CombinesCorrectly(string relativePath)
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            string expectedPath = Path.Combine(basePath.Value, relativePath);
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(relativePath);
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithPathContainingSpaces_CombinesCorrectly()
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            string relativePath = "folder with spaces/file name.txt";
-            string expectedPath = Path.Combine(basePath.Value, relativePath);
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(relativePath);
-        }
-
-        [Fact]
-        public void CreateFromRelative_PreservesOriginalValue()
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            string relativePath = "some/relative/path";
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert - OriginalValue should be the relative path, not the combined path
-            result.OriginalValue.ShouldBe(relativePath);
-            result.Value.ShouldNotBe(relativePath);
-            result.Value.ShouldContain(relativePath);
-        }
-
-        [Fact]
-        public void CreateFromRelative_ResultIsRooted()
-        {
-            // Arrange
-            AbsolutePath basePath = GetTestBasePath();
-            string relativePath = "any/relative/path";
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert
-            Path.IsPathRooted(result.Value).ShouldBeTrue();
-        }
-
-        [Fact]
-        public void CreateFromRelative_WithWhitespaceOnlyPath_WhenWave18_4Enabled_CombinesCorrectly()
-        {
-            // Whitespace-only is not empty, so it should be processed by Path.Combine
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            AbsolutePath basePath = GetTestBasePath();
-            string relativePath = "   ";
-            string expectedPath = Path.Combine(basePath.Value, relativePath);
-
-            // Act
-            AbsolutePath result = AbsolutePath.CreateFromRelative(relativePath, basePath);
-
-            // Assert
-            result.Value.ShouldBe(expectedPath);
-            result.OriginalValue.ShouldBe(relativePath);
-
-            ChangeWaves.ResetStateForTests();
-        }
-
-        [Fact]
-        public void CreateFromRelative_ChangeWaveTransition_BehaviorChangesCorrectly()
-        {
-            // This test verifies that the behavior changes when the wave is enabled/disabled
-            AbsolutePath basePath = GetTestBasePath();
-
-            // Test 1: Wave disabled - empty path returns as-is
-            using (TestEnvironment testEnv1 = TestEnvironment.Create())
-            {
-                ChangeWaves.ResetStateForTests();
-                testEnv1.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
-                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-                AbsolutePath result = AbsolutePath.CreateFromRelative(string.Empty, basePath);
-                result.Value.ShouldBe(string.Empty);
-
-                ChangeWaves.ResetStateForTests();
-            }
-
-            // Test 2: Wave enabled - empty path throws
-            using (TestEnvironment testEnv2 = TestEnvironment.Create())
-            {
-                ChangeWaves.ResetStateForTests();
-                BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-                Should.Throw<ArgumentException>(() => AbsolutePath.CreateFromRelative(string.Empty, basePath));
-
-                ChangeWaves.ResetStateForTests();
-            }
-        }
-
-        #endregion
     }
 }
diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs
index ebd8eb5d66b..90a8910006d 100644
--- a/src/Framework/PathHelpers/AbsolutePath.cs
+++ b/src/Framework/PathHelpers/AbsolutePath.cs
@@ -97,36 +97,19 @@ private static void ValidatePath(string path)
             }
 #endif
         }
+
         /// <summary>
-        /// Creates an <see cref="AbsolutePath"/> from a relative path combined with a base path.
+        /// Initializes a new instance of the <see cref="AbsolutePath"/> struct by combining an absolute path with a relative path.
         /// </summary>
         /// <param name="path">The path to combine with the base path.</param>
         /// <param name="basePath">The base path to combine with.</param>
-        /// <returns>The combined absolute path.</returns>
-        /// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is empty and ChangeWave 18.4 is enabled.</exception>
-        internal static AbsolutePath CreateFromRelative(string path, AbsolutePath basePath)
+        public AbsolutePath(string path, AbsolutePath basePath)
         {
-            // Opt-out for null/empty path when Wave18_4 is disabled - return as-is for legacy behavior.
-            if (!ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_4) && string.IsNullOrEmpty(path))
-            {
-                return new AbsolutePath(path!, path!, ignoreRootedCheck: true);
-            }
-
-            if (path == "")
+            if (path.Length == 0)
             {
                 throw new ArgumentException("Path must not be empty.", nameof(path));
             }
 
-            return new AbsolutePath(path, basePath);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AbsolutePath"/> struct by combining an absolute path with a relative path.
-        /// </summary>
-        /// <param name="path">The path to combine with the base path.</param>
-        /// <param name="basePath">The base path to combine with.</param>
-        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.

From 7dfe40b83a9735792cc5e11d0005ac73e53c1aaf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 17:51:41 +0100
Subject: [PATCH 05/13] docs

---
 src/Framework/PathHelpers/AbsolutePath.cs | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs
index 90a8910006d..4f5bd529d4d 100644
--- a/src/Framework/PathHelpers/AbsolutePath.cs
+++ b/src/Framework/PathHelpers/AbsolutePath.cs
@@ -103,11 +103,12 @@ private static void ValidatePath(string path)
         /// </summary>
         /// <param name="path">The path to combine with the base path.</param>
         /// <param name="basePath">The base path to combine with.</param>
+        /// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is null or empty.</exception>
         public AbsolutePath(string path, AbsolutePath basePath)
         {
-            if (path.Length == 0)
+            if (string.IsNullOrEmpty(path))
             {
-                throw new ArgumentException("Path must not be empty.", nameof(path));
+                throw new ArgumentException("Path must not be null or empty.", nameof(path));
             }
 
             // This function should not throw when path has illegal characters.

From 93fce8ee47b203c3b6ce961448a723cf477fcbbb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 17:52:39 +0100
Subject: [PATCH 06/13] docs

---
 src/Framework/PathHelpers/AbsolutePath.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Framework/PathHelpers/AbsolutePath.cs b/src/Framework/PathHelpers/AbsolutePath.cs
index 4f5bd529d4d..3db31daff64 100644
--- a/src/Framework/PathHelpers/AbsolutePath.cs
+++ b/src/Framework/PathHelpers/AbsolutePath.cs
@@ -42,6 +42,7 @@ namespace Microsoft.Build.Framework
         /// Initializes a new instance of the <see cref="AbsolutePath"/> struct.
         /// </summary>
         /// <param name="path">The absolute path string.</param>
+        /// <exception cref="ArgumentException">Thrown if <paramref name="path"/> is null, empty, or not a rooted path.</exception>
         public AbsolutePath(string path)
         {
             ValidatePath(path);

From 9234a98f466833a63a13050d6ec4c7e4e847273e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 17:56:23 +0100
Subject: [PATCH 07/13] get rid of usage

---
 .../MultiProcessTaskEnvironmentDriver.cs       |  2 +-
 .../MultiThreadedTaskEnvironmentDriver.cs      |  2 +-
 src/Framework.UnitTests/AbsolutePath_Tests.cs  | 18 +++++++++++++++++-
 3 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
index 6544b57accf..490ed0ca9bc 100644
--- a/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
@@ -40,7 +40,7 @@ public AbsolutePath ProjectDirectory
         /// <inheritdoc/>
         public AbsolutePath GetAbsolutePath(string path)
         {
-            return AbsolutePath.CreateFromRelative(path, ProjectDirectory);
+            return new AbsolutePath(path, ProjectDirectory);
         }
 
         /// <inheritdoc/>
diff --git a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
index 7f18d99d7fb..cf54c430d0d 100644
--- a/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
+++ b/src/Build/BackEnd/TaskExecutionHost/MultiThreadedTaskEnvironmentDriver.cs
@@ -71,7 +71,7 @@ public AbsolutePath ProjectDirectory
         /// <inheritdoc/>
         public AbsolutePath GetAbsolutePath(string path)
         {
-            return AbsolutePath.CreateFromRelative(path, ProjectDirectory);
+            return new AbsolutePath(path, ProjectDirectory);
         }
 
         /// <inheritdoc/>
diff --git a/src/Framework.UnitTests/AbsolutePath_Tests.cs b/src/Framework.UnitTests/AbsolutePath_Tests.cs
index 5b864549960..b890d9938bc 100644
--- a/src/Framework.UnitTests/AbsolutePath_Tests.cs
+++ b/src/Framework.UnitTests/AbsolutePath_Tests.cs
@@ -44,11 +44,27 @@ public void AbsolutePath_FromAbsolutePath_ShouldPreservePath()
             Path.IsPathRooted(absolutePath.Value).ShouldBeTrue();
         }
 
+        [Theory]
+        [InlineData(null)]
+        [InlineData("")]
+        public void AbsolutePath_NullOrEmpty_ShouldThrow(string? path)
+        {
+            Should.Throw<ArgumentException>(() => new AbsolutePath(path!));
+        }
+
+        [Theory]
+        [InlineData(null)]
+        [InlineData("")]
+        public void AbsolutePath_NullOrEmptyWithBasePath_ShouldThrow(string? path)
+        {
+            var basePath = GetTestBasePath();
+            Should.Throw<ArgumentException>(() => new AbsolutePath(path!, basePath));
+        }
+
         [Theory]
         [InlineData("subfolder")]
         [InlineData("deep/nested/path")]
         [InlineData(".")]
-        [InlineData("")]
         [InlineData("..")]
         public void AbsolutePath_FromRelativePath_ShouldResolveAgainstBase(string relativePath)
         {

From d43aa032e4ea020e118fa2ab450fded86b21f6f3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 18:07:10 +0100
Subject: [PATCH 08/13] fix copy to continue on path parsing error

---
 src/Tasks/Copy.cs | 29 +++++++++++++++++++++++------
 1 file changed, 23 insertions(+), 6 deletions(-)

diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs
index a3adcf74260..bbf66e93406 100644
--- a/src/Tasks/Copy.cs
+++ b/src/Tasks/Copy.cs
@@ -311,7 +311,7 @@ private void LogAlwaysRetryDiagnosticFromResources(string messageResourceName, p
 
             if (FailIfNotIncremental)
             {
-                 // Before the introduction of AbsolutePath, this logged full paths, so preserve that behavior
+                // Before the introduction of AbsolutePath, this logged full paths, so preserve that behavior
                 Log.LogError(FileComment, sourceFileState.Path, destinationFileState.Path);
                 return false;
             }
@@ -508,8 +508,17 @@ private bool CopySingleThreaded(
                 string destSpec = DestinationFiles[i].ItemSpec;
 
                 // Compute absolute paths once - reused for ETW, deduplication dictionary, and FileState
-                AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec);
-                AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec);
+                try
+                {
+                    AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec);
+                    AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec);
+                }
+                catch (ArgumentException ex)
+                {
+                    Log.LogErrorWithCodeFromResources("Copy.Error", sourceSpec, destSpec, ex.Message);
+                    success = false;
+                    continue;
+                }
 
                 MSBuildEventSource.Log.CopyUpToDateStart(destAbsolutePath);
                 if (filesActuallyCopied.TryGetValue(destAbsolutePath, out string originalSource))
@@ -663,9 +672,17 @@ void ProcessPartition()
                             string destSpec = destItem.ItemSpec;
 
                             // Compute absolute paths once - reused for ETW, deduplication check, and FileState
-                            AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec);
-                            AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec);
-                            
+                            try
+                            {
+                                AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec);
+                                AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec);
+                            }
+                            catch (ArgumentException ex)
+                            {
+                                Log.LogErrorWithCodeFromResources("Copy.Error", sourceSpec, destSpec, ex.Message);
+                                continue;
+                            }
+
                             // Check if we just copied from this location to the destination, don't copy again.
                             MSBuildEventSource.Log.CopyUpToDateStart(destAbsolutePath);
                             bool copyComplete = false;

From e83ec5226cbf29c2f488b8a769a368411dcb35a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 18:13:11 +0100
Subject: [PATCH 09/13] fix touch

---
 src/Tasks/Touch.cs | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/Tasks/Touch.cs b/src/Tasks/Touch.cs
index 8f442777033..9c826745873 100644
--- a/src/Tasks/Touch.cs
+++ b/src/Tasks/Touch.cs
@@ -95,8 +95,16 @@ internal bool ExecuteImpl(
 
             foreach (ITaskItem file in Files)
             {
-                AbsolutePath path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec));
-
+                try
+                {
+                    AbsolutePath path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec));
+                }
+                catch (ArgumentException ex)
+                {
+                    Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file.ItemSpec, ex.Message);
+                    retVal = false;
+                    continue;
+                }       
                 // For speed, eliminate duplicates caused by poor targets authoring
                 if (touchedFilesSet.Contains(path))
                 {

From 6e085b68dd66f35a48736db66667d502ccceac35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 18:23:03 +0100
Subject: [PATCH 10/13] success false

---
 src/Tasks/Copy.cs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs
index bbf66e93406..7dd5592a00e 100644
--- a/src/Tasks/Copy.cs
+++ b/src/Tasks/Copy.cs
@@ -680,6 +680,7 @@ void ProcessPartition()
                             catch (ArgumentException ex)
                             {
                                 Log.LogErrorWithCodeFromResources("Copy.Error", sourceSpec, destSpec, ex.Message);
+                                success = false;
                                 continue;
                             }
 

From ff9e97bda1af830c3368cea0b866c2dd41c913d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 18:25:18 +0100
Subject: [PATCH 11/13] remove obsolete test

---
 .../BackEnd/TaskEnvironment_Tests.cs          | 36 -------------------
 1 file changed, 36 deletions(-)

diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
index 35aa3770fff..1d806552c25 100644
--- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
@@ -432,41 +432,5 @@ public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Enabled_Th
                 ChangeWaves.ResetStateForTests();
             }
         }
-
-        [Theory]
-        [MemberData(nameof(EnvironmentTypes))]
-        public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Disabled_ReturnsNullPath(string environmentType)
-        {
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            var taskEnvironment = CreateTaskEnvironment(environmentType);
-
-            // When Wave18_4 is disabled, null path returns as-is
-            var absolutePath = taskEnvironment.GetAbsolutePath(null!);
-
-            absolutePath.Value.ShouldBeNull();
-            absolutePath.OriginalValue.ShouldBeNull();
-
-            ChangeWaves.ResetStateForTests();
-        }
-
-        [Theory]
-        [MemberData(nameof(EnvironmentTypes))]
-        public void TaskEnvironment_GetAbsolutePath_WithNullPath_WhenWave18_4Enabled_Throws(string environmentType)
-        {
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            var taskEnvironment = CreateTaskEnvironment(environmentType);
-
-            // When Wave18_4 is enabled, null path should throw
-            Should.Throw<ArgumentNullException>(() => taskEnvironment.GetAbsolutePath(null!));
-
-            ChangeWaves.ResetStateForTests();
-        }
     }
 }

From 72d1eca5c2bad216a62b50a6dd3d342ddb4d332b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Tue, 27 Jan 2026 18:28:11 +0100
Subject: [PATCH 12/13] rm invalid tests

---
 .../BackEnd/TaskEnvironment_Tests.cs          | 48 -------------------
 1 file changed, 48 deletions(-)

diff --git a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
index 1d806552c25..4a9d6126ced 100644
--- a/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
+++ b/src/Build.UnitTests/BackEnd/TaskEnvironment_Tests.cs
@@ -384,53 +384,5 @@ public void TaskEnvironment_GetAbsolutePath_WithInvalidPathChars_ShouldNotThrow(
                 DisposeTaskEnvironment(taskEnvironment);
             }
         }
-
-        [Theory]
-        [MemberData(nameof(EnvironmentTypes))]
-        public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Disabled_ReturnsEmptyPath(string environmentType)
-        {
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            testEnv.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_4.ToString());
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            var taskEnvironment = CreateTaskEnvironment(environmentType);
-
-            try
-            {
-                // When Wave18_4 is disabled, empty path returns as-is (legacy behavior)
-                var absolutePath = taskEnvironment.GetAbsolutePath(string.Empty);
-
-                absolutePath.Value.ShouldBe(string.Empty);
-                absolutePath.OriginalValue.ShouldBe(string.Empty);
-            }
-            finally
-            {
-                DisposeTaskEnvironment(taskEnvironment);
-                ChangeWaves.ResetStateForTests();
-            }
-        }
-
-        [Theory]
-        [MemberData(nameof(EnvironmentTypes))]
-        public void TaskEnvironment_GetAbsolutePath_WithEmptyPath_WhenWave18_4Enabled_Throws(string environmentType)
-        {
-            using TestEnvironment testEnv = TestEnvironment.Create();
-            ChangeWaves.ResetStateForTests();
-            BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly();
-
-            var taskEnvironment = CreateTaskEnvironment(environmentType);
-
-            try
-            {
-                // When Wave18_4 is enabled, empty path should throw
-                Should.Throw<ArgumentException>(() => taskEnvironment.GetAbsolutePath(string.Empty));
-            }
-            finally
-            {
-                DisposeTaskEnvironment(taskEnvironment);
-                ChangeWaves.ResetStateForTests();
-            }
-        }
     }
 }

From c0a25d1900533f741843601cd3bb413cf7365327 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= <janprovaznik@microsoft.com>
Date: Wed, 28 Jan 2026 10:57:51 +0100
Subject: [PATCH 13/13] variable scope

---
 src/Tasks/Copy.cs  | 12 ++++++++----
 src/Tasks/Touch.cs |  6 ++++--
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/src/Tasks/Copy.cs b/src/Tasks/Copy.cs
index 7dd5592a00e..c3899515a90 100644
--- a/src/Tasks/Copy.cs
+++ b/src/Tasks/Copy.cs
@@ -508,10 +508,12 @@ private bool CopySingleThreaded(
                 string destSpec = DestinationFiles[i].ItemSpec;
 
                 // Compute absolute paths once - reused for ETW, deduplication dictionary, and FileState
+                AbsolutePath sourceAbsolutePath;
+                AbsolutePath destAbsolutePath;
                 try
                 {
-                    AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec);
-                    AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec);
+                    sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec);
+                    destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec);
                 }
                 catch (ArgumentException ex)
                 {
@@ -672,10 +674,12 @@ void ProcessPartition()
                             string destSpec = destItem.ItemSpec;
 
                             // Compute absolute paths once - reused for ETW, deduplication check, and FileState
+                            AbsolutePath sourceAbsolutePath;
+                            AbsolutePath destAbsolutePath;
                             try
                             {
-                                AbsolutePath sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec);
-                                AbsolutePath destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec);
+                                sourceAbsolutePath = TaskEnvironment.GetAbsolutePath(sourceSpec);
+                                destAbsolutePath = TaskEnvironment.GetAbsolutePath(destSpec);
                             }
                             catch (ArgumentException ex)
                             {
diff --git a/src/Tasks/Touch.cs b/src/Tasks/Touch.cs
index 9c826745873..7e1e68f175c 100644
--- a/src/Tasks/Touch.cs
+++ b/src/Tasks/Touch.cs
@@ -95,16 +95,18 @@ internal bool ExecuteImpl(
 
             foreach (ITaskItem file in Files)
             {
+                AbsolutePath path;
                 try
                 {
-                    AbsolutePath path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec));
+                    path = TaskEnvironment.GetAbsolutePath(FrameworkFileUtilities.FixFilePath(file.ItemSpec));
                 }
                 catch (ArgumentException ex)
                 {
                     Log.LogErrorWithCodeFromResources("Touch.FileDoesNotExist", file.ItemSpec, ex.Message);
                     retVal = false;
                     continue;
-                }       
+                }
+
                 // For speed, eliminate duplicates caused by poor targets authoring
                 if (touchedFilesSet.Contains(path))
                 {
