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/ShowPreviewMessage.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ShowPreviewMessage.cs
index a83fe3cc3e5a..363934df9156 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/ShowPreviewMessage.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ShowPreviewMessage.cs
@@ -10,23 +10,32 @@ namespace Microsoft.NET.Build.Tasks
///
/// Provides a localizable mechanism for logging messages with different levels of importance from the SDK targets.
///
+ [MSBuildMultiThreadableTask]
public class ShowPreviewMessage : TaskBase
{
+ private static readonly object s_previewMessageLock = new();
+
protected override void ExecuteCore()
{
const string previewMessageKey = "Microsoft.NET.Build.Tasks.DisplayPreviewMessageKey";
- object messageDisplayed =
- BuildEngine4.GetRegisteredTaskObject(previewMessageKey, RegisteredTaskObjectLifetime.Build);
- if (messageDisplayed == null)
+ if (BuildEngine4.GetRegisteredTaskObject(previewMessageKey, RegisteredTaskObjectLifetime.Build) is not null)
+ {
+ return;
+ }
+
+ lock (s_previewMessageLock)
{
- Log.LogMessage(MessageImportance.High, Strings.UsingPreviewSdk);
+ if (BuildEngine4.GetRegisteredTaskObject(previewMessageKey, RegisteredTaskObjectLifetime.Build) is null)
+ {
+ Log.LogMessage(MessageImportance.High, Strings.UsingPreviewSdk);
- BuildEngine4.RegisterTaskObject(
- previewMessageKey,
- new object(),
- RegisteredTaskObjectLifetime.Build,
- true);
+ BuildEngine4.RegisterTaskObject(
+ previewMessageKey,
+ new object(),
+ RegisteredTaskObjectLifetime.Build,
+ 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