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/CollectSDKReferencesDesignTime.cs b/src/Tasks/Microsoft.NET.Build.Tasks/CollectSDKReferencesDesignTime.cs index 6ba13610c14f..db8f9c3b7e71 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/CollectSDKReferencesDesignTime.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/CollectSDKReferencesDesignTime.cs @@ -16,8 +16,19 @@ namespace Microsoft.NET.Build.Tasks /// Tracking issue https://github.com/dotnet/roslyn-project-system/issues/587 /// [MSBuildMultiThreadableTask] - public class CollectSDKReferencesDesignTime : TaskBase + public class CollectSDKReferencesDesignTime : 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 ITaskItem[] SdkReferences { get; set; } @@ -30,14 +41,12 @@ public class CollectSDKReferencesDesignTime : TaskBase [Output] public ITaskItem[] SDKReferencesDesignTime { get; set; } - private HashSet ImplicitPackageReferences { get; set; } - protected override void ExecuteCore() { - ImplicitPackageReferences = GetImplicitPackageReferences(DefaultImplicitPackages); + var implicitPackageReferences = GetImplicitPackageReferences(DefaultImplicitPackages); var sdkDesignTimeList = new List(SdkReferences); - sdkDesignTimeList.AddRange(GetImplicitPackageReferences()); + sdkDesignTimeList.AddRange(GetImplicitPackageReferences(implicitPackageReferences)); SDKReferencesDesignTime = sdkDesignTimeList.ToArray(); } @@ -64,7 +73,7 @@ internal static HashSet GetImplicitPackageReferences(string defaultImpli return implicitPackageReferences; } - private IEnumerable GetImplicitPackageReferences() + private IEnumerable GetImplicitPackageReferences(HashSet implicitPackageReferences) { var implicitPackages = new List(); foreach (var packageReference in PackageReferences) @@ -73,7 +82,7 @@ private IEnumerable GetImplicitPackageReferences() var isImplicitlyDefinedString = packageReference.GetMetadata(MetadataKeys.IsImplicitlyDefined); if (string.IsNullOrEmpty(isImplicitlyDefinedString)) { - isImplicitlyDefined = ImplicitPackageReferences.Contains(packageReference.ItemSpec); + isImplicitlyDefined = implicitPackageReferences.Contains(packageReference.ItemSpec); } else { diff --git a/test/Microsoft.NET.Build.Tasks.Tests/GivenACollectSDKReferencesDesignTimeMultiThreading.cs b/test/Microsoft.NET.Build.Tasks.Tests/GivenACollectSDKReferencesDesignTimeMultiThreading.cs new file mode 100644 index 000000000000..904175c0bb56 --- /dev/null +++ b/test/Microsoft.NET.Build.Tasks.Tests/GivenACollectSDKReferencesDesignTimeMultiThreading.cs @@ -0,0 +1,438 @@ +// 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 System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Build.Framework; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + [Collection(nameof(CurrentDirectoryMutatingTestCollection))] + public class GivenACollectSDKReferencesDesignTimeMultiThreading + { + [Fact] + public async Task ConcurrentExecutionWithInjectedTaskEnvironmentsProducesCorrectResults() + { + const int concurrency = 64; + var originalCurrentDirectory = Directory.GetCurrentDirectory(); + var sharedSdkReferences = new ITaskItem[] + { + new MockTaskItem(@"relative\SharedSDK", new Dictionary + { + { "RelativeMetadata", @"metadata\value" } + }) + }; + var sharedPackageReferences = new ITaskItem[] + { + new MockTaskItem(@"relative\ImplicitByMetadata", new Dictionary + { + { MetadataKeys.IsImplicitlyDefined, "True" }, + { MetadataKeys.Version, "1.0.0" } + }), + new MockTaskItem(@"relative\ImplicitByDefaultList", new Dictionary + { + { MetadataKeys.Version, "2.0.0" } + }), + new MockTaskItem(@"relative\ExplicitPackage", new Dictionary + { + { MetadataKeys.Version, "3.0.0" } + }) + }; + var taskInstances = new CollectSDKReferencesDesignTime[concurrency]; + var executeTasks = new Task[concurrency]; + using var startBarrier = new Barrier(concurrency); + + for (int i = 0; i < concurrency; i++) + { + var uniqueSdkReference = $@"relative\UniqueSDK\{i}"; + var uniqueImplicitByMetadata = $@"relative\UniqueImplicitByMetadata\{i}"; + var uniqueImplicitByDefaultList = $@"relative\UniqueImplicitByDefaultList\{i}"; + var uniqueExplicitPackage = $@"relative\UniqueExplicitPackage\{i}"; + var task = new CollectSDKReferencesDesignTime + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Path.Combine( + originalCurrentDirectory, + "non-default-project", + i.ToString())), + SdkReferences = sharedSdkReferences.Concat(new ITaskItem[] + { + new MockTaskItem(uniqueSdkReference, new Dictionary + { + { "RelativeMetadata", $@"metadata\unique\{i}" } + }) + }).ToArray(), + PackageReferences = sharedPackageReferences.Concat(new ITaskItem[] + { + new MockTaskItem(uniqueImplicitByMetadata, new Dictionary + { + { MetadataKeys.IsImplicitlyDefined, "True" }, + { MetadataKeys.Version, $"4.0.{i}" } + }), + new MockTaskItem(uniqueImplicitByDefaultList, new Dictionary + { + { MetadataKeys.Version, $"5.0.{i}" } + }), + new MockTaskItem(uniqueExplicitPackage, new Dictionary + { + { MetadataKeys.Version, $"6.0.{i}" } + }) + }).ToArray(), + DefaultImplicitPackages = $@"relative\ImplicitByDefaultList;{uniqueImplicitByDefaultList}" + }; + taskInstances[i] = task; + } + + for (int i = 0; i < concurrency; i++) + { + int localI = i; + var t = taskInstances[localI]; + executeTasks[localI] = Task.Factory.StartNew( + () => + { + startBarrier.SignalAndWait(); + return t.Execute(); + }, + CancellationToken.None, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); + } + + var executionResults = await Task.WhenAll(executeTasks); + + Directory.GetCurrentDirectory().Should().Be(originalCurrentDirectory); + for (int i = 0; i < concurrency; i++) + { + executionResults[i].Should().BeTrue($"task {i} should succeed"); + + var task = taskInstances[i]; + task.SDKReferencesDesignTime.Should().NotBeNull($"task {i} should produce output"); + task.SDKReferencesDesignTime.Length.Should().Be(6, $"task {i} should aggregate shared and unique SDK refs and implicit packages"); + + task.SDKReferencesDesignTime.Should().Contain( + item => item.ItemSpec == @"relative\SharedSDK", + $"task {i} should preserve the shared relative SDK reference"); + task.SDKReferencesDesignTime.Should().Contain( + item => item.ItemSpec == $@"relative\UniqueSDK\{i}", + $"task {i} should preserve its unique relative SDK reference"); + + var implicitByMetadata = task.SDKReferencesDesignTime.FirstOrDefault(item => item.ItemSpec == @"relative\ImplicitByMetadata"); + implicitByMetadata.Should().NotBeNull($"task {i} should include implicit package from metadata"); + implicitByMetadata.GetMetadata(MetadataKeys.SDKPackageItemSpec).Should().Be(string.Empty); + implicitByMetadata.GetMetadata(MetadataKeys.Name).Should().Be(@"relative\ImplicitByMetadata"); + implicitByMetadata.GetMetadata(MetadataKeys.IsImplicitlyDefined).Should().Be("True"); + implicitByMetadata.GetMetadata(MetadataKeys.Version).Should().Be("1.0.0"); + + var implicitByDefaultList = task.SDKReferencesDesignTime.FirstOrDefault(item => item.ItemSpec == @"relative\ImplicitByDefaultList"); + implicitByDefaultList.Should().NotBeNull($"task {i} should include implicit package from DefaultImplicitPackages"); + implicitByDefaultList.GetMetadata(MetadataKeys.Name).Should().Be(@"relative\ImplicitByDefaultList"); + implicitByDefaultList.GetMetadata(MetadataKeys.IsImplicitlyDefined).Should().Be("True"); + implicitByDefaultList.GetMetadata(MetadataKeys.Version).Should().Be("2.0.0"); + + var uniqueImplicitByMetadata = $@"relative\UniqueImplicitByMetadata\{i}"; + var uniqueImplicitByMetadataItem = task.SDKReferencesDesignTime.FirstOrDefault(item => item.ItemSpec == uniqueImplicitByMetadata); + uniqueImplicitByMetadataItem.Should().NotBeNull($"task {i} should include its unique implicit package from metadata"); + uniqueImplicitByMetadataItem.GetMetadata(MetadataKeys.Name).Should().Be(uniqueImplicitByMetadata); + uniqueImplicitByMetadataItem.GetMetadata(MetadataKeys.IsImplicitlyDefined).Should().Be("True"); + uniqueImplicitByMetadataItem.GetMetadata(MetadataKeys.Version).Should().Be($"4.0.{i}"); + + var uniqueImplicitByDefaultList = $@"relative\UniqueImplicitByDefaultList\{i}"; + var uniqueImplicitByDefaultListItem = task.SDKReferencesDesignTime.FirstOrDefault(item => item.ItemSpec == uniqueImplicitByDefaultList); + uniqueImplicitByDefaultListItem.Should().NotBeNull($"task {i} should include its unique implicit package from DefaultImplicitPackages"); + uniqueImplicitByDefaultListItem.GetMetadata(MetadataKeys.Name).Should().Be(uniqueImplicitByDefaultList); + uniqueImplicitByDefaultListItem.GetMetadata(MetadataKeys.IsImplicitlyDefined).Should().Be("True"); + uniqueImplicitByDefaultListItem.GetMetadata(MetadataKeys.Version).Should().Be($"5.0.{i}"); + + task.SDKReferencesDesignTime.Should().NotContain( + item => item.ItemSpec == $@"relative\UniqueExplicitPackage\{i}", + $"task {i} should not include its unique explicit package"); + } + } + + [Fact] + public void NonDefaultProjectDirectoryDoesNotChangeRelativeOutputsOrCurrentDirectory() + { + var originalCurrentDirectory = Directory.GetCurrentDirectory(); + var testRoot = Path.Combine( + originalCurrentDirectory, + $"{nameof(NonDefaultProjectDirectoryDoesNotChangeRelativeOutputsOrCurrentDirectory)}-{Guid.NewGuid():N}"); + var processCurrentDirectory = Path.Combine(testRoot, "current-directory"); + var projectDirectory = Path.Combine(testRoot, "project-directory"); + + try + { + Directory.CreateDirectory(processCurrentDirectory); + Directory.CreateDirectory(projectDirectory); + Directory.SetCurrentDirectory(processCurrentDirectory); + + var task = new CollectSDKReferencesDesignTime + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDirectory), + SdkReferences = new[] + { + new MockTaskItem(@"relative\ExistingSDK", new Dictionary()) + }, + PackageReferences = new[] + { + new MockTaskItem(@"relative\ImplicitPackage", new Dictionary + { + { MetadataKeys.Version, @"relative\version.txt" } + }) + }, + DefaultImplicitPackages = @"relative\ImplicitPackage" + }; + + Directory.GetCurrentDirectory().Should().Be(processCurrentDirectory); + task.TaskEnvironment.ProjectDirectory.Value.Should().Be(projectDirectory); + task.TaskEnvironment.ProjectDirectory.Value.Should().NotBe(Directory.GetCurrentDirectory()); + + task.Execute().Should().BeTrue(); + + Directory.GetCurrentDirectory().Should().Be(processCurrentDirectory); + task.SDKReferencesDesignTime.Select(r => r.ItemSpec).Should().Equal( + @"relative\ExistingSDK", + @"relative\ImplicitPackage"); + + var implicitPackage = task.SDKReferencesDesignTime.Single(i => i.ItemSpec == @"relative\ImplicitPackage"); + implicitPackage.GetMetadata(MetadataKeys.Name).Should().Be(@"relative\ImplicitPackage"); + implicitPackage.GetMetadata(MetadataKeys.Version).Should().Be(@"relative\version.txt"); + } + finally + { + Directory.SetCurrentDirectory(originalCurrentDirectory); + if (Directory.Exists(testRoot)) + { + Directory.Delete(testRoot, recursive: true); + } + } + } + + [Fact] + public void ImplicitPackagesAreIdentifiedByMetadata() + { + var engine = new MockBuildEngine(); + var task = new CollectSDKReferencesDesignTime + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + SdkReferences = new[] + { + new MockTaskItem("ExistingSDK", new Dictionary()) + }, + PackageReferences = new[] + { + // This package is marked as implicit via metadata + new MockTaskItem("PackageA", new Dictionary + { + { MetadataKeys.IsImplicitlyDefined, "True" }, + { MetadataKeys.Version, "2.0.0" } + }), + // This package is not implicit + new MockTaskItem("PackageB", new Dictionary + { + { MetadataKeys.Version, "3.0.0" } + }) + }, + DefaultImplicitPackages = "" + }; + + task.Execute().Should().BeTrue(); + + task.SDKReferencesDesignTime.Length.Should().Be(2, "should include 1 SDK ref + 1 implicit package"); + + var implicitPkg = task.SDKReferencesDesignTime.FirstOrDefault(i => i.ItemSpec == "PackageA"); + implicitPkg.Should().NotBeNull("PackageA should be included as implicit"); + implicitPkg.GetMetadata(MetadataKeys.IsImplicitlyDefined).Should().Be("True"); + implicitPkg.GetMetadata(MetadataKeys.Version).Should().Be("2.0.0"); + + task.SDKReferencesDesignTime.Should().NotContain(i => i.ItemSpec == "PackageB", + "PackageB should not be included as it's not implicit"); + } + + [Fact] + public void ImplicitPackagesAreIdentifiedByDefaultImplicitPackagesList() + { + var engine = new MockBuildEngine(); + var task = new CollectSDKReferencesDesignTime + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + SdkReferences = new[] + { + new MockTaskItem("ExistingSDK", new Dictionary()) + }, + PackageReferences = new[] + { + // This package is in the DefaultImplicitPackages list (no metadata) + new MockTaskItem("Microsoft.NETCore.App", new Dictionary + { + { MetadataKeys.Version, "5.0.0" } + }), + // This package is not implicit + new MockTaskItem("Newtonsoft.Json", new Dictionary + { + { MetadataKeys.Version, "13.0.1" } + }) + }, + DefaultImplicitPackages = "Microsoft.NETCore.App;Microsoft.AspNetCore.App" + }; + + task.Execute().Should().BeTrue(); + + task.SDKReferencesDesignTime.Length.Should().Be(2, "should include 1 SDK ref + 1 implicit package"); + + var implicitPkg = task.SDKReferencesDesignTime.FirstOrDefault(i => i.ItemSpec == "Microsoft.NETCore.App"); + implicitPkg.Should().NotBeNull("Microsoft.NETCore.App should be included as implicit"); + implicitPkg.GetMetadata(MetadataKeys.IsImplicitlyDefined).Should().Be("True"); + implicitPkg.GetMetadata(MetadataKeys.Name).Should().Be("Microsoft.NETCore.App"); + implicitPkg.GetMetadata(MetadataKeys.Version).Should().Be("5.0.0"); + + task.SDKReferencesDesignTime.Should().NotContain(i => i.ItemSpec == "Newtonsoft.Json", + "Newtonsoft.Json should not be included as it's not implicit"); + } + + [Fact] + public void EmptyDefaultImplicitPackagesHandledCorrectly() + { + var engine = new MockBuildEngine(); + var task = new CollectSDKReferencesDesignTime + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + SdkReferences = new[] + { + new MockTaskItem("SDK1", new Dictionary()) + }, + PackageReferences = new[] + { + new MockTaskItem("Package1", new Dictionary + { + { MetadataKeys.Version, "1.0.0" } + }) + }, + DefaultImplicitPackages = "" + }; + + task.Execute().Should().BeTrue(); + + task.SDKReferencesDesignTime.Length.Should().Be(1, "should only include the SDK reference"); + task.SDKReferencesDesignTime[0].ItemSpec.Should().Be("SDK1"); + } + + [Fact] + public void MetadataOverridesDefaultImplicitPackagesList() + { + var engine = new MockBuildEngine(); + var task = new CollectSDKReferencesDesignTime + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + SdkReferences = new ITaskItem[0], + PackageReferences = new[] + { + // This package has explicit metadata saying it's not implicit + new MockTaskItem("Microsoft.NETCore.App", new Dictionary + { + { MetadataKeys.IsImplicitlyDefined, "False" }, + { MetadataKeys.Version, "5.0.0" } + }) + }, + // Even though it's in the default list, metadata takes precedence + DefaultImplicitPackages = "Microsoft.NETCore.App" + }; + + task.Execute().Should().BeTrue(); + + task.SDKReferencesDesignTime.Should().BeEmpty( + "package with IsImplicitlyDefined=False should not be included even if in DefaultImplicitPackages"); + } + + [Fact] + public void CaseInsensitivePackageNameMatching() + { + var engine = new MockBuildEngine(); + var task = new CollectSDKReferencesDesignTime + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + SdkReferences = new ITaskItem[0], + PackageReferences = new[] + { + // Package name has different casing than the DefaultImplicitPackages list + new MockTaskItem("microsoft.netcore.app", new Dictionary + { + { MetadataKeys.Version, "5.0.0" } + }) + }, + DefaultImplicitPackages = "Microsoft.NETCore.App" + }; + + task.Execute().Should().BeTrue(); + + task.SDKReferencesDesignTime.Length.Should().Be(1, + "case-insensitive matching should identify the package as implicit"); + task.SDKReferencesDesignTime[0].ItemSpec.Should().Be("microsoft.netcore.app"); + } + + [Fact] + public void MultipleImplicitPackagesFromBothSourcesAreIncluded() + { + var engine = new MockBuildEngine(); + var task = new CollectSDKReferencesDesignTime + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + SdkReferences = new[] + { + new MockTaskItem("SDK1", new Dictionary()), + new MockTaskItem("SDK2", new Dictionary()) + }, + PackageReferences = new[] + { + // From DefaultImplicitPackages + new MockTaskItem("Microsoft.NETCore.App", new Dictionary + { + { MetadataKeys.Version, "5.0.0" } + }), + // From metadata + new MockTaskItem("CustomImplicit", new Dictionary + { + { MetadataKeys.IsImplicitlyDefined, "True" }, + { MetadataKeys.Version, "1.0.0" } + }), + // Not implicit + new MockTaskItem("ExplicitPackage", new Dictionary + { + { MetadataKeys.Version, "2.0.0" } + }) + }, + DefaultImplicitPackages = "Microsoft.NETCore.App" + }; + + task.Execute().Should().BeTrue(); + + task.SDKReferencesDesignTime.Length.Should().Be(4, + "should include 2 SDK refs + 2 implicit packages"); + + task.SDKReferencesDesignTime.Select(i => i.ItemSpec).Should().Contain(new[] + { + "SDK1", "SDK2", "Microsoft.NETCore.App", "CustomImplicit" + }); + } + + } + + [CollectionDefinition(nameof(CurrentDirectoryMutatingTestCollection), DisableParallelization = true)] + public sealed class CurrentDirectoryMutatingTestCollection + { + } +} 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