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