diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/RunReadyToRunCompiler.cs b/src/Tasks/Microsoft.NET.Build.Tasks/RunReadyToRunCompiler.cs index e35ead24aece..d2d31916f47f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/RunReadyToRunCompiler.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/RunReadyToRunCompiler.cs @@ -8,8 +8,15 @@ namespace Microsoft.NET.Build.Tasks { - public class RunReadyToRunCompiler : ToolTask + [MSBuildMultiThreadableTask] + public class RunReadyToRunCompiler : ToolTask, IMultiThreadableTask { +#if NET + public override TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; +#elif NETFRAMEWORK + public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback; +#endif + public ITaskItem CrossgenTool { get; set; } public ITaskItem Crossgen2Tool { get; set; } @@ -70,7 +77,40 @@ protected override string ToolName } } - protected override string GenerateFullPathToTool() => ToolName; + protected override string GenerateFullPathToTool() => TaskEnvironment.GetAbsolutePath(ToolName); + +#if NETFRAMEWORK + protected override System.Diagnostics.ProcessStartInfo GetProcessStartInfo( + string pathToTool, + string commandLineCommands, + string responseFileSwitch) + { + System.Diagnostics.ProcessStartInfo startInfo = base.GetProcessStartInfo( + pathToTool, + commandLineCommands, + responseFileSwitch); + System.Diagnostics.ProcessStartInfo environmentStartInfo = TaskEnvironment.GetProcessStartInfo(); + + // The minimum .NET Framework ToolTask predates TaskEnvironment and starts with process-global state. + startInfo.WorkingDirectory = environmentStartInfo.WorkingDirectory; + startInfo.EnvironmentVariables.Clear(); + foreach (string key in environmentStartInfo.EnvironmentVariables.Keys) + { + startInfo.EnvironmentVariables[key] = environmentStartInfo.EnvironmentVariables[key]; + } + + if (EnvironmentVariables != null) + { + foreach (string variable in EnvironmentVariables) + { + string[] nameValuePair = variable.Split(['='], 2); + startInfo.EnvironmentVariables[nameValuePair[0]] = nameValuePair[1]; + } + } + + return startInfo; + } +#endif private string DiaSymReader => CrossgenTool.GetMetadata(MetadataKeys.DiaSymReader); @@ -105,13 +145,15 @@ protected override bool ValidateParameters() Log.LogError(Strings.Crossgen2ToolMissingWhenUseCrossgen2IsSet); return false; } - if (!File.Exists(Crossgen2Tool.ItemSpec)) + if (string.IsNullOrEmpty(Crossgen2Tool.ItemSpec) || + !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; @@ -119,7 +161,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; @@ -153,12 +195,15 @@ protected override bool ValidateParameters() Log.LogError(Strings.CrossgenToolMissingWhenUseCrossgen2IsNotSet); return false; } - if (!File.Exists(CrossgenTool.ItemSpec)) + if (string.IsNullOrEmpty(CrossgenTool.ItemSpec) || + !File.Exists(TaskEnvironment.GetAbsolutePath(CrossgenTool.ItemSpec))) { Log.LogError(Strings.CrossgenToolExecutableNotFound, CrossgenTool.ItemSpec); return false; } - if (!File.Exists(CrossgenTool.GetMetadata(MetadataKeys.JitPath))) + string jitPath = CrossgenTool.GetMetadata(MetadataKeys.JitPath); + if (string.IsNullOrEmpty(jitPath) || + !File.Exists(TaskEnvironment.GetAbsolutePath(jitPath))) { Log.LogError(Strings.JitLibraryNotFound, MetadataKeys.JitPath); return false; @@ -171,7 +216,8 @@ 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; @@ -183,7 +229,8 @@ protected override bool ValidateParameters() Log.LogError(Strings.MissingOutputPDBImagePath); } - if (!File.Exists(_outputR2RImage)) + if (string.IsNullOrEmpty(_outputR2RImage) || + !File.Exists(TaskEnvironment.GetAbsolutePath(_outputR2RImage))) { Log.LogError(Strings.PDBGeneratorInputExecutableNotFound, _outputR2RImage); return false; @@ -196,7 +243,8 @@ protected override bool ValidateParameters() if (!_createCompositeImage) { _inputAssembly = CompilationEntry.ItemSpec; - if (!File.Exists(_inputAssembly)) + if (string.IsNullOrEmpty(_inputAssembly) || + !File.Exists(TaskEnvironment.GetAbsolutePath(_inputAssembly))) { Log.LogError(Strings.InputAssemblyNotFound, _inputAssembly); return false; @@ -433,7 +481,8 @@ 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)); + string outputDirectory = Path.GetDirectoryName(_outputR2RImage); + Directory.CreateDirectory(TaskEnvironment.GetAbsolutePath(outputDirectory)); WarningsDetected = false; diff --git a/test/Microsoft.NET.Build.Tasks.Tests/GivenARunReadyToRunCompilerMultiThreading.cs b/test/Microsoft.NET.Build.Tasks.Tests/GivenARunReadyToRunCompilerMultiThreading.cs new file mode 100644 index 000000000000..cb886e114bd0 --- /dev/null +++ b/test/Microsoft.NET.Build.Tasks.Tests/GivenARunReadyToRunCompilerMultiThreading.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using FluentAssertions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Build.Tasks.UnitTests; + +[TestClass] +public class GivenARunReadyToRunCompilerMultiThreading : SdkTest +{ + [TestMethod] + public void RelativePathsResolveIndependentlyForEachTask() + { + string firstProjectDirectory = TestAssetsManager.CreateTestDirectory(identifier: "first").Path; + string secondProjectDirectory = TestAssetsManager.CreateTestDirectory(identifier: "second").Path; + string relativeToolPath = Path.Combine("tools", "crossgen2"); + string relativeInputPath = Path.Combine("input", "app.dll"); + + CreateFile(Path.Combine(firstProjectDirectory, relativeToolPath)); + CreateFile(Path.Combine(firstProjectDirectory, relativeInputPath)); + CreateFile(Path.Combine(secondProjectDirectory, relativeToolPath)); + CreateFile(Path.Combine(secondProjectDirectory, relativeInputPath)); + + var firstTask = CreateTask(firstProjectDirectory, relativeToolPath, relativeInputPath); + var secondTask = CreateTask(secondProjectDirectory, relativeToolPath, relativeInputPath); + + firstTask.ValidateParametersForTest().Should().BeTrue(); + secondTask.ValidateParametersForTest().Should().BeTrue(); + firstTask.GenerateFullPathToToolForTest().Should().Be(Path.Combine(firstProjectDirectory, relativeToolPath)); + secondTask.GenerateFullPathToToolForTest().Should().Be(Path.Combine(secondProjectDirectory, relativeToolPath)); + } + + [TestMethod] + public void EmptyToolPathUsesExistingValidationDiagnostic() + { + string projectDirectory = TestAssetsManager.CreateTestDirectory().Path; + var task = CreateTask(projectDirectory, string.Empty, Path.Combine("input", "app.dll")); + + task.ValidateParametersForTest().Should().BeFalse(); + ((MockBuildEngine)task.BuildEngine).Errors.Should().ContainSingle(); + } + + private static TestableRunReadyToRunCompiler CreateTask( + string projectDirectory, + string relativeToolPath, + string relativeInputPath) + { + var crossgen2Tool = new TaskItem(relativeToolPath); + crossgen2Tool.SetMetadata(MetadataKeys.TargetOS, "windows"); + crossgen2Tool.SetMetadata(MetadataKeys.TargetArch, "x64"); + + var compilationEntry = new TaskItem(relativeInputPath); + compilationEntry.SetMetadata(MetadataKeys.OutputR2RImage, Path.Combine("output", "app.dll")); + + return new TestableRunReadyToRunCompiler + { + BuildEngine = new MockBuildEngine(), + TaskEnvironment = TaskEnvironmentHelper.CreateForTest(projectDirectory), + Crossgen2Tool = crossgen2Tool, + CompilationEntry = compilationEntry, + ImplementationAssemblyReferences = [], + UseCrossgen2 = true, + }; + } + + private static void CreateFile(string path) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllBytes(path, []); + } + + private sealed class TestableRunReadyToRunCompiler : RunReadyToRunCompiler + { + public bool ValidateParametersForTest() => ValidateParameters(); + + public string GenerateFullPathToToolForTest() => GenerateFullPathToTool(); + } +} \ No newline at end of file