diff --git a/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs b/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs index 705ee86a2ab3..a0ea4b86be96 100644 --- a/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs +++ b/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs @@ -46,7 +46,7 @@ public static string DotnetHomePath { get { - return CliFolderPathCalculatorCore.GetDotnetHomePath() + return new CliFolderPathCalculatorCore().GetDotnetHomePath() ?? throw new ConfigurationException( string.Format( LocalizableStrings.FailedToDetermineUserHomeDirectory, diff --git a/src/Common/CliFolderPathCalculatorCore.cs b/src/Common/CliFolderPathCalculatorCore.cs index ccdac9fd0282..8cf438139621 100644 --- a/src/Common/CliFolderPathCalculatorCore.cs +++ b/src/Common/CliFolderPathCalculatorCore.cs @@ -3,12 +3,31 @@ namespace Microsoft.DotNet.Configurer { - static class CliFolderPathCalculatorCore + class CliFolderPathCalculatorCore { public const string DotnetHomeVariableName = "DOTNET_CLI_HOME"; public const string DotnetProfileDirectoryName = ".dotnet"; - public static string? GetDotnetUserProfileFolderPath() + private readonly Func _getEnvironmentVariable; + + /// + /// Creates an instance that reads environment variables from the process environment. + /// + public CliFolderPathCalculatorCore() + : this(Environment.GetEnvironmentVariable) + { + } + + /// + /// Creates an instance that reads environment variables via the supplied delegate. + /// Use this from MSBuild tasks to route reads through TaskEnvironment. + /// + public CliFolderPathCalculatorCore(Func getEnvironmentVariable) + { + _getEnvironmentVariable = getEnvironmentVariable ?? throw new ArgumentNullException(nameof(getEnvironmentVariable)); + } + + public string? GetDotnetUserProfileFolderPath() { string? homePath = GetDotnetHomePath(); if (homePath is null) @@ -19,9 +38,9 @@ static class CliFolderPathCalculatorCore return Path.Combine(homePath, DotnetProfileDirectoryName); } - public static string? GetDotnetHomePath() + public string? GetDotnetHomePath() { - var home = Environment.GetEnvironmentVariable(DotnetHomeVariableName); + var home = _getEnvironmentVariable(DotnetHomeVariableName); if (string.IsNullOrEmpty(home)) { home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); @@ -33,5 +52,6 @@ static class CliFolderPathCalculatorCore return home; } + } } diff --git a/src/RazorSdk/Tool/ServerCommand.cs b/src/RazorSdk/Tool/ServerCommand.cs index 6b091b014ac6..e120c6ca7c4d 100644 --- a/src/RazorSdk/Tool/ServerCommand.cs +++ b/src/RazorSdk/Tool/ServerCommand.cs @@ -169,7 +169,7 @@ internal static string GetPidFilePath() var path = Environment.GetEnvironmentVariable("DOTNET_BUILD_PIDFILE_DIRECTORY"); if (string.IsNullOrEmpty(path)) { - var homePath = CliFolderPathCalculatorCore.GetDotnetHomePath(); + var homePath = new CliFolderPathCalculatorCore().GetDotnetHomePath(); if (homePath is null) { // Couldn't locate the user profile directory. Bail. diff --git a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs index ae38c8e2f61c..5633b7268d6b 100644 --- a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs +++ b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs @@ -308,7 +308,7 @@ private sealed class CachedState }; // First check if requested SDK resolves to a workload SDK pack - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath(); + string? userProfileDir = new CliFolderPathCalculatorCore().GetDotnetUserProfileFolderPath(); ResolutionResult? workloadResult = null; if (dotnetRoot is not null && netcoreSdkVersion is not null) { diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs index c2b483303823..c88f71f1decd 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs @@ -62,7 +62,7 @@ private class CachedState resolverContext.State = cachedState; } - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath(); + string? userProfileDir = new CliFolderPathCalculatorCore().GetDotnetUserProfileFolderPath(); ResolutionResult? result = null; if (cachedState.DotnetRootPath is not null && cachedState.SdkVersion is not null) { diff --git a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs index cb9e4b35fdd2..b41598f528f3 100644 --- a/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs +++ b/src/Tasks/Common/ProcessTaskEnvironmentDriver.cs @@ -103,6 +103,7 @@ public ProcessStartInfo GetProcessStartInfo() var startInfo = new ProcessStartInfo { WorkingDirectory = _projectDirectory.Value, + UseShellExecute = false, }; // Populate environment from the scoped environment dictionary diff --git a/src/Tasks/Common/TaskEnvironmentDefaults.cs b/src/Tasks/Common/TaskEnvironmentDefaults.cs new file mode 100644 index 000000000000..7ef5666a4175 --- /dev/null +++ b/src/Tasks/Common/TaskEnvironmentDefaults.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// 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. + +#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. + /// + internal static TaskEnvironment Create() => + new TaskEnvironment(new ProcessTaskEnvironmentDriver(Environment.CurrentDirectory)); + } +} + +#endif diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs index 5a849a3042e7..4bd457e6b7a9 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenADependencyContextBuilder.cs @@ -73,7 +73,7 @@ public void ItBuildsDependencyContextsFromProjectLockFiles( .Build(); JObject result = Save(dependencyContext); - JObject baseline = ReadJson($"{baselineFileName}.deps.json"); + JObject baseline = ReadJson(Path.Combine(TestLockFiles.TestAssemblyDirectory, $"{baselineFileName}.deps.json")); try { diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProcessFrameworkReferencesMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProcessFrameworkReferencesMultiThreading.cs new file mode 100644 index 000000000000..5fffe9b0459c --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAProcessFrameworkReferencesMultiThreading.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Build.Framework; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + [Collection("CWD-Dependent")] + + public class GivenAProcessFrameworkReferencesMultiThreading + { + [Fact] + public void EmptyFrameworkReferences_DoesNotCrash() + { + var task = new ProcessFrameworkReferences + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + TargetFrameworkVersion = "8.0", + TargetingPackRoot = "", + RuntimeGraphPath = "", + FrameworkReferences = Array.Empty(), + KnownFrameworkReferences = Array.Empty(), + KnownRuntimePacks = Array.Empty(), + KnownCrossgen2Packs = Array.Empty(), + KnownILCompilerPacks = Array.Empty(), + KnownILLinkPacks = Array.Empty(), + KnownWebAssemblySdkPacks = Array.Empty(), + }; + + // Should not throw; may return true (nothing to process) or false (missing required props) + var act = () => task.Execute(); + act.Should().NotThrow("empty framework references should be handled gracefully"); + } + + [Fact] + public void ProjectDirectory_UsedInsteadOfEnvironmentCurrentDirectory() + { + // Verify that the task uses TaskEnvironment.ProjectDirectory for global.json search + // instead of Environment.CurrentDirectory + var projectDir = Path.Combine(Path.GetTempPath(), "pfr-mt-" + Guid.NewGuid().ToString("N")); + var otherDir = Path.Combine(Path.GetTempPath(), "pfr-decoy-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDir); + Directory.CreateDirectory(otherDir); + var savedCwd = Directory.GetCurrentDirectory(); + + try + { + // Multiprocess mode: CWD == projectDir + Directory.SetCurrentDirectory(projectDir); + var (result1, engine1) = RunTask(projectDir); + + // Multithreaded mode: CWD == otherDir + Directory.SetCurrentDirectory(otherDir); + var (result2, engine2) = RunTask(projectDir); + + // Both should produce the same result + result1.Should().Be(result2, + "task should return the same result regardless of CWD"); + engine1.Errors.Count.Should().Be(engine2.Errors.Count, + "error count should match between multiprocess and multithreaded modes"); + engine1.Warnings.Count.Should().Be(engine2.Warnings.Count, + "warning count should match between multiprocess and multithreaded modes"); + } + finally + { + Directory.SetCurrentDirectory(savedCwd); + Directory.Delete(projectDir, true); + if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); + } + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task ProcessFrameworkReferences_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new ProcessFrameworkReferences + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + TargetFrameworkVersion = "8.0", + TargetingPackRoot = "", + RuntimeGraphPath = "", + FrameworkReferences = Array.Empty(), + KnownFrameworkReferences = Array.Empty(), + KnownRuntimePacks = Array.Empty(), + KnownCrossgen2Packs = Array.Empty(), + KnownILCompilerPacks = Array.Empty(), + KnownILLinkPacks = Array.Empty(), + KnownWebAssemblySdkPacks = Array.Empty(), + }; + startGate.Wait(); + task.Execute(); + } + catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + + private static (bool result, MockBuildEngine engine) RunTask(string projectDir) + { + var engine = new MockBuildEngine(); + var task = new ProcessFrameworkReferences + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + TargetFrameworkVersion = "8.0", + TargetingPackRoot = "", + RuntimeGraphPath = "", + FrameworkReferences = Array.Empty(), + KnownFrameworkReferences = Array.Empty(), + KnownRuntimePacks = Array.Empty(), + KnownCrossgen2Packs = Array.Empty(), + KnownILCompilerPacks = Array.Empty(), + KnownILLinkPacks = Array.Empty(), + KnownWebAssemblySdkPacks = Array.Empty(), + }; + + bool result; + try { result = task.Execute(); } + catch { result = false; } + return (result, engine); + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveFrameworkReferencesMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveFrameworkReferencesMultiThreading.cs new file mode 100644 index 000000000000..482906e8f868 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveFrameworkReferencesMultiThreading.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Build.Framework; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + public class GivenAResolveFrameworkReferencesMultiThreading + { + [Fact] + public void EmptyInputs_DoesNotCrash() + { + var task = new ResolveFrameworkReferences + { + BuildEngine = new MockBuildEngine(), + FrameworkReferences = Array.Empty(), + ResolvedTargetingPacks = Array.Empty(), + ResolvedRuntimePacks = Array.Empty(), + }; + + var result = task.Execute(); + + result.Should().BeTrue("empty inputs should succeed with no output"); + (task.ResolvedFrameworkReferences ?? Array.Empty()).Should().BeEmpty(); + } + + [Fact] + public void ResolvesFrameworkReferences_WithMatchingPacks() + { + var fwRef = new MockTaskItem("Microsoft.NETCore.App", new Dictionary()); + + var targetingPack = new MockTaskItem("Microsoft.NETCore.App", new Dictionary()); + targetingPack.SetMetadata("FrameworkName", "Microsoft.NETCore.App"); + targetingPack.SetMetadata("NuGetPackageVersion", "8.0.0"); + targetingPack.SetMetadata("Path", @"C:\packs\targeting"); + + var runtimePack = new MockTaskItem("Microsoft.NETCore.App.Runtime.win-x64", new Dictionary()); + runtimePack.SetMetadata("FrameworkName", "Microsoft.NETCore.App"); + runtimePack.SetMetadata("NuGetPackageVersion", "8.0.0"); + runtimePack.SetMetadata("PackageDirectory", @"C:\packs\runtime"); + + var task = new ResolveFrameworkReferences + { + BuildEngine = new MockBuildEngine(), + FrameworkReferences = new ITaskItem[] { fwRef }, + ResolvedTargetingPacks = new ITaskItem[] { targetingPack }, + ResolvedRuntimePacks = new ITaskItem[] { runtimePack }, + }; + + var result = task.Execute(); + + result.Should().BeTrue(); + task.ResolvedFrameworkReferences.Should().NotBeEmpty(); + } + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task ResolveFrameworkReferences_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new ResolveFrameworkReferences + { + BuildEngine = new MockBuildEngine(), + FrameworkReferences = Array.Empty(), + ResolvedTargetingPacks = Array.Empty(), + ResolvedRuntimePacks = Array.Empty(), + }; + startGate.Wait(); + task.Execute(); + } + catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveRuntimePackAssetsMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveRuntimePackAssetsMultiThreading.cs new file mode 100644 index 000000000000..ca5565b393ba --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveRuntimePackAssetsMultiThreading.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Build.Framework; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + [Collection("CWD-Dependent")] + + public class GivenAResolveRuntimePackAssetsMultiThreading + { + [Fact] + public void EmptyRuntimePacks_DoesNotCrash() + { + var task = new ResolveRuntimePackAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + ResolvedRuntimePacks = Array.Empty(), + }; + + var result = task.Execute(); + + result.Should().BeTrue("empty runtime packs should succeed"); + task.RuntimePackAssets.Should().BeEmpty(); + } + + [Fact] + public void GetAbsolutePath_ProducesSameResultRegardlessOfCwd() + { + // ResolveRuntimePackAssets replaced Path.GetFullPath with TaskEnvironment.GetAbsolutePath. + // Verify that results are identical whether CWD matches projectDir or not. + var projectDir = Path.Combine(Path.GetTempPath(), "rrpa-mt-" + Guid.NewGuid().ToString("N")); + var otherDir = Path.Combine(Path.GetTempPath(), "rrpa-decoy-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDir); + Directory.CreateDirectory(otherDir); + var savedCwd = Directory.GetCurrentDirectory(); + + try + { + // Multiprocess: CWD == projectDir + Directory.SetCurrentDirectory(projectDir); + var (result1, engine1) = RunTask(projectDir); + + // Multithreaded: CWD == otherDir + Directory.SetCurrentDirectory(otherDir); + var (result2, engine2) = RunTask(projectDir); + + result1.Should().Be(result2, + "task result should be identical regardless of CWD"); + engine1.Errors.Count.Should().Be(engine2.Errors.Count, + "error count should match"); + } + finally + { + Directory.SetCurrentDirectory(savedCwd); + Directory.Delete(projectDir, true); + if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); + } + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task ResolveRuntimePackAssets_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new ResolveRuntimePackAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + ResolvedRuntimePacks = Array.Empty(), + }; + startGate.Wait(); + task.Execute(); + } + catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + + private static (bool result, MockBuildEngine engine) RunTask(string projectDir) + { + var engine = new MockBuildEngine(); + var task = new ResolveRuntimePackAssets + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + ResolvedRuntimePacks = Array.Empty(), + }; + + bool result; + try { result = task.Execute(); } + catch { result = false; } + return (result, engine); + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveRuntimePackAssetsTask.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveRuntimePackAssetsTask.cs index 7fdc7ae15351..c1b8d1afbe82 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveRuntimePackAssetsTask.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveRuntimePackAssetsTask.cs @@ -25,6 +25,7 @@ public void ItFiltersSatelliteResources() var task = new ResolveRuntimePackAssets() { BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(testDirectory), FrameworkReferences = new TaskItem[] { new TaskItem("TestFramework") }, ResolvedRuntimePacks = new TaskItem[] { @@ -51,8 +52,8 @@ public void ItFiltersSatelliteResources() task.Execute(); task.RuntimePackAssets.Should().HaveCount(1); - string expectedResource = Path.Combine("runtimes", "de", "a.resources.dll"); - task.RuntimePackAssets.FirstOrDefault().ItemSpec.Should().Contain(expectedResource); + string expectedResource = Path.Combine("runtimes", "de", "a.resources.dll").Replace('\\', '/'); + task.RuntimePackAssets.FirstOrDefault().ItemSpec.Replace('\\', '/').Should().Contain(expectedResource); } } } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveTargetingPackAssetsMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveTargetingPackAssetsMultiThreading.cs new file mode 100644 index 000000000000..e7a143d232a4 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveTargetingPackAssetsMultiThreading.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Build.Framework; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + [Collection("CWD-Dependent")] + + public class GivenAResolveTargetingPackAssetsMultiThreading + { + [Fact] + public void EmptyTargetingPacks_DoesNotCrash() + { + var task = new ResolveTargetingPackAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + FrameworkReferences = Array.Empty(), + ResolvedTargetingPacks = Array.Empty(), + RuntimeFrameworks = Array.Empty(), + GenerateErrorForMissingTargetingPacks = false, + }; + + var result = task.Execute(); + + result.Should().BeTrue("empty targeting packs should succeed"); + task.ReferencesToAdd.Should().BeEmpty(); + } + + [Fact] + public void TaskEnvironmentProperty_CanBeSet() + { + var task = new ResolveTargetingPackAssets(); + var te = TaskEnvironmentHelper.CreateForTest(); + + var act = () => task.TaskEnvironment = te; + act.Should().NotThrow("TaskEnvironment property should be settable"); + task.TaskEnvironment.Should().Be(te); + } + + [Fact] + public void CacheLookup_ReadsFromTaskEnvironment_NotProcessEnvironment() + { + // Verify that the cache lookup flag is read from TaskEnvironment, + // not from the static process environment. This ensures thread-safe + // per-task configuration. + var taskEnv = TaskEnvironmentHelper.CreateForTest(); + + var task = new ResolveTargetingPackAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = taskEnv, + FrameworkReferences = Array.Empty(), + ResolvedTargetingPacks = Array.Empty(), + RuntimeFrameworks = Array.Empty(), + GenerateErrorForMissingTargetingPacks = false, + }; + + // Task should succeed regardless of ALLOW_TARGETING_PACK_CACHING value + var result = task.Execute(); + result.Should().BeTrue("task should succeed with empty inputs"); + } + + [Fact] + public void EmptyTargetingPacks_ProducesSameResultsRegardlessOfCwd() + { + var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"rtpa-mt-{Guid.NewGuid():N}")); + var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"rtpa-decoy-{Guid.NewGuid():N}")); + Directory.CreateDirectory(projectDir); + Directory.CreateDirectory(otherDir); + var savedCwd = Directory.GetCurrentDirectory(); + try + { + // --- CWD = projectDir (multiprocess mode) --- + Directory.SetCurrentDirectory(projectDir); + var (result1, engine1) = RunTask(projectDir); + + // --- CWD = otherDir (multithreaded mode) --- + Directory.SetCurrentDirectory(otherDir); + var (result2, engine2) = RunTask(projectDir); + + result1.Should().Be(result2, + "task should return the same success/failure regardless of CWD"); + engine1.Errors.Count.Should().Be(engine2.Errors.Count, + "error count should be the same in both environments"); + engine1.Warnings.Count.Should().Be(engine2.Warnings.Count, + "warning count should be the same in both environments"); + } + finally + { + Directory.SetCurrentDirectory(savedCwd); + if (Directory.Exists(projectDir)) Directory.Delete(projectDir, true); + if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); + } + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task ResolveTargetingPackAssets_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new ResolveTargetingPackAssets + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + FrameworkReferences = Array.Empty(), + ResolvedTargetingPacks = Array.Empty(), + RuntimeFrameworks = Array.Empty(), + GenerateErrorForMissingTargetingPacks = false, + }; + startGate.Wait(); + task.Execute(); + } + catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + + private static (bool result, MockBuildEngine engine) RunTask(string projectDir) + { + var engine = new MockBuildEngine(); + var task = new ResolveTargetingPackAssets + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + FrameworkReferences = Array.Empty(), + ResolvedTargetingPacks = Array.Empty(), + RuntimeFrameworks = Array.Empty(), + GenerateErrorForMissingTargetingPacks = false, + }; + + var result = task.Execute(); + return (result, engine); + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveTargetingPackAssetsTask.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveTargetingPackAssetsTask.cs index 76bf702ad9a3..5c9fdedc2364 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveTargetingPackAssetsTask.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveTargetingPackAssetsTask.cs @@ -145,6 +145,7 @@ private ResolveTargetingPackAssets InitializeTask(string mockPackageDirectory, I var task = new ResolveTargetingPackAssets { BuildEngine = buildEngine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(mockPackageDirectory), FrameworkReferences = DefaultFrameworkReferences(), ResolvedTargetingPacks = DefaultTargetingPacks(mockPackageDirectory), ProjectLanguage = "C#" @@ -235,7 +236,8 @@ private ResolveTargetingPackAssets InitializeTaskForHashTesting(out IEnumerable< inputProperties = typeof(ResolveTargetingPackAssets) .GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public) .Where(p => !p.IsDefined(typeof(OutputAttribute)) && - p.Name != nameof(ResolvePackageAssets.DesignTimeBuild)) + p.Name != nameof(ResolvePackageAssets.DesignTimeBuild) && + p.Name != nameof(ResolveTargetingPackAssets.TaskEnvironment)) .OrderBy(p => p.Name, StringComparer.Ordinal); var requiredProperties = inputProperties diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAShowMissingWorkloadsMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAShowMissingWorkloadsMultiThreading.cs new file mode 100644 index 000000000000..56d18b22be33 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAShowMissingWorkloadsMultiThreading.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Build.Framework; +using Xunit; + +namespace Microsoft.NET.Build.Tasks.UnitTests +{ + [Collection("CWD-Dependent")] + + public class GivenAShowMissingWorkloadsMultiThreading + { + [Fact] + public void EmptyMissingWorkloadPacks_DoesNotCrash() + { + var task = new ShowMissingWorkloads + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + MissingWorkloadPacks = Array.Empty(), + NetCoreRoot = "", + NETCoreSdkVersion = "8.0.100", + }; + + var act = () => task.Execute(); + act.Should().NotThrow("empty missing workload packs should be handled gracefully"); + } + + [Fact] + public void ProjectDirectory_UsedInsteadOfCwd() + { + // ShowMissingWorkloads replaced Environment.CurrentDirectory with + // TaskEnvironment.ProjectDirectory for global.json search. + var projectDir = Path.Combine(Path.GetTempPath(), "smw-mt-" + Guid.NewGuid().ToString("N")); + var otherDir = Path.Combine(Path.GetTempPath(), "smw-decoy-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(projectDir); + Directory.CreateDirectory(otherDir); + var savedCwd = Directory.GetCurrentDirectory(); + + try + { + // Multiprocess: CWD == projectDir + Directory.SetCurrentDirectory(projectDir); + var (result1, engine1) = RunTask(projectDir); + + // Multithreaded: CWD == otherDir + Directory.SetCurrentDirectory(otherDir); + var (result2, engine2) = RunTask(projectDir); + + result1.Should().Be(result2, + "task result should match regardless of CWD"); + engine1.Errors.Count.Should().Be(engine2.Errors.Count, + "error count should match"); + engine1.Warnings.Count.Should().Be(engine2.Warnings.Count, + "warning count should match"); + } + finally + { + Directory.SetCurrentDirectory(savedCwd); + Directory.Delete(projectDir, true); + if (Directory.Exists(otherDir)) Directory.Delete(otherDir, true); + } + } + + [Theory] + [InlineData(4)] + [InlineData(16)] + public async System.Threading.Tasks.Task ShowMissingWorkloads_ConcurrentExecution(int parallelism) + { + var errors = new ConcurrentBag(); + using var startGate = new ManualResetEventSlim(false); + var tasks = new System.Threading.Tasks.Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + int idx = i; + tasks[idx] = System.Threading.Tasks.Task.Run(() => + { + try + { + var task = new ShowMissingWorkloads + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(), + MissingWorkloadPacks = Array.Empty(), + NetCoreRoot = "", + NETCoreSdkVersion = "8.0.100", + }; + startGate.Wait(); + task.Execute(); + } + catch (Exception ex) { errors.Add($"Thread {idx}: {ex.Message}"); } + }); + } + startGate.Set(); + await System.Threading.Tasks.Task.WhenAll(tasks); + + errors.Should().BeEmpty(); + } + + private static (bool result, MockBuildEngine engine) RunTask(string projectDir) + { + var engine = new MockBuildEngine(); + var task = new ShowMissingWorkloads + { + BuildEngine = engine, + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir), + MissingWorkloadPacks = Array.Empty(), + NetCoreRoot = "", + NETCoreSdkVersion = "8.0.100", + }; + + bool result; + try { result = task.Execute(); } + catch { result = false; } + return (result, engine); + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs index dc383cfdeaa0..1076e8169314 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenThatWeHaveErrorCodes.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Globalization; +using System.Reflection; using System.Text.RegularExpressions; using FluentAssertions; using Xunit; @@ -95,7 +96,8 @@ public void ThereAreNoGapsDuplicatesOrIncorrectlyFormattedCodes() [Fact] public void ResxIsCommentedWithCorrectStrBegin() { - var doc = XDocument.Load("Strings.resx"); + var resxPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Strings.resx"); + var doc = XDocument.Load(resxPath); var ns = doc.Root.Name.Namespace; foreach (var data in doc.Root.Elements(ns + "data")) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs index cabcd3734b17..bd36b001b52f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/ProcessFrameworkReferencesTests.cs @@ -169,6 +169,7 @@ private static ProcessFrameworkReferences CreateTask(TaskConfiguration config) var task = new ProcessFrameworkReferences { BuildEngine = config.UseCachingEngine ? new MockBuildEngine() : new MockNeverCacheBuildEngine4(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(Path.GetTempPath()), EnableTargetingPackDownload = config.EnableTargetingPackDownload, EnableRuntimePackDownload = config.EnableRuntimePackDownload, TargetFrameworkIdentifier = config.TargetFrameworkIdentifier ?? ".NETCoreApp", diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/TestLockFiles.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/TestLockFiles.cs index 78d07dcacbd7..48f546bcc720 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/TestLockFiles.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/TestLockFiles.cs @@ -8,9 +8,15 @@ namespace Microsoft.NET.Build.Tasks.UnitTests { internal static class TestLockFiles { + // Use the assembly location so that tests are not affected by CWD changes + // from other test classes (e.g., multithreading parity tests that call + // Directory.SetCurrentDirectory). + internal static readonly string TestAssemblyDirectory = + Path.GetDirectoryName(typeof(TestLockFiles).Assembly.Location)!; + public static LockFile GetLockFile(string lockFilePrefix) { - string filePath = Path.Combine("LockFiles", $"{lockFilePrefix}.project.lock.json"); + string filePath = Path.Combine(TestAssemblyDirectory, "LockFiles", $"{lockFilePrefix}.project.lock.json"); return LockFileUtilities.GetLockFile(filePath, NullLogger.Instance); } diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs b/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs index c1486460ec37..d0e3c58f2fab 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs @@ -7,12 +7,33 @@ namespace Microsoft.NET.Build.Tasks { - internal static class FrameworkReferenceResolver + internal class FrameworkReferenceResolver { - public static string GetDefaultReferenceAssembliesPath() + private readonly Func _getEnvironmentVariable; + + /// + /// Creates an instance that reads environment variables from the process environment. + /// + public FrameworkReferenceResolver() + : this(Environment.GetEnvironmentVariable) + { + } + + /// + /// Creates an instance that reads environment variables via the supplied delegate. + /// Use this from MSBuild tasks to route reads through TaskEnvironment. + /// + public FrameworkReferenceResolver(Func getEnvironmentVariable) + { + _getEnvironmentVariable = getEnvironmentVariable ?? throw new ArgumentNullException(nameof(getEnvironmentVariable)); + } + + public string GetDefaultReferenceAssembliesPath() { - // Allow setting the reference assemblies path via an environment variable - var referenceAssembliesPath = DotNetReferenceAssembliesPathResolver.Resolve(); + // Allow setting the reference assemblies path via an environment variable. + // We read this directly instead of calling DotNetReferenceAssembliesPathResolver.Resolve() + // because that runtime method uses process-global Environment.GetEnvironmentVariable. + var referenceAssembliesPath = _getEnvironmentVariable(DotNetReferenceAssembliesPathResolver.DotNetReferenceAssembliesPathEnv); if (!string.IsNullOrEmpty(referenceAssembliesPath)) { @@ -28,12 +49,12 @@ public static string GetDefaultReferenceAssembliesPath() // References assemblies are in %ProgramFiles(x86)% on // 64 bit machines - var programFiles = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); + var programFiles = _getEnvironmentVariable("ProgramFiles(x86)"); if (string.IsNullOrEmpty(programFiles)) { // On 32 bit machines they are in %ProgramFiles% - programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + programFiles = _getEnvironmentVariable("ProgramFiles"); } if (string.IsNullOrEmpty(programFiles)) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index e3245a4f5103..ea70b0da6fbf 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -244,7 +244,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) .WithReferenceProjectInfos(referenceProjects) .WithRuntimePackAssets(runtimePackAssets) .WithCompilationOptions(compilationOptions) - .WithReferenceAssembliesPath(FrameworkReferenceResolver.GetDefaultReferenceAssembliesPath()) + .WithReferenceAssembliesPath(new FrameworkReferenceResolver().GetDefaultReferenceAssembliesPath()) .WithPackagesThatWereFiltered(GetFilteredPackages()); if (CompileReferences.Length > 0) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs index ff790291ce1b..d117919eb47f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs @@ -20,8 +20,20 @@ namespace Microsoft.NET.Build.Tasks /// targeting packs which provide the reference assemblies, and creates RuntimeFramework /// items, which are written to the runtimeconfig file /// - public class ProcessFrameworkReferences : TaskBase + [MSBuildMultiThreadableTask] + public class ProcessFrameworkReferences : TaskBase, IMultiThreadableTask { +#if NETFRAMEWORK + private TaskEnvironment _taskEnvironment; + public TaskEnvironment TaskEnvironment + { + get => _taskEnvironment ??= TaskEnvironmentDefaults.Create(); + set => _taskEnvironment = value; + } +#else + public TaskEnvironment TaskEnvironment { get; set; } = null!; +#endif + public string? TargetFrameworkIdentifier { get; set; } [Required] @@ -1112,7 +1124,7 @@ private string GetRuntimeFrameworkVersion( { IEnumerable GetPackFolders() { - var packRootEnvironmentVariable = Environment.GetEnvironmentVariable(EnvironmentVariableNames.WORKLOAD_PACK_ROOTS); + var packRootEnvironmentVariable = TaskEnvironment.GetEnvironmentVariable(EnvironmentVariableNames.WORKLOAD_PACK_ROOTS); if (!string.IsNullOrEmpty(packRootEnvironmentVariable)) { foreach (var packRoot in packRootEnvironmentVariable.Split(Path.PathSeparator)) @@ -1124,7 +1136,7 @@ IEnumerable GetPackFolders() if (!string.IsNullOrEmpty(NetCoreRoot) && !string.IsNullOrEmpty(NETCoreSdkVersion)) { if (WorkloadFileBasedInstall.IsUserLocal(NetCoreRoot, NETCoreSdkVersion) && - CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath() is { } userProfileDir) + new CliFolderPathCalculatorCore(TaskEnvironment.GetEnvironmentVariable).GetDotnetUserProfileFolderPath() is { } userProfileDir) { yield return Path.Combine(userProfileDir, "packs"); } @@ -1177,11 +1189,11 @@ private Lazy LazyCreateWorkloadResolver() { return new(() => { - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath(); + string? userProfileDir = new CliFolderPathCalculatorCore(TaskEnvironment.GetEnvironmentVariable).GetDotnetUserProfileFolderPath(); // When running MSBuild tasks, the current directory is always the project directory, so we can use that as the // starting point to search for global.json - string? globalJsonPath = SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory); + string? globalJsonPath = SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(TaskEnvironment.ProjectDirectory); var manifestProvider = new SdkDirectoryWorkloadManifestProvider(NetCoreRoot, NETCoreSdkVersion, userProfileDir, globalJsonPath); return WorkloadResolver.Create(manifestProvider, NetCoreRoot, NETCoreSdkVersion, userProfileDir); diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveRuntimePackAssets.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveRuntimePackAssets.cs index 317f0f217c08..822bb94b92b4 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveRuntimePackAssets.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveRuntimePackAssets.cs @@ -8,8 +8,20 @@ namespace Microsoft.NET.Build.Tasks { - public class ResolveRuntimePackAssets : TaskBase + [MSBuildMultiThreadableTask] + public class ResolveRuntimePackAssets : TaskBase, IMultiThreadableTask { +#if NETFRAMEWORK + private TaskEnvironment _taskEnvironment; + public TaskEnvironment TaskEnvironment + { + get => _taskEnvironment ??= TaskEnvironmentDefaults.Create(); + set => _taskEnvironment = value; + } +#else + public TaskEnvironment TaskEnvironment { get; set; } +#endif + public ITaskItem[] ResolvedRuntimePacks { get; set; } public ITaskItem[] FrameworkReferences { get; set; } = Array.Empty(); @@ -196,7 +208,7 @@ private void AddRuntimePackAssetsFromManifest(List runtimePackAssets, } // Call GetFullPath to normalize slashes - string assetPath = Path.GetFullPath(Path.Combine(runtimePackRoot, fileElement.Attribute("Path").Value)); + string assetPath = TaskEnvironment.GetAbsolutePath(Path.Combine(runtimePackRoot, fileElement.Attribute("Path").Value)); string typeAttributeValue = fileElement.Attribute("Type").Value; string assetType; diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveTargetingPackAssets.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveTargetingPackAssets.cs index f64fa18ee228..36318de2c9f5 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveTargetingPackAssets.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveTargetingPackAssets.cs @@ -10,8 +10,20 @@ namespace Microsoft.NET.Build.Tasks { - public class ResolveTargetingPackAssets : TaskBase + [MSBuildMultiThreadableTask] + public class ResolveTargetingPackAssets : TaskBase, IMultiThreadableTask { +#if NETFRAMEWORK + private TaskEnvironment _taskEnvironment; + public TaskEnvironment TaskEnvironment + { + get => _taskEnvironment ??= TaskEnvironmentDefaults.Create(); + set => _taskEnvironment = value; + } +#else + public TaskEnvironment TaskEnvironment { get; set; } +#endif + public ITaskItem[] FrameworkReferences { get; set; } = Array.Empty(); public ITaskItem[] ResolvedTargetingPacks { get; set; } = Array.Empty(); @@ -46,7 +58,7 @@ public class ResolveTargetingPackAssets : TaskBase [Output] public ITaskItem[] UsedRuntimeFrameworks { get; set; } - private static readonly bool s_allowCacheLookup = Environment.GetEnvironmentVariable(ALLOW_TARGETING_PACK_CACHING) != "0"; + private bool AllowCacheLookup() => TaskEnvironment.GetEnvironmentVariable(ALLOW_TARGETING_PACK_CACHING) != "0"; public ResolveTargetingPackAssets() { @@ -60,7 +72,9 @@ protected override void ExecuteCore() ResolvedAssetsCacheEntry results; - if (s_allowCacheLookup && + bool allowCacheLookup = AllowCacheLookup(); + + if (allowCacheLookup && BuildEngine4?.GetRegisteredTaskObject( cacheKey, RegisteredTaskObjectLifetime.AppDomain /* really "until process exit" */) @@ -76,9 +90,9 @@ protected override void ExecuteCore() } else { - results = Resolve(inputs, BuildEngine4); + results = Resolve(inputs, BuildEngine4, allowCacheLookup); - if (s_allowCacheLookup) + if (allowCacheLookup) { BuildEngine4?.RegisterTaskObject(cacheKey, results, RegisteredTaskObjectLifetime.AppDomain, allowEarlyCollection: true); } @@ -107,7 +121,7 @@ protected override void ExecuteCore() NetCoreTargetingPackRoot, ProjectLanguage); - private static ResolvedAssetsCacheEntry Resolve(StronglyTypedInputs inputs, IBuildEngine4 buildEngine) + private static ResolvedAssetsCacheEntry Resolve(StronglyTypedInputs inputs, IBuildEngine4 buildEngine, bool allowCacheLookup) { List referencesToAdd = new(); List analyzersToAdd = new(); @@ -212,7 +226,7 @@ private static ResolvedAssetsCacheEntry Resolve(StronglyTypedInputs inputs, IBui targetingPack.NuGetPackageVersion, inputs.ProjectLanguage); - AddItemsFromFrameworkList(definition, buildEngine, referencesToAdd, analyzersToAdd); + AddItemsFromFrameworkList(definition, buildEngine, referencesToAdd, analyzersToAdd, allowCacheLookup); if (File.Exists(platformManifestPath)) { @@ -295,11 +309,11 @@ private static void AddNetStandardTargetingPackAssets(TargetingPack targetingPac } } - private static void AddItemsFromFrameworkList(FrameworkListDefinition definition, IBuildEngine4 buildEngine4, List referenceItems, List analyzerItems) + private static void AddItemsFromFrameworkList(FrameworkListDefinition definition, IBuildEngine4 buildEngine4, List referenceItems, List analyzerItems, bool allowCacheLookup) { string frameworkListKey = definition.CacheKey(); - if (s_allowCacheLookup && + if (allowCacheLookup && buildEngine4?.GetRegisteredTaskObject( frameworkListKey, RegisteredTaskObjectLifetime.AppDomain) @@ -390,7 +404,7 @@ private static void AddItemsFromFrameworkList(FrameworkListDefinition definition } } - if (s_allowCacheLookup) + if (allowCacheLookup) { FrameworkList list = new() { diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs index e25457948c0d..a7336c20acd6 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs @@ -12,8 +12,20 @@ namespace Microsoft.NET.Build.Tasks { - public class ShowMissingWorkloads : TaskBase + [MSBuildMultiThreadableTask] + public class ShowMissingWorkloads : TaskBase, IMultiThreadableTask { +#if NETFRAMEWORK + private TaskEnvironment _taskEnvironment; + public TaskEnvironment TaskEnvironment + { + get => _taskEnvironment ??= TaskEnvironmentDefaults.Create(); + set => _taskEnvironment = value; + } +#else + public TaskEnvironment TaskEnvironment { get; set; } +#endif + private static readonly string MauiCrossPlatTopLevelVSWorkloads = "Microsoft.VisualStudio.Workload.NetCrossPlat"; private static readonly string MauiComponentGroupVSWorkload = "Microsoft.VisualStudio.ComponentGroup.Maui.All"; private static readonly string WasmTopLevelVSWorkload = "Microsoft.VisualStudio.Workload.NetWeb"; @@ -38,11 +50,11 @@ protected override void ExecuteCore() { if (MissingWorkloadPacks.Any()) { - string userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath(); + string userProfileDir = new CliFolderPathCalculatorCore(TaskEnvironment.GetEnvironmentVariable).GetDotnetUserProfileFolderPath(); // When running MSBuild tasks, the current directory is always the project directory, so we can use that as the // starting point to search for global.json - string globalJsonPath = SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory); + string globalJsonPath = SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(TaskEnvironment.ProjectDirectory); var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(NetCoreRoot, NETCoreSdkVersion, userProfileDir, globalJsonPath); var workloadResolver = Create(workloadManifestProvider, NetCoreRoot, NETCoreSdkVersion, userProfileDir);