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/GivenAPrepareForReadyToRunCompilationMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAPrepareForReadyToRunCompilationMultiThreading.cs
new file mode 100644
index 000000000000..932028a70dc5
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAPrepareForReadyToRunCompilationMultiThreading.cs
@@ -0,0 +1,185 @@
+// 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 Microsoft.Build.Utilities;
+using Xunit;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ public class GivenAPrepareForReadyToRunCompilationMultiThreading
+ {
+ [Fact]
+ public void NullInputs_DoNotCrash()
+ {
+ var engine = new MockBuildEngine();
+ var task = new PrepareForReadyToRunCompilation
+ {
+ BuildEngine = engine,
+ MainAssembly = new TaskItem("nonexistent.dll"),
+ OutputPath = "output",
+ IncludeSymbolsInSingleFile = false,
+ Assemblies = null,
+ ReadyToRunUseCrossgen2 = false,
+ };
+
+ task.TaskEnvironment = TaskEnvironmentHelper.CreateForTest();
+
+ // With null Assemblies and no crossgen tool, Execute should complete without NRE
+ var result = task.Execute();
+
+ // Task may log errors about missing crossgen tool, but must not throw NRE
+ result.Should().BeTrue("null Assemblies causes early return with no errors");
+ }
+
+ [Fact]
+ public void DiaSymReaderPath_IsResolvedRelativeToProjectDirectory()
+ {
+ var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"r2r-prepare-mt-{Guid.NewGuid():N}"));
+ Directory.CreateDirectory(projectDir);
+ try
+ {
+ // Create a fake DiaSymReader file at a relative path
+ var toolsDir = Path.Combine(projectDir, "tools");
+ Directory.CreateDirectory(toolsDir);
+ File.WriteAllText(Path.Combine(toolsDir, "diasymreader.dll"), "fake");
+
+ var crossgenTool = new TaskItem("tools\\crossgen.exe");
+ crossgenTool.SetMetadata("DiaSymReader", "tools\\diasymreader.dll");
+ crossgenTool.SetMetadata("JitPath", "tools\\clrjit.dll");
+
+ var task = new PrepareForReadyToRunCompilation
+ {
+ BuildEngine = new MockBuildEngine(),
+ MainAssembly = new TaskItem("test.dll"),
+ OutputPath = "output",
+ IncludeSymbolsInSingleFile = false,
+ Assemblies = null,
+ ReadyToRunUseCrossgen2 = false,
+ CrossgenTool = crossgenTool,
+ EmitSymbols = true,
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDir),
+ };
+
+ var result = task.Execute();
+
+ // With null Assemblies, ProcessInputFileList returns early.
+ // The key point is that DiaSymReader at "tools\diasymreader.dll"
+ // is resolved via TaskEnvironment relative to projectDir and found.
+ result.Should().BeTrue("null Assemblies produces no errors");
+ }
+ finally
+ {
+ Directory.Delete(projectDir, true);
+ }
+ }
+
+ [Fact]
+ public void DualModeParity_SameResultRegardlessOfCwd()
+ {
+ var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"r2r-prepare-dual-{Guid.NewGuid():N}"));
+ var otherDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"r2r-prepare-other-{Guid.NewGuid():N}"));
+ Directory.CreateDirectory(projectDir);
+ Directory.CreateDirectory(otherDir);
+ try
+ {
+ // Create a fake DiaSymReader file under projectDir
+ var toolsDir = Path.Combine(projectDir, "tools");
+ Directory.CreateDirectory(toolsDir);
+ File.WriteAllText(Path.Combine(toolsDir, "diasymreader.dll"), "fake");
+
+ var taskEnv = TaskEnvironmentHelper.CreateForTest(projectDir);
+
+ // Run with CWD = projectDir
+ var savedCwd = Directory.GetCurrentDirectory();
+ Directory.SetCurrentDirectory(projectDir);
+
+ var engine1 = new MockBuildEngine();
+ var crossgen1 = new TaskItem("tools\\crossgen.exe");
+ crossgen1.SetMetadata("DiaSymReader", "tools\\diasymreader.dll");
+ crossgen1.SetMetadata("JitPath", "tools\\clrjit.dll");
+
+ var task1 = new PrepareForReadyToRunCompilation
+ {
+ BuildEngine = engine1,
+ MainAssembly = new TaskItem("test.dll"),
+ OutputPath = "output",
+ IncludeSymbolsInSingleFile = false,
+ Assemblies = null,
+ ReadyToRunUseCrossgen2 = false,
+ CrossgenTool = crossgen1,
+ EmitSymbols = true,
+ TaskEnvironment = taskEnv,
+ };
+ var result1 = task1.Execute();
+
+ // Run with CWD = otherDir (different from projectDir)
+ Directory.SetCurrentDirectory(otherDir);
+
+ var engine2 = new MockBuildEngine();
+ var crossgen2 = new TaskItem("tools\\crossgen.exe");
+ crossgen2.SetMetadata("DiaSymReader", "tools\\diasymreader.dll");
+ crossgen2.SetMetadata("JitPath", "tools\\clrjit.dll");
+
+ var task2 = new PrepareForReadyToRunCompilation
+ {
+ BuildEngine = engine2,
+ MainAssembly = new TaskItem("test.dll"),
+ OutputPath = "output",
+ IncludeSymbolsInSingleFile = false,
+ Assemblies = null,
+ ReadyToRunUseCrossgen2 = false,
+ CrossgenTool = crossgen2,
+ EmitSymbols = true,
+ TaskEnvironment = taskEnv,
+ };
+ var result2 = task2.Execute();
+
+ Directory.SetCurrentDirectory(savedCwd);
+
+ // Both runs should produce the same result regardless of CWD
+ result1.Should().Be(result2, "TaskEnvironment resolves paths independent of CWD");
+ engine1.Errors.Count.Should().Be(engine2.Errors.Count,
+ "same errors regardless of CWD");
+ }
+ finally
+ {
+ Directory.Delete(projectDir, true);
+ Directory.Delete(otherDir, true);
+ }
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(16)]
+ public void PrepareForReadyToRunCompilation_ConcurrentExecution(int parallelism)
+ {
+ var errors = new ConcurrentBag();
+ var barrier = new Barrier(parallelism);
+ Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
+ {
+ try
+ {
+ var task = new PrepareForReadyToRunCompilation
+ {
+ BuildEngine = new MockBuildEngine(),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ MainAssembly = new TaskItem("nonexistent.dll"),
+ OutputPath = "output",
+ IncludeSymbolsInSingleFile = false,
+ Assemblies = null,
+ ReadyToRunUseCrossgen2 = false,
+ };
+ barrier.SignalAndWait();
+ task.Execute();
+ }
+ catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
+ });
+ errors.Should().BeEmpty();
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveReadyToRunCompilersMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveReadyToRunCompilersMultiThreading.cs
new file mode 100644
index 000000000000..7ce3c19681a8
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAResolveReadyToRunCompilersMultiThreading.cs
@@ -0,0 +1,100 @@
+// 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 Microsoft.Build.Utilities;
+using Xunit;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ public class GivenAResolveReadyToRunCompilersMultiThreading
+ {
+ [Fact]
+ public void EmptyRuntimePacks_LogsErrorGracefully()
+ {
+ var engine = new MockBuildEngine();
+ var task = new ResolveReadyToRunCompilers
+ {
+ BuildEngine = engine,
+ RuntimePacks = new ITaskItem[] { new TaskItem("SomePack") },
+ TargetingPacks = Array.Empty(),
+ RuntimeGraphPath = "nonexistent.json",
+ NETCoreSdkRuntimeIdentifier = "win-x64",
+ ReadyToRunUseCrossgen2 = false,
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ // RuntimePacks has no matching NETCore.App pack, so it should log error gracefully
+ var result = task.Execute();
+
+ result.Should().BeFalse("no valid NETCore.App runtime pack exists");
+ engine.Errors.Should().NotBeEmpty("should log an error about missing runtime pack");
+ // Verify no NullReferenceException — error is about missing pack, not a crash
+ engine.Errors.Select(e => e.Message).Should().NotContain(
+ e => e.Contains("NullReference", StringComparison.OrdinalIgnoreCase),
+ "should not crash with NullReferenceException");
+ }
+
+ [Fact]
+ public void TaskEnvironmentProperty_IsWirable()
+ {
+ var projectDir = Path.GetFullPath(Path.Combine(Path.GetTempPath(), $"resolve-r2r-mt-{Guid.NewGuid():N}"));
+ Directory.CreateDirectory(projectDir);
+ try
+ {
+ var task = new ResolveReadyToRunCompilers
+ {
+ BuildEngine = new MockBuildEngine(),
+ RuntimePacks = new ITaskItem[] { new TaskItem("SomePack") },
+ TargetingPacks = Array.Empty(),
+ RuntimeGraphPath = "runtime.json",
+ NETCoreSdkRuntimeIdentifier = "win-x64",
+ };
+
+ var teProp = task.GetType().GetProperty("TaskEnvironment");
+ teProp.Should().NotBeNull("task must have a TaskEnvironment property after migration");
+ teProp!.SetValue(task, TaskEnvironmentHelper.CreateForTest(projectDir));
+
+ // The task should have TaskEnvironment set
+ task.TaskEnvironment.Should().NotBeNull();
+ }
+ finally
+ {
+ Directory.Delete(projectDir, true);
+ }
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(16)]
+ public void ResolveReadyToRunCompilers_ConcurrentExecution(int parallelism)
+ {
+ var errors = new ConcurrentBag();
+ var barrier = new Barrier(parallelism);
+ Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
+ {
+ try
+ {
+ var task = new ResolveReadyToRunCompilers
+ {
+ BuildEngine = new MockBuildEngine(),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ RuntimePacks = new ITaskItem[] { new TaskItem("SomePack") },
+ TargetingPacks = Array.Empty(),
+ RuntimeGraphPath = "nonexistent.json",
+ NETCoreSdkRuntimeIdentifier = "win-x64",
+ ReadyToRunUseCrossgen2 = false,
+ };
+ barrier.SignalAndWait();
+ task.Execute();
+ }
+ catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
+ });
+ errors.Should().BeEmpty();
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenARunCsWinRTGeneratorMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenARunCsWinRTGeneratorMultiThreading.cs
new file mode 100644
index 000000000000..6f455b29d1c9
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenARunCsWinRTGeneratorMultiThreading.cs
@@ -0,0 +1,98 @@
+// 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 Microsoft.Build.Utilities;
+using Xunit;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ public class GivenARunCsWinRTGeneratorMultiThreading
+ {
+ [Fact]
+ public void NullInputs_DoNotCrashWithNullReferenceException()
+ {
+ // RunCsWinRTGenerator extends ToolTask — full Execute() tries to launch
+ // an external process. We only verify that null/empty required properties
+ // don't cause NullReferenceException.
+ var engine = new MockBuildEngine();
+ var task = new RunCsWinRTGenerator
+ {
+ BuildEngine = engine,
+ ReferenceAssemblyPaths = null,
+ OutputAssemblyPath = null,
+ InteropAssemblyDirectory = null,
+ CsWinRTToolsDirectory = null,
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ // Execute will fail validation due to null required properties,
+ // but should not throw NullReferenceException
+ bool threw = false;
+ bool result = false;
+ try
+ {
+ result = task.Execute();
+ }
+ catch (NullReferenceException)
+ {
+ threw = true;
+ }
+
+ threw.Should().BeFalse("null properties should be handled gracefully, not throw NRE");
+ if (!result)
+ {
+ // Validation failed as expected — check warnings/errors are about missing inputs
+ var allMessages = engine.Warnings.Select(w => w.Message)
+ .Concat(engine.Errors.Select(e => e.Message))
+ .ToList();
+ allMessages.Should().NotBeEmpty("validation should produce diagnostic messages");
+ }
+ }
+
+ [Fact]
+ public void TaskEnvironmentProperty_IsWirable()
+ {
+ var task = new RunCsWinRTGenerator();
+
+ var teProp = task.GetType().GetProperty("TaskEnvironment");
+ teProp.Should().NotBeNull("task must have a TaskEnvironment property after migration");
+
+ var env = TaskEnvironmentHelper.CreateForTest();
+ teProp!.SetValue(task, env);
+ task.TaskEnvironment.Should().NotBeNull();
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(16)]
+ public void RunCsWinRTGenerator_ConcurrentExecution(int parallelism)
+ {
+ var errors = new ConcurrentBag();
+ var barrier = new Barrier(parallelism);
+ Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
+ {
+ try
+ {
+ var task = new RunCsWinRTGenerator
+ {
+ BuildEngine = new MockBuildEngine(),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ ReferenceAssemblyPaths = null,
+ OutputAssemblyPath = null,
+ InteropAssemblyDirectory = null,
+ CsWinRTToolsDirectory = null,
+ };
+ barrier.SignalAndWait();
+ task.Execute();
+ }
+ catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
+ });
+ errors.Should().BeEmpty();
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenARunReadyToRunCompilerMultiThreading.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenARunReadyToRunCompilerMultiThreading.cs
new file mode 100644
index 000000000000..ce86960873cc
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenARunReadyToRunCompilerMultiThreading.cs
@@ -0,0 +1,96 @@
+// 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 Microsoft.Build.Utilities;
+using Xunit;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ public class GivenARunReadyToRunCompilerMultiThreading
+ {
+ [Fact]
+ public void NullInputs_DoNotCrashWithNullReferenceException()
+ {
+ // RunReadyToRunCompiler extends ToolTask — full Execute() launches crossgen2.
+ // We only verify that null/empty properties don't cause NullReferenceException.
+ var engine = new MockBuildEngine();
+
+ var compilationEntry = new TaskItem("test.dll");
+ // Don't set any metadata — tests graceful handling of missing metadata
+
+ var task = new RunReadyToRunCompiler
+ {
+ BuildEngine = engine,
+ CompilationEntry = compilationEntry,
+ ImplementationAssemblyReferences = Array.Empty(),
+ CrossgenTool = null,
+ Crossgen2Tool = null,
+ UseCrossgen2 = false,
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ };
+
+ // Execute should fail validation (missing crossgen tool) but not throw NRE
+ bool threw = false;
+ bool result = false;
+ try
+ {
+ result = task.Execute();
+ }
+ catch (NullReferenceException)
+ {
+ threw = true;
+ }
+
+ threw.Should().BeFalse("null CrossgenTool should be handled gracefully, not throw NRE");
+ result.Should().BeFalse("validation should fail without crossgen tool");
+ engine.Errors.Should().NotBeEmpty("should log error about missing crossgen tool");
+ }
+
+ [Fact]
+ public void TaskEnvironmentProperty_IsWirable()
+ {
+ var task = new RunReadyToRunCompiler();
+
+ var teProp = task.GetType().GetProperty("TaskEnvironment");
+ teProp.Should().NotBeNull("task must have a TaskEnvironment property after migration");
+
+ var env = TaskEnvironmentHelper.CreateForTest();
+ teProp!.SetValue(task, env);
+ task.TaskEnvironment.Should().NotBeNull();
+ }
+
+ [Theory]
+ [InlineData(4)]
+ [InlineData(16)]
+ public void RunReadyToRunCompiler_ConcurrentExecution(int parallelism)
+ {
+ var errors = new ConcurrentBag();
+ var barrier = new Barrier(parallelism);
+ Parallel.For(0, parallelism, new ParallelOptions { MaxDegreeOfParallelism = parallelism }, i =>
+ {
+ try
+ {
+ var task = new RunReadyToRunCompiler
+ {
+ BuildEngine = new MockBuildEngine(),
+ TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
+ CompilationEntry = new TaskItem("test.dll"),
+ ImplementationAssemblyReferences = Array.Empty(),
+ CrossgenTool = null,
+ Crossgen2Tool = null,
+ UseCrossgen2 = false,
+ };
+ barrier.SignalAndWait();
+ task.Execute();
+ }
+ catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
+ });
+ errors.Should().BeEmpty();
+ }
+ }
+}
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/PrepareForReadyToRunCompilation.cs b/src/Tasks/Microsoft.NET.Build.Tasks/PrepareForReadyToRunCompilation.cs
index 423e5e6e1aea..77bc57b184d7 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/PrepareForReadyToRunCompilation.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/PrepareForReadyToRunCompilation.cs
@@ -11,8 +11,20 @@
namespace Microsoft.NET.Build.Tasks
{
- public class PrepareForReadyToRunCompilation : TaskBase
+ [MSBuildMultiThreadableTask]
+ public class PrepareForReadyToRunCompilation : 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 MainAssembly { get; set; }
public ITaskItem[] Assemblies { get; set; }
@@ -98,6 +110,8 @@ private bool IsTargetLinux
protected override void ExecuteCore()
{
+ string absoluteOutputPath = TaskEnvironment.GetAbsolutePath(OutputPath);
+
if (ReadyToRunUseCrossgen2)
{
string isVersion5 = Crossgen2Tool.GetMetadata(MetadataKeys.IsVersion5);
@@ -117,10 +131,10 @@ protected override void ExecuteCore()
bool hasValidDiaSymReaderLib =
ReadyToRunUseCrossgen2 && !_crossgen2IsVersion5 ||
- !string.IsNullOrEmpty(diaSymReaderPath) && File.Exists(diaSymReaderPath);
+ !string.IsNullOrEmpty(diaSymReaderPath) && File.Exists(TaskEnvironment.GetAbsolutePath(diaSymReaderPath));
// Process input lists of files
- ProcessInputFileList(Assemblies, _compileList, _symbolsCompileList, _r2rFiles, _r2rReferences, _r2rCompositeReferences, _r2rCompositeInput, _r2rCompositeUnrootedInput, hasValidDiaSymReaderLib);
+ ProcessInputFileList(Assemblies, _compileList, _symbolsCompileList, _r2rFiles, _r2rReferences, _r2rCompositeReferences, _r2rCompositeInput, _r2rCompositeUnrootedInput, hasValidDiaSymReaderLib, absoluteOutputPath);
}
private void ProcessInputFileList(
@@ -132,7 +146,8 @@ private void ProcessInputFileList(
List r2rCompositeReferenceList,
List r2rCompositeInputList,
List r2rCompositeUnrootedInput,
- bool hasValidDiaSymReaderLib)
+ bool hasValidDiaSymReaderLib,
+ string absoluteOutputPath)
{
if (inputFiles == null)
{
@@ -145,7 +160,7 @@ private void ProcessInputFileList(
foreach (var file in inputFiles)
{
- var eligibility = GetInputFileEligibility(file, Crossgen2Composite, exclusionSet, compositeExclusionSet, compositeRootSet);
+ var eligibility = GetInputFileEligibility(file, Crossgen2Composite, exclusionSet, compositeExclusionSet, compositeRootSet, TaskEnvironment);
if (eligibility.NoEligibility)
{
@@ -164,7 +179,7 @@ private void ProcessInputFileList(
}
var outputR2RImageRelativePath = file.GetMetadata(MetadataKeys.RelativePath);
- var outputR2RImage = Path.Combine(OutputPath, outputR2RImageRelativePath);
+ var outputR2RImage = Path.Combine(absoluteOutputPath, outputR2RImageRelativePath);
string outputPDBImage = null;
string outputPDBImageRelativePath = null;
@@ -190,7 +205,7 @@ private void ProcessInputFileList(
}
else
{
- using (FileStream fs = new(file.ItemSpec, FileMode.Open, FileAccess.Read))
+ using (FileStream fs = new(TaskEnvironment.GetAbsolutePath(file.ItemSpec), FileMode.Open, FileAccess.Read))
{
PEReader pereader = new(fs);
MetadataReader mdReader = pereader.GetMetadataReader();
@@ -292,8 +307,8 @@ private void ProcessInputFileList(
compositeR2RFinalImageRelativePath = Path.ChangeExtension(compositeR2RImageRelativePath, ".dylib");
}
- var compositeR2RImage = Path.Combine(OutputPath, compositeR2RImageRelativePath);
- var compositeR2RImageFinal = Path.Combine(OutputPath, compositeR2RFinalImageRelativePath);
+ var compositeR2RImage = Path.Combine(absoluteOutputPath, compositeR2RImageRelativePath);
+ var compositeR2RImageFinal = Path.Combine(absoluteOutputPath, compositeR2RFinalImageRelativePath);
TaskItem r2rCompilationEntry = new(MainAssembly)
{
@@ -431,7 +446,7 @@ private static bool IsNonCompositeReadyToRunImage(PEReader peReader)
}
}
- private static Eligibility GetInputFileEligibility(ITaskItem file, bool compositeCompile, HashSet exclusionSet, HashSet r2rCompositeExclusionSet, HashSet r2rCompositeRootSet)
+ private static Eligibility GetInputFileEligibility(ITaskItem file, bool compositeCompile, HashSet exclusionSet, HashSet r2rCompositeExclusionSet, HashSet r2rCompositeRootSet, TaskEnvironment taskEnvironment)
{
// Check to see if this is a valid ILOnly image that we can compile
if (!file.ItemSpec.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && !file.ItemSpec.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
@@ -440,7 +455,7 @@ private static Eligibility GetInputFileEligibility(ITaskItem file, bool composit
return Eligibility.None;
}
- using (FileStream fs = new(file.ItemSpec, FileMode.Open, FileAccess.Read))
+ using (FileStream fs = new(taskEnvironment.GetAbsolutePath(file.ItemSpec), FileMode.Open, FileAccess.Read))
{
try
{
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveReadyToRunCompilers.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveReadyToRunCompilers.cs
index 29a23d69ad8d..441163f92844 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/ResolveReadyToRunCompilers.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ResolveReadyToRunCompilers.cs
@@ -9,8 +9,19 @@
namespace Microsoft.NET.Build.Tasks
{
- public class ResolveReadyToRunCompilers : TaskBase
+ [MSBuildMultiThreadableTask]
+ public class ResolveReadyToRunCompilers : 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 bool EmitSymbols { get; set; }
public bool ReadyToRunUseCrossgen2 { get; set; }
public string PerfmapFormatVersion { get; set; }
@@ -392,7 +403,7 @@ private bool GetCrossgenComponentsPaths()
return false;
}
- return File.Exists(_crossgenTool.ToolPath) && File.Exists(_crossgenTool.ClrJitPath);
+ return File.Exists(TaskEnvironment.GetAbsolutePath(_crossgenTool.ToolPath)) && File.Exists(TaskEnvironment.GetAbsolutePath(_crossgenTool.ClrJitPath));
}
private bool GetCrossgen2ComponentsPaths(bool version5)
@@ -420,14 +431,14 @@ private bool GetCrossgen2ComponentsPaths(bool version5)
{
string clrJitFileName = string.Format(v5_clrJitFileNamePattern, GetTargetSpecForVersion5());
_crossgen2Tool.ClrJitPath = Path.Combine(_crossgen2Tool.PackagePath, "tools", clrJitFileName);
- if (!File.Exists(_crossgen2Tool.ClrJitPath))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(_crossgen2Tool.ClrJitPath)))
{
return false;
}
}
_crossgen2Tool.ToolPath = Path.Combine(_crossgen2Tool.PackagePath, "tools", toolFileName);
- return File.Exists(_crossgen2Tool.ToolPath);
+ return File.Exists(TaskEnvironment.GetAbsolutePath(_crossgen2Tool.ToolPath));
}
// Keep in sync with JitConfigProvider.GetTargetSpec in .NET 5
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/RunCsWinRTGenerator.cs b/src/Tasks/Microsoft.NET.Build.Tasks/RunCsWinRTGenerator.cs
index 3c28ee32aa74..68a6e591705b 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/RunCsWinRTGenerator.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/RunCsWinRTGenerator.cs
@@ -15,8 +15,19 @@ namespace Microsoft.NET.Build.Tasks;
///
/// The custom MSBuild task that invokes the 'cswinrtgen' tool.
///
-public sealed class RunCsWinRTGenerator : ToolTask
+[MSBuildMultiThreadableTask]
+public sealed class RunCsWinRTGenerator : ToolTask, IMultiThreadableTask
{
+#if NETFRAMEWORK
+ private TaskEnvironment _taskEnvironment;
+ public TaskEnvironment TaskEnvironment
+ {
+ get => _taskEnvironment ??= TaskEnvironmentDefaults.Create();
+ set => _taskEnvironment = value;
+ }
+#else
+ public TaskEnvironment TaskEnvironment { get; set; } = null!;
+#endif
///
/// Gets or sets the paths to assembly files that are reference assemblies, representing
/// the entire surface area for compilation. These assemblies are the full set of assemblies
@@ -133,21 +144,21 @@ protected override bool ValidateParameters()
return false;
}
- if (InteropAssemblyDirectory is null || !Directory.Exists(InteropAssemblyDirectory))
+ if (InteropAssemblyDirectory is null || !Directory.Exists(TaskEnvironment.GetAbsolutePath(InteropAssemblyDirectory)))
{
Log.LogWarning("Generated assembly directory '{0}' is invalid or does not exist.", InteropAssemblyDirectory);
return false;
}
- if (DebugReproDirectory is not null && !Directory.Exists(DebugReproDirectory))
+ if (DebugReproDirectory is not null && !Directory.Exists(TaskEnvironment.GetAbsolutePath(DebugReproDirectory)))
{
Log.LogWarning("Debug repro directory '{0}' is invalid or does not exist.", DebugReproDirectory);
return false;
}
- if (CsWinRTToolsDirectory is null || !Directory.Exists(CsWinRTToolsDirectory))
+ if (CsWinRTToolsDirectory is null || !Directory.Exists(TaskEnvironment.GetAbsolutePath(CsWinRTToolsDirectory)))
{
Log.LogWarning("Tools directory '{0}' is invalid or does not exist.", CsWinRTToolsDirectory);
@@ -189,7 +200,7 @@ protected override string GenerateFullPathToTool()
// This makes it easy to run the task against a local build of 'cswinrtgen'.
if (effectiveArchitecture?.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase) is true)
{
- return Path.Combine(CsWinRTToolsDirectory!, ToolName);
+ return TaskEnvironment.GetAbsolutePath(Path.Combine(CsWinRTToolsDirectory!, ToolName));
}
// If the architecture is not specified, determine it based on the current process architecture
@@ -203,7 +214,7 @@ protected override string GenerateFullPathToTool()
// The tool is inside an architecture-specific subfolder, as it's a native binary
string architectureDirectory = $"win-{effectiveArchitecture}";
- return Path.Combine(CsWinRTToolsDirectory!, architectureDirectory, ToolName);
+ return TaskEnvironment.GetAbsolutePath(Path.Combine(CsWinRTToolsDirectory!, architectureDirectory, ToolName));
}
///
@@ -211,13 +222,13 @@ protected override string GenerateResponseFileCommands()
{
StringBuilder args = new();
- IEnumerable referenceAssemblyPaths = ReferenceAssemblyPaths!.Select(static path => path.ItemSpec);
+ IEnumerable referenceAssemblyPaths = ReferenceAssemblyPaths!.Select(path => (string)TaskEnvironment.GetAbsolutePath(path.ItemSpec));
string referenceAssemblyPathsArg = string.Join(",", referenceAssemblyPaths);
AppendResponseFileCommand(args, "--reference-assembly-paths", referenceAssemblyPathsArg);
- AppendResponseFileCommand(args, "--output-assembly-path", EffectiveOutputAssemblyItemSpec);
- AppendResponseFileCommand(args, "--generated-assembly-directory", InteropAssemblyDirectory!);
- AppendResponseFileOptionalCommand(args, "--debug-repro-directory", DebugReproDirectory);
+ AppendResponseFileCommand(args, "--output-assembly-path", TaskEnvironment.GetAbsolutePath(EffectiveOutputAssemblyItemSpec));
+ AppendResponseFileCommand(args, "--generated-assembly-directory", TaskEnvironment.GetAbsolutePath(InteropAssemblyDirectory!));
+ AppendResponseFileOptionalCommand(args, "--debug-repro-directory", DebugReproDirectory is not null ? TaskEnvironment.GetAbsolutePath(DebugReproDirectory) : null);
AppendResponseFileCommand(args, "--use-windows-ui-xaml-projections", UseWindowsUIXamlProjections.ToString());
AppendResponseFileCommand(args, "--validate-winrt-runtime-assembly-version", ValidateWinRTRuntimeAssemblyVersion.ToString());
AppendResponseFileCommand(args, "--validate-winrt-runtime-dll-version-2-references", ValidateWinRTRuntimeDllVersion2References.ToString());
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/RunReadyToRunCompiler.cs b/src/Tasks/Microsoft.NET.Build.Tasks/RunReadyToRunCompiler.cs
index c5c7523099e7..9cd2678fc501 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/RunReadyToRunCompiler.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/RunReadyToRunCompiler.cs
@@ -8,8 +8,19 @@
namespace Microsoft.NET.Build.Tasks
{
- public class RunReadyToRunCompiler : ToolTask
+ [MSBuildMultiThreadableTask]
+ public class RunReadyToRunCompiler : ToolTask, 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 CrossgenTool { get; set; }
public ITaskItem Crossgen2Tool { get; set; }
@@ -67,7 +78,7 @@ protected override string ToolName
}
}
- protected override string GenerateFullPathToTool() => ToolName;
+ protected override string GenerateFullPathToTool() => TaskEnvironment.GetAbsolutePath(ToolName);
private string DiaSymReader => CrossgenTool.GetMetadata(MetadataKeys.DiaSymReader);
@@ -100,13 +111,13 @@ protected override bool ValidateParameters()
Log.LogError(Strings.Crossgen2ToolMissingWhenUseCrossgen2IsSet);
return false;
}
- if (!File.Exists(Crossgen2Tool.ItemSpec))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(Crossgen2Tool.ItemSpec)))
{
Log.LogError(Strings.Crossgen2ToolExecutableNotFound, Crossgen2Tool.ItemSpec);
return false;
}
string hostPath = DotNetHostPath;
- if (!string.IsNullOrEmpty(hostPath) && !File.Exists(hostPath))
+ if (!string.IsNullOrEmpty(hostPath) && !File.Exists(TaskEnvironment.GetAbsolutePath(hostPath)))
{
Log.LogError(Strings.DotNetHostExecutableNotFound, hostPath);
return false;
@@ -114,7 +125,7 @@ protected override bool ValidateParameters()
string jitPath = Crossgen2Tool.GetMetadata(MetadataKeys.JitPath);
if (!string.IsNullOrEmpty(jitPath))
{
- if (!File.Exists(jitPath))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(jitPath)))
{
Log.LogError(Strings.JitLibraryNotFound, jitPath);
return false;
@@ -148,12 +159,12 @@ protected override bool ValidateParameters()
Log.LogError(Strings.CrossgenToolMissingWhenUseCrossgen2IsNotSet);
return false;
}
- if (!File.Exists(CrossgenTool.ItemSpec))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(CrossgenTool.ItemSpec)))
{
Log.LogError(Strings.CrossgenToolExecutableNotFound, CrossgenTool.ItemSpec);
return false;
}
- if (!File.Exists(CrossgenTool.GetMetadata(MetadataKeys.JitPath)))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(CrossgenTool.GetMetadata(MetadataKeys.JitPath))))
{
Log.LogError(Strings.JitLibraryNotFound, MetadataKeys.JitPath);
return false;
@@ -166,7 +177,7 @@ protected override bool ValidateParameters()
{
_outputR2RImage = CompilationEntry.ItemSpec;
- if (!string.IsNullOrEmpty(DiaSymReader) && !File.Exists(DiaSymReader))
+ if (!string.IsNullOrEmpty(DiaSymReader) && !File.Exists(TaskEnvironment.GetAbsolutePath(DiaSymReader)))
{
Log.LogError(Strings.DiaSymReaderLibraryNotFound, DiaSymReader);
return false;
@@ -178,7 +189,7 @@ protected override bool ValidateParameters()
Log.LogError(Strings.MissingOutputPDBImagePath);
}
- if (!File.Exists(_outputR2RImage))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(_outputR2RImage)))
{
Log.LogError(Strings.PDBGeneratorInputExecutableNotFound, _outputR2RImage);
return false;
@@ -191,7 +202,7 @@ protected override bool ValidateParameters()
if (!_createCompositeImage)
{
_inputAssembly = CompilationEntry.ItemSpec;
- if (!File.Exists(_inputAssembly))
+ if (!File.Exists(TaskEnvironment.GetAbsolutePath(_inputAssembly)))
{
Log.LogError(Strings.InputAssemblyNotFound, _inputAssembly);
return false;
@@ -231,13 +242,14 @@ private string GetAssemblyReferencesCommands()
if (IsPdbCompilation && string.Equals(Path.GetFileName(reference.ItemSpec), Path.GetFileName(_outputR2RImage), StringComparison.OrdinalIgnoreCase))
continue;
+ string absoluteRef = TaskEnvironment.GetAbsolutePath(reference.ItemSpec);
if (UseCrossgen2 && !IsPdbCompilation)
{
- result.AppendLine($"-r:\"{reference.ItemSpec}\"");
+ result.AppendLine($"-r:\"{absoluteRef}\"");
}
else
{
- result.AppendLine($"-r \"{reference.ItemSpec}\"");
+ result.AppendLine($"-r \"{absoluteRef}\"");
}
}
}
@@ -249,7 +261,7 @@ protected override string GenerateCommandLineCommands()
{
if (ActuallyUseCrossgen2 && !string.IsNullOrEmpty(DotNetHostPath))
{
- return $"\"{Crossgen2Tool.ItemSpec}\"";
+ return $"\"{TaskEnvironment.GetAbsolutePath(Crossgen2Tool.ItemSpec)}\"";
}
return null;
}
@@ -279,19 +291,19 @@ private string GenerateCrossgenResponseFile()
if (!string.IsNullOrEmpty(DiaSymReader))
{
- result.AppendLine($"/DiasymreaderPath \"{DiaSymReader}\"");
+ result.AppendLine($"/DiasymreaderPath \"{TaskEnvironment.GetAbsolutePath(DiaSymReader)}\"");
}
result.AppendLine(_createPDBCommand);
- result.AppendLine($"\"{_outputR2RImage}\"");
+ result.AppendLine($"\"{TaskEnvironment.GetAbsolutePath(_outputR2RImage)}\"");
}
else
{
result.AppendLine("/MissingDependenciesOK");
- result.AppendLine($"/JITPath \"{CrossgenTool.GetMetadata(MetadataKeys.JitPath)}\"");
+ result.AppendLine($"/JITPath \"{TaskEnvironment.GetAbsolutePath(CrossgenTool.GetMetadata(MetadataKeys.JitPath))}\"");
result.Append(GetAssemblyReferencesCommands());
- result.AppendLine($"/out \"{_outputR2RImage}\"");
- result.AppendLine($"\"{_inputAssembly}\"");
+ result.AppendLine($"/out \"{TaskEnvironment.GetAbsolutePath(_outputR2RImage)}\"");
+ result.AppendLine($"\"{TaskEnvironment.GetAbsolutePath(_inputAssembly)}\"");
}
return result.ToString();
@@ -304,7 +316,7 @@ private string GenerateCrossgen2ResponseFile()
string jitPath = Crossgen2Tool.GetMetadata(MetadataKeys.JitPath);
if (!string.IsNullOrEmpty(jitPath))
{
- result.AppendLine($"--jitpath:\"{jitPath}\"");
+ result.AppendLine($"--jitpath:\"{TaskEnvironment.GetAbsolutePath(jitPath)}\"");
}
else
{
@@ -320,12 +332,12 @@ private string GenerateCrossgen2ResponseFile()
if (Crossgen2Tool.GetMetadata(MetadataKeys.TargetOS) == "windows")
{
result.AppendLine("--pdb");
- result.AppendLine($"--pdb-path:{Path.GetDirectoryName(_outputPDBImage)}");
+ result.AppendLine($"--pdb-path:{TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(_outputPDBImage))}");
}
else
{
result.AppendLine("--perfmap");
- result.AppendLine($"--perfmap-path:{Path.GetDirectoryName(_outputPDBImage)}");
+ result.AppendLine($"--perfmap-path:{TaskEnvironment.GetAbsolutePath(Path.GetDirectoryName(_outputPDBImage))}");
string perfmapFormatVersion = Crossgen2Tool.GetMetadata(MetadataKeys.PerfmapFormatVersion);
if (!string.IsNullOrEmpty(perfmapFormatVersion))
@@ -339,7 +351,7 @@ private string GenerateCrossgen2ResponseFile()
{
foreach (var mibc in Crossgen2PgoFiles)
{
- result.AppendLine($"-m:\"{mibc.ItemSpec}\"");
+ result.AppendLine($"-m:\"{TaskEnvironment.GetAbsolutePath(mibc.ItemSpec)}\"");
}
}
@@ -364,7 +376,7 @@ private string GenerateCrossgen2ResponseFile()
if (Crossgen2IsVersion5)
result.AppendLine("--inputbubble");
- result.AppendLine($"--out:\"{_outputR2RImage}\"");
+ result.AppendLine($"--out:\"{TaskEnvironment.GetAbsolutePath(_outputR2RImage)}\"");
result.Append(GetAssemblyReferencesCommands());
@@ -372,25 +384,25 @@ private string GenerateCrossgen2ResponseFile()
// parsing logic will append this string to the working directory if it's a relative path, so any double quotes will result in errors.
foreach (var reference in ReadyToRunCompositeBuildInput)
{
- result.AppendLine(reference.ItemSpec);
+ result.AppendLine(TaskEnvironment.GetAbsolutePath(reference.ItemSpec));
}
if (ReadyToRunCompositeUnrootedBuildInput != null)
{
foreach (var unrooted in ReadyToRunCompositeUnrootedBuildInput)
{
- result.AppendLine($"-u:\"{unrooted.ItemSpec}\"");
+ result.AppendLine($"-u:\"{TaskEnvironment.GetAbsolutePath(unrooted.ItemSpec)}\"");
}
}
}
else
{
result.Append(GetAssemblyReferencesCommands());
- result.AppendLine($"--out:\"{_outputR2RImage}\"");
+ result.AppendLine($"--out:\"{TaskEnvironment.GetAbsolutePath(_outputR2RImage)}\"");
// Note: do not add double quotes around the input assembly, even if the file path contains spaces. The command line
// parsing logic will append this string to the working directory if it's a relative path, so any double quotes will result in errors.
- result.AppendLine($"{_inputAssembly}");
+ result.AppendLine($"{TaskEnvironment.GetAbsolutePath(_inputAssembly)}");
}
return result.ToString();
@@ -400,7 +412,7 @@ protected override int ExecuteTool(string pathToTool, string responseFileCommand
{
// Ensure output sub-directories exists - Crossgen does not create directories for output files. Any relative path used with the
// '/out' parameter has to have an existing directory.
- Directory.CreateDirectory(Path.GetDirectoryName(_outputR2RImage));
+ Directory.CreateDirectory(Path.GetDirectoryName(TaskEnvironment.GetAbsolutePath(_outputR2RImage)));
WarningsDetected = false;