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