5 // Atsushi Enomoto (atsushi@xamarin.com)
7 // Copyright (C) 2013 Xamarin Inc. (http://www.xamarin.com)
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 using System.Collections;
30 using System.Collections.Generic;
31 using Microsoft.Build.BuildEngine;
32 using Microsoft.Build.Execution;
33 using Microsoft.Build.Framework;
34 using Microsoft.Build.Evaluation;
37 using Microsoft.Build.Exceptions;
39 namespace Microsoft.Build.Internal
48 public BuildEngine4 (BuildSubmission submission)
50 this.submission = submission;
51 event_source = new EventSource ();
52 if (submission.BuildManager.OngoingBuildParameters.Loggers != null)
53 foreach (var l in submission.BuildManager.OngoingBuildParameters.Loggers)
54 l.Initialize (event_source);
57 BuildSubmission submission;
58 ProjectInstance project;
59 ProjectTargetInstance current_target;
60 ProjectTaskInstance current_task;
61 EventSource event_source;
63 public ProjectCollection Projects {
64 get { return submission.BuildManager.OngoingBuildParameters.ProjectCollection; }
68 // While we are not faced to implement those features, there are some modern task execution requirements.
70 // This will have to be available for "out of process" nodes (see NodeAffinity).
71 // NodeAffinity is set per project file at BuildManager.HostServices.
72 // When NodeAffinity is set to OutOfProc, it should probably launch different build host
73 // that runs separate build tasks. (.NET has MSBuildTaskHost.exe which I guess is about that.)
75 // Also note that the complete implementation has to support LoadInSeparateAppDomainAttribute
76 // (which is most likely derived from AppDomainIsolatedBuildTask) that marks a task to run
77 // in separate AppDomain.
79 public void BuildProject (Func<bool> checkCancel, BuildResult result, ProjectInstance project, IEnumerable<string> targetNames, IDictionary<string,string> globalProperties, IDictionary<string,string> targetOutputs, string toolsVersion)
81 var parameters = submission.BuildManager.OngoingBuildParameters;
82 var buildTaskFactory = new BuildTaskFactory (BuildTaskDatabase.GetDefaultTaskDatabase (parameters.GetToolset (toolsVersion)), submission.BuildRequest.ProjectInstance.TaskDatabase);
83 BuildProject (new InternalBuildArguments () { CheckCancel = checkCancel, Result = result, Project = project, TargetNames = targetNames, GlobalProperties = globalProperties, TargetOutputs = targetOutputs, ToolsVersion = toolsVersion, BuildTaskFactory = buildTaskFactory });
86 class InternalBuildArguments
88 public Func<bool> CheckCancel;
89 public BuildResult Result;
90 public ProjectInstance Project;
91 public IEnumerable<string> TargetNames;
92 public IDictionary<string,string> GlobalProperties;
93 public IDictionary<string,string> TargetOutputs;
94 public string ToolsVersion;
95 public BuildTaskFactory BuildTaskFactory;
97 public void AddTargetResult (string targetName, TargetResult targetResult)
99 if (!Result.HasResultsForTarget (targetName))
100 Result.AddResultsForTarget (targetName, targetResult);
104 void BuildProject (InternalBuildArguments args)
106 var request = submission.BuildRequest;
107 var parameters = submission.BuildManager.OngoingBuildParameters;
108 this.project = args.Project;
110 event_source.FireBuildStarted (this, new BuildStartedEventArgs ("Build Started", null));
112 var initialPropertiesFormatted = "Initial Properties:\n" + string.Join (Environment.NewLine, project.Properties.OrderBy (p => p.Name).Select (p => string.Format ("{0} = {1}", p.Name, p.EvaluatedValue)).ToArray ());
113 event_source.FireMessageRaised (this, new BuildMessageEventArgs (initialPropertiesFormatted, null, null, MessageImportance.Low));
115 // null targets -> success. empty targets -> success(!)
116 if (request.TargetNames == null)
117 args.Result.OverallResult = BuildResultCode.Success;
119 foreach (var targetName in request.TargetNames.Where (t => t != null))
120 args.AddTargetResult (targetName, BuildTargetByName (targetName, args));
122 // FIXME: check .NET behavior, whether cancellation always results in failure.
123 args.Result.OverallResult = args.CheckCancel () ? BuildResultCode.Failure : args.Result.ResultsByTarget.Select (p => p.Value).Any (r => r.ResultCode == TargetResultCode.Failure) ? BuildResultCode.Failure : BuildResultCode.Success;
125 event_source.FireBuildFinished (this, new BuildFinishedEventArgs ("Build Finished.", null, args.Result.OverallResult == BuildResultCode.Success));
128 TargetResult BuildTargetByName (string targetName, InternalBuildArguments args)
130 var targetResult = new TargetResult ();
132 var request = submission.BuildRequest;
133 var parameters = submission.BuildManager.OngoingBuildParameters;
134 ProjectTargetInstance target;
136 // FIXME: check skip condition
138 targetResult.Skip ();
139 // null key is allowed and regarded as blind success(!) (as long as it could retrieve target)
140 else if (!request.ProjectInstance.Targets.TryGetValue (targetName, out target))
141 targetResult.Failure (new InvalidOperationException (string.Format ("target '{0}' was not found in project '{1}'", targetName, project.FullPath)));
143 current_target = target;
145 if (!DoBuildTarget (targetResult, args))
148 current_target = null;
150 Func<string,ITaskItem> creator = s => new TargetOutputTaskItem () { ItemSpec = s };
151 var items = args.Project.GetAllItems (target.Outputs, string.Empty, creator, creator, s => true, (t, s) => {
153 targetResult.Success (items);
154 event_source.FireTargetFinished (this, new TargetFinishedEventArgs ("Target Finished", null, targetName, project.FullPath, target.FullPath, true));
159 bool DoBuildTarget (TargetResult targetResult, InternalBuildArguments args)
161 var request = submission.BuildRequest;
162 var target = current_target;
164 // process DependsOnTargets first.
165 foreach (var dep in project.ExpandString (target.DependsOnTargets).Split (';').Where (s => !string.IsNullOrEmpty (s)).Select (s => s.Trim ())) {
166 var result = BuildTargetByName (dep, args);
167 args.AddTargetResult (dep, result);
168 if (result.ResultCode == TargetResultCode.Failure) {
169 targetResult.Failure (null);
174 event_source.FireTargetStarted (this, new TargetStartedEventArgs ("Target Started", null, target.Name, project.FullPath, target.FullPath));
176 // Here we check cancellation (only after TargetStarted event).
177 if (args.CheckCancel ()) {
178 targetResult.Failure (new BuildAbortedException ("Build has canceled"));
182 var propsToRestore = new Dictionary<string,string> ();
183 var itemsToRemove = new List<ProjectItemInstance> ();
185 // Evaluate additional target properties
186 foreach (var c in target.Children.OfType<ProjectPropertyGroupTaskInstance> ()) {
187 if (!args.Project.EvaluateCondition (c.Condition))
189 foreach (var p in c.Properties) {
190 if (!args.Project.EvaluateCondition (p.Condition))
192 var value = args.Project.ExpandString (p.Value);
193 propsToRestore.Add (p.Name, project.GetPropertyValue (value));
194 project.SetProperty (p.Name, value);
198 // Evaluate additional target items
199 foreach (var c in target.Children.OfType<ProjectItemGroupTaskInstance> ()) {
200 if (!args.Project.EvaluateCondition (c.Condition))
202 foreach (var item in c.Items) {
203 Func<string,ProjectItemInstance> creator = i => new ProjectItemInstance (project, item.ItemType, item.Metadata.Select (m => new KeyValuePair<string,string> (m.Name, m.Value)), i);
204 foreach (var ti in project.GetAllItems (item.Include, item.Exclude, creator, creator, s => s == item.ItemType, (ti, s) => ti.SetMetadata ("RecurseDir", s)))
205 itemsToRemove.Add (ti);
209 foreach (var c in target.Children.OfType<ProjectOnErrorInstance> ()) {
210 if (!args.Project.EvaluateCondition (c.Condition))
212 throw new NotImplementedException ();
216 foreach (var ti in target.Children.OfType<ProjectTaskInstance> ()) {
218 if (!args.Project.EvaluateCondition (ti.Condition)) {
219 event_source.FireMessageRaised (this, new BuildMessageEventArgs (string.Format ("Task '{0}' was skipped because condition '{1}' wasn't met.", ti.Name, ti.Condition), null, null, MessageImportance.Low));
222 if (!RunBuildTask (ti, targetResult, args))
226 // restore temporary property state to the original state.
227 foreach (var p in propsToRestore) {
228 if (p.Value == string.Empty)
229 project.RemoveProperty (p.Key);
231 project.SetProperty (p.Key, p.Value);
233 foreach (var item in itemsToRemove)
234 project.RemoveItem (item);
239 bool RunBuildTask (ProjectTaskInstance ti, TargetResult targetResult, InternalBuildArguments args)
241 var request = submission.BuildRequest;
242 var target = current_target;
244 var host = request.HostServices == null ? null : request.HostServices.GetHostObject (request.ProjectFullPath, target.Name, ti.Name);
246 // Create Task instance.
247 var factoryIdentityParameters = new Dictionary<string,string> ();
249 factoryIdentityParameters ["MSBuildRuntime"] = ti.MSBuildRuntime;
250 factoryIdentityParameters ["MSBuildArchitecture"] = ti.MSBuildArchitecture;
252 var task = args.BuildTaskFactory.CreateTask (ti.Name, factoryIdentityParameters, this);
253 event_source.FireMessageRaised (this, new BuildMessageEventArgs (string.Format ("Using task {0} from {1}", ti.Name, task.GetType ().AssemblyQualifiedName), null, null, MessageImportance.Low));
254 task.HostObject = host;
255 task.BuildEngine = this;
257 // Prepare task parameters.
258 var props = task.GetType ().GetProperties ()
259 .Where (p => p.CanWrite && p.GetCustomAttributes (typeof (RequiredAttribute), true).Any ());
260 var missings = props.Where (p => !ti.Parameters.Any (tp => tp.Key.Equals (p.Name, StringComparison.OrdinalIgnoreCase)));
262 throw new InvalidOperationException (string.Format ("Task {0} of type {1} is used without specifying mandatory property: {2}",
263 ti.Name, task.GetType (), string.Join (", ", missings.Select (p => p.Name).ToArray ())));
265 foreach (var p in ti.Parameters) {
266 var prop = task.GetType ().GetProperty (p.Key);
267 var value = project.ExpandString (p.Value);
269 throw new InvalidOperationException (string.Format ("Task {0} does not have property {1}", ti.Name, p.Key));
271 throw new InvalidOperationException (string.Format ("Task {0} has property {1} but it is read-only.", ti.Name, p.Key));
272 var valueInstance = ConvertTo (value, prop.PropertyType);
273 prop.SetValue (task, valueInstance, null);
277 event_source.FireTaskStarted (this, new TaskStartedEventArgs ("Task Started", null, project.FullPath, ti.FullPath, ti.Name));
278 var taskSuccess = task.Execute ();
281 event_source.FireTaskFinished (this, new TaskFinishedEventArgs ("Task Finished", null, project.FullPath, ti.FullPath, ti.Name, false));
282 targetResult.Failure (null);
283 if (!ContinueOnError) {
284 event_source.FireTargetFinished (this, new TargetFinishedEventArgs ("Target Failed", null, target.Name, project.FullPath, target.FullPath, false));
288 // Evaluate task output properties and items.
289 event_source.FireTaskFinished (this, new TaskFinishedEventArgs ("Task Finished", null, project.FullPath, ti.FullPath, ti.Name, true));
290 foreach (var to in ti.Outputs) {
291 if (!project.EvaluateCondition (to.Condition))
293 var toItem = to as ProjectTaskOutputItemInstance;
294 var toProp = to as ProjectTaskOutputPropertyInstance;
295 string taskParameter = toItem != null ? toItem.TaskParameter : toProp.TaskParameter;
296 var pi = task.GetType ().GetProperty (taskParameter);
298 throw new InvalidOperationException (string.Format ("Task {0} does not have property {1} specified as TaskParameter", ti.Name, toItem.TaskParameter));
300 throw new InvalidOperationException (string.Format ("Task {0} has property {1} specified as TaskParameter, but it is write-only", ti.Name, toItem.TaskParameter));
302 args.Project.AddItem (toItem.ItemType, ConvertFrom (pi.GetValue (task, null)));
304 args.Project.SetProperty (toProp.PropertyName, ConvertFrom (pi.GetValue (task, null)));
311 object ConvertTo (string source, Type targetType)
313 if (targetType == typeof (ITaskItem) || targetType.IsSubclassOf (typeof (ITaskItem)))
314 return new TargetOutputTaskItem () { ItemSpec = WindowsCompatibilityExtensions.NormalizeFilePath (source) };
315 if (targetType.IsArray)
316 return new ArrayList (source.Split (';').Where (s => !string.IsNullOrEmpty (s)).Select (s => ConvertTo (s, targetType.GetElementType ())).ToArray ())
317 .ToArray (targetType.GetElementType ());
319 return Convert.ChangeType (source, targetType);
322 string ConvertFrom (object source)
326 if (source is ITaskItem)
327 return ((ITaskItem) source).ItemSpec;
328 if (source.GetType ().IsArray)
329 return string.Join (":", ((Array) source).Cast<object> ().Select (o => ConvertFrom (o)).ToArray ());
331 return (string) Convert.ChangeType (source, typeof (string));
334 class TargetOutputTaskItem : ITaskItem2
336 Hashtable metadata = new Hashtable ();
338 #region ITaskItem2 implementation
339 public string GetMetadataValueEscaped (string metadataName)
341 return ProjectCollection.Escape ((string) metadata [metadataName]);
343 public void SetMetadataValueLiteral (string metadataName, string metadataValue)
345 metadata [metadataName] = ProjectCollection.Unescape (metadataValue);
347 public IDictionary CloneCustomMetadataEscaped ()
349 var ret = new Hashtable ();
350 foreach (DictionaryEntry e in metadata)
351 ret [e.Key] = ProjectCollection.Escape ((string) e.Value);
354 public string EvaluatedIncludeEscaped {
355 get { return ProjectCollection.Escape (ItemSpec); }
356 set { ItemSpec = ProjectCollection.Unescape (value); }
359 #region ITaskItem implementation
360 public IDictionary CloneCustomMetadata ()
362 return new Hashtable (metadata);
364 public void CopyMetadataTo (ITaskItem destinationItem)
366 foreach (DictionaryEntry e in metadata)
367 destinationItem.SetMetadata ((string) e.Key, (string) e.Value);
369 public string GetMetadata (string metadataName)
371 var wk = ProjectCollection.GetWellKnownMetadata (metadataName, ItemSpec, Path.GetFullPath, null);
374 return (string) metadata [metadataName];
376 public void RemoveMetadata (string metadataName)
378 metadata.Remove (metadataName);
380 public void SetMetadata (string metadataName, string metadataValue)
382 metadata [metadataName] = metadataValue;
384 public string ItemSpec { get; set; }
385 public int MetadataCount {
386 get { return metadata.Count; }
388 public ICollection MetadataNames {
389 get { return metadata.Keys; }
395 #region IBuildEngine4 implementation
397 // task objects are not in use anyways though...
399 class TaskObjectRegistration
401 public TaskObjectRegistration (object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection)
406 AllowEarlyCollection = allowEarlyCollection;
408 public object Key { get; private set; }
409 public object Object { get; private set; }
410 public RegisteredTaskObjectLifetime Lifetime { get; private set; }
411 public bool AllowEarlyCollection { get; private set; }
414 List<TaskObjectRegistration> task_objects = new List<TaskObjectRegistration> ();
416 public object GetRegisteredTaskObject (object key, RegisteredTaskObjectLifetime lifetime)
418 var reg = task_objects.FirstOrDefault (t => t.Key == key && t.Lifetime == lifetime);
419 return reg != null ? reg.Object : null;
422 public void RegisterTaskObject (object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection)
424 task_objects.Add (new TaskObjectRegistration (key, obj, lifetime, allowEarlyCollection));
427 public object UnregisterTaskObject (object key, RegisteredTaskObjectLifetime lifetime)
429 var reg = task_objects.FirstOrDefault (t => t.Key == key && t.Lifetime == lifetime);
431 task_objects.Remove (reg);
437 #region IBuildEngine3 implementation
439 public BuildEngineResult BuildProjectFilesInParallel (string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IList<string>[] removeGlobalProperties, string[] toolsVersion, bool returnTargetOutputs)
441 throw new NotImplementedException ();
444 public void Reacquire ()
446 throw new NotImplementedException ();
451 throw new NotImplementedException ();
456 #region IBuildEngine2 implementation
458 public bool BuildProjectFile (string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion)
460 var proj = GetProjectInstance (projectFileName, toolsVersion);
461 var globalPropertiesThatMakeSense = new Dictionary<string,string> ();
462 foreach (DictionaryEntry p in globalProperties)
463 globalPropertiesThatMakeSense [(string) p.Key] = (string) p.Value;
464 var result = new BuildResult ();
465 var outputs = new Dictionary<string, string> ();
466 BuildProject (() => false, result, proj, targetNames, globalPropertiesThatMakeSense, outputs, toolsVersion);
467 foreach (var p in outputs)
468 targetOutputs [p.Key] = p.Value;
469 return result.OverallResult == BuildResultCode.Success;
472 public bool BuildProjectFilesInParallel (string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion)
474 throw new NotImplementedException ();
477 public bool IsRunningMultipleNodes {
479 throw new NotImplementedException ();
483 ProjectInstance GetProjectInstance (string projectFileName, string toolsVersion)
485 string fullPath = Path.GetFullPath (projectFileName);
486 if (submission.BuildRequest.ProjectFullPath == fullPath)
487 return submission.BuildRequest.ProjectInstance;
488 // FIXME: could also be filtered by global properties
489 // http://msdn.microsoft.com/en-us/library/microsoft.build.evaluation.projectcollection.getloadedprojects.aspx
490 var project = Projects.GetLoadedProjects (projectFileName).FirstOrDefault (p => p.ToolsVersion == toolsVersion);
492 throw new InvalidOperationException (string.Format ("Project '{0}' is not loaded", projectFileName));
493 return submission.BuildManager.GetProjectInstanceForBuild (project);
498 #region IBuildEngine implementation
500 public bool BuildProjectFile (string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs)
502 return BuildProjectFile (projectFileName, targetNames, globalProperties, targetOutputs, Projects.DefaultToolsVersion);
505 public void LogCustomEvent (CustomBuildEventArgs e)
507 event_source.FireCustomEventRaised (this, e);
510 public void LogErrorEvent (BuildErrorEventArgs e)
512 event_source.FireErrorRaised (this, e);
515 public void LogMessageEvent (BuildMessageEventArgs e)
517 event_source.FireMessageRaised (this, e);
520 public void LogWarningEvent (BuildWarningEventArgs e)
522 event_source.FireWarningRaised (this, e);
525 public int ColumnNumberOfTaskNode {
526 get { return current_task.Location != null ? current_task.Location.Column : 0; }
529 public bool ContinueOnError {
530 get { return current_task != null && project.EvaluateCondition (current_task.Condition) && EvaluateContinueOnError (current_task.ContinueOnError); }
533 bool EvaluateContinueOnError (string value)
536 case "WarnAndContinue":
537 case "ErrorAndContinue":
542 // empty means "stop on error", so don't pass empty string to EvaluateCondition().
543 return !string.IsNullOrEmpty (value) && project.EvaluateCondition (value);
546 public int LineNumberOfTaskNode {
547 get { return current_task.Location != null ? current_task.Location.Line : 0; }
550 public string ProjectFileOfTaskNode {
551 get { return current_task.FullPath; }