Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Tasks/Common/ProcessTaskEnvironmentDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public ProcessStartInfo GetProcessStartInfo()
var startInfo = new ProcessStartInfo
{
WorkingDirectory = _projectDirectory.Value,
UseShellExecute = false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for this change?

};

// Populate environment from the scoped environment dictionary
Expand Down
26 changes: 26 additions & 0 deletions src/Tasks/Common/TaskEnvironmentDefaults.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
internal static TaskEnvironment Create() =>
new TaskEnvironment(new ProcessTaskEnvironmentDriver(Environment.CurrentDirectory));
}
}

#endif
Original file line number Diff line number Diff line change
@@ -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<string>();
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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ITaskItem>(),
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<ITaskItem>(),
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<string>();
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<ITaskItem>(),
RuntimeGraphPath = "nonexistent.json",
NETCoreSdkRuntimeIdentifier = "win-x64",
ReadyToRunUseCrossgen2 = false,
};
barrier.SignalAndWait();
task.Execute();
}
catch (Exception ex) { errors.Add($"Thread {i}: {ex.Message}"); }
});
errors.Should().BeEmpty();
}
}
}
Loading
Loading