Skip to content
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace Microsoft.Build.BackEnd
/// id validation checks to fail.
/// </remarks>
[RunInMTA]
internal class CallTarget : ITask
internal class CallTarget : ITask, IMultiThreadableTask
{
/// <summary>
/// The task logging helper
Expand Down Expand Up @@ -57,6 +57,11 @@ internal class CallTarget : ITask
/// </summary>
public bool UseResultsCache { get; set; } = false;

/// <summary>
/// Task environment for isolated execution.
/// </summary>
public TaskEnvironment TaskEnvironment { get; set; }

#endregion

#region ITask Members
Expand Down Expand Up @@ -113,7 +118,8 @@ public Task<bool> ExecuteInternal()
targetOutputs: _targetOutputs,
unloadProjectsOnCompletion: false,
toolsVersion: null,
skipNonexistentTargets: false);
skipNonexistentTargets: false,
taskEnvironment: TaskEnvironment);
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace Microsoft.Build.BackEnd
/// <remarks>
/// This class implements the "MSBuild" task, which hands off child project files to the MSBuild engine to be built.
/// </remarks>
internal class MSBuild : ITask
internal class MSBuild : ITask, IMultiThreadableTask
{
/// <summary>
/// Enum describing the behavior when a project doesn't exist on disk.
Expand Down Expand Up @@ -215,6 +215,11 @@ public string SkipNonexistentProjects
/// </remarks>
/// </summary>
public bool SkipNonexistentTargets { get; set; }

/// <summary>
/// Task environment for isolated execution.
/// </summary>
public TaskEnvironment TaskEnvironment { get; set; }
#endregion

#region ITask Members
Expand Down Expand Up @@ -310,7 +315,7 @@ public async Task<bool> ExecuteInternal()
{
ITaskItem project = Projects[i];

string projectPath = FileUtilities.AttemptToShortenPath(project.ItemSpec);
AbsolutePath projectPath = TaskEnvironment.GetAbsolutePath(FileUtilities.AttemptToShortenPath(project.ItemSpec));

if (StopOnFirstFailure && !success)
{
Expand Down Expand Up @@ -368,7 +373,8 @@ public async Task<bool> ExecuteInternal()
_targetOutputs,
UnloadProjectsOnCompletion,
ToolsVersion,
SkipNonexistentTargets);
SkipNonexistentTargets,
TaskEnvironment);

if (!executeResult)
{
Expand Down Expand Up @@ -439,7 +445,8 @@ private async Task<bool> BuildProjectsInParallel(Dictionary<string, string> prop
_targetOutputs,
UnloadProjectsOnCompletion,
ToolsVersion,
SkipNonexistentTargets);
SkipNonexistentTargets,
TaskEnvironment);

if (!executeResult)
{
Expand Down Expand Up @@ -531,30 +538,31 @@ internal static async Task<bool> ExecuteTargets(
List<ITaskItem> targetOutputs,
bool unloadProjectsOnCompletion,
string toolsVersion,
bool skipNonexistentTargets)
bool skipNonexistentTargets,
TaskEnvironment taskEnvironment)
{
bool success = true;

// We don't log a message about the project and targets we're going to
// build, because it'll all be in the immediately subsequent ProjectStarted event.

var projectDirectory = new string[projects.Length];
var projectNames = new string[projects.Length];
var projectNames = new AbsolutePath[projects.Length];
var toolsVersions = new string[projects.Length];
var projectProperties = new Dictionary<string, string>[projects.Length];
var undefinePropertiesPerProject = new List<string>[projects.Length];

for (int i = 0; i < projectNames.Length; i++)
{
projectNames[i] = null;
projectNames[i] = default;
projectProperties[i] = propertiesTable;

if (projects[i] != null)
{
// Retrieve projectDirectory only the first time. It never changes anyway.
string projectPath = FileUtilities.AttemptToShortenPath(projects[i].ItemSpec);
projectDirectory[i] = Path.GetDirectoryName(projectPath);
projectNames[i] = projects[i].ItemSpec;
projectNames[i] = taskEnvironment.GetAbsolutePath(projects[i].ItemSpec);
toolsVersions[i] = toolsVersion;

// If the user specified a different set of global properties for this project, then
Expand All @@ -563,7 +571,7 @@ internal static async Task<bool> ExecuteTargets(
{
if (!PropertyParser.GetTableWithEscaping(
log,
ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.OverridingProperties", projectNames[i]),
ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.OverridingProperties", projects[i].ItemSpec),
ItemMetadataNames.PropertiesMetadataName,
projects[i].GetMetadata(ItemMetadataNames.PropertiesMetadataName).Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries),
out Dictionary<string, string> preProjectPropertiesTable))
Expand Down Expand Up @@ -592,7 +600,7 @@ internal static async Task<bool> ExecuteTargets(

if (log != null && propertiesToUndefine.Length > 0)
{
log.LogMessageFromResources(MessageImportance.Low, "General.ProjectUndefineProperties", projectNames[i]);
log.LogMessageFromResources(MessageImportance.Low, "General.ProjectUndefineProperties", projects[i].ItemSpec);
foreach (string property in propertiesToUndefine)
{
undefinePropertiesPerProject[i].Add(property);
Expand All @@ -607,7 +615,7 @@ internal static async Task<bool> ExecuteTargets(
{
if (!PropertyParser.GetTableWithEscaping(
log,
ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.AdditionalProperties", projectNames[i]),
ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("General.AdditionalProperties", projects[i].ItemSpec),
ItemMetadataNames.AdditionalPropertiesMetadataName,
projects[i].GetMetadata(ItemMetadataNames.AdditionalPropertiesMetadataName).Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries),
out Dictionary<string, string> additionalProjectPropertiesTable))
Expand Down Expand Up @@ -660,7 +668,7 @@ internal static async Task<bool> ExecuteTargets(
// as the *calling* project file.

var taskHost = (TaskHost)buildEngine;
BuildEngineResult result = await taskHost.InternalBuildProjects(projectNames, targetList, projectProperties, undefinePropertiesPerProject, toolsVersions, true /* ask that target outputs are returned in the buildengineresult */, skipNonexistentTargets);
BuildEngineResult result = await taskHost.InternalBuildProjects(projectNames.ToStringArray(), targetList, projectProperties, undefinePropertiesPerProject, toolsVersions, true /* ask that target outputs are returned in the buildengineresult */, skipNonexistentTargets);

bool currentTargetResult = result.Result;
IList<IDictionary<string, ITaskItem[]>> targetOutputsPerProject = result.TargetOutputsPerProject;
Expand Down
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.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.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
Loading
Loading