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;