|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// THE ASSEMBLY BUILT FROM THIS SOURCE FILE HAS BEEN DEPRECATED FOR YEARS. IT IS BUILT ONLY TO PROVIDE
// BACKWARD COMPATIBILITY FOR API USERS WHO HAVE NOT YET MOVED TO UPDATED APIS. PLEASE DO NOT SEND PULL
// REQUESTS THAT CHANGE THIS FILE WITHOUT FIRST CHECKING WITH THE MAINTAINERS THAT THE FIX IS REQUIRED.
using System;
using System.Xml;
using System.Diagnostics; // for debugger display attribute
using System.Collections;
using System.Collections.Generic;
using Microsoft.Build.Framework;
using Microsoft.Build.BuildEngine.Shared;
using error = Microsoft.Build.BuildEngine.Shared.ErrorUtilities;
namespace Microsoft.Build.BuildEngine
{
/// <summary>
/// This class represents a single target in its parent project.
/// </summary>
[DebuggerDisplay("Target (Name = { Name }, Condition = { Condition })")]
public class Target : IEnumerable
{
#region Enums
/// <summary>
/// This enumeration contains a list of the possible states that the target could be in, in terms of the build process.
/// </summary>
internal enum BuildState
{
// This build for this target has not begun. We don't even know if
// we're going to end up needing to build this target at all.
NotStarted,
// We've determined that this target needs to be built, and we're
// in the process of doing exactly that. Building other targets
// that are dependents of this target is considered to be part of
// building this target. And so, while we're building other dependent
// targets, our state will be set to "InProgress".
InProgress,
// This target (and naturally all dependent targets) has been
// successfully built.
CompletedSuccessfully,
// We have attempted to build this target and all its dependent
// targets. However, something failed during that process, and
// we consider ourselves done with this target.
CompletedUnsuccessfully,
// This target is to be skipped. This state is the result of a target
// having a condition attribute and that condition evaluating to false.
Skipped
}
#endregion
#region Member Data
// The evaluated name of the target.
private string targetName;
// The parent project.object. We will need this in order get the
// complete list of targets, items, properties, etc.
private Project parentProject;
// The parent Engine object.
private Engine parentEngine;
// The state of this target, in terms of the build.
private BuildState buildState;
// The <Target> XML element, if this is a persisted item. For virtual
// items (i.e., those generated by tasks), this would be null.
private XmlElement targetElement = null;
// This is the "Condition" attribute on the <Target> element.
private XmlAttribute conditionAttribute = null;
// This is the "DependsOnTargets" attribute on the <Target> element.
private XmlAttribute dependsOnTargetsAttribute = null;
// This is the "Inputs" attribute on the <Target> element.
private XmlAttribute inputsAttribute = null;
// This is the "Outputs" attribute on the <Target> element.
private XmlAttribute outputsAttribute = null;
// This contains all of the child task nodes in this <Target> node.
private ArrayList taskElementList = null;
// If this is a persisted <Target> element, this boolean tells us whether
// it came from the main parentProject.file or an imported parentProject.file.
private bool importedFromAnotherProject = false;
// If the Inputs or Outputs attribute changes then we will have to re-calculate the
// targetParameters
private bool recalculateBatchableParameters = false;
// the project file that the target XML was defined in -- this file could be different from the file of this target's
// parent project if the target was defined in an imported project file
private string projectFileOfTargetElement;
// the outputs of the target as BuildItems (if it builds successfully)
private List<BuildItem> targetOutputItems;
// We check the target's condition to ensure it doesn't reference item metadata in an attempt to batch.
private bool conditionCheckedForInvalidMetadataReferences = false;
private TargetExecutionWrapper executionState = null;
private List<string> batchableTargetParameters = null;
// TargetId
private int id;
#endregion
#region Constructors
/// <summary>
/// Initializes a persisted target from an existing <Target> element which exists either in the main parent project
/// file or one of the imported files.
/// </summary>
/// <param name="targetElement"></param>
/// <param name="project"></param>
/// <param name="importedFromAnotherProject"></param>
internal Target
(
XmlElement targetElement,
Project project,
bool importedFromAnotherProject
)
{
// Make sure a valid node has been given to us.
error.VerifyThrow(targetElement != null, "Need a valid XML node.");
// Make sure this really is the <target> node.
ProjectXmlUtilities.VerifyThrowElementName(targetElement, XMakeElements.target);
this.targetElement = targetElement;
this.parentProject = project;
this.parentEngine = project.ParentEngine;
this.conditionAttribute = null;
this.taskElementList = null;
this.importedFromAnotherProject = importedFromAnotherProject;
this.buildState = BuildState.NotStarted;
this.id = project.ParentEngine.GetNextTargetId();
// The target name and target dependendencies (dependencies on other
// targets) are specified as attributes of the <target> element.
XmlAttribute returnsAttribute = null;
// Loop through all the attributes on the <target> element.
foreach (XmlAttribute targetAttribute in targetElement.Attributes)
{
switch (targetAttribute.Name)
{
// Process the "condition" attribute.
case XMakeAttributes.condition:
this.conditionAttribute = targetAttribute;
break;
// Process the "name" attribute.
case XMakeAttributes.name:
this.targetName = EscapingUtilities.UnescapeAll(targetAttribute.Value);
// Target names cannot contain MSBuild special characters, embedded properties,
// or item lists.
int indexOfSpecialCharacter = this.targetName.IndexOfAny(XMakeElements.illegalTargetNameCharacters);
if (indexOfSpecialCharacter >= 0)
{
ProjectErrorUtilities.VerifyThrowInvalidProject(false,
targetAttribute, "NameInvalid", targetName, targetName[indexOfSpecialCharacter]);
}
break;
// Process the "dependsOnTargets" attribute.
case XMakeAttributes.dependsOnTargets:
this.dependsOnTargetsAttribute = targetAttribute;
break;
case XMakeAttributes.inputs:
this.inputsAttribute = targetAttribute;
recalculateBatchableParameters = true;
break;
case XMakeAttributes.outputs:
this.outputsAttribute = targetAttribute;
recalculateBatchableParameters = true;
break;
// This is only recognized by the new OM:
// so that the compat tests keep passing,
// ignore it.
case XMakeAttributes.keepDuplicateOutputs:
break;
// This is only recognized by the new OM:
// so that the compat tests keep passing,
// ignore it.
case XMakeAttributes.returns:
returnsAttribute = targetAttribute;
break;
// These are only recognized by the new OM:
// while the solution wrapper generator is using
// the old OM to parse projects for dependencies,
// we must make sure to not fail for these
case XMakeAttributes.beforeTargets:
case XMakeAttributes.afterTargets:
break;
default:
ProjectXmlUtilities.ThrowProjectInvalidAttribute(targetAttribute);
break;
}
}
// Hack to help the 3.5 engine at least pretend to still be able to build on top of
// the 4.0 targets. In cases where there is no Outputs attribute, just a Returns attribute,
// we can approximate the correct behaviour by making the Returns attribute our "outputs" attribute.
if (this.outputsAttribute == null && returnsAttribute != null)
{
this.outputsAttribute = returnsAttribute;
recalculateBatchableParameters = true;
}
// It's considered an error if a target does not have a name.
ProjectErrorUtilities.VerifyThrowInvalidProject(!string.IsNullOrEmpty(targetName),
targetElement, "MissingRequiredAttribute", XMakeAttributes.name, XMakeElements.target);
this.taskElementList = new ArrayList();
// Process each of the child nodes beneath the <Target>.
XmlElement anyOnErrorElement = null;
List<XmlElement> childElements = ProjectXmlUtilities.GetValidChildElements(targetElement);
foreach (XmlElement childElement in childElements)
{
bool onErrorOutOfOrder = false;
switch (childElement.Name)
{
case XMakeElements.onError:
anyOnErrorElement = childElement;
break;
default:
onErrorOutOfOrder = (anyOnErrorElement != null);
this.taskElementList.Add(new BuildTask(childElement,
this, this.importedFromAnotherProject));
break;
}
// Check for out-of-order OnError
ProjectErrorUtilities.VerifyThrowInvalidProject(!onErrorOutOfOrder,
anyOnErrorElement, "NodeMustBeLastUnderElement", XMakeElements.onError, XMakeElements.target, childElement.Name);
}
}
#endregion
#region Properties
/// <summary>
/// Id for the target
/// </summary>
internal int Id
{
get
{
return this.id;
}
}
/// <summary>
/// Gets the target's name as specified in the "Name" attribute. The value of this attribute is never evaluated.
/// </summary>
/// <value>The target name string.</value>
public string Name
{
get
{
return this.targetName;
}
}
/// <summary>
/// Gets the target's unevaluated "DependsOnTargets" string.
/// Returns unevaluated.
/// </summary>
/// <value>The raw "DependsOnTargets" string.</value>
public string DependsOnTargets
{
get
{
return ProjectXmlUtilities.GetAttributeValue(this.dependsOnTargetsAttribute);
}
set
{
this.dependsOnTargetsAttribute = SetOrRemoveTargetAttribute(XMakeAttributes.dependsOnTargets, value);
}
}
/// <summary>
/// Gets the target's unevaluated "Inputs" string.
/// Returns unevaluated.
/// </summary>
/// <value>The raw "Inputs" string.</value>
public string Inputs
{
get
{
return ProjectXmlUtilities.GetAttributeValue(this.inputsAttribute);
}
set
{
this.inputsAttribute = SetOrRemoveTargetAttribute(XMakeAttributes.inputs, value);
recalculateBatchableParameters = true;
}
}
/// <summary>
/// Gets the target's unevaluated "Outputs" string.
/// Returns unevaluated.
/// </summary>
/// <value>The raw "Outputs" string.</value>
public string Outputs
{
get
{
return ProjectXmlUtilities.GetAttributeValue(this.outputsAttribute);
}
set
{
this.outputsAttribute = SetOrRemoveTargetAttribute(XMakeAttributes.outputs, value);
recalculateBatchableParameters = true;
}
}
/// <summary>
/// Accessor for the item's "condition". Returned unevaluated.
/// </summary>
/// <returns>Condition string.</returns>
/// <value>The raw condition string.</value>
public string Condition
{
get
{
return ProjectXmlUtilities.GetAttributeValue(this.conditionAttribute);
}
set
{
this.conditionAttribute = SetOrRemoveTargetAttribute(XMakeAttributes.condition, value);
this.conditionCheckedForInvalidMetadataReferences = false;
}
}
/// <summary>
/// Read-only accessor for accessing the XML attribute for "Condition". Callers should
/// never try and modify this. Go through this.Condition to change the condition.
/// </summary>
internal XmlAttribute ConditionAttribute
{
get
{
return this.conditionAttribute;
}
}
/// <summary>
/// Gets the XML representing this target.
/// </summary>
/// <value>The XmlElement for the target.</value>
internal XmlElement TargetElement
{
get
{
return this.targetElement;
}
}
/// <summary>
/// Gets the target's unevaluated "DependsOnTargets" XML element.
/// </summary>
internal XmlAttribute DependsOnTargetsAttribute
{
get
{
return this.dependsOnTargetsAttribute;
}
}
/// <summary>
/// Gets the filename/path of the project this target was defined in. This file could be different from the file of this
/// target's parent project, because the target could be imported. If the target is only defined in-memory, then it may
/// not have a filename associated with it.
/// </summary>
/// <value>The filename/path string of this target's original project, or empty string.</value>
internal string ProjectFileOfTargetElement
{
get
{
if (projectFileOfTargetElement == null)
{
projectFileOfTargetElement = XmlUtilities.GetXmlNodeFile(TargetElement, parentProject.FullFileName);
}
return projectFileOfTargetElement;
}
}
/// <summary>
/// Read-only accessor for this target's parent Project object.
/// </summary>
/// <value></value>
internal Project ParentProject
{
get
{
return this.parentProject;
}
set
{
this.parentProject = value;
}
}
/// <summary>
/// Read-only accessor for this target's parent Project object.
/// </summary>
/// <value></value>
internal Engine ParentEngine
{
get
{
return this.parentEngine;
}
}
/// <summary>
/// Calculates the batchable target parameters, which can be changed if inputs and outputs are
/// set after target creation.
/// </summary>
internal List<string> GetBatchableTargetParameters()
{
if (recalculateBatchableParameters)
{
batchableTargetParameters = new List<string>();
if (inputsAttribute != null)
{
batchableTargetParameters.Add(inputsAttribute.Value);
}
if (outputsAttribute != null)
{
batchableTargetParameters.Add(outputsAttribute.Value);
}
recalculateBatchableParameters = false;
}
else if (batchableTargetParameters == null)
{
batchableTargetParameters = new List<string>();
}
return batchableTargetParameters;
}
/// <summary>
/// This returns a boolean telling you whether this particular target
/// was imported from another project, or whether it was defined
/// in the main project.
/// </summary>
/// <value></value>
public bool IsImported
{
get
{
return this.importedFromAnotherProject;
}
}
internal BuildState TargetBuildState
{
get
{
return this.buildState;
}
}
internal TargetExecutionWrapper ExecutionState
{
get
{
return executionState;
}
}
#endregion
#region Methods
/// <summary>
/// Allows the caller to use a foreach loop to enumerate through the individual
/// BuildTask objects contained within this Target.
/// </summary>
/// <returns></returns>
public IEnumerator GetEnumerator
(
)
{
error.VerifyThrow(this.taskElementList != null, "List of TaskElements not initialized!");
return this.taskElementList.GetEnumerator();
}
/// <summary>
/// Sets the build state back to "NotStarted".
/// </summary>
internal void ResetBuildStatus
(
)
{
this.buildState = BuildState.NotStarted;
}
/// <summary>
/// Update the target data structures since the target has completed
/// </summary>
internal void UpdateTargetStateOnBuildCompletion
(
BuildState stateOfBuild,
List<BuildItem> targetOutputItemList
)
{
this.buildState = stateOfBuild;
this.targetOutputItems = targetOutputItemList;
// Clear the execution state since the build is completed
executionState = null;
}
/// <summary>
/// Builds this target if it has not already been built as part of its parent project. Before we actually execute the
/// tasks for this target, though, we first call on all the dependent targets to build themselves.
/// This function may throw InvalidProjectFileException
/// </summary>
internal void Build
(
ProjectBuildState buildContext
)
{
// Depending on the build state, we may do different things.
switch (buildState)
{
case BuildState.InProgress:
// In single proc mode if the build state was already "in progress"
// and somebody just told us to build ourselves, it means that there is
// a loop (circular dependency) in the target dependency graph. In multi
// proc mode we need to analyze the dependency graph before we can
// tell if there a circular dependency or if two independent chains
// of targets happen to need the result of this target.
if (parentEngine.Router.SingleThreadedMode || buildContext.ContainsCycle(this.Name))
{
ProjectErrorUtilities.VerifyThrowInvalidProject(false, TargetElement, "CircularDependency", targetName);
}
else
{
buildContext.CurrentBuildContextState = ProjectBuildState.BuildContextState.WaitingForTarget;
this.executionState.AddWaitingBuildContext(buildContext);
}
break;
case BuildState.CompletedSuccessfully:
case BuildState.CompletedUnsuccessfully:
// If this target has already been built as part of this project,
// we're not going to build it again. Just return the result
// from when it was built previously. Note: This condition
// could really only ever hold true if the user specifically
// requested us to build multiple targets and there existed
// a direct or indirect dependency relationship between two or
// more of those top-level targets.
// Note: we aren't really entering the target in question here, so don't use the target
// event context. Using the target ID for skipped messages would force us to
// cache the individual target IDs for unloaded projects and it's not really worth the trouble.
// Just use the parent event context.
parentEngine.LoggingServices.LogComment(buildContext.ProjectBuildEventContext,
(buildState == BuildState.CompletedSuccessfully) ? "TargetAlreadyCompleteSuccess" : "TargetAlreadyCompleteFailure",
this.targetName);
// Only contexts which are generated from an MSBuild task could need
// the outputs of this target, such contexts have a non-null evaluation
// request
if ((buildState == BuildState.CompletedSuccessfully) &&
(buildContext.BuildRequest.OutputsByTarget != null &&
buildContext.NameOfBlockingTarget == null))
{
error.VerifyThrow(
String.Equals(EscapingUtilities.UnescapeAll(buildContext.NameOfTargetInProgress), this.Name, StringComparison.OrdinalIgnoreCase),
"The name of the target in progress is inconsistent with the target being built");
error.VerifyThrow(targetOutputItems != null,
"If the target built successfully, we must have its outputs.");
buildContext.BuildRequest.OutputsByTarget[Name] = targetOutputItems.ToArray();
}
if (buildContext.NameOfBlockingTarget == null)
{
buildContext.BuildRequest.ResultByTarget[Name] = buildState;
}
break;
case BuildState.NotStarted:
case BuildState.Skipped:
{
// Always have to create a new context in build as other projects or targets may try and build this target
BuildEventContext targetBuildEventContext = new BuildEventContext
(
buildContext.ProjectBuildEventContext.NodeId,
this.id,
buildContext.ProjectBuildEventContext.ProjectContextId,
buildContext.ProjectBuildEventContext.TaskId
);
Expander expander = new Expander(this.parentProject.evaluatedProperties, this.parentProject.evaluatedItemsByName);
// We first make sure no batching was attempted with the target's condition.
if (!conditionCheckedForInvalidMetadataReferences)
{
if (ExpressionShredder.ContainsMetadataExpressionOutsideTransform(this.Condition))
{
ProjectErrorUtilities.ThrowInvalidProject(this.conditionAttribute, "TargetConditionHasInvalidMetadataReference", targetName, this.Condition);
}
conditionCheckedForInvalidMetadataReferences = true;
}
// If condition is false (based on propertyBag), set this target's state to
// "Skipped" since we won't actually build it.
if (!Utilities.EvaluateCondition(this.Condition, this.conditionAttribute,
expander, null, ParserOptions.AllowProperties | ParserOptions.AllowItemLists,
parentEngine.LoggingServices, targetBuildEventContext))
{
buildState = BuildState.Skipped;
if (buildContext.NameOfBlockingTarget == null)
{
buildContext.BuildRequest.ResultByTarget[Name] = buildState;
}
if (!parentEngine.LoggingServices.OnlyLogCriticalEvents)
{
// Expand the expression for the Log.
string expanded = expander.ExpandAllIntoString(this.Condition, this.conditionAttribute);
// By design: Not building dependencies. This is what NAnt does too.
parentEngine.LoggingServices.LogComment(targetBuildEventContext, "TargetSkippedFalseCondition",
this.targetName, this.Condition, expanded);
}
}
else
{
// This target has not been built yet. So build it!
// Change our state to "in progress". TargetParameters will need to be re-calculated if Inputs and Outputs attribute has changed.
buildState = BuildState.InProgress;
List<string> batchableTargetParameters = GetBatchableTargetParameters();
executionState = new TargetExecutionWrapper(this, taskElementList, batchableTargetParameters, targetElement, expander, targetBuildEventContext);
ContinueBuild(buildContext, null);
}
}
break;
default:
error.VerifyThrow(false, "Build state {0} not handled in Target.Build method", buildState);
break;
}
}
/// <summary>
/// This method is called repeatedly to execute the target in multi-threaded mode. In single
/// threaded mode it is called once and it loops internally until the execution is finished.
/// </summary>
/// <param name="buildContext">Context within which the target is being executed</param>
/// <param name="taskExecutionContext">Result of last execution (multi-threaded only)</param>
internal void ContinueBuild(ProjectBuildState buildContext, TaskExecutionContext taskExecutionContext)
{
executionState.ContinueBuild(buildContext, taskExecutionContext);
}
/// <summary>
/// Executes a task within a target. This method initializes a task engine for the given task, and then executes the task
/// using the engine.
/// </summary>
/// <param name="taskNode"></param>
/// <param name="hostObject"></param>
/// <returns>true, if successful</returns>
internal bool ExecuteOneTask(XmlElement taskNode, ITaskHost hostObject)
{
string projectFileOfTaskNode = XmlUtilities.GetXmlNodeFile(taskNode, parentProject.FullFileName);
BuildEventContext targetBuildEventContext = new BuildEventContext
(
ParentProject.ProjectBuildEventContext.NodeId,
this.id,
ParentProject.ProjectBuildEventContext.ProjectContextId,
ParentProject.ProjectBuildEventContext.TaskId
);
int handleId = parentEngine.EngineCallback.CreateTaskContext(ParentProject, this, null, taskNode,
EngineCallback.inProcNode, targetBuildEventContext);
TaskExecutionModule taskExecutionModule = parentEngine.NodeManager.TaskExecutionModule;
TaskEngine taskEngine = new TaskEngine(taskNode, hostObject, parentProject.FullFileName, projectFileOfTaskNode, parentEngine.LoggingServices, handleId, taskExecutionModule, targetBuildEventContext);
return taskEngine.ExecuteTask
(
TaskExecutionMode.ExecuteTaskAndGatherOutputs,
new Lookup(parentProject.evaluatedItemsByName, parentProject.evaluatedProperties, ParentProject.ItemDefinitionLibrary)
);
}
/// <summary>
/// Indicates that something has changed within the <Target> element, so the project
/// needs to be saved and re-evaluated at next build.
/// </summary>
internal void MarkTargetAsDirty
(
)
{
// This is a change to the contents of the project file.
this.ParentProject?.MarkProjectAsDirty();
}
/// <summary>
/// Sets or removes an attribute from the target element. Marks the target dirty after the update
/// </summary>
/// <param name="attributeName"></param>
/// <param name="attributeValue"></param>
/// <returns>XmlAttribute which has been updated</returns>
internal XmlAttribute SetOrRemoveTargetAttribute
(
string attributeName,
string attributeValue
)
{
// If this Target object is not actually represented by a
// <Target> element in the parentProject.file, then do not allow
// the caller to set the condition.
error.VerifyThrowInvalidOperation(this.targetElement != null, "CannotSetCondition");
// If this item was imported from another parentProject. we don't allow modifying it.
error.VerifyThrowInvalidOperation(!this.importedFromAnotherProject, "CannotModifyImportedProjects");
XmlAttribute updatedAttribute = ProjectXmlUtilities.SetOrRemoveAttribute(targetElement, attributeName, attributeValue);
// Mark the project dirty after an attribute has been updated
this.MarkTargetAsDirty();
return updatedAttribute;
}
/// <summary>
/// Adds a task with the specified name to the end of this target. This method
/// does all of the work to manipulate the project's XML content.
/// </summary>
/// <param name="taskName"></param>
public BuildTask AddNewTask
(
string taskName
)
{
error.VerifyThrow(this.taskElementList != null, "Arraylist not initialized!");
error.VerifyThrowArgumentLength(taskName, nameof(taskName));
// Confirm that it's not an imported target.
error.VerifyThrowInvalidOperation(!this.IsImported, "CannotModifyImportedProjects");
// Create the XML for the new task node and append it to the very end of the <Target> element.
XmlElement newTaskElement = this.targetElement.OwnerDocument.CreateElement(taskName, XMakeAttributes.defaultXmlNamespace);
this.targetElement.AppendChild(newTaskElement);
// Create a new BuildTask object, and add it to our list.
BuildTask newTask = new BuildTask(newTaskElement, this, false);
this.taskElementList.Add(newTask);
this.MarkTargetAsDirty();
return newTask;
}
/// <summary>
/// Removes the specified BuildTask from the target. This method correctly updates
/// the project's XML content, so the task will no longer show up when the project
/// is saved out.
/// </summary>
/// <param name="taskElement"></param>
public void RemoveTask
(
BuildTask taskElement
)
{
// Confirm that it's not an imported target.
error.VerifyThrowInvalidOperation(!this.IsImported, "CannotModifyImportedProjects");
error.VerifyThrow(this.taskElementList != null, "Arraylist not initialized!");
error.VerifyThrowArgumentNull(taskElement, nameof(taskElement));
// Confirm that the BuildTask belongs to this Target.
error.VerifyThrowInvalidOperation(taskElement.ParentTarget == this,
"IncorrectObjectAssociation", "BuildTask", "Target");
// Remove the BuildTask from our list.
this.taskElementList.Remove(taskElement);
// Remove the task's XML from the project document.
this.targetElement.RemoveChild(taskElement.TaskXmlElement);
// Dissociate the BuildTask from this target.
taskElement.ParentTarget = null;
this.MarkTargetAsDirty();
}
#endregion
}
}
|