Merge pull request #5714 from alexischr/update_bockbuild
[mono.git] / mcs / class / Microsoft.Build.Utilities / Microsoft.Build.Utilities / ToolTask.cs
1 //
2 // ToolTask.cs: Base class for command line tool tasks. 
3 //
4 // Authors:
5 //   Marek Sieradzki (marek.sieradzki@gmail.com)
6 //   Ankit Jain (jankit@novell.com)
7 //   Marek Safar (marek.safar@gmail.com)
8 //
9 // (C) 2005 Marek Sieradzki
10 // Copyright 2009 Novell, Inc (http://www.novell.com)
11 // Copyright 2014 Xamarin Inc
12 //
13 // Permission is hereby granted, free of charge, to any person obtaining
14 // a copy of this software and associated documentation files (the
15 // "Software"), to deal in the Software without restriction, including
16 // without limitation the rights to use, copy, modify, merge, publish,
17 // distribute, sublicense, and/or sell copies of the Software, and to
18 // permit persons to whom the Software is furnished to do so, subject to
19 // the following conditions:
20 //
21 // The above copyright notice and this permission notice shall be
22 // included in all copies or substantial portions of the Software.
23 //
24 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31
32 using System;
33 using System.Diagnostics;
34 using System.Collections;
35 using System.Collections.Specialized;
36 using System.IO;
37 using System.Resources;
38 using System.Text;
39 using System.Text.RegularExpressions;
40 using Microsoft.Build.Framework;
41 using Mono.XBuild.Utilities;
42 using System.Threading;
43 using System.Collections.Generic;
44
45 using SCS = System.Collections.Specialized;
46
47 namespace Microsoft.Build.Utilities
48 {
49         public abstract class ToolTask : Task
50                 , ICancelableTask
51         {
52                 int                     exitCode;
53                 int                     timeout;
54                 string                  toolPath, toolExe;
55                 Encoding                responseFileEncoding;
56                 MessageImportance       standardErrorLoggingImportance;
57                 MessageImportance       standardOutputLoggingImportance;
58                 StringBuilder toolOutput;
59                 bool typeLoadException;
60                 ManualResetEvent canceled;
61
62                 protected ToolTask ()
63                         : this (null, null)
64                 {
65                         this.standardErrorLoggingImportance = MessageImportance.High;
66                         this.standardOutputLoggingImportance = MessageImportance.Normal;
67                 }
68
69                 protected ToolTask (ResourceManager taskResources)
70                         : this (taskResources, null)
71                 {
72                 }
73
74                 protected ToolTask (ResourceManager taskResources,
75                                    string helpKeywordPrefix)
76                 {
77                         this.TaskResources = taskResources;
78                         this.HelpKeywordPrefix = helpKeywordPrefix;
79                         this.responseFileEncoding = Encoding.UTF8;
80                         this.timeout = Int32.MaxValue;
81                         canceled = new ManualResetEvent (false);
82                 }
83
84                 [MonoTODO]
85                 protected virtual bool CallHostObjectToExecute ()
86                 {
87                         return true;
88                 }
89
90                 string CreateToolPath ()
91                 {
92                         string tp;
93                         if (string.IsNullOrEmpty (ToolPath)) {
94                                 tp = GenerateFullPathToTool ();
95                                 if (string.IsNullOrEmpty (tp))
96                                         return null;
97
98                                 //
99                                 // GenerateFullPathToTool can return path including tool name
100                                 //
101                                 if (string.IsNullOrEmpty (ToolExe))
102                                         return tp;
103
104                                 tp = Path.GetDirectoryName (tp);
105                         } else {
106                                 tp = ToolPath;
107                         }
108
109                         var     path = Path.Combine (tp, ToolExe);
110                         if (!File.Exists (path)) {
111                                 if (Log != null)
112                                         Log.LogError ("Tool executable '{0}' could not be found", path);
113                                 return null;
114                         }
115
116                         return path;
117                 }
118
119                 public override bool Execute ()
120                 {
121                         if (SkipTaskExecution ())
122                                 return true;
123
124                         var tool_path = CreateToolPath ();
125                         if (tool_path == null)
126                                 return false;
127
128                         exitCode = ExecuteTool (tool_path, GenerateResponseFileCommands (),
129                                 GenerateCommandLineCommands ());
130
131                         // HandleTaskExecutionErrors is called only if exitCode != 0
132                         return exitCode == 0 || HandleTaskExecutionErrors ();
133                 }
134                 
135                 [MonoTODO]
136                 protected virtual string GetWorkingDirectory ()
137                 {
138                         return null;
139                 }
140                 
141                 protected virtual int ExecuteTool (string pathToTool,
142                                                    string responseFileCommands,
143                                                    string commandLineCommands)
144
145                 {
146                         if (pathToTool == null)
147                                 throw new ArgumentNullException ("pathToTool");
148
149                         string responseFileName;
150                         responseFileName = null;
151                         toolOutput = new StringBuilder ();
152
153                         try {
154                                 string responseFileSwitch = String.Empty;
155                                 if (!String.IsNullOrEmpty (responseFileCommands)) {
156                                         responseFileName = Path.GetTempFileName ();
157                                         File.WriteAllText (responseFileName, responseFileCommands);
158                                         responseFileSwitch = GetResponseFileSwitch (responseFileName);
159                                 }
160
161                                 var pinfo = GetProcessStartInfo (pathToTool, commandLineCommands, responseFileSwitch);
162                                 LogToolCommand (String.Format ("Tool {0} execution started with arguments: {1} {2}",
163                                                 pinfo.FileName, commandLineCommands, responseFileCommands));
164
165                                 var pendingLineFragmentOutput = new StringBuilder ();
166                                 var pendingLineFragmentError = new StringBuilder ();
167                                 var environmentOverride = GetAndLogEnvironmentVariables ();
168                                 try {
169                                         // When StartProcess returns, the process has already .Start()'ed
170                                         // If we subscribe to the events after that, then for processes that
171                                         // finish executing before we can subscribe, we won't get the output/err
172                                         // events at all!
173                                         ProcessWrapper pw = ProcessService.StartProcess (pinfo,
174                                                         (_, msg) => ProcessLine (pendingLineFragmentOutput, msg, StandardOutputLoggingImportance),
175                                                         (_, msg) => ProcessLine (pendingLineFragmentError, msg, StandardErrorLoggingImportance),
176                                                         null,
177                                                         environmentOverride);
178
179                                         pw.WaitForOutput (timeout == Int32.MaxValue ? Int32.MaxValue : timeout);
180
181                                         // Process any remaining line
182                                         ProcessLine (pendingLineFragmentOutput, StandardOutputLoggingImportance, true);
183                                         ProcessLine (pendingLineFragmentError, StandardErrorLoggingImportance, true);
184
185                                         exitCode = pw.ExitCode;
186                                         pw.Dispose ();
187                                 } catch (System.ComponentModel.Win32Exception e) {
188                                         Log.LogError ("Error executing tool '{0}': {1}", pathToTool, e.Message);
189                                         return -1;
190                                 }
191
192                                 if (typeLoadException)
193                                         ProcessTypeLoadException ();
194
195                                 pendingLineFragmentOutput.Length = 0;
196                                 pendingLineFragmentError.Length = 0;
197
198                                 Log.LogMessage (MessageImportance.Low, "Tool {0} execution finished.", pathToTool);
199                                 return exitCode;
200                         } finally {
201                                 DeleteTempFile (responseFileName);
202                         }
203                 }
204
205                 void ProcessTypeLoadException ()
206                 {
207                         string output_str = toolOutput.ToString ();
208                         Regex reg  = new Regex (@".*WARNING.*used in (mscorlib|System),.*",
209                                         RegexOptions.Multiline);
210
211                         if (reg.Match (output_str).Success)
212                                 Log.LogError (
213                                         "Error: A referenced assembly may be built with an incompatible " +
214                                         "CLR version. See the compilation output for more details.");
215                         else
216                                 Log.LogError (
217                                         "Error: A dependency of a referenced assembly may be missing, or " +
218                                         "you may be referencing an assembly created with a newer CLR " +
219                                         "version. See the compilation output for more details.");
220
221                         Log.LogError (output_str);
222                 }
223
224                 void ProcessLine (StringBuilder outputBuilder, MessageImportance importance, bool isLastLine)
225                 {
226                         if (outputBuilder.Length == 0)
227                                 return;
228
229                         if (isLastLine && !outputBuilder.ToString ().EndsWith (Environment.NewLine))
230                                 // last line, but w/o an trailing newline, so add that
231                                 outputBuilder.Append (Environment.NewLine);
232
233                         ProcessLine (outputBuilder, null, importance);
234                 }
235
236                 void ProcessLine (StringBuilder outputBuilder, string line, MessageImportance importance)
237                 {
238                         // Add to any line fragment from previous call
239                         if (line != null)
240                                 outputBuilder.Append (line);
241
242                         // Don't remove empty lines!
243                         var lines = outputBuilder.ToString ().Split (new string [] {Environment.NewLine}, StringSplitOptions.None);
244
245                         // Clear the builder. If any incomplete line is found,
246                         // then it will get added back
247                         outputBuilder.Length = 0;
248                         for (int i = 0; i < lines.Length; i ++) {
249                                 string singleLine = lines [i];
250                                 if (i == lines.Length - 1 && !singleLine.EndsWith (Environment.NewLine)) {
251                                         // Last line doesn't end in newline, could be part of
252                                         // a bigger line. Save for later processing
253                                         outputBuilder.Append (singleLine);
254                                         continue;
255                                 }
256
257                                 toolOutput.AppendLine (singleLine);
258
259                                 // in case of typeLoadException, collect all the output
260                                 // and then handle in ProcessTypeLoadException
261                                 if (!typeLoadException)
262                                         LogEventsFromTextOutput (singleLine, importance);
263                         }
264                 }
265
266                 protected virtual void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance)
267                 {
268                         if (singleLine.Length == 0) {
269                                 Log.LogMessage (singleLine, messageImportance);
270                                 return;
271                         }
272
273                         if (singleLine.StartsWith ("Unhandled Exception: System.TypeLoadException") ||
274                             singleLine.StartsWith ("Unhandled Exception: System.IO.FileNotFoundException")) {
275                                 typeLoadException = true;
276                         }
277
278                         // When IncludeDebugInformation is true, prevents the debug symbols stats from braeking this.
279                         if (singleLine.StartsWith ("WROTE SYMFILE") ||
280                                 singleLine.StartsWith ("OffsetTable") ||
281                                 singleLine.StartsWith ("Compilation succeeded") ||
282                                 singleLine.StartsWith ("Compilation failed"))
283                                 return;
284
285                         var result = MSBuildErrorParser.TryParseLine (singleLine);
286                         if (result == null) {
287                                 Log.LogMessage (messageImportance, singleLine);
288                                 return;
289                         }
290
291                         string filename = result.Origin ?? GetType ().Name.ToUpper ();
292
293                         if (result.IsError) {
294                                 Log.LogError (
295                                         result.Subcategory,
296                                         result.Code,
297                                         null,
298                                         filename,
299                                         result.Line,
300                                         result.Column,
301                                         result.EndLine,
302                                         result.EndColumn,
303                                         result.Message
304                                 );
305                         } else {
306                                 Log.LogWarning (
307                                         result.Subcategory,
308                                         result.Code,
309                                         null,
310                                         filename,
311                                         result.Line,
312                                         result.Column,
313                                         result.EndLine,
314                                         result.EndColumn,
315                                         result.Message
316                                 );
317                         }
318                 }
319
320                 protected virtual string GenerateCommandLineCommands ()
321                 {
322                         return "";
323                 }
324
325                 protected abstract string GenerateFullPathToTool ();
326
327                 protected virtual string GenerateResponseFileCommands ()
328                 {
329                         return "";
330                 }
331
332                 protected virtual string GetResponseFileSwitch (string responseFilePath)
333                 {
334                         return String.Format ("@{0}", responseFilePath);
335                 }
336
337                 protected ProcessStartInfo GetProcessStartInfo (string pathToTool, string commandLineCommands, string responseFileSwitch)
338                 {
339                         var pinfo = new ProcessStartInfo (pathToTool, String.Format ("{0} {1}", commandLineCommands, responseFileSwitch));
340
341                         pinfo.WorkingDirectory = GetWorkingDirectory () ?? Environment.CurrentDirectory;
342                         pinfo.UseShellExecute = false;
343                         pinfo.CreateNoWindow = true;
344                         pinfo.RedirectStandardOutput = true;
345                         pinfo.RedirectStandardError = true;
346
347                         return pinfo;
348                 }
349
350                 protected virtual bool HandleTaskExecutionErrors ()
351                 {
352                         if (!Log.HasLoggedErrors && exitCode != 0)
353                                 Log.LogError ("Tool exited with code: {0}. Output: {1}", exitCode,
354                                                 toolOutput != null ? toolOutput.ToString () : String.Empty);
355                         toolOutput = null;
356
357                         return ExitCode == 0 && !Log.HasLoggedErrors;
358                 }
359
360                 protected virtual HostObjectInitializationStatus InitializeHostObject ()
361                 {
362                         return HostObjectInitializationStatus.NoActionReturnSuccess;
363                 }
364
365                 protected virtual void LogToolCommand (string message)
366                 {
367                         Log.LogMessage (MessageImportance.Normal, message);
368                 }
369                 
370                 [MonoTODO]
371                 protected virtual void LogPathToTool (string toolName,
372                                                       string pathToTool)
373                 {
374                 }
375
376                 protected virtual bool SkipTaskExecution()
377                 {
378                         return !ValidateParameters ();
379                 }
380
381                 protected virtual bool ValidateParameters()
382                 {
383                         return true;
384                 }
385
386                 protected void DeleteTempFile (string fileName)
387                 {
388                         if (String.IsNullOrEmpty (fileName))
389                                 return;
390
391                         try {
392                                 File.Delete (fileName);
393                         } catch (IOException ioe) {
394                                 Log.LogWarning ("Unable to delete temporary file '{0}' : {1}", ioe.Message);
395                         } catch (UnauthorizedAccessException uae) {
396                                 Log.LogWarning ("Unable to delete temporary file '{0}' : {1}", uae.Message);
397                         }
398                 }
399
400                 // If EnvironmentVariables is defined, then merge EnvironmentOverride
401                 // EnvironmentOverride is Obsolete'd in 4.0
402                 //
403                 // Returns the final set of environment variables and logs them
404                 Dictionary<string, string> GetAndLogEnvironmentVariables ()
405                 {
406                         var env_vars = GetEnvironmentVariables ();
407                         if (env_vars == null)
408                                 return env_vars;
409
410                         Log.LogMessage (MessageImportance.Low, "Environment variables being passed to the tool:");
411                         foreach (var entry in env_vars)
412                                 Log.LogMessage (MessageImportance.Low, "\t{0}={1}", (string)entry.Key, (string)entry.Value);
413
414                         return env_vars;
415                 }
416
417                 Dictionary<string, string> GetEnvironmentVariables ()
418                 {
419                         var env_vars = new Dictionary<string, string> (StringComparer.InvariantCultureIgnoreCase);
420
421                         if (EnvironmentVariables != null) {
422                                 foreach (string pair in EnvironmentVariables) {
423                                         string [] key_value = pair.Split ('=');
424                                         if (!String.IsNullOrEmpty (key_value [0]))
425                                                 env_vars [key_value [0]] = key_value.Length > 1 ? key_value [1] : String.Empty;
426                                 }
427                         }
428
429                         if (EnvironmentOverride != null)
430                                 foreach (DictionaryEntry entry in EnvironmentOverride)
431                                         env_vars [(string)entry.Key] = (string)entry.Value;
432
433                         return env_vars;
434                 }
435
436                 protected virtual StringDictionary EnvironmentOverride
437                 {
438                         get { return null; }
439                 }
440
441                 // Ignore EnvironmentOverride if this is set
442                 public string[] EnvironmentVariables { get; set; }
443
444                 [Output]
445                 public int ExitCode {
446                         get { return exitCode; }
447                 }
448
449                 protected virtual Encoding ResponseFileEncoding
450                 {
451                         get { return responseFileEncoding; }
452                 }
453
454                 protected virtual Encoding StandardErrorEncoding
455                 {
456                         get { return Console.Error.Encoding; }
457                 }
458
459                 protected virtual MessageImportance StandardErrorLoggingImportance {
460                         get { return standardErrorLoggingImportance; }
461                 }
462
463                 protected virtual Encoding StandardOutputEncoding
464                 {
465                         get { return Console.Out.Encoding; }
466                 }
467
468                 protected virtual MessageImportance StandardOutputLoggingImportance {
469                         get { return standardOutputLoggingImportance; }
470                 }
471
472                 protected virtual bool HasLoggedErrors {
473                         get { return Log.HasLoggedErrors; }
474                 }
475
476                 public virtual int Timeout
477                 {
478                         get { return timeout; }
479                         set { timeout = value; }
480                 }
481
482                 public virtual string ToolExe
483                 {
484                         get {
485                                 if (string.IsNullOrEmpty (toolExe))
486                                         return ToolName;
487                                 else
488                                         return toolExe;
489                         }
490                         set { toolExe = value; }
491                 }
492
493                 protected abstract string ToolName
494                 {
495                         get;
496                 }
497
498                 public string ToolPath
499                 {
500                         get { return toolPath; }
501                         set { toolPath  = value; }
502                 }
503
504                 protected ManualResetEvent ToolCanceled {
505                         get {
506                                 return canceled;
507                         }
508                 }
509
510                 public virtual void Cancel ()
511                 {
512                         canceled.Set ();
513                 }
514
515                 protected MessageImportance StandardErrorImportanceToUse {
516                         get {
517                                 return MessageImportance.Normal;
518                         }
519                 }
520
521                 protected MessageImportance StandardOutputImportanceToUse {
522                         get {
523                                 return MessageImportance.Low;
524                         }
525                 }
526
527                 public bool LogStandardErrorAsError { get; set; }
528                 public string StandardOutputImportance { get; set; }
529         }
530 }