diff --git a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs
index cb9e4b35fdd2..b41598f528f3 100644
--- a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs
+++ b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs
@@ -103,6 +103,7 @@ public ProcessStartInfo GetProcessStartInfo()
var startInfo = new ProcessStartInfo
{
WorkingDirectory = _projectDirectory.Value,
+ UseShellExecute = false,
};
// Populate environment from the scoped environment dictionary
diff --git a/src/Tasks/Common/TaskEnvironmentDefaults.cs b/src/Tasks/Common/TaskEnvironmentDefaults.cs
new file mode 100644
index 000000000000..7ef5666a4175
--- /dev/null
+++ b/src/Tasks/Common/TaskEnvironmentDefaults.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// Provides a default TaskEnvironment for single-threaded MSBuild execution.
+// When MSBuild supports IMultiThreadableTask, it sets TaskEnvironment directly.
+// This fallback ensures tasks work with older MSBuild versions that do not set it.
+
+#if NETFRAMEWORK
+
+using System;
+
+namespace Microsoft.Build.Framework
+{
+ internal static class TaskEnvironmentDefaults
+ {
+ ///
+ /// Creates a default TaskEnvironment backed by the current process environment.
+ /// Uses Environment.CurrentDirectory as the project directory, which in single-threaded
+ /// MSBuild is set to the project directory before task execution.
+ ///
+ internal static TaskEnvironment Create() =>
+ new TaskEnvironment(new ProcessTaskEnvironmentDriver(Environment.CurrentDirectory));
+ }
+}
+
+#endif
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenACreateComHostMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenACreateComHostMultiThreading.cs
new file mode 100644
index 000000000000..62c522bbe526
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenACreateComHostMultiThreading.cs
@@ -0,0 +1,215 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Build.Framework;
+using Xunit;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ [Collection("CWD-Dependent")]
+
+ public class GivenACreateComHostMultiThreading
+ {
+ [Fact]
+ public void Paths_AreResolvedRelativeToProjectDirectory()
+ {
+ // Create a unique temp directory to act as the project directory
+ var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"comhost-mt-{Guid.NewGuid():N}"));
+ Directory.CreateDirectory(projectDir);
+ try
+ {
+ // Create relative path structure under the project dir
+ var sourceDir = Path.Combine(projectDir, "source");
+ var destDir = Path.Combine(projectDir, "output");
+ Directory.CreateDirectory(sourceDir);
+ Directory.CreateDirectory(destDir);
+
+ // Create a fake comhost source file and clsid map
+ File.WriteAllText(Path.Combine(sourceDir, "comhost.dll"), "fake-source");
+ File.WriteAllText(Path.Combine(sourceDir, "clsidmap.bin"), "fake-clsid");
+
+ var task = new CreateComHost
+ {
+ BuildEngine = new MockBuildEngine(),
+ ComHostSourcePath = Path.Combine("source", "comhost.dll"),
+ ComHostDestinationPath = Path.Combine("output", "comhost.dll"),
+ ClsidMapPath = Path.Combine("source", "clsidmap.bin"),
+ };
+
+ // Set TaskEnvironment for path resolution
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir);
+
+ // ComHost.Create will throw because our fake files aren't valid PE binaries.
+ // The key assertion is that the exception comes from PE processing (ResourceUpdater),
+ // NOT from "file not found" — proving paths were resolved via TaskEnvironment.
+ try
+ {
+ task.Execute();
+ }
+ catch (Exception)
+ {
+ // Expected — ComHost.Create fails on fake binaries
+ }
+
+ // Verify that any errors logged are NOT about missing files
+ var engine = (MockBuildEngine)task.BuildEngine;
+ var errors = engine.Errors.Select(e => e.Message).ToList();
+ errors.Should().NotContain(e => e.Contains("not found", StringComparison.OrdinalIgnoreCase)
+ && e.Contains("comhost", StringComparison.OrdinalIgnoreCase),
+ "paths should be resolved via TaskEnvironment, not CWD");
+ }
+ finally
+ {
+ Directory.Delete(projectDir, true);
+ }
+ }
+
+ [Fact]
+ public void ItProducesSameErrorsInMultiProcessAndMultiThreadedEnvironments()
+ {
+ var projectDir = Path.Combine(Path.GetTempPath(), "comhost-test-" + Guid.NewGuid().ToString("N"));
+ var otherDir = Path.Combine(Path.GetTempPath(), "comhost-decoy-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(projectDir);
+ Directory.CreateDirectory(otherDir);
+ var savedCwd = Directory.GetCurrentDirectory();
+ try
+ {
+ // Create fake comhost and clsidmap under projectDir
+ var sourceDir = Path.Combine(projectDir, "source");
+ var outputDir = Path.Combine(projectDir, "output");
+ Directory.CreateDirectory(sourceDir);
+ Directory.CreateDirectory(outputDir);
+ File.WriteAllText(Path.Combine(sourceDir, "comhost.dll"), "fake-comhost-pe");
+ File.WriteAllText(Path.Combine(sourceDir, "clsidmap.bin"), "fake-clsid");
+
+ var comHostSource = Path.Combine("source", "comhost.dll");
+ var comHostDest = Path.Combine("output", "comhost.dll");
+ var clsidMap = Path.Combine("source", "clsidmap.bin");
+
+ // --- Multiprocess mode: CWD == projectDir ---
+ Directory.SetCurrentDirectory(projectDir);
+ var (result1, engine1, ex1) = RunTask(comHostSource, comHostDest, clsidMap, projectDir);
+
+ // --- Multithreaded mode: CWD == otherDir ---
+ Directory.SetCurrentDirectory(otherDir);
+ var (result2, engine2, ex2) = RunTask(comHostSource, comHostDest, clsidMap, projectDir);
+
+ // Both should produce the same result
+ result1.Should().Be(result2,
+ "task should return the same success/failure in both environments");
+ (ex1 == null).Should().Be(ex2 == null,
+ "both environments should agree on whether an exception is thrown");
+ if (ex1 != null && ex2 != null)
+ {
+ ex1.GetType().Should().Be(ex2.GetType(),
+ "exception type should be identical in both environments");
+ }
+ engine1.Errors.Count.Should().Be(engine2.Errors.Count,
+ "error count should be the same in both environments");
+ for (int i = 0; i < engine1.Errors.Count; i++)
+ {
+ engine1.Errors[i].Message.Should().Be(
+ engine2.Errors[i].Message,
+ $"error message [{i}] should be identical in both environments");
+ }
+ engine1.Warnings.Count.Should().Be(engine2.Warnings.Count,
+ "warning count should be the same in both environments");
+ }
+ finally
+ {
+ Directory.SetCurrentDirectory(savedCwd);
+ if (Directory.Exists(projectDir))
+ Directory.Delete(projectDir, true);
+ if (Directory.Exists(otherDir))
+ Directory.Delete(otherDir, true);
+ }
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(16)]
+ public async System.Threading.Tasks.Task CreateComHost_ConcurrentExecution(int parallelism)
+ {
+ // These tasks work with PE binaries. With fake inputs they will throw/fail,
+ // but the concurrent execution should not produce different failure modes
+ // (no shared-state corruption, no deadlocks, no data races).
+ var results = new ConcurrentBag<(bool success, string exType)>();
+ using var startGate = new ManualResetEventSlim(false);
+ var tasks = new System.Threading.Tasks.Task[parallelism];
+ for (int i = 0; i < parallelism; i++)
+ {
+ int idx = i;
+ tasks[idx] = System.Threading.Tasks.Task.Run(() =>
+ {
+ var projectDir = Path.Combine(Path.GetTempPath(), $"comhost-conc-{idx}-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(projectDir);
+ try
+ {
+ var sourceDir = Path.Combine(projectDir, "source");
+ var outputDir = Path.Combine(projectDir, "output");
+ Directory.CreateDirectory(sourceDir);
+ Directory.CreateDirectory(outputDir);
+ File.WriteAllText(Path.Combine(sourceDir, "comhost.dll"), "fake-comhost-pe");
+ File.WriteAllText(Path.Combine(sourceDir, "clsidmap.bin"), "fake-clsid");
+
+ var task = new CreateComHost
+ {
+ BuildEngine = new MockBuildEngine(),
+ ComHostSourcePath = Path.Combine("source", "comhost.dll"),
+ ComHostDestinationPath = Path.Combine("output", "comhost.dll"),
+ ClsidMapPath = Path.Combine("source", "clsidmap.bin"),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
+ };
+
+ startGate.Wait();
+ var result = task.Execute();
+ results.Add((result, "none"));
+ }
+ catch (Exception ex)
+ {
+ results.Add((false, ex.GetType().Name));
+ }
+ finally
+ {
+ if (Directory.Exists(projectDir))
+ Directory.Delete(projectDir, true);
+ }
+ });
+ }
+ startGate.Set();
+ await System.Threading.Tasks.Task.WhenAll(tasks);
+
+ // All threads should get the same outcome (all succeed or all fail the same way)
+ results.Select(r => r.exType).Distinct().Should().HaveCount(1,
+ "all threads should experience the same failure mode");
+ }
+
+ private static (bool result, MockBuildEngine engine, Exception? exception) RunTask(
+ string comHostSource, string comHostDest, string clsidMap, string projectDir)
+ {
+ var engine = new MockBuildEngine();
+ var task = new CreateComHost
+ {
+ BuildEngine = engine,
+ ComHostSourcePath = comHostSource,
+ ComHostDestinationPath = comHostDest,
+ ClsidMapPath = clsidMap,
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
+ };
+
+ try
+ {
+ var result = task.Execute();
+ return (result, engine, null);
+ }
+ catch (Exception ex)
+ {
+ return (false, engine, ex);
+ }
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRegFreeComManifestMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRegFreeComManifestMultiThreading.cs
new file mode 100644
index 000000000000..4eb5820ab4e2
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateRegFreeComManifestMultiThreading.cs
@@ -0,0 +1,217 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Build.Framework;
+using Xunit;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ [Collection("CWD-Dependent")]
+
+ public class GivenAGenerateRegFreeComManifestMultiThreading
+ {
+ [Fact]
+ public void IntermediateAssembly_IsResolvedRelativeToProjectDirectory()
+ {
+ var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"regfree-mt-{Guid.NewGuid():N}"));
+ Directory.CreateDirectory(projectDir);
+ try
+ {
+ // Place a copy of this test assembly at a relative path under the project dir
+ var thisAssemblyPath = typeof(GivenAGenerateRegFreeComManifestMultiThreading).Assembly.Location;
+ var binDir = Path.Combine(projectDir, "bin");
+ Directory.CreateDirectory(binDir);
+ var assemblyFileName = Path.GetFileName(thisAssemblyPath);
+ File.Copy(thisAssemblyPath, Path.Combine(binDir, assemblyFileName));
+
+ // Create fake clsidmap and manifest output paths
+ File.WriteAllText(Path.Combine(binDir, "clsidmap.bin"), "{}");
+
+ var task = new GenerateRegFreeComManifest
+ {
+ BuildEngine = new MockBuildEngine(),
+ IntermediateAssembly = Path.Combine("bin", assemblyFileName),
+ ComHostName = "test.comhost.dll",
+ ClsidMapPath = Path.Combine("bin", "clsidmap.bin"),
+ ComManifestPath = Path.Combine("bin", "test.manifest"),
+ };
+
+ // Set TaskEnvironment for path resolution
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir);
+
+ // Execute — will try to read the assembly version from the relative path
+ // resolved via TaskEnvironment, then create the manifest
+ try
+ {
+ task.Execute();
+ }
+ catch (Exception)
+ {
+ // May fail due to invalid clsidmap content, but that's ok
+ }
+
+ // If the IntermediateAssembly was resolved correctly, the manifest file
+ // should be created at the absolute path (or at least attempted)
+ var engine = (MockBuildEngine)task.BuildEngine;
+ var errors = engine.Errors.Select(e => e.Message).ToList();
+ // Should NOT have file-not-found for the intermediate assembly
+ errors.Should().NotContain(e => e.Contains("not found", StringComparison.OrdinalIgnoreCase)
+ && e.Contains(assemblyFileName, StringComparison.OrdinalIgnoreCase),
+ "IntermediateAssembly should be resolved via TaskEnvironment");
+ }
+ finally
+ {
+ Directory.Delete(projectDir, true);
+ }
+ }
+
+ [Fact]
+ public void ItProducesSameErrorsInMultiProcessAndMultiThreadedEnvironments()
+ {
+ var projectDir = Path.Combine(Path.GetTempPath(), "regfree-test-" + Guid.NewGuid().ToString("N"));
+ var otherDir = Path.Combine(Path.GetTempPath(), "regfree-decoy-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(projectDir);
+ Directory.CreateDirectory(otherDir);
+ var savedCwd = Directory.GetCurrentDirectory();
+ try
+ {
+ // Copy a real assembly so FileUtilities.TryGetAssemblyVersion works
+ var thisAssemblyPath = typeof(GivenAGenerateRegFreeComManifestMultiThreading).Assembly.Location;
+ var binDir = Path.Combine(projectDir, "bin");
+ Directory.CreateDirectory(binDir);
+ var assemblyFileName = Path.GetFileName(thisAssemblyPath);
+ File.Copy(thisAssemblyPath, Path.Combine(binDir, assemblyFileName));
+
+ // Create a fake clsidmap
+ File.WriteAllText(Path.Combine(binDir, "clsidmap.bin"), "{}");
+
+ var assemblyRelPath = Path.Combine("bin", assemblyFileName);
+ var clsidMapRelPath = Path.Combine("bin", "clsidmap.bin");
+ var manifestRelPath = Path.Combine("bin", "test.manifest");
+
+ // --- Multiprocess mode: CWD == projectDir ---
+ Directory.SetCurrentDirectory(projectDir);
+ var (result1, engine1, ex1) = RunTask(assemblyRelPath, clsidMapRelPath, manifestRelPath, projectDir);
+
+ // --- Multithreaded mode: CWD == otherDir ---
+ Directory.SetCurrentDirectory(otherDir);
+ var (result2, engine2, ex2) = RunTask(assemblyRelPath, clsidMapRelPath, manifestRelPath, projectDir);
+
+ // Both should produce the same result
+ result1.Should().Be(result2,
+ "task should return the same success/failure in both environments");
+ (ex1 == null).Should().Be(ex2 == null,
+ "both environments should agree on whether an exception is thrown");
+ if (ex1 != null && ex2 != null)
+ {
+ ex1.GetType().Should().Be(ex2.GetType(),
+ "exception type should be identical in both environments");
+ }
+ engine1.Errors.Count.Should().Be(engine2.Errors.Count,
+ "error count should be the same in both environments");
+ for (int i = 0; i < engine1.Errors.Count; i++)
+ {
+ engine1.Errors[i].Message.Should().Be(
+ engine2.Errors[i].Message,
+ $"error message [{i}] should be identical in both environments");
+ }
+ engine1.Warnings.Count.Should().Be(engine2.Warnings.Count,
+ "warning count should be the same in both environments");
+ }
+ finally
+ {
+ Directory.SetCurrentDirectory(savedCwd);
+ if (Directory.Exists(projectDir))
+ Directory.Delete(projectDir, true);
+ if (Directory.Exists(otherDir))
+ Directory.Delete(otherDir, true);
+ }
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(16)]
+ public async System.Threading.Tasks.Task GenerateRegFreeComManifest_ConcurrentExecution(int parallelism)
+ {
+ var results = new ConcurrentBag<(bool success, string exType)>();
+ using var startGate = new ManualResetEventSlim(false);
+ var tasks = new System.Threading.Tasks.Task[parallelism];
+ for (int i = 0; i < parallelism; i++)
+ {
+ int idx = i;
+ tasks[idx] = System.Threading.Tasks.Task.Run(() =>
+ {
+ var projectDir = Path.Combine(Path.GetTempPath(), $"regfree-conc-{idx}-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(projectDir);
+ try
+ {
+ var thisAssemblyPath = typeof(GivenAGenerateRegFreeComManifestMultiThreading).Assembly.Location;
+ var binDir = Path.Combine(projectDir, "bin");
+ Directory.CreateDirectory(binDir);
+ var assemblyFileName = Path.GetFileName(thisAssemblyPath);
+ File.Copy(thisAssemblyPath, Path.Combine(binDir, assemblyFileName));
+ File.WriteAllText(Path.Combine(binDir, "clsidmap.bin"), "{}");
+
+ var task = new GenerateRegFreeComManifest
+ {
+ BuildEngine = new MockBuildEngine(),
+ IntermediateAssembly = Path.Combine("bin", assemblyFileName),
+ ComHostName = "test.comhost.dll",
+ ClsidMapPath = Path.Combine("bin", "clsidmap.bin"),
+ ComManifestPath = Path.Combine("bin", "test.manifest"),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
+ };
+
+ startGate.Wait();
+ var result = task.Execute();
+ results.Add((result, "none"));
+ }
+ catch (Exception ex)
+ {
+ results.Add((false, ex.GetType().Name));
+ }
+ finally
+ {
+ if (Directory.Exists(projectDir))
+ Directory.Delete(projectDir, true);
+ }
+ });
+ }
+ startGate.Set();
+ await System.Threading.Tasks.Task.WhenAll(tasks);
+
+ // All threads should get the same outcome (all succeed or all fail the same way)
+ results.Select(r => r.exType).Distinct().Should().HaveCount(1,
+ "all threads should experience the same failure mode");
+ }
+
+ private static (bool result, MockBuildEngine engine, Exception? exception) RunTask(
+ string assemblyRelPath, string clsidMapRelPath, string manifestRelPath, string projectDir)
+ {
+ var engine = new MockBuildEngine();
+ var task = new GenerateRegFreeComManifest
+ {
+ BuildEngine = engine,
+ IntermediateAssembly = assemblyRelPath,
+ ComHostName = "test.comhost.dll",
+ ClsidMapPath = clsidMapRelPath,
+ ComManifestPath = manifestRelPath,
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
+ };
+
+ try
+ {
+ var result = task.Execute();
+ return (result, engine, null);
+ }
+ catch (Exception ex)
+ {
+ return (false, engine, ex);
+ }
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateShimsMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateShimsMultiThreading.cs
new file mode 100644
index 000000000000..2bf0a075b715
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAGenerateShimsMultiThreading.cs
@@ -0,0 +1,272 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Concurrent;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Xunit;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ [Collection("CWD-Dependent")]
+
+ public class GivenAGenerateShimsMultiThreading
+ {
+ [Fact]
+ public void Paths_AreResolvedRelativeToProjectDirectory()
+ {
+ var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"shims-mt-{Guid.NewGuid():N}"));
+ Directory.CreateDirectory(projectDir);
+ try
+ {
+ // Create fake apphost and assembly under projectDir
+ var toolsDir = Path.Combine(projectDir, "tools");
+ var binDir = Path.Combine(projectDir, "bin");
+ Directory.CreateDirectory(toolsDir);
+ Directory.CreateDirectory(binDir);
+ File.WriteAllText(Path.Combine(toolsDir, "apphost.exe"), "not a real apphost");
+ File.WriteAllText(Path.Combine(binDir, "test.dll"), "not a real assembly");
+
+ var apphostItem = new TaskItem(Path.Combine("tools", "apphost.exe"));
+ apphostItem.SetMetadata(MetadataKeys.RuntimeIdentifier, "linux-x64");
+
+ var task = new GenerateShims
+ {
+ BuildEngine = new MockBuildEngine(),
+ ApphostsForShimRuntimeIdentifiers = new ITaskItem[] { apphostItem },
+ IntermediateAssembly = Path.Combine("bin", "test.dll"),
+ PackageId = "TestPackage",
+ PackageVersion = "1.0.0",
+ TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0",
+ ToolCommandName = "test-tool",
+ ToolEntryPoint = "test-tool.dll",
+ PackagedShimOutputDirectory = "shims",
+ ShimRuntimeIdentifiers = new ITaskItem[] { new TaskItem("linux-x64") },
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
+ };
+
+ // Will throw because fake files aren't valid PE binaries.
+ // Key assertion: exception is from PE processing, NOT "file not found"
+ try
+ {
+ task.Execute();
+ }
+ catch (Exception)
+ {
+ // Expected — HostWriter.CreateAppHost fails on fake binaries
+ }
+
+ var engine = (MockBuildEngine)task.BuildEngine;
+ var errors = engine.Errors.Select(e => e.Message).ToList();
+ errors.Should().NotContain(e => e.Contains("not found", StringComparison.OrdinalIgnoreCase)
+ && e.Contains("apphost", StringComparison.OrdinalIgnoreCase),
+ "paths should be resolved via TaskEnvironment, not CWD");
+ }
+ finally
+ {
+ Directory.Delete(projectDir, true);
+ }
+ }
+
+ [Fact]
+ public void ItProducesSameErrorsInMultiProcessAndMultiThreadedEnvironments()
+ {
+ var projectDir = Path.Combine(Path.GetTempPath(), "shims-test-" + Guid.NewGuid().ToString("N"));
+ var otherDir = Path.Combine(Path.GetTempPath(), "shims-decoy-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(projectDir);
+ Directory.CreateDirectory(otherDir);
+ var savedCwd = Directory.GetCurrentDirectory();
+ try
+ {
+ // Create a fake apphost file under projectDir
+ var apphostRelPath = Path.Combine("tools", "apphost.exe");
+ var apphostAbsPath = Path.Combine(projectDir, apphostRelPath);
+ Directory.CreateDirectory(Path.GetDirectoryName(apphostAbsPath)!);
+ File.WriteAllText(apphostAbsPath, "not a real apphost binary");
+
+ // Create a fake intermediate assembly under projectDir
+ var assemblyRelPath = Path.Combine("bin", "test.dll");
+ var assemblyAbsPath = Path.Combine(projectDir, assemblyRelPath);
+ Directory.CreateDirectory(Path.GetDirectoryName(assemblyAbsPath)!);
+ File.WriteAllText(assemblyAbsPath, "not a real assembly");
+
+ var outputRelPath = "shims";
+
+ // --- Multiprocess mode: CWD == projectDir ---
+ Directory.SetCurrentDirectory(projectDir);
+ var (result1, engine1, ex1) = RunTask(apphostRelPath, assemblyRelPath, outputRelPath, projectDir);
+
+ // --- Multithreaded mode: CWD == otherDir ---
+ Directory.SetCurrentDirectory(otherDir);
+ var (result2, engine2, ex2) = RunTask(apphostRelPath, assemblyRelPath, outputRelPath, projectDir);
+
+ // Both should produce the same result
+ result1.Should().Be(result2,
+ "task should return the same success/failure in both environments");
+ (ex1 == null).Should().Be(ex2 == null,
+ "both environments should agree on whether an exception is thrown");
+ if (ex1 != null && ex2 != null)
+ {
+ ex1.GetType().Should().Be(ex2.GetType(),
+ "exception type should be identical in both environments");
+ }
+ engine1.Errors.Count.Should().Be(engine2.Errors.Count,
+ "error count should be the same in both environments");
+ for (int i = 0; i < engine1.Errors.Count; i++)
+ {
+ engine1.Errors[i].Message.Should().Be(
+ engine2.Errors[i].Message,
+ $"error message [{i}] should be identical in both environments");
+ }
+ engine1.Warnings.Count.Should().Be(engine2.Warnings.Count,
+ "warning count should be the same in both environments");
+ }
+ finally
+ {
+ Directory.SetCurrentDirectory(savedCwd);
+ if (Directory.Exists(projectDir))
+ Directory.Delete(projectDir, true);
+ if (Directory.Exists(otherDir))
+ Directory.Delete(otherDir, true);
+ }
+ }
+
+ [Fact]
+ public void EmptyShimRuntimeIdentifiers_DoesNotCrash()
+ {
+ var engine = new MockBuildEngine();
+ var task = new GenerateShims
+ {
+ BuildEngine = engine,
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ ApphostsForShimRuntimeIdentifiers = Array.Empty(),
+ IntermediateAssembly = "test.dll",
+ PackageId = "TestPackage",
+ PackageVersion = "1.0.0",
+ TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0",
+ ToolCommandName = "test-tool",
+ ToolEntryPoint = "test-tool.dll",
+ PackagedShimOutputDirectory = "shims",
+ ShimRuntimeIdentifiers = Array.Empty(),
+ };
+
+ try
+ {
+ // With no ShimRuntimeIdentifiers, the loop body never executes
+ var result = task.Execute();
+
+ result.Should().BeTrue("empty ShimRuntimeIdentifiers means no work to do");
+ engine.Errors.Should().BeEmpty("empty ShimRuntimeIdentifiers should not produce errors");
+ }
+ catch (FileNotFoundException ex) when (ex.FileName?.Contains("HostModel") == true)
+ {
+ // Microsoft.NET.HostModel is excluded from test output (ExcludeAssets="Runtime"
+ // in Tasks .csproj). The assembly is normally loaded from the SDK redist layout.
+ // This test still validates construction and property assignment succeed.
+ }
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(16)]
+ public async System.Threading.Tasks.Task GenerateShims_ConcurrentExecution(int parallelism)
+ {
+ var results = new ConcurrentBag<(bool success, string exType)>();
+ using var startGate = new ManualResetEventSlim(false);
+ var tasks = new System.Threading.Tasks.Task[parallelism];
+ for (int i = 0; i < parallelism; i++)
+ {
+ int idx = i;
+ tasks[idx] = System.Threading.Tasks.Task.Run(() =>
+ {
+ var projectDir = Path.Combine(Path.GetTempPath(), $"shims-conc-{idx}-{Guid.NewGuid():N}");
+ Directory.CreateDirectory(projectDir);
+ try
+ {
+ var toolsDir = Path.Combine(projectDir, "tools");
+ var binDir = Path.Combine(projectDir, "bin");
+ Directory.CreateDirectory(toolsDir);
+ Directory.CreateDirectory(binDir);
+ File.WriteAllText(Path.Combine(toolsDir, "apphost.exe"), "not a real apphost");
+ File.WriteAllText(Path.Combine(binDir, "test.dll"), "not a real assembly");
+
+ var apphostItem = new TaskItem(Path.Combine("tools", "apphost.exe"));
+ apphostItem.SetMetadata(MetadataKeys.RuntimeIdentifier, "linux-x64");
+
+ var task = new GenerateShims
+ {
+ BuildEngine = new MockBuildEngine(),
+ ApphostsForShimRuntimeIdentifiers = new ITaskItem[] { apphostItem },
+ IntermediateAssembly = Path.Combine("bin", "test.dll"),
+ PackageId = "TestPackage",
+ PackageVersion = "1.0.0",
+ TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0",
+ ToolCommandName = "test-tool",
+ ToolEntryPoint = "test-tool.dll",
+ PackagedShimOutputDirectory = "shims",
+ ShimRuntimeIdentifiers = new ITaskItem[] { new TaskItem("linux-x64") },
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
+ };
+
+ startGate.Wait();
+ var result = task.Execute();
+ results.Add((result, "none"));
+ }
+ catch (Exception ex)
+ {
+ results.Add((false, ex.GetType().Name));
+ }
+ finally
+ {
+ if (Directory.Exists(projectDir))
+ Directory.Delete(projectDir, true);
+ }
+ });
+ }
+ startGate.Set();
+ await System.Threading.Tasks.Task.WhenAll(tasks);
+
+ // All threads should get the same outcome (all succeed or all fail the same way)
+ results.Select(r => r.exType).Distinct().Should().HaveCount(1,
+ "all threads should experience the same failure mode");
+ }
+
+ private static (bool result, MockBuildEngine engine, Exception? exception) RunTask(
+ string apphostRelPath, string assemblyRelPath, string outputRelPath, string projectDir)
+ {
+ var apphostItem = new TaskItem(apphostRelPath);
+ apphostItem.SetMetadata(MetadataKeys.RuntimeIdentifier, "linux-x64");
+
+ var shimRid = new TaskItem("linux-x64");
+
+ var engine = new MockBuildEngine();
+ var task = new GenerateShims
+ {
+ BuildEngine = engine,
+ ApphostsForShimRuntimeIdentifiers = new ITaskItem[] { apphostItem },
+ IntermediateAssembly = assemblyRelPath,
+ PackageId = "TestPackage",
+ PackageVersion = "1.0.0",
+ TargetFrameworkMoniker = ".NETCoreApp,Version=v8.0",
+ ToolCommandName = "test-tool",
+ ToolEntryPoint = "test-tool.dll",
+ PackagedShimOutputDirectory = outputRelPath,
+ ShimRuntimeIdentifiers = new ITaskItem[] { shimRid },
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
+ };
+
+ try
+ {
+ var result = task.Execute();
+ return (result, engine, null);
+ }
+ catch (Exception ex)
+ {
+ return (false, engine, ex);
+ }
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs
index dc383cfdeaa0..1076e8169314 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs
@@ -5,6 +5,7 @@
using System.Collections;
using System.Globalization;
+using System.Reflection;
using System.Text.RegularExpressions;
using FluentAssertions;
using Xunit;
@@ -95,7 +96,8 @@ public void ThereAreNoGapsDuplicatesOrIncorrectlyFormattedCodes()
[Fact]
public void ResxIsCommentedWithCorrectStrBegin()
{
- var doc = XDocument.Load("Strings.resx");
+ var resxPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Strings.resx");
+ var doc = XDocument.Load(resxPath);
var ns = doc.Root.Name.Namespace;
foreach (var data in doc.Root.Elements(ns + "data"))
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/CreateComHost.cs b/src/Tasks/Microsoft.NET.Build.Tasks/CreateComHost.cs
index acab7f5a6800..19d28621051c 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/CreateComHost.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/CreateComHost.cs
@@ -8,8 +8,19 @@
namespace Microsoft.NET.Build.Tasks
{
- public class CreateComHost : TaskBase
+ [MSBuildMultiThreadableTask]
+ public class CreateComHost : TaskBase, IMultiThreadableTask
{
+#if NETFRAMEWORK
+ private TaskEnvironment _taskEnvironment;
+ public TaskEnvironment TaskEnvironment
+ {
+ get => _taskEnvironment ??= TaskEnvironmentDefaults.Create();
+ set => _taskEnvironment = value;
+ }
+#else
+ public TaskEnvironment TaskEnvironment { get; set; }
+#endif
[Required]
public string ComHostSourcePath { get; set; }
@@ -37,10 +48,19 @@ protected override void ExecuteCore()
return;
}
+ // Absolutize type library paths
+ if (typeLibIdMap != null)
+ {
+ foreach (var key in typeLibIdMap.Keys.ToList())
+ {
+ typeLibIdMap[key] = TaskEnvironment.GetAbsolutePath(typeLibIdMap[key]);
+ }
+ }
+
ComHost.Create(
- ComHostSourcePath,
- ComHostDestinationPath,
- ClsidMapPath,
+ TaskEnvironment.GetAbsolutePath(ComHostSourcePath),
+ TaskEnvironment.GetAbsolutePath(ComHostDestinationPath),
+ TaskEnvironment.GetAbsolutePath(ClsidMapPath),
typeLibIdMap);
}
catch (TypeLibraryDoesNotExistException ex)
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRegFreeComManifest.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRegFreeComManifest.cs
index 83736b888e11..3d6f474170c3 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRegFreeComManifest.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateRegFreeComManifest.cs
@@ -8,8 +8,20 @@
namespace Microsoft.NET.Build.Tasks
{
- public class GenerateRegFreeComManifest : TaskBase
+ [MSBuildMultiThreadableTask]
+ public class GenerateRegFreeComManifest : TaskBase, IMultiThreadableTask
{
+#if NETFRAMEWORK
+ private TaskEnvironment _taskEnvironment;
+ public TaskEnvironment TaskEnvironment
+ {
+ get => _taskEnvironment ??= TaskEnvironmentDefaults.Create();
+ set => _taskEnvironment = value;
+ }
+#else
+ public TaskEnvironment TaskEnvironment { get; set; }
+#endif
+
[Required]
public string IntermediateAssembly { get; set; }
@@ -40,12 +52,14 @@ protected override void ExecuteCore()
return;
}
+ var absoluteIntermediateAssembly = TaskEnvironment.GetAbsolutePath(IntermediateAssembly);
+
RegFreeComManifest.CreateManifestFromClsidmap(
- Path.GetFileNameWithoutExtension(IntermediateAssembly),
+ Path.GetFileNameWithoutExtension(absoluteIntermediateAssembly),
ComHostName,
- FileUtilities.TryGetAssemblyVersion(IntermediateAssembly).ToString(),
- ClsidMapPath,
- ComManifestPath);
+ FileUtilities.TryGetAssemblyVersion(absoluteIntermediateAssembly).ToString(),
+ TaskEnvironment.GetAbsolutePath(ClsidMapPath),
+ TaskEnvironment.GetAbsolutePath(ComManifestPath));
}
catch (TypeLibraryDoesNotExistException ex)
{
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateShims.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateShims.cs
index b3e9b8ba6a07..5a8e536c9f8e 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateShims.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateShims.cs
@@ -10,13 +10,24 @@
namespace Microsoft.NET.Build.Tasks
{
- public sealed class GenerateShims : TaskBase
+ [MSBuildMultiThreadableTask]
+ public sealed class GenerateShims : TaskBase, IMultiThreadableTask
{
+#if NETFRAMEWORK
+ private TaskEnvironment _taskEnvironment;
+ public TaskEnvironment TaskEnvironment
+ {
+ get => _taskEnvironment ??= TaskEnvironmentDefaults.Create();
+ set => _taskEnvironment = value;
+ }
+#else
+ public TaskEnvironment TaskEnvironment { get; set; }
+#endif
///
/// Relative paths for Apphost for different ShimRuntimeIdentifiers with RuntimeIdentifier as meta data
///
[Required]
- public ITaskItem[] ApphostsForShimRuntimeIdentifiers { get; private set; }
+ public ITaskItem[] ApphostsForShimRuntimeIdentifiers { get; set; }
[Required]
public string IntermediateAssembly { get; set; }
@@ -75,13 +86,19 @@ public sealed class GenerateShims : TaskBase
protected override void ExecuteCore()
{
+ if (ShimRuntimeIdentifiers == null || ShimRuntimeIdentifiers.Length == 0)
+ {
+ EmbeddedApphostPaths = Array.Empty();
+ return;
+ }
+
var embeddedApphostPaths = new List();
foreach (var runtimeIdentifier in ShimRuntimeIdentifiers.Select(r => r.ItemSpec))
{
- var resolvedApphostAssetPath = GetApphostAsset(ApphostsForShimRuntimeIdentifiers, runtimeIdentifier);
+ var resolvedApphostAssetPath = (string)TaskEnvironment.GetAbsolutePath(GetApphostAsset(ApphostsForShimRuntimeIdentifiers, runtimeIdentifier));
var packagedShimOutputDirectoryAndRid = Path.Combine(
- PackagedShimOutputDirectory,
+ (string)TaskEnvironment.GetAbsolutePath(PackagedShimOutputDirectory),
runtimeIdentifier);
var appHostDestinationFilePath = Path.Combine(
@@ -115,7 +132,7 @@ protected override void ExecuteCore()
appHostDestinationFilePath: appHostDestinationFilePath,
appBinaryFilePath: appBinaryFilePath,
windowsGraphicalUserInterface: windowsGraphicalUserInterface,
- assemblyToCopyResourcesFrom: IntermediateAssembly);
+ assemblyToCopyResourcesFrom: (string)TaskEnvironment.GetAbsolutePath(IntermediateAssembly));
}
else
{