From c524af12fd539c27730707d45865cab6ec7657f0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:30:49 +0100 Subject: [PATCH 1/8] Migrate Exec task to TaskEnvironment API (#13171) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jan Provaznik --- src/Tasks.UnitTests/Exec_Tests.cs | 163 ++++++++++++++++++ src/Tasks/Exec.cs | 74 +++++++- src/UnitTests.Shared/TaskEnvironmentHelper.cs | 19 ++ 3 files changed, 249 insertions(+), 7 deletions(-) diff --git a/src/Tasks.UnitTests/Exec_Tests.cs b/src/Tasks.UnitTests/Exec_Tests.cs index 87fc5dc829c..0fdea7242f4 100644 --- a/src/Tasks.UnitTests/Exec_Tests.cs +++ b/src/Tasks.UnitTests/Exec_Tests.cs @@ -36,6 +36,7 @@ private Exec PrepareExec(string command) { IBuildEngine2 mockEngine = new MockEngine(_output); Exec exec = new Exec(); + exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); exec.BuildEngine = mockEngine; exec.Command = command; return exec; @@ -45,6 +46,7 @@ private ExecWrapper PrepareExecWrapper(string command) { IBuildEngine2 mockEngine = new MockEngine(_output); ExecWrapper exec = new ExecWrapper(); + exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); exec.BuildEngine = mockEngine; exec.Command = command; return exec; @@ -904,6 +906,7 @@ public void ValidateParametersNoCommand() public void SetEnvironmentVariableParameter() { Exec exec = new Exec(); + exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); exec.BuildEngine = new MockEngine(); exec.Command = NativeMethodsShared.IsWindows ? "echo [%MYENVVAR%]" : "echo [$myenvvar]"; exec.EnvironmentVariables = new[] { "myenvvar=myvalue" }; @@ -1068,6 +1071,166 @@ public void ConsoleOutputDoesNotTrimLeadingWhitespace() exec.ConsoleOutput[0].ItemSpec.ShouldBe(lineWithLeadingWhitespace); } } + + /// + /// Runs an Exec task that lists directory contents and asserts expected/unexpected files in the output. + /// + /// The TaskEnvironment to configure on the Exec task. + /// The WorkingDirectory to set, or null to use the default. + /// A filename that must appear in the output. + /// A filename that must NOT appear in the output, or null to skip. + private void ExecuteListCommandInDirectory( + TaskEnvironment taskEnvironment, + string workingDirectory, + string expectedFile, + string notExpectedFile = null) + { + Exec exec = new Exec(); + exec.TaskEnvironment = taskEnvironment; + exec.BuildEngine = new MockEngine(_output); + exec.Command = NativeMethodsShared.IsWindows ? "dir /b" : "ls"; + exec.ConsoleToMSBuild = true; + + if (workingDirectory != null) + { + exec.WorkingDirectory = workingDirectory; + } + + bool result = exec.Execute(); + + result.ShouldBeTrue(); + ((MockEngine)exec.BuildEngine).AssertLogContains(expectedFile); + if (notExpectedFile != null) + { + ((MockEngine)exec.BuildEngine).AssertLogDoesntContain(notExpectedFile); + } + } + + /// + /// Verify that Exec resolves relative WorkingDirectory via TaskEnvironment.GetAbsolutePath in multiprocess mode. + /// + [Fact] + public void ExecResolvesRelativeWorkingDirectoryWithMultiProcessDriver() + { + using (var testEnv = TestEnvironment.Create(_output)) + { + var projectDir = testEnv.CreateFolder(); + var subDir = Directory.CreateDirectory(Path.Combine(projectDir.Path, "subdir")); + File.WriteAllText(Path.Combine(subDir.FullName, "testfile.txt"), "test content"); + + var differentDir = testEnv.CreateFolder(); + var decoySubDir = Directory.CreateDirectory(Path.Combine(differentDir.Path, "subdir")); + File.WriteAllText(Path.Combine(decoySubDir.FullName, "decoyfile.txt"), "decoy content"); + + string originalDirectory = Directory.GetCurrentDirectory(); + try + { + Directory.SetCurrentDirectory(projectDir.Path); + + ExecuteListCommandInDirectory( + TaskEnvironmentHelper.CreateForTest(), + workingDirectory: "subdir", + expectedFile: "testfile.txt", + notExpectedFile: "decoyfile.txt"); + } + finally + { + Directory.SetCurrentDirectory(originalDirectory); + } + } + } + + /// + /// Verify that Exec uses TaskEnvironment.ProjectDirectory when WorkingDirectory is not specified. + /// Uses MultiThreadedTaskEnvironmentDriver so process CWD differs from project directory. + /// + [Fact] + public void ExecUsesProjectDirectoryAsDefaultWorkingDirectory() + { + using (var testEnv = TestEnvironment.Create(_output)) + { + var projectDir = testEnv.CreateFolder(); + File.WriteAllText(Path.Combine(projectDir.Path, "projectfile.txt"), "project content"); + + var differentCwd = testEnv.CreateFolder(); + File.WriteAllText(Path.Combine(differentCwd.Path, "decoyfile.txt"), "decoy content"); + + string originalDirectory = Directory.GetCurrentDirectory(); + TaskEnvironment taskEnvironment = null; + try + { + Directory.SetCurrentDirectory(differentCwd.Path); + + taskEnvironment = TaskEnvironmentHelper.CreateMultithreadedForTest(projectDir.Path); + ExecuteListCommandInDirectory( + taskEnvironment, + workingDirectory: null, + expectedFile: "projectfile.txt", + notExpectedFile: "decoyfile.txt"); + } + finally + { + taskEnvironment?.Dispose(); + Directory.SetCurrentDirectory(originalDirectory); + } + } + } + + /// + /// Verify that Exec correctly handles absolute WorkingDirectory paths. + /// + [Fact] + public void ExecHandlesAbsoluteWorkingDirectory() + { + using (var testEnv = TestEnvironment.Create(_output)) + { + var workDir = testEnv.CreateFolder(); + File.WriteAllText(Path.Combine(workDir.Path, "absolutedir.txt"), "absolute content"); + + ExecuteListCommandInDirectory( + TaskEnvironmentHelper.CreateForTest(), + workingDirectory: workDir.Path, + expectedFile: "absolutedir.txt"); + } + } + + /// + /// Verify that Exec resolves relative WorkingDirectory relative to TaskEnvironment.ProjectDirectory, + /// not the process current directory. Uses MultiThreadedTaskEnvironmentDriver to simulate + /// multithreaded mode where process CWD differs from project directory. + /// + [Fact] + public void ExecResolvesRelativeWorkingDirectoryRelativeToProjectDirectory() + { + using (var testEnv = TestEnvironment.Create(_output)) + { + var projectDir = testEnv.CreateFolder(); + var subDir = Directory.CreateDirectory(Path.Combine(projectDir.Path, "builddir")); + File.WriteAllText(Path.Combine(subDir.FullName, "multithreaded.txt"), "multithreaded content"); + + var differentCwd = testEnv.CreateFolder(); + File.WriteAllText(Path.Combine(differentCwd.Path, "decoyfile.txt"), "decoy content"); + + string originalDirectory = Directory.GetCurrentDirectory(); + TaskEnvironment taskEnvironment = null; + try + { + Directory.SetCurrentDirectory(differentCwd.Path); + + taskEnvironment = TaskEnvironmentHelper.CreateMultithreadedForTest(projectDir.Path); + ExecuteListCommandInDirectory( + taskEnvironment, + workingDirectory: "builddir", + expectedFile: "multithreaded.txt", + notExpectedFile: "decoyfile.txt"); + } + finally + { + taskEnvironment?.Dispose(); + Directory.SetCurrentDirectory(originalDirectory); + } + } + } } internal sealed class ExecWrapper : Exec diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index d59c05f4d04..3c557005442 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; @@ -21,7 +22,8 @@ namespace Microsoft.Build.Tasks /// for it to complete, and then returns True if the process completed successfully, and False if an error occurred. /// // UNDONE: ToolTask has a "UseCommandProcessor" flag that duplicates much of the code in this class. Remove the duplication. - public class Exec : ToolTaskExtension + [MSBuildMultiThreadableTask] + public class Exec : ToolTaskExtension, IMultiThreadableTask { #region Constructors @@ -46,7 +48,7 @@ public Exec() // Are the encodings for StdErr and StdOut streams valid private bool _encodingParametersValid = true; - private string _workingDirectory; + private AbsolutePath _workingDirectory; private ITaskItem[] _outputs; internal bool workingDirectoryIsUNC; // internal for unit testing private string _batchFile; @@ -82,6 +84,9 @@ public string Command public bool IgnoreExitCode { get; set; } + /// + public TaskEnvironment TaskEnvironment { get; set; } + /// /// Enable the pipe of the standard out to an item (StandardOutput). /// @@ -458,10 +463,12 @@ protected override bool ValidateParameters() } // determine what the working directory for the exec command is going to be -- if the user specified a working - // directory use that, otherwise it's the current directory + // directory use that, otherwise default to the project directory (TaskEnvironment.ProjectDirectory). Using the + // project directory instead of the process current directory is important for correctness in multithreaded (/mt) + // builds, where the process working directory may not match the project being built. _workingDirectory = !string.IsNullOrEmpty(WorkingDirectory) - ? WorkingDirectory - : Directory.GetCurrentDirectory(); + ? TaskEnvironment.GetAbsolutePath(WorkingDirectory) + : TaskEnvironment.ProjectDirectory; // check if the working directory we're going to use for the exec command is a UNC path workingDirectoryIsUNC = FileUtilitiesRegex.StartsWithUncPattern(_workingDirectory); @@ -470,7 +477,7 @@ protected override bool ValidateParameters() // will not be able to auto-map to the UNC path if (workingDirectoryIsUNC && NativeMethods.AllDrivesMapped()) { - Log.LogErrorWithCodeFromResources("Exec.AllDriveLettersMappedError", _workingDirectory); + Log.LogErrorWithCodeFromResources("Exec.AllDriveLettersMappedError", _workingDirectory.OriginalValue); return false; } @@ -533,7 +540,7 @@ protected override string GetWorkingDirectory() // So verify it's valid here. if (!FileSystems.Default.DirectoryExists(_workingDirectory)) { - throw new DirectoryNotFoundException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Exec.InvalidWorkingDirectory", _workingDirectory)); + throw new DirectoryNotFoundException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Exec.InvalidWorkingDirectory", _workingDirectory.OriginalValue)); } if (workingDirectoryIsUNC) @@ -560,6 +567,59 @@ internal string GetWorkingDirectoryAccessor() return GetWorkingDirectory(); } + /// + /// Gets the ProcessStartInfo for the spawned process, with environment variables from TaskEnvironment. + /// In multithreaded mode, TaskEnvironment contains the virtualized environment for this project, + /// which must be passed to the spawned process since it won't inherit it from the (shared) process environment. + /// + protected override ProcessStartInfo GetProcessStartInfo( + string pathToTool, + string commandLineCommands, + string responseFileSwitch) + { + // Get the base ProcessStartInfo with all ToolTask settings (command line, redirections, encodings, etc.) + // This also applies EnvironmentVariables overrides from the task property. + ProcessStartInfo startInfo = base.GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch); + + // Replace the inherited process environment with the virtualized one from TaskEnvironment. + // TaskEnvironment.GetProcessStartInfo() already configures env vars and working directory correctly + // for both multithreaded (virtualized) and multi-process (inherited) modes. + ProcessStartInfo taskEnvStartInfo = TaskEnvironment.GetProcessStartInfo(); + startInfo.Environment.Clear(); + foreach (var kvp in taskEnvStartInfo.Environment) + { + startInfo.Environment[kvp.Key] = kvp.Value; + } + + // Re-apply obsolete EnvironmentOverride and EnvironmentVariables property overrides — + // they should take precedence over TaskEnvironment. The base class already applied these, + // but we cleared the environment above, so we need to re-apply them. +#pragma warning disable 0618 // obsolete + Dictionary envOverrides = EnvironmentOverride; + if (envOverrides != null) + { + foreach (KeyValuePair entry in envOverrides) + { + startInfo.Environment[entry.Key] = entry.Value; + } + } +#pragma warning restore 0618 + + if (EnvironmentVariables != null) + { + foreach (string entry in EnvironmentVariables) + { + string[] nameValuePair = entry.Split(['='], 2); + if (nameValuePair.Length == 2 && nameValuePair[0].Length > 0) + { + startInfo.Environment[nameValuePair[0]] = nameValuePair[1]; + } + } + } + + return startInfo; + } + /// /// Adds the arguments for cmd.exe /// diff --git a/src/UnitTests.Shared/TaskEnvironmentHelper.cs b/src/UnitTests.Shared/TaskEnvironmentHelper.cs index 172c48ceb5b..0194ed86669 100644 --- a/src/UnitTests.Shared/TaskEnvironmentHelper.cs +++ b/src/UnitTests.Shared/TaskEnvironmentHelper.cs @@ -20,5 +20,24 @@ public static TaskEnvironment CreateForTest() { return TaskEnvironment.Fallback; } + + /// + /// Creates a TaskEnvironment backed by the multi-threaded driver which virtualizes + /// environment variables and current directory. This allows testing of multithreaded mode + /// behavior where each project has its own isolated environment. + /// + /// The project directory to use for the task environment. + /// A TaskEnvironment suitable for testing multithreaded mode scenarios. + /// + /// The caller is responsible for disposing the TaskEnvironment via TaskEnvironment.Dispose(), + /// which will clean up the underlying driver's thread-local state. + /// + // CA2000 is suppressed because the driver is owned by the TaskEnvironment and disposed via TaskEnvironment.Dispose() +#pragma warning disable CA2000 + public static TaskEnvironment CreateMultithreadedForTest(string projectDirectory) + { + return new TaskEnvironment(new MultiThreadedTaskEnvironmentDriver(projectDirectory)); + } +#pragma warning restore CA2000 } } From 37642075aaff36cfda8fe2f6bce6a68934a1ac82 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Wed, 29 Apr 2026 15:11:48 +0200 Subject: [PATCH 2/8] Remove GetProcessStartInfo override --- src/Tasks/Exec.cs | 56 ----------------------------------------------- 1 file changed, 56 deletions(-) diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index 3c557005442..490a6deb378 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -84,9 +84,6 @@ public string Command public bool IgnoreExitCode { get; set; } - /// - public TaskEnvironment TaskEnvironment { get; set; } - /// /// Enable the pipe of the standard out to an item (StandardOutput). /// @@ -567,59 +564,6 @@ internal string GetWorkingDirectoryAccessor() return GetWorkingDirectory(); } - /// - /// Gets the ProcessStartInfo for the spawned process, with environment variables from TaskEnvironment. - /// In multithreaded mode, TaskEnvironment contains the virtualized environment for this project, - /// which must be passed to the spawned process since it won't inherit it from the (shared) process environment. - /// - protected override ProcessStartInfo GetProcessStartInfo( - string pathToTool, - string commandLineCommands, - string responseFileSwitch) - { - // Get the base ProcessStartInfo with all ToolTask settings (command line, redirections, encodings, etc.) - // This also applies EnvironmentVariables overrides from the task property. - ProcessStartInfo startInfo = base.GetProcessStartInfo(pathToTool, commandLineCommands, responseFileSwitch); - - // Replace the inherited process environment with the virtualized one from TaskEnvironment. - // TaskEnvironment.GetProcessStartInfo() already configures env vars and working directory correctly - // for both multithreaded (virtualized) and multi-process (inherited) modes. - ProcessStartInfo taskEnvStartInfo = TaskEnvironment.GetProcessStartInfo(); - startInfo.Environment.Clear(); - foreach (var kvp in taskEnvStartInfo.Environment) - { - startInfo.Environment[kvp.Key] = kvp.Value; - } - - // Re-apply obsolete EnvironmentOverride and EnvironmentVariables property overrides — - // they should take precedence over TaskEnvironment. The base class already applied these, - // but we cleared the environment above, so we need to re-apply them. -#pragma warning disable 0618 // obsolete - Dictionary envOverrides = EnvironmentOverride; - if (envOverrides != null) - { - foreach (KeyValuePair entry in envOverrides) - { - startInfo.Environment[entry.Key] = entry.Value; - } - } -#pragma warning restore 0618 - - if (EnvironmentVariables != null) - { - foreach (string entry in EnvironmentVariables) - { - string[] nameValuePair = entry.Split(['='], 2); - if (nameValuePair.Length == 2 && nameValuePair[0].Length > 0) - { - startInfo.Environment[nameValuePair[0]] = nameValuePair[1]; - } - } - } - - return startInfo; - } - /// /// Adds the arguments for cmd.exe /// From 3a8ad2a4514b95df308a2d861e25e56e3ea0767b Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Thu, 30 Apr 2026 16:48:39 +0200 Subject: [PATCH 3/8] Remove interface --- src/Tasks/Exec.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index 490a6deb378..7017fdb3202 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; @@ -23,7 +22,7 @@ namespace Microsoft.Build.Tasks /// // UNDONE: ToolTask has a "UseCommandProcessor" flag that duplicates much of the code in this class. Remove the duplication. [MSBuildMultiThreadableTask] - public class Exec : ToolTaskExtension, IMultiThreadableTask + public class Exec : ToolTaskExtension { #region Constructors From e78cb8a79352fc66e49f60c5a397176454b48039 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Fri, 15 May 2026 14:47:52 +0200 Subject: [PATCH 4/8] Fix CreateTemporaryBatchFile, improve logging --- src/Tasks/Exec.cs | 14 ++++++++++---- src/Tasks/Resources/Strings.resx | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index 7017fdb3202..ed457efa691 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -197,7 +197,7 @@ public ITaskItem[] Outputs /// private void CreateTemporaryBatchFile() { - var encoding = EncodingUtilities.BatchFileEncoding(Command + WorkingDirectory, UseUtf8Encoding); + var encoding = EncodingUtilities.BatchFileEncoding(Command + _workingDirectory.Value, UseUtf8Encoding); // Temporary file with the extension .Exec.bat _batchFile = FileUtilities.GetTemporaryFileName(".exec.cmd"); @@ -473,7 +473,10 @@ protected override bool ValidateParameters() // will not be able to auto-map to the UNC path if (workingDirectoryIsUNC && NativeMethods.AllDrivesMapped()) { - Log.LogErrorWithCodeFromResources("Exec.AllDriveLettersMappedError", _workingDirectory.OriginalValue); + Log.LogErrorWithCodeFromResources( + "Exec.AllDriveLettersMappedError", + _workingDirectory.OriginalValue, + _workingDirectory.Value); return false; } @@ -495,7 +498,8 @@ internal bool ValidateParametersAccessor() /// path to cmd.exe protected override string GenerateFullPathToTool() { - return CommandProcessorPath.Value; + return CommandProcessorPath.Value + ?? TaskEnvironment.GetEnvironmentVariable("ComSpec"); } private static readonly Lazy CommandProcessorPath = new Lazy(() => @@ -509,9 +513,11 @@ protected override string GenerateFullPathToTool() // Work around https://github.com/dotnet/msbuild/issues/2273 and // https://github.com/dotnet/corefx/issues/19110, which result in // a bad path being returned above on Nano Server SKUs of Windows. + // Returning null signals GenerateFullPathToTool to consult TaskEnvironment + // for the ComSpec environment variable. if (!FileSystems.Default.FileExists(systemCmd)) { - return Environment.GetEnvironmentVariable("ComSpec"); + return null; } #endif diff --git a/src/Tasks/Resources/Strings.resx b/src/Tasks/Resources/Strings.resx index fb697866cd8..17970c2be96 100644 --- a/src/Tasks/Resources/Strings.resx +++ b/src/Tasks/Resources/Strings.resx @@ -394,7 +394,7 @@ If this bucket overflows, pls. contact 'vsppbdev'. --> - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. From 0ad8a523d08d158c437340059f8a9a7dbf098654 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Fri, 15 May 2026 15:03:03 +0200 Subject: [PATCH 5/8] Use .value. Improve tests --- src/Tasks.UnitTests/Exec_Tests.cs | 25 ++----------------- src/Tasks/Exec.cs | 2 +- src/Tasks/Resources/xlf/Strings.cs.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.de.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.es.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.fr.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.it.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.ja.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.ko.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.pl.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.pt-BR.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.ru.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.tr.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.zh-Hans.xlf | 4 +-- src/Tasks/Resources/xlf/Strings.zh-Hant.xlf | 4 +-- src/UnitTests.Shared/TaskEnvironmentHelper.cs | 19 -------------- 16 files changed, 29 insertions(+), 69 deletions(-) diff --git a/src/Tasks.UnitTests/Exec_Tests.cs b/src/Tasks.UnitTests/Exec_Tests.cs index 0fdea7242f4..0427042cb00 100644 --- a/src/Tasks.UnitTests/Exec_Tests.cs +++ b/src/Tasks.UnitTests/Exec_Tests.cs @@ -36,7 +36,6 @@ private Exec PrepareExec(string command) { IBuildEngine2 mockEngine = new MockEngine(_output); Exec exec = new Exec(); - exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); exec.BuildEngine = mockEngine; exec.Command = command; return exec; @@ -46,7 +45,6 @@ private ExecWrapper PrepareExecWrapper(string command) { IBuildEngine2 mockEngine = new MockEngine(_output); ExecWrapper exec = new ExecWrapper(); - exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); exec.BuildEngine = mockEngine; exec.Command = command; return exec; @@ -906,7 +904,6 @@ public void ValidateParametersNoCommand() public void SetEnvironmentVariableParameter() { Exec exec = new Exec(); - exec.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(); exec.BuildEngine = new MockEngine(); exec.Command = NativeMethodsShared.IsWindows ? "echo [%MYENVVAR%]" : "echo [$myenvvar]"; exec.EnvironmentVariables = new[] { "myenvvar=myvalue" }; @@ -1161,7 +1158,7 @@ public void ExecUsesProjectDirectoryAsDefaultWorkingDirectory() { Directory.SetCurrentDirectory(differentCwd.Path); - taskEnvironment = TaskEnvironmentHelper.CreateMultithreadedForTest(projectDir.Path); + taskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path); ExecuteListCommandInDirectory( taskEnvironment, workingDirectory: null, @@ -1176,24 +1173,6 @@ public void ExecUsesProjectDirectoryAsDefaultWorkingDirectory() } } - /// - /// Verify that Exec correctly handles absolute WorkingDirectory paths. - /// - [Fact] - public void ExecHandlesAbsoluteWorkingDirectory() - { - using (var testEnv = TestEnvironment.Create(_output)) - { - var workDir = testEnv.CreateFolder(); - File.WriteAllText(Path.Combine(workDir.Path, "absolutedir.txt"), "absolute content"); - - ExecuteListCommandInDirectory( - TaskEnvironmentHelper.CreateForTest(), - workingDirectory: workDir.Path, - expectedFile: "absolutedir.txt"); - } - } - /// /// Verify that Exec resolves relative WorkingDirectory relative to TaskEnvironment.ProjectDirectory, /// not the process current directory. Uses MultiThreadedTaskEnvironmentDriver to simulate @@ -1217,7 +1196,7 @@ public void ExecResolvesRelativeWorkingDirectoryRelativeToProjectDirectory() { Directory.SetCurrentDirectory(differentCwd.Path); - taskEnvironment = TaskEnvironmentHelper.CreateMultithreadedForTest(projectDir.Path); + taskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path); ExecuteListCommandInDirectory( taskEnvironment, workingDirectory: "builddir", diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index ed457efa691..e7067f1e759 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -245,7 +245,7 @@ private void CreateTemporaryBatchFile() // https://support.microsoft.com/en-us/kb/156276 if (workingDirectoryIsUNC) { - sw.WriteLine("pushd " + _workingDirectory); + sw.WriteLine("pushd " + _workingDirectory.Value); } } else diff --git a/src/Tasks/Resources/xlf/Strings.cs.xlf b/src/Tasks/Resources/xlf/Strings.cs.xlf index dde273ca0ae..6d80b18e8f1 100644 --- a/src/Tasks/Resources/xlf/Strings.cs.xlf +++ b/src/Tasks/Resources/xlf/Strings.cs.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: Všechna písmena jednotek od A: po Z: jsou aktuálně použita. Protože pracovní adresář {0} je cesta UNC, potřebuje úloha Exec volné písmeno jednotky pro namapování cesty UNC. Odpojením některých sdílených prostředků uvolněte písmena jednotek. Můžete také zadat místní pracovní adresář. Potom spusťte příkaz znovu. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.de.xlf b/src/Tasks/Resources/xlf/Strings.de.xlf index 99732a18ae6..47f29884e77 100644 --- a/src/Tasks/Resources/xlf/Strings.de.xlf +++ b/src/Tasks/Resources/xlf/Strings.de.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: Alle Laufwerkbuchstaben von A: bis Z: werden zurzeit verwendet. Da das Arbeitsverzeichnis "{0}" ein UNC-Pfad ist, ist für die Exec-Aufgabe ein freier Laufwerkbuchstabe für die Zuordnung des UNC-Pfads erforderlich. Trennen Sie die Verbindung mindestens einer freigegebenen Ressource, um Laufwerkbuchstaben freizugeben, oder geben Sie ein lokales Arbeitsverzeichnis an, bevor Sie diesen Befehl wiederholen. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.es.xlf b/src/Tasks/Resources/xlf/Strings.es.xlf index 2ba711a7b04..100ad25c6f1 100644 --- a/src/Tasks/Resources/xlf/Strings.es.xlf +++ b/src/Tasks/Resources/xlf/Strings.es.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: En este momento están en uso todas las letras de unidad, de la A: a la Z: Como el directorio de trabajo "{0}" es una ruta de acceso UNC, la tarea "Exec" requiere una letra de unidad libre a la que asignar la ruta de acceso UNC. Desconéctese de uno o varios recursos compartidos para liberar letras de unidad, o bien especifique un directorio de trabajo local antes de intentar ejecutar este comando de nuevo. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.fr.xlf b/src/Tasks/Resources/xlf/Strings.fr.xlf index a1bbc4343c5..f5a077aec2c 100644 --- a/src/Tasks/Resources/xlf/Strings.fr.xlf +++ b/src/Tasks/Resources/xlf/Strings.fr.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: Toutes les lettres de lecteur de A: à Z: sont actuellement utilisées. Le répertoire de travail "{0}" étant un chemin UNC, la tâche "Exec" nécessite une lettre de lecteur disponible à laquelle le chemin UNC sera mappé. Déconnectez-vous d'une ou de plusieurs ressources partagées pour libérer des lettres de lecteur ou spécifiez un répertoire de travail local avant de réexécuter cette commande. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.it.xlf b/src/Tasks/Resources/xlf/Strings.it.xlf index b5aba491c4a..57f637534a6 100644 --- a/src/Tasks/Resources/xlf/Strings.it.xlf +++ b/src/Tasks/Resources/xlf/Strings.it.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: attualmente sono in uso tutte le lettere di unità comprese tra A: e Z:. Poiché la directory di lavoro "{0}" è un percorso UNC, per l'attività "Exec" è necessaria una lettera di unità disponibile a cui mappare il percorso UNC. Eseguire la disconnessione da una o più risorse condivise per rendere disponibili lettere di unità oppure specificare una directory di lavoro locale prima di provare a eseguire nuovamente questo comando. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.ja.xlf b/src/Tasks/Resources/xlf/Strings.ja.xlf index 1c9a490b7e7..0f224fede9d 100644 --- a/src/Tasks/Resources/xlf/Strings.ja.xlf +++ b/src/Tasks/Resources/xlf/Strings.ja.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: A: から Z: までのすべてのドライブ文字は使用中です。作業ディレクトリ "{0}" は UNC パスであるため、"Exec" タスクには UNC パスをマップできる使用可能なドライブ文字が必要です。このコマンドを再度試してみる前に、1 つ以上の共有リソースを切断してドライブ文字を解放するか、ローカルの作業ディレクトリを指定してください。 + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.ko.xlf b/src/Tasks/Resources/xlf/Strings.ko.xlf index 5a2d828cb01..ea41e129ab5 100644 --- a/src/Tasks/Resources/xlf/Strings.ko.xlf +++ b/src/Tasks/Resources/xlf/Strings.ko.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: A:에서 Z:까지 모든 드라이브 문자가 현재 사용 중입니다. 작업 디렉터리 "{0}"이(가) UNC 경로이므로 "Exec" 작업을 수행하려면 UNC 경로를 매핑하는 데 사용할 수 있는 드라이브 문자가 필요합니다. 하나 이상의 공유 리소스에서 연결을 끊어 사용 가능한 드라이브 문자를 만들거나 로컬 작업 디렉터리를 지정한 다음 이 명령을 다시 실행하세요. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.pl.xlf b/src/Tasks/Resources/xlf/Strings.pl.xlf index 8e8b2953fcb..a4ceb944295 100644 --- a/src/Tasks/Resources/xlf/Strings.pl.xlf +++ b/src/Tasks/Resources/xlf/Strings.pl.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: Wszystkie litery dysków od A: do Z: są obecnie zajęte. Ponieważ katalog roboczy „{0}” jest ścieżką UNC, zadanie „Exec” potrzebuje wolnej litery dysku do zamapowania ścieżki UNC. Odłącz się od co najmniej jednego zasobu udostępnionego, aby zwolnić litery dysków, lub określ lokalny katalog roboczy przed ponownym użyciem tego polecenia. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf index bdbc7054eee..8147f3adbbe 100644 --- a/src/Tasks/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Tasks/Resources/xlf/Strings.pt-BR.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: Todas as letras de unidade de A: a Z: estão em uso no momento. Como o diretório de trabalho "{0}" é um caminho UNC, a tarefa "Exec" precisa de uma letra da unidade livre na qual mapear o caminho UNC. Desconecte um ou mais recursos compartilhados para liberar algumas letras da unidade ou especifique um diretório de trabalho local antes de tentar usar esse comando novamente. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.ru.xlf b/src/Tasks/Resources/xlf/Strings.ru.xlf index f5fedc7bcbe..aa4e0b03ae9 100644 --- a/src/Tasks/Resources/xlf/Strings.ru.xlf +++ b/src/Tasks/Resources/xlf/Strings.ru.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: все буквы дисков (от A: до Z:) сейчас используются. Поскольку рабочий каталог "{0}" указан в виде UNC-пути, задаче Exec требуется свободная буква диска для сопоставления с этим UNC-путем. Перед повторением этой команды отключитесь от одного или нескольких общих ресурсов, чтобы освободить буквы дисков, или укажите локальный рабочий каталог. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.tr.xlf b/src/Tasks/Resources/xlf/Strings.tr.xlf index 88671129bf6..923aa9c5714 100644 --- a/src/Tasks/Resources/xlf/Strings.tr.xlf +++ b/src/Tasks/Resources/xlf/Strings.tr.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: A: ile Z: arasındaki tüm sürücü harfleri şu anda kullanımda. "{0}" çalışma dizini bir UNC yolu olduğundan, "Exec" görevinin UNC yolunu eşleyeceği serbest bir sürücü harfi gerekiyor. Sürücü harflerini serbest bırakmak üzere bir veya birden çok paylaşılan kaynağın bağlantısını kesin veya bu komutu yeniden denemeden önce yerel bir çalışma dizini belirtin. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf index 85c481533a7..62a2b70669b 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hans.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: 所有驱动器号(从 A: 到 Z:)当前都在使用。由于工作目录“{0}”是 UNC 路径,“Exec”任务需要将 UNC 路径映射到一个空闲的驱动器号。请从一个或多个共享资源断开连接以释放驱动器号,或在再次尝试执行此命令前指定本地工作目录。 + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf index f5241dfc562..d40312bcfe3 100644 --- a/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Tasks/Resources/xlf/Strings.zh-Hant.xlf @@ -490,8 +490,8 @@ {StrBegin="MSB3924: "} - MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" is a UNC path, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. - MSB3071: 從 A: 到 Z: 的所有磁碟機代號目前都在使用中。由於工作目錄 "{0}" 是 UNC 路徑,因此 "Exec" 工作需要有可用的磁碟機代號以便對應 UNC 路徑。請先中斷一或多個共用資源的連線以釋放磁碟機代號,或指定本機工作目錄,再嘗試重新執行這個命令。 + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. + MSB3071: All drive letters from A: through Z: are currently in use. Since the working directory "{0}" (resolved to UNC path "{1}") requires a UNC mapping, the "Exec" task needs a free drive letter to map the UNC path to. Disconnect from one or more shared resources to free up drive letters, or specify a local working directory before attempting this command again. {StrBegin="MSB3071: "}LOCALIZATION: "Exec", "A:", and "Z:" should not be localized. diff --git a/src/UnitTests.Shared/TaskEnvironmentHelper.cs b/src/UnitTests.Shared/TaskEnvironmentHelper.cs index 0194ed86669..172c48ceb5b 100644 --- a/src/UnitTests.Shared/TaskEnvironmentHelper.cs +++ b/src/UnitTests.Shared/TaskEnvironmentHelper.cs @@ -20,24 +20,5 @@ public static TaskEnvironment CreateForTest() { return TaskEnvironment.Fallback; } - - /// - /// Creates a TaskEnvironment backed by the multi-threaded driver which virtualizes - /// environment variables and current directory. This allows testing of multithreaded mode - /// behavior where each project has its own isolated environment. - /// - /// The project directory to use for the task environment. - /// A TaskEnvironment suitable for testing multithreaded mode scenarios. - /// - /// The caller is responsible for disposing the TaskEnvironment via TaskEnvironment.Dispose(), - /// which will clean up the underlying driver's thread-local state. - /// - // CA2000 is suppressed because the driver is owned by the TaskEnvironment and disposed via TaskEnvironment.Dispose() -#pragma warning disable CA2000 - public static TaskEnvironment CreateMultithreadedForTest(string projectDirectory) - { - return new TaskEnvironment(new MultiThreadedTaskEnvironmentDriver(projectDirectory)); - } -#pragma warning restore CA2000 } } From 25626604f96e221c274e3f23a6fd54f33b082c75 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Fri, 15 May 2026 15:25:32 +0200 Subject: [PATCH 6/8] Improve tests --- src/Tasks.UnitTests/Exec_Tests.cs | 105 ++++++++++-------------------- 1 file changed, 36 insertions(+), 69 deletions(-) diff --git a/src/Tasks.UnitTests/Exec_Tests.cs b/src/Tasks.UnitTests/Exec_Tests.cs index 0427042cb00..f3d2f3fc463 100644 --- a/src/Tasks.UnitTests/Exec_Tests.cs +++ b/src/Tasks.UnitTests/Exec_Tests.cs @@ -1103,44 +1103,6 @@ private void ExecuteListCommandInDirectory( } } - /// - /// Verify that Exec resolves relative WorkingDirectory via TaskEnvironment.GetAbsolutePath in multiprocess mode. - /// - [Fact] - public void ExecResolvesRelativeWorkingDirectoryWithMultiProcessDriver() - { - using (var testEnv = TestEnvironment.Create(_output)) - { - var projectDir = testEnv.CreateFolder(); - var subDir = Directory.CreateDirectory(Path.Combine(projectDir.Path, "subdir")); - File.WriteAllText(Path.Combine(subDir.FullName, "testfile.txt"), "test content"); - - var differentDir = testEnv.CreateFolder(); - var decoySubDir = Directory.CreateDirectory(Path.Combine(differentDir.Path, "subdir")); - File.WriteAllText(Path.Combine(decoySubDir.FullName, "decoyfile.txt"), "decoy content"); - - string originalDirectory = Directory.GetCurrentDirectory(); - try - { - Directory.SetCurrentDirectory(projectDir.Path); - - ExecuteListCommandInDirectory( - TaskEnvironmentHelper.CreateForTest(), - workingDirectory: "subdir", - expectedFile: "testfile.txt", - notExpectedFile: "decoyfile.txt"); - } - finally - { - Directory.SetCurrentDirectory(originalDirectory); - } - } - } - - /// - /// Verify that Exec uses TaskEnvironment.ProjectDirectory when WorkingDirectory is not specified. - /// Uses MultiThreadedTaskEnvironmentDriver so process CWD differs from project directory. - /// [Fact] public void ExecUsesProjectDirectoryAsDefaultWorkingDirectory() { @@ -1173,42 +1135,47 @@ public void ExecUsesProjectDirectoryAsDefaultWorkingDirectory() } } - /// - /// Verify that Exec resolves relative WorkingDirectory relative to TaskEnvironment.ProjectDirectory, - /// not the process current directory. Uses MultiThreadedTaskEnvironmentDriver to simulate - /// multithreaded mode where process CWD differs from project directory. - /// [Fact] - public void ExecResolvesRelativeWorkingDirectoryRelativeToProjectDirectory() + public void InvalidRelativeWorkingDirectory_LogsOriginalPathNotAbsolutized() { - using (var testEnv = TestEnvironment.Create(_output)) - { - var projectDir = testEnv.CreateFolder(); - var subDir = Directory.CreateDirectory(Path.Combine(projectDir.Path, "builddir")); - File.WriteAllText(Path.Combine(subDir.FullName, "multithreaded.txt"), "multithreaded content"); + using var testEnv = TestEnvironment.Create(_output); + var projectDir = testEnv.CreateFolder(); + const string relativeDir = "testDir"; + string absolutePath = Path.Combine(projectDir.Path, relativeDir); - var differentCwd = testEnv.CreateFolder(); - File.WriteAllText(Path.Combine(differentCwd.Path, "decoyfile.txt"), "decoy content"); + var exec = new Exec + { + BuildEngine = new MockEngine(_output), + Command = "echo hi", + WorkingDirectory = relativeDir, + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path), + }; + + exec.Execute().ShouldBeFalse(); + + var engine = (MockEngine)exec.BuildEngine; + engine.AssertLogContains("MSB6003"); + engine.AssertLogContains(relativeDir); + engine.AssertLogDoesntContain(absolutePath); + } - string originalDirectory = Directory.GetCurrentDirectory(); - TaskEnvironment taskEnvironment = null; - try - { - Directory.SetCurrentDirectory(differentCwd.Path); + [Fact] + public void Exec_RelativeWorkingDirectory_ResolvedAgainstProjectDirectory() + { + using var testEnv = TestEnvironment.Create(_output); + var projectDir = testEnv.CreateFolder(); + Directory.CreateDirectory(Path.Combine(projectDir.Path, "builddir")); - taskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path); - ExecuteListCommandInDirectory( - taskEnvironment, - workingDirectory: "builddir", - expectedFile: "multithreaded.txt", - notExpectedFile: "decoyfile.txt"); - } - finally - { - taskEnvironment?.Dispose(); - Directory.SetCurrentDirectory(originalDirectory); - } - } + var exec = new Exec + { + BuildEngine = new MockEngine(_output), + Command = "echo hi", + WorkingDirectory = "builddir", + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path), + }; + + exec.ValidateParametersAccessor().ShouldBeTrue(); + exec.GetWorkingDirectoryAccessor().ShouldBe(Path.Combine(projectDir.Path, "builddir")); } } From bb65ac83dbee2db978aa6395a616174a96af4239 Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova <150850103+OvesN@users.noreply.github.com> Date: Wed, 20 May 2026 17:17:50 +0200 Subject: [PATCH 7/8] Update src/Tasks/Exec.cs Co-authored-by: Rainer Sigwald --- src/Tasks/Exec.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index e7067f1e759..833832bd6b3 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -459,9 +459,7 @@ protected override bool ValidateParameters() } // determine what the working directory for the exec command is going to be -- if the user specified a working - // directory use that, otherwise default to the project directory (TaskEnvironment.ProjectDirectory). Using the - // project directory instead of the process current directory is important for correctness in multithreaded (/mt) - // builds, where the process working directory may not match the project being built. + // directory use that, otherwise default to the project directory (TaskEnvironment.ProjectDirectory). _workingDirectory = !string.IsNullOrEmpty(WorkingDirectory) ? TaskEnvironment.GetAbsolutePath(WorkingDirectory) : TaskEnvironment.ProjectDirectory; From 66349ed6d9b7d3cd9425cf0b65d009c7fedef5aa Mon Sep 17 00:00:00 2001 From: Veronika Ovsyannikova Date: Wed, 20 May 2026 18:05:13 +0200 Subject: [PATCH 8/8] Adress CR comments --- src/Tasks.UnitTests/Exec_Tests.cs | 5 +++-- src/Tasks/Exec.cs | 9 ++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Tasks.UnitTests/Exec_Tests.cs b/src/Tasks.UnitTests/Exec_Tests.cs index f3d2f3fc463..40bc1521676 100644 --- a/src/Tasks.UnitTests/Exec_Tests.cs +++ b/src/Tasks.UnitTests/Exec_Tests.cs @@ -1096,10 +1096,11 @@ private void ExecuteListCommandInDirectory( bool result = exec.Execute(); result.ShouldBeTrue(); - ((MockEngine)exec.BuildEngine).AssertLogContains(expectedFile); + string[] outputLines = exec.ConsoleOutput.Select(item => item.ItemSpec).ToArray(); + outputLines.ShouldContain(expectedFile); if (notExpectedFile != null) { - ((MockEngine)exec.BuildEngine).AssertLogDoesntContain(notExpectedFile); + outputLines.ShouldNotContain(notExpectedFile); } } diff --git a/src/Tasks/Exec.cs b/src/Tasks/Exec.cs index 833832bd6b3..b34d6d61bf7 100644 --- a/src/Tasks/Exec.cs +++ b/src/Tasks/Exec.cs @@ -496,8 +496,7 @@ internal bool ValidateParametersAccessor() /// path to cmd.exe protected override string GenerateFullPathToTool() { - return CommandProcessorPath.Value - ?? TaskEnvironment.GetEnvironmentVariable("ComSpec"); + return CommandProcessorPath.Value; } private static readonly Lazy CommandProcessorPath = new Lazy(() => @@ -511,11 +510,11 @@ protected override string GenerateFullPathToTool() // Work around https://github.com/dotnet/msbuild/issues/2273 and // https://github.com/dotnet/corefx/issues/19110, which result in // a bad path being returned above on Nano Server SKUs of Windows. - // Returning null signals GenerateFullPathToTool to consult TaskEnvironment - // for the ComSpec environment variable. if (!FileSystems.Default.FileExists(systemCmd)) { - return null; +#pragma warning disable MSBuildTask0002 // We do not support changing of ComSpec during execution + return Environment.GetEnvironmentVariable("ComSpec"); +#pragma warning restore MSBuildTask0002 } #endif