diff --git a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs
deleted file mode 100644
index 8f522decc9b0..000000000000
--- a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs
+++ /dev/null
@@ -1,117 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-// This is a polyfill for the MultiProcessTaskEnvironmentDriver from MSBuild.
-// Adapted for use in the SDK tasks project where NativeMethodsShared is not available.
-// See: https://github.com/dotnet/msbuild/blob/main/src/Build/BackEnd/TaskExecutionHost/MultiProcessTaskEnvironmentDriver.cs
-
-#if NETFRAMEWORK
-
-#nullable enable
-
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-
-namespace Microsoft.Build.Framework
-{
- ///
- /// Default implementation of that directly interacts with the file system
- /// and environment variables. Used for multi-process mode and as a test helper.
- ///
- internal sealed class ProcessTaskEnvironmentDriver : ITaskEnvironmentDriver
- {
- private AbsolutePath _projectDirectory;
- private readonly Dictionary _environmentVariables;
-
- ///
- /// Initializes a new instance with the specified project directory.
- ///
- public ProcessTaskEnvironmentDriver(string projectDirectory)
- {
- _projectDirectory = new AbsolutePath(projectDirectory);
-
- // Seed from the current process environment
- _environmentVariables = new Dictionary(StringComparer.OrdinalIgnoreCase);
- foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
- {
- if (entry.Key is string key && entry.Value is string value)
- {
- _environmentVariables[key] = value;
- }
- }
- }
-
- ///
- public AbsolutePath ProjectDirectory
- {
- get => _projectDirectory;
- set => _projectDirectory = value;
- }
-
- ///
- public AbsolutePath GetAbsolutePath(string path)
- {
- if (Path.IsPathRooted(path))
- {
- return new AbsolutePath(path);
- }
-
- return new AbsolutePath(path, _projectDirectory);
- }
-
- ///
- public string? GetEnvironmentVariable(string name)
- {
- return _environmentVariables.TryGetValue(name, out var value) ? value : null;
- }
-
- ///
- public IReadOnlyDictionary GetEnvironmentVariables()
- {
- return new Dictionary(_environmentVariables, StringComparer.OrdinalIgnoreCase);
- }
-
- ///
- public void SetEnvironmentVariable(string name, string? value)
- {
- if (value == null)
- {
- _environmentVariables.Remove(name);
- }
- else
- {
- _environmentVariables[name] = value;
- }
- }
-
- ///
- public void SetEnvironment(IDictionary newEnvironment)
- {
- _environmentVariables.Clear();
- foreach (var kvp in newEnvironment)
- {
- _environmentVariables[kvp.Key] = kvp.Value;
- }
- }
-
- ///
- public ProcessStartInfo GetProcessStartInfo()
- {
- // No SDK task calls this method. It exists only to satisfy the ITaskEnvironmentDriver
- // interface contract. ToolTask subclasses use their own process-start logic instead.
- throw new NotImplementedException(
- "ProcessTaskEnvironmentDriver.GetProcessStartInfo is not used by SDK tasks.");
- }
-
- ///
- public void Dispose()
- {
- // No resources to clean up in this implementation.
- }
- }
-}
-
-#endif
diff --git a/src/Tasks/Common/TaskEnvironmentDefaults.cs b/src/Tasks/Common/TaskEnvironmentDefaults.cs
index 7ef5666a4175..f666cac3163f 100644
--- a/src/Tasks/Common/TaskEnvironmentDefaults.cs
+++ b/src/Tasks/Common/TaskEnvironmentDefaults.cs
@@ -4,23 +4,24 @@
// 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.
+//
+// Delegates to MSBuild's public TaskEnvironment.Fallback API
+// (see https://github.com/dotnet/msbuild/pull/13462) so we no longer carry our
+// own polyfill driver implementation.
#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.
+ /// Returns the MSBuild-provided fallback TaskEnvironment, which is backed by the
+ /// current process environment and uses Environment.CurrentDirectory as the project
+ /// directory.
///
- internal static TaskEnvironment Create() =>
- new TaskEnvironment(new ProcessTaskEnvironmentDriver(Environment.CurrentDirectory));
+ internal static TaskEnvironment Create() => TaskEnvironment.Fallback;
}
}
-#endif
+#endif
\ No newline at end of file
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveAppHosts.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveAppHosts.cs
index 00c6bf6fdcc3..7cd629c53fa7 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveAppHosts.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveAppHosts.cs
@@ -9,8 +9,11 @@
namespace Microsoft.NET.Build.Tasks
{
- public class ResolveAppHosts : TaskBase
+ [MSBuildMultiThreadableTask]
+ public class ResolveAppHosts : TaskBase, IMultiThreadableTask
{
+ public TaskEnvironment TaskEnvironment { get; set; } = null!;
+
public string TargetFrameworkIdentifier { get; set; }
public string TargetFrameworkVersion { get; set; }
@@ -287,16 +290,19 @@ private ITaskItem GetHostItem(string runtimeIdentifier,
hostNameWithoutExtension + (isExecutable ? ExecutableExtension.ForRuntimeIdentifier(bestAppHostRuntimeIdentifier) : ".dll"));
TaskItem appHostItem = new(itemName);
- string appHostPackPath = null;
+
+ AbsolutePath? resolvedPackDirectory = null;
+
if (!string.IsNullOrEmpty(TargetingPackRoot))
{
- appHostPackPath = Path.Combine(TargetingPackRoot, hostPackName, appHostPackVersion);
+ resolvedPackDirectory = TaskEnvironment.GetAbsolutePath(Path.Combine(TargetingPackRoot, hostPackName, appHostPackVersion));
}
- if (appHostPackPath != null && Directory.Exists(appHostPackPath))
+
+ if (resolvedPackDirectory != null && Directory.Exists(resolvedPackDirectory.Value))
{
- // Use AppHost from packs folder
- appHostItem.SetMetadata(MetadataKeys.PackageDirectory, appHostPackPath);
- appHostItem.SetMetadata(MetadataKeys.Path, Path.Combine(appHostPackPath, hostRelativePathInPackage));
+ // Use AppHost from packs folder. Use OriginalValue to preserve relativity in output metadata.
+ appHostItem.SetMetadata(MetadataKeys.PackageDirectory, resolvedPackDirectory.Value.OriginalValue);
+ appHostItem.SetMetadata(MetadataKeys.Path, Path.Combine(resolvedPackDirectory.Value.OriginalValue, hostRelativePathInPackage));
}
else if (EnableAppHostPackDownload)
{
diff --git a/test/Microsoft.NET.Build.Tasks.Tests/GivenAResolveAppHostsMultiThreading.cs b/test/Microsoft.NET.Build.Tasks.Tests/GivenAResolveAppHostsMultiThreading.cs
new file mode 100644
index 000000000000..14eedb6a8bd8
--- /dev/null
+++ b/test/Microsoft.NET.Build.Tasks.Tests/GivenAResolveAppHostsMultiThreading.cs
@@ -0,0 +1,146 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using FluentAssertions;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using Xunit;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ [CollectionDefinition(GivenAResolveAppHostsMultiThreading.CollectionName, DisableParallelization = true)]
+ public sealed class ResolveAppHostsCurrentDirectoryCollection
+ {
+ }
+
+ ///
+ /// Tests for ResolveAppHosts multi-threading support.
+ /// Verifies TaskEnvironment-based path resolution against ProjectDirectory (not process CWD)
+ /// and that output metadata preserves the original path form.
+ ///
+ [Collection(CollectionName)]
+ public class GivenAResolveAppHostsMultiThreading : IDisposable
+ {
+ internal const string CollectionName = "ResolveAppHosts current directory tests";
+ private const string PackName = "Microsoft.NETCore.App.Host.win-x64";
+ private const string PackVersion = "8.0.0";
+ private const string RuntimeGraphJson = "{ \"runtimes\": { \"win-x64\": { \"#import\": [] } } }";
+
+ private readonly string _testDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ private readonly string _decoyDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ private readonly string _originalCurrentDirectory = Directory.GetCurrentDirectory();
+
+ public GivenAResolveAppHostsMultiThreading()
+ {
+ Directory.CreateDirectory(_testDir);
+ Directory.CreateDirectory(_decoyDir);
+ Directory.SetCurrentDirectory(_decoyDir);
+ }
+
+ public void Dispose()
+ {
+ Directory.SetCurrentDirectory(_originalCurrentDirectory);
+ DeleteDirectory(_testDir);
+ DeleteDirectory(_decoyDir);
+ }
+
+ [Fact]
+ public void RelativeTargetingPackRoot_OutputMetadataRemainsRelative()
+ {
+ string relativePackRoot = "packs";
+ Directory.Exists(Path.Combine(_decoyDir, relativePackRoot)).Should().BeFalse(
+ "the process CWD must not contain the relative pack root, or CWD fallback regressions can pass");
+
+ string nativePath = Path.Combine(_testDir, relativePackRoot, PackName, PackVersion, "runtimes", "win-x64", "native");
+ CreateHostFiles(nativePath);
+
+ string runtimeGraphPath = Path.Combine(_testDir, "runtime.json");
+ File.WriteAllText(runtimeGraphPath, RuntimeGraphJson);
+
+ var task = CreateTask(
+ taskEnv: TaskEnvironmentHelper.CreateForTest(_testDir),
+ targetingPackRoot: relativePackRoot,
+ runtimeGraphPath: runtimeGraphPath);
+
+ task.Execute().Should().BeTrue();
+ Directory.GetCurrentDirectory().Should().Be(_decoyDir,
+ "ResolveAppHosts must not mutate process current directory");
+
+ string expectedPackDir = Path.Combine(relativePackRoot, PackName, PackVersion);
+ AssertRelativePackMetadata(task.AppHost, expectedPackDir, "apphost.exe");
+ AssertRelativePackMetadata(task.SingleFileHost, expectedPackDir, "singlefilehost.exe");
+ AssertRelativePackMetadata(task.ComHost, expectedPackDir, "comhost.dll");
+ AssertRelativePackMetadata(task.IjwHost, expectedPackDir, "ijwhost.dll");
+ }
+
+ private static void AssertRelativePackMetadata(ITaskItem[] hostItems, string expectedPackDir, string hostFileName)
+ {
+ hostItems.Should().NotBeNull().And.HaveCount(1);
+ string packageDirectory = hostItems[0].GetMetadata(MetadataKeys.PackageDirectory);
+ string path = hostItems[0].GetMetadata(MetadataKeys.Path);
+
+ packageDirectory.Should().Be(expectedPackDir);
+ Path.IsPathRooted(packageDirectory).Should().BeFalse();
+ path.Should().Be(Path.Combine(expectedPackDir, "runtimes", "win-x64", "native", hostFileName));
+ Path.IsPathRooted(path).Should().BeFalse();
+ }
+
+ private static ResolveAppHosts CreateTask(
+ TaskEnvironment taskEnv,
+ string targetingPackRoot,
+ string runtimeGraphPath)
+ {
+ var knownAppHostPacks = new ITaskItem[]
+ {
+ new TaskItem("Microsoft.NETCore.App.Host", new Dictionary
+ {
+ { "TargetFramework", "net8.0" },
+ { "AppHostRuntimeIdentifiers", "win-x64" },
+ { "AppHostPackNamePattern", "Microsoft.NETCore.App.Host.**RID**" },
+ { "AppHostPackVersion", PackVersion },
+ { MetadataKeys.ExcludedRuntimeIdentifiers, "" }
+ })
+ };
+
+ return new ResolveAppHosts
+ {
+ BuildEngine = new MockBuildEngine(),
+ TaskEnvironment = taskEnv,
+ TargetFrameworkIdentifier = ".NETCoreApp",
+ TargetFrameworkVersion = "8.0",
+ TargetingPackRoot = targetingPackRoot,
+ AppHostRuntimeIdentifier = "win-x64",
+ DotNetAppHostExecutableNameWithoutExtension = "apphost",
+ DotNetSingleFileHostExecutableNameWithoutExtension = "singlefilehost",
+ DotNetComHostLibraryNameWithoutExtension = "comhost",
+ DotNetIjwHostLibraryNameWithoutExtension = "ijwhost",
+ RuntimeGraphPath = runtimeGraphPath,
+ KnownAppHostPacks = knownAppHostPacks,
+ NuGetRestoreSupported = true,
+ EnableAppHostPackDownload = false,
+ };
+ }
+
+ private static void CreateHostFiles(string nativePath)
+ {
+ Directory.CreateDirectory(nativePath);
+ File.WriteAllText(Path.Combine(nativePath, "apphost.exe"), "fake apphost");
+ File.WriteAllText(Path.Combine(nativePath, "singlefilehost.exe"), "fake");
+ File.WriteAllText(Path.Combine(nativePath, "comhost.dll"), "fake");
+ File.WriteAllText(Path.Combine(nativePath, "ijwhost.dll"), "fake");
+ }
+
+ private static void DeleteDirectory(string path)
+ {
+ if (Directory.Exists(path))
+ {
+ Directory.Delete(path, recursive: true);
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.NET.Build.Tasks.Tests/TaskEnvironmentHelper.cs b/test/Microsoft.NET.Build.Tasks.Tests/TaskEnvironmentHelper.cs
index 5702d9f59aa4..12b2b5e3cb82 100644
--- a/test/Microsoft.NET.Build.Tasks.Tests/TaskEnvironmentHelper.cs
+++ b/test/Microsoft.NET.Build.Tasks.Tests/TaskEnvironmentHelper.cs
@@ -2,16 +2,11 @@
// The .NET Foundation licenses this file to you under the MIT license.
// Helper class for creating TaskEnvironment instances in tests.
-// NOT gated with #if — always available in the test project.
-// Adapted from: https://github.com/dotnet/msbuild/blob/main/src/UnitTests.Shared/TaskEnvironmentHelper.cs
-
-using System;
-using System.Collections;
-using System.Collections.Generic;
+// Delegates to MSBuild's public TaskEnvironment factory APIs (see
+// https://github.com/dotnet/msbuild/pull/13462) instead of constructing a
+// reflection-based driver locally.
using System.IO;
-using System.Linq;
-using System.Reflection;
using Microsoft.Build.Framework;
namespace Microsoft.NET.Build.Tasks.UnitTests
@@ -22,142 +17,19 @@ namespace Microsoft.NET.Build.Tasks.UnitTests
public static class TaskEnvironmentHelper
{
///
- /// Creates a TaskEnvironment using the current working directory as the project directory.
+ /// Creates a TaskEnvironment using the current process environment and CWD.
///
public static TaskEnvironment CreateForTest()
{
- return CreateForTest(Directory.GetCurrentDirectory());
+ return TaskEnvironment.Fallback;
}
///
/// Creates a TaskEnvironment with the specified project directory.
- /// Uses reflection to work around internal visibility of ITaskEnvironmentDriver and TaskEnvironment ctor.
///
public static TaskEnvironment CreateForTest(string projectDirectory)
{
- // Get the internal ITaskEnvironmentDriver type from Microsoft.Build.Framework
- var driverInterfaceType = typeof(TaskEnvironment).Assembly
- .GetType("Microsoft.Build.Framework.ITaskEnvironmentDriver", throwOnError: true)!;
-
- // Create a DispatchProxy that implements ITaskEnvironmentDriver dynamically.
- // DispatchProxy.Create() is called via reflection since TInterface is internal.
- var createMethod = typeof(DispatchProxy)
- .GetMethods(BindingFlags.Public | BindingFlags.Static)
- .First(m => m.Name == nameof(DispatchProxy.Create) && m.GetGenericArguments().Length == 2)
- .MakeGenericMethod(driverInterfaceType, typeof(TestDriverProxy));
-
- var proxy = createMethod.Invoke(null, null)!;
-
- // Initialize the proxy with the project directory
- ((TestDriverProxy)proxy).Initialize(projectDirectory);
-
- // Call the internal TaskEnvironment(ITaskEnvironmentDriver) constructor via reflection
- var ctor = typeof(TaskEnvironment)
- .GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)
- .FirstOrDefault(c =>
- {
- var parameters = c.GetParameters();
- return parameters.Length == 1 && parameters[0].ParameterType == driverInterfaceType;
- });
-
- if (ctor is null)
- {
- throw new InvalidOperationException("Could not find TaskEnvironment constructor with ITaskEnvironmentDriver parameter.");
- }
-
- return (TaskEnvironment)ctor.Invoke(new[] { proxy });
- }
- }
-
- ///
- /// DispatchProxy-based implementation of the internal ITaskEnvironmentDriver interface.
- /// Stores its own project directory independently from the process's CWD,
- /// enabling tests to verify tasks resolve paths relative to ProjectDirectory, not CWD.
- ///
- internal class TestDriverProxy : DispatchProxy
- {
- private string _projectDirectory = string.Empty;
- private Dictionary _environmentVariables = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- internal void Initialize(string projectDirectory)
- {
- _projectDirectory = projectDirectory;
-
- // Seed from the current process environment
- foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
- {
- if (entry.Key is string key && entry.Value is string value)
- _environmentVariables[key] = value;
- }
- }
-
- protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
- {
- if (targetMethod == null) return null;
-
- return targetMethod.Name switch
- {
- "get_ProjectDirectory" => new AbsolutePath(_projectDirectory),
- "set_ProjectDirectory" => SetProjectDir(args),
- "GetAbsolutePath" => ResolveAbsolutePath((string)args![0]!),
- "GetEnvironmentVariable" => DoGetEnvVar(args),
- "GetEnvironmentVariables" => GetEnvVars(),
- "SetEnvironmentVariable" => DoSetEnvVar(args),
- "SetEnvironment" => DoSetEnv(args),
- "GetProcessStartInfo" => throw new NotImplementedException(
- "GetProcessStartInfo is not used by SDK tasks."),
- "Dispose" => null,
- _ => throw new NotSupportedException($"Method '{targetMethod.Name}' is not supported by {nameof(TestDriverProxy)}."),
- };
+ return TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDirectory);
}
-
- private object? SetProjectDir(object?[]? args)
- {
- _projectDirectory = ((AbsolutePath)args![0]!).Value;
- return null;
- }
-
- private AbsolutePath ResolveAbsolutePath(string path)
- {
- if (Path.IsPathRooted(path))
- return new AbsolutePath(path);
- return new AbsolutePath(path, new AbsolutePath(_projectDirectory));
- }
-
- private object? DoGetEnvVar(object?[]? args)
- {
- var name = (string)args![0]!;
- return _environmentVariables.TryGetValue(name, out var value) ? value : null;
- }
-
- private IReadOnlyDictionary GetEnvVars()
- {
- return new Dictionary(_environmentVariables, StringComparer.OrdinalIgnoreCase);
- }
-
- private object? DoSetEnvVar(object?[]? args)
- {
- var name = (string)args![0]!;
- var value = (string?)args[1];
- if (value == null)
- {
- _environmentVariables.Remove(name);
- }
- else
- {
- _environmentVariables[name] = value;
- }
- return null;
- }
-
- private object? DoSetEnv(object?[]? args)
- {
- var newEnv = (IDictionary)args![0]!;
- _environmentVariables.Clear();
- foreach (var kvp in newEnv)
- _environmentVariables[kvp.Key] = kvp.Value;
- return null;
- }
-
}
-}
+}
\ No newline at end of file