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