Skip to content
12 changes: 6 additions & 6 deletions documentation/specs/multithreading/thread-safe-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,16 @@ public interface IMultiThreadableTask : ITask

public class TaskEnvironment
{
public virtual AbsolutePath ProjectDirectory { get; internal set; }
public AbsolutePath ProjectDirectory { get; internal set; }

// This function resolves paths relative to ProjectDirectory.
public virtual AbsolutePath GetAbsolutePath(string path);
public AbsolutePath GetAbsolutePath(string path);

public virtual string? GetEnvironmentVariable(string name);
public virtual IReadOnlyDictionary<string, string> GetEnvironmentVariables();
public virtual void SetEnvironmentVariable(string name, string? value);
public string? GetEnvironmentVariable(string name);
public IReadOnlyDictionary<string, string> GetEnvironmentVariables();
public void SetEnvironmentVariable(string name, string? value);

public virtual ProcessStartInfo GetProcessStartInfo();
public ProcessStartInfo GetProcessStartInfo();
}
```

Expand Down
9 changes: 9 additions & 0 deletions src/Build.UnitTests/BackEnd/MockTaskBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext;

Expand Down Expand Up @@ -122,6 +123,14 @@ public Task<WorkUnitResult> ExecuteTask(TargetLoggingContext targetLoggingContex
return Task<WorkUnitResult>.FromResult(new WorkUnitResult(WorkUnitResultCode.Success, WorkUnitActionCode.Continue, null));
}

/// <summary>
/// Sets the task environment on the TaskExecutionHost for use with IMultiThreadableTask instances.
/// </summary>
/// <param name="taskEnvironment">The task environment to set, or null to clear.</param>
public void SetTaskEnvironment(TaskEnvironment taskEnvironment)
{
}

#endregion

#region IBuildComponent Members
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,17 @@ public void SubmitBuildRequest(BuildRequest request)
}
else
{
BuildRequestEntry entry = new BuildRequestEntry(request, _configCache[request.ConfigurationId]);
BuildRequestConfiguration config = _configCache[request.ConfigurationId];

TaskEnvironment taskEnvironment = null;
if (_componentHost.BuildParameters.MultiThreaded)
{
string projectDirectoryFullPath = Path.GetDirectoryName(config.ProjectFullPath);
var environmentVariables = new Dictionary<string, string>(_componentHost.BuildParameters.BuildProcessEnvironmentInternal);
taskEnvironment = new TaskEnvironment(new MultithreadedTaskEnvironmentDriver(projectDirectoryFullPath, environmentVariables));
}

BuildRequestEntry entry = new BuildRequestEntry(request, config, taskEnvironment);

entry.OnStateChanged += BuildRequestEntry_StateChanged;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using Microsoft.Build.Execution;
using Microsoft.Build.Shared;
using Microsoft.Build.Framework;
using BuildAbortedException = Microsoft.Build.Exceptions.BuildAbortedException;

#nullable disable
Expand Down Expand Up @@ -119,7 +120,8 @@ internal class BuildRequestEntry
/// </summary>
/// <param name="request">The originating build request.</param>
/// <param name="requestConfiguration">The build request configuration.</param>
internal BuildRequestEntry(BuildRequest request, BuildRequestConfiguration requestConfiguration)
/// <param name="taskEnvironment">Task enviroment information that would be passed to tasks executing for the build request. If set to null will use stub environment.</param>
internal BuildRequestEntry(BuildRequest request, BuildRequestConfiguration requestConfiguration, TaskEnvironment taskEnvironment = null)
{
ErrorUtilities.VerifyThrowArgumentNull(request);
ErrorUtilities.VerifyThrowArgumentNull(requestConfiguration);
Expand All @@ -128,6 +130,7 @@ internal BuildRequestEntry(BuildRequest request, BuildRequestConfiguration reque
GlobalLock = new LockType();
Request = request;
RequestConfiguration = requestConfiguration;
TaskEnvironment = taskEnvironment ?? new TaskEnvironment(StubTaskEnvironmentDriver.Instance);
_blockingGlobalRequestId = BuildRequest.InvalidGlobalRequestId;
Result = null;
ChangeState(BuildRequestEntryState.Ready);
Expand Down Expand Up @@ -184,6 +187,12 @@ public IRequestBuilder Builder
_requestBuilder = value;
}
}

/// <summary>
/// Gets or sets the task environment for this request.
/// Tasks implementing IMultiThreadableTask will use this environment for file system and environment operations.
/// </summary>
public TaskEnvironment TaskEnvironment { get; set; }

/// <summary>
/// Informs the entry that it has configurations which need to be resolved.
Expand Down
7 changes: 7 additions & 0 deletions src/Build/BackEnd/Components/RequestBuilder/ITaskBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using TargetLoggingContext = Microsoft.Build.BackEnd.Logging.TargetLoggingContext;

#nullable disable
Expand Down Expand Up @@ -52,5 +53,11 @@ internal interface ITaskBuilder
/// <param name="cancellationToken">The cancellation token used to cancel processing of the task.</param>
/// <returns>A Task representing the work to be done.</returns>
Task<WorkUnitResult> ExecuteTask(TargetLoggingContext targetLoggingContext, BuildRequestEntry requestEntry, ITargetBuilderCallback targetBuilderCallback, ProjectTargetInstanceChild task, TaskExecutionMode mode, Lookup lookupForInference, Lookup lookupForExecution, CancellationToken cancellationToken);

/// <summary>
/// Sets the task environment on the TaskExecutionHost for use with IMultiThreadableTask instances.
/// </summary>
/// <param name="taskEnvironment">The task environment to set, or null to clear.</param>
void SetTaskEnvironment(TaskEnvironment taskEnvironment);
}
}
50 changes: 43 additions & 7 deletions src/Build/BackEnd/Components/RequestBuilder/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1080,11 +1080,18 @@ private void RaiseResourceRequest(ResourceRequest request)
/// This is because if the project has not been saved, this directory may not exist, yet it is often useful to still be able to build the project.
/// No errors are masked by doing this: errors loading the project from disk are reported at load time, if necessary.
/// </summary>
private void SetProjectCurrentDirectory()
private void SetProjectDirectory()
{
if (_componentHost.BuildParameters.SaveOperatingEnvironment)
{
NativeMethodsShared.SetCurrentDirectory(_requestEntry.ProjectRootDirectory);
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0))
{
_requestEntry.TaskEnvironment.ProjectDirectory = new Framework.PathHelpers.AbsolutePath(_requestEntry.ProjectRootDirectory, ignoreRootedCheck: true);
}
else
{
NativeMethodsShared.SetCurrentDirectory(_requestEntry.ProjectRootDirectory);
}
}
}

Expand Down Expand Up @@ -1134,7 +1141,14 @@ private async Task<BuildResult> BuildProject()
{
foreach (ProjectPropertyInstance environmentProperty in environmentProperties)
{
Environment.SetEnvironmentVariable(environmentProperty.Name, environmentProperty.EvaluatedValue, EnvironmentVariableTarget.Process);
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0))
{
_requestEntry.TaskEnvironment.SetEnvironmentVariable(environmentProperty.Name, environmentProperty.EvaluatedValue);
}
else
{
Environment.SetEnvironmentVariable(environmentProperty.Name, environmentProperty.EvaluatedValue, EnvironmentVariableTarget.Process);
}
}
}

Expand Down Expand Up @@ -1190,7 +1204,7 @@ private async Task<BuildResult> BuildProject()
_requestEntry.RequestConfiguration.Project.ProjectFileLocation, "NoTargetSpecified");

// Set the current directory to that required by the project.
SetProjectCurrentDirectory();
SetProjectDirectory();

// Transfer results and state from the previous node, if necessary.
// In order for the check for target completeness for this project to be valid, all of the target results from the project must be present
Expand Down Expand Up @@ -1412,7 +1426,15 @@ private void RestoreOperatingEnvironment()

// Restore the saved environment variables.
SetEnvironmentVariableBlock(_requestEntry.RequestConfiguration.SavedEnvironmentVariables);
NativeMethodsShared.SetCurrentDirectory(_requestEntry.RequestConfiguration.SavedCurrentDirectory);

if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0))
{
_requestEntry.TaskEnvironment.ProjectDirectory = new Framework.PathHelpers.AbsolutePath(_requestEntry.RequestConfiguration.SavedCurrentDirectory, ignoreRootedCheck: true);
}
else
{
NativeMethodsShared.SetCurrentDirectory(_requestEntry.RequestConfiguration.SavedCurrentDirectory);
}
}
}

Expand All @@ -1435,7 +1457,14 @@ private void ClearVariablesNotInEnvironment(FrozenDictionary<string, string> sav
{
if (!savedEnvironment.ContainsKey(entry.Key))
{
Environment.SetEnvironmentVariable(entry.Key, null);
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0))
{
_requestEntry.TaskEnvironment.SetEnvironmentVariable(entry.Key, null);
}
else
{
Environment.SetEnvironmentVariable(entry.Key, null);
}
}
}
}
Expand All @@ -1453,7 +1482,14 @@ private void UpdateEnvironmentVariables(FrozenDictionary<string, string> savedEn
string value;
if (!currentEnvironment.TryGetValue(entry.Key, out value) || !String.Equals(entry.Value, value, StringComparison.Ordinal))
{
Environment.SetEnvironmentVariable(entry.Key, entry.Value);
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0))
{
_requestEntry.TaskEnvironment.SetEnvironmentVariable(entry.Key, entry.Value);
}
else
{
Environment.SetEnvironmentVariable(entry.Key, entry.Value);
}
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/Build/BackEnd/Components/RequestBuilder/TargetBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,20 @@ public async Task<BuildResult> BuildTargets(ProjectLoggingContext loggingContext
ITaskBuilder taskBuilder = _componentHost.GetComponent(BuildComponentType.TaskBuilder) as ITaskBuilder;
try
{
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0) && _requestEntry.TaskEnvironment != null)
{
taskBuilder?.SetTaskEnvironment(_requestEntry.TaskEnvironment);
}

await ProcessTargetStack(taskBuilder);
}
finally
{
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0))
{
taskBuilder?.SetTaskEnvironment(null);
}

// If there are still targets left on the stack, they need to be removed from the 'active targets' list
foreach (TargetEntry target in _targetsToBuild)
{
Expand Down Expand Up @@ -266,6 +276,11 @@ async Task<ITargetResult[]> ITargetBuilderCallback.LegacyCallTarget(string[] tar
ITaskBuilder taskBuilder = _componentHost.GetComponent(BuildComponentType.TaskBuilder) as ITaskBuilder;
try
{
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0) && _requestEntry.TaskEnvironment != null)
{
taskBuilder?.SetTaskEnvironment(_requestEntry.TaskEnvironment);
}

// Flag set to true if one of the targets we call fails.
bool errorResult = false;

Expand Down Expand Up @@ -312,6 +327,11 @@ async Task<ITargetResult[]> ITargetBuilderCallback.LegacyCallTarget(string[] tar
_targetsToBuild.Pop();
}

if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0))
{
taskBuilder?.SetTaskEnvironment(null);
}

_legacyCallTargetContinueOnError = originalLegacyCallTargetContinueOnError;
((IBuildComponent)taskBuilder).ShutdownComponent();
}
Expand Down
20 changes: 19 additions & 1 deletion src/Build/BackEnd/Components/RequestBuilder/TaskBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Framework.PathHelpers;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using ElementLocation = Microsoft.Build.Construction.ElementLocation;
Expand Down Expand Up @@ -130,6 +131,16 @@ internal TaskBuilder()
{
}

/// <summary>
/// Sets the task environment on the TaskExecutionHost for use with IMultiThreadableTask instances.
/// </summary>
/// <param name="taskEnvironment">The task environment to set, or null to clear.</param>
public void SetTaskEnvironment(TaskEnvironment taskEnvironment)
{
ErrorUtilities.VerifyThrow(_taskExecutionHost != null, "TaskExecutionHost must be initialized before setting TaskEnvironment.");
_taskExecutionHost.TaskEnvironment = taskEnvironment;
}

/// <summary>
/// Builds the task specified by the XML.
/// </summary>
Expand Down Expand Up @@ -415,7 +426,14 @@ private async ValueTask<WorkUnitResult> ExecuteBucket(TaskHost taskHost, ItemBuc
// If that directory does not exist, do nothing. (Do not check first as it is almost always there and it is slow)
// This is because if the project has not been saved, this directory may not exist, yet it is often useful to still be able to build the project.
// No errors are masked by doing this: errors loading the project from disk are reported at load time, if necessary.
NativeMethodsShared.SetCurrentDirectory(_buildRequestEntry.ProjectRootDirectory);
if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_0))
{
_buildRequestEntry.TaskEnvironment.ProjectDirectory = new AbsolutePath(_buildRequestEntry.ProjectRootDirectory, ignoreRootedCheck: true);
}
else
{
NativeMethodsShared.SetCurrentDirectory(_buildRequestEntry.ProjectRootDirectory);
}
}

if (howToExecuteTask == TaskExecutionMode.ExecuteTaskAndGatherOutputs)
Expand Down
21 changes: 17 additions & 4 deletions src/Build/BackEnd/TaskExecutionHost/TaskExecutionHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ internal class TaskExecutionHost : IDisposable

private readonly PropertyTrackingSetting _propertyTrackingSettings;

/// <summary>
/// The task environment to be used by IMultiThreadableTask instances.
/// </summary>
internal TaskEnvironment TaskEnvironment { get; set; }

/// <summary>
/// Constructor
/// </summary>
Expand Down Expand Up @@ -367,6 +372,11 @@ public bool InitializeForBatch(TaskLoggingContext loggingContext, ItemBucket bat

TaskInstance.BuildEngine = _buildEngine;
TaskInstance.HostObject = _taskHost;

if (TaskInstance is IMultiThreadableTask multiThreadableTask)
{
multiThreadableTask.TaskEnvironment = TaskEnvironment;
}

return true;

Expand Down Expand Up @@ -614,6 +624,7 @@ public bool Execute()

try
{
Debug.Assert(TaskInstance is not IMultiThreadableTask multiThreadableTask || multiThreadableTask.TaskEnvironment != null, "task environment missing for multi-threadable task");
taskReturnValue = TaskInstance.Execute();
}
finally
Expand Down Expand Up @@ -728,7 +739,7 @@ private bool SetTaskItemParameter(TaskPropertyInfo parameter, ITaskItem item)
{
return InternalSetTaskParameter(parameter, item);
}

/// <summary>
/// Called on the local side.
/// </summary>
Expand Down Expand Up @@ -976,7 +987,8 @@ private ITask InstantiateTask(int scheduledNodeId, IDictionary<string, string> t
#endif
IsOutOfProc,
scheduledNodeId,
ProjectInstance.GetProperty);
ProjectInstance.GetProperty,
TaskEnvironment);
}
else
{
Expand Down Expand Up @@ -1793,10 +1805,11 @@ private ITask CreateTaskHostTaskForOutOfProcFactory(IDictionary<string, string>
_buildComponentHost,
taskHostParameters,
taskLoadedType,
true
true,
#if FEATURE_APPDOMAIN
, AppDomainSetup
AppDomainSetup,
#endif
TaskEnvironment
);
#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter
}
Expand Down
Loading
Loading