[xbuild] Fix case sensitive environment variables for ToolTask.
[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.toolPath = MonoLocationHelper.GetBinDir ();
76                         this.responseFileEncoding = Encoding.UTF8;
77                         this.timeout = Int32.MaxValue;
78                 }
79
80                 [MonoTODO]
81                 protected virtual bool CallHostObjectToExecute ()
82                 {
83                         return true;
84                 }
85
86                 public override bool Execute ()
87                 {
88                         if (SkipTaskExecution ())
89                                 return true;
90
91                         exitCode = ExecuteTool (GenerateFullPathToTool (), GenerateResponseFileCommands (),
92                                 GenerateCommandLineCommands ());
93
94                         // HandleTaskExecutionErrors is called only if exitCode != 0
95                         return exitCode == 0 || HandleTaskExecutionErrors ();
96                 }
97                 
98                 [MonoTODO]
99                 protected virtual string GetWorkingDirectory ()
100                 {
101                         return null;
102                 }
103                 
104                 protected virtual int ExecuteTool (string pathToTool,
105                                                    string responseFileCommands,
106                                                    string commandLineCommands)
107
108                 {
109                         if (pathToTool == null)
110                                 throw new ArgumentNullException ("pathToTool");
111
112                         string responseFileName;
113                         responseFileName = null;
114                         toolOutput = new StringBuilder ();
115
116                         try {
117                                 string arguments = commandLineCommands;
118                                 if (!String.IsNullOrEmpty (responseFileCommands)) {
119                                         responseFileName = Path.GetTempFileName ();
120                                         File.WriteAllText (responseFileName, responseFileCommands);
121                                         arguments = arguments + " " + GetResponseFileSwitch (responseFileName);
122                                 }
123
124                                 LogToolCommand (String.Format ("Tool {0} execution started with arguments: {1} {2}",
125                                                 pathToTool, commandLineCommands, responseFileCommands));
126
127                                 ProcessStartInfo pinfo = new ProcessStartInfo (pathToTool, arguments);
128                                 pinfo.WorkingDirectory = GetWorkingDirectory () ?? Environment.CurrentDirectory;
129
130                                 pinfo.UseShellExecute = false;
131                                 pinfo.RedirectStandardOutput = true;
132                                 pinfo.RedirectStandardError = true;
133
134                                 var pendingLineFragmentOutput = new StringBuilder ();
135                                 var pendingLineFragmentError = new StringBuilder ();
136                                 var environmentOverride = GetAndLogEnvironmentVariables ();
137                                 try {
138                                         // When StartProcess returns, the process has already .Start()'ed
139                                         // If we subscribe to the events after that, then for processes that
140                                         // finish executing before we can subscribe, we won't get the output/err
141                                         // events at all!
142                                         ProcessWrapper pw = ProcessService.StartProcess (pinfo,
143                                                         (_, msg) => ProcessLine (pendingLineFragmentOutput, msg, StandardOutputLoggingImportance),
144                                                         (_, msg) => ProcessLine (pendingLineFragmentError, msg, StandardErrorLoggingImportance),
145                                                         null,
146                                                         environmentOverride);
147
148                                         pw.WaitForOutput (timeout == Int32.MaxValue ? Int32.MaxValue : timeout);
149
150                                         // Process any remaining line
151                                         ProcessLine (pendingLineFragmentOutput, StandardOutputLoggingImportance, true);
152                                         ProcessLine (pendingLineFragmentError, StandardErrorLoggingImportance, true);
153
154                                         exitCode = pw.ExitCode;
155                                         pw.Dispose ();
156                                 } catch (System.ComponentModel.Win32Exception e) {
157                                         Log.LogError ("Error executing tool '{0}': {1}", pathToTool, e.Message);
158                                         return -1;
159                                 }
160
161                                 if (typeLoadException)
162                                         ProcessTypeLoadException ();
163
164                                 pendingLineFragmentOutput.Length = 0;
165                                 pendingLineFragmentError.Length = 0;
166
167                                 Log.LogMessage (MessageImportance.Low, "Tool {0} execution finished.", pathToTool);
168                                 return exitCode;
169                         } finally {
170                                 DeleteTempFile (responseFileName);
171                         }
172                 }
173
174                 void ProcessTypeLoadException ()
175                 {
176                         string output_str = toolOutput.ToString ();
177                         Regex reg  = new Regex (@".*WARNING.*used in (mscorlib|System),.*",
178                                         RegexOptions.Multiline);
179
180                         if (reg.Match (output_str).Success)
181                                 Log.LogError (
182                                         "Error: A referenced assembly may be built with an incompatible " +
183                                         "CLR version. See the compilation output for more details.");
184                         else
185                                 Log.LogError (
186                                         "Error: A dependency of a referenced assembly may be missing, or " +
187                                         "you may be referencing an assembly created with a newer CLR " +
188                                         "version. See the compilation output for more details.");
189
190                         Log.LogError (output_str);
191                 }
192
193                 void ProcessLine (StringBuilder outputBuilder, MessageImportance importance, bool isLastLine)
194                 {
195                         if (outputBuilder.Length == 0)
196                                 return;
197
198                         if (isLastLine && !outputBuilder.ToString ().EndsWith (Environment.NewLine))
199                                 // last line, but w/o an trailing newline, so add that
200                                 outputBuilder.Append (Environment.NewLine);
201
202                         ProcessLine (outputBuilder, null, importance);
203                 }
204
205                 void ProcessLine (StringBuilder outputBuilder, string line, MessageImportance importance)
206                 {
207                         // Add to any line fragment from previous call
208                         if (line != null)
209                                 outputBuilder.Append (line);
210
211                         // Don't remove empty lines!
212                         var lines = outputBuilder.ToString ().Split (new string [] {Environment.NewLine}, StringSplitOptions.None);
213
214                         // Clear the builder. If any incomplete line is found,
215                         // then it will get added back
216                         outputBuilder.Length = 0;
217                         for (int i = 0; i < lines.Length; i ++) {
218                                 string singleLine = lines [i];
219                                 if (i == lines.Length - 1 && !singleLine.EndsWith (Environment.NewLine)) {
220                                         // Last line doesn't end in newline, could be part of
221                                         // a bigger line. Save for later processing
222                                         outputBuilder.Append (singleLine);
223                                         continue;
224                                 }
225
226                                 toolOutput.AppendLine (singleLine);
227
228                                 // in case of typeLoadException, collect all the output
229                                 // and then handle in ProcessTypeLoadException
230                                 if (!typeLoadException)
231                                         LogEventsFromTextOutput (singleLine, importance);
232                         }
233                 }
234
235                 protected virtual void LogEventsFromTextOutput (string singleLine, MessageImportance importance)
236                 {
237                         if (singleLine.Length == 0) {
238                                 Log.LogMessage (singleLine, importance);
239                                 return;
240                         }
241
242                         if (singleLine.StartsWith ("Unhandled Exception: System.TypeLoadException") ||
243                             singleLine.StartsWith ("Unhandled Exception: System.IO.FileNotFoundException")) {
244                                 typeLoadException = true;
245                         }
246
247                         // When IncludeDebugInformation is true, prevents the debug symbols stats from braeking this.
248                         if (singleLine.StartsWith ("WROTE SYMFILE") ||
249                                 singleLine.StartsWith ("OffsetTable") ||
250                                 singleLine.StartsWith ("Compilation succeeded") ||
251                                 singleLine.StartsWith ("Compilation failed"))
252                                 return;
253
254                         Match match = CscErrorRegex.Match (singleLine);
255                         if (!match.Success) {
256                                 Log.LogMessage (importance, singleLine);
257                                 return;
258                         }
259
260                         string filename = match.Result ("${file}") ?? "";
261                         string line = match.Result ("${line}");
262                         int lineNumber = !string.IsNullOrEmpty (line) ? Int32.Parse (line) : 0;
263
264                         string col = match.Result ("${column}");
265                         int columnNumber = 0;
266                         if (!string.IsNullOrEmpty (col))
267                                 columnNumber = col == "255+" ? -1 : Int32.Parse (col);
268
269                         string category = match.Result ("${level}");
270                         string code = match.Result ("${number}");
271                         string text = match.Result ("${message}");
272
273                         if (String.Compare (category, "warning", StringComparison.OrdinalIgnoreCase) == 0) {
274                                 Log.LogWarning (null, code, null, filename, lineNumber, columnNumber, -1,
275                                         -1, text, null);
276                         } else if (String.Compare (category, "error", StringComparison.OrdinalIgnoreCase) == 0) {
277                                 Log.LogError (null, code, null, filename, lineNumber, columnNumber, -1,
278                                         -1, text, null);
279                         } else {
280                                 Log.LogMessage (importance, singleLine);
281                         }
282                 }
283
284                 protected virtual string GenerateCommandLineCommands ()
285                 {
286                         return null;
287                 }
288
289                 protected abstract string GenerateFullPathToTool ();
290
291                 protected virtual string GenerateResponseFileCommands ()
292                 {
293                         return null;
294                 }
295
296                 protected virtual string GetResponseFileSwitch (string responseFilePath)
297                 {
298                         return String.Format ("@{0}", responseFilePath);
299                 }
300
301                 protected virtual bool HandleTaskExecutionErrors ()
302                 {
303                         if (!Log.HasLoggedErrors && exitCode != 0)
304                                 Log.LogError ("Tool exited with code: {0}. Output: {1}", exitCode,
305                                                 toolOutput != null ? toolOutput.ToString () : String.Empty);
306                         toolOutput = null;
307
308                         return ExitCode == 0 && !Log.HasLoggedErrors;
309                 }
310
311                 protected virtual HostObjectInitializationStatus InitializeHostObject ()
312                 {
313                         return HostObjectInitializationStatus.NoActionReturnSuccess;
314                 }
315
316                 protected virtual void LogToolCommand (string message)
317                 {
318                         Log.LogMessage (MessageImportance.Normal, message);
319                 }
320                 
321                 [MonoTODO]
322                 protected virtual void LogPathToTool (string toolName,
323                                                       string pathToTool)
324                 {
325                 }
326
327                 protected virtual bool SkipTaskExecution()
328                 {
329                         return !ValidateParameters ();
330                 }
331
332                 protected virtual bool ValidateParameters()
333                 {
334                         return true;
335                 }
336
337                 protected void DeleteTempFile (string fileName)
338                 {
339                         if (String.IsNullOrEmpty (fileName))
340                                 return;
341
342                         try {
343                                 File.Delete (fileName);
344                         } catch (IOException ioe) {
345                                 Log.LogWarning ("Unable to delete temporary file '{0}' : {1}", ioe.Message);
346                         } catch (UnauthorizedAccessException uae) {
347                                 Log.LogWarning ("Unable to delete temporary file '{0}' : {1}", uae.Message);
348                         }
349                 }
350
351                 // If EnvironmentVariables is defined, then merge EnvironmentOverride
352                 // EnvironmentOverride is Obsolete'd in 4.0
353                 //
354                 // Returns the final set of environment variables and logs them
355                 SCS.StringDictionary GetAndLogEnvironmentVariables ()
356                 {
357                         var env_vars = GetEnvironmentVariables ();
358                         if (env_vars == null)
359                                 return env_vars;
360
361                         Log.LogMessage (MessageImportance.Low, "Environment variables being passed to the tool:");
362                         foreach (DictionaryEntry entry in env_vars)
363                                 Log.LogMessage (MessageImportance.Low, "\t{0}={1}", (string)entry.Key, (string)entry.Value);
364
365                         return env_vars;
366                 }
367
368                 SCS.StringDictionary GetEnvironmentVariables ()
369                 {
370                         if (EnvironmentVariables == null || EnvironmentVariables.Length == 0)
371                                 return EnvironmentOverride;
372
373                         var env_vars = new SCS.ProcessStringDictionary ();
374                         foreach (string pair in EnvironmentVariables) {
375                                 string [] key_value = pair.Split ('=');
376                                 if (!String.IsNullOrEmpty (key_value [0]))
377                                         env_vars [key_value [0]] = key_value.Length > 1 ? key_value [1] : String.Empty;
378                         }
379
380                         if (EnvironmentOverride != null)
381                                 foreach (DictionaryEntry entry in EnvironmentOverride)
382                                         env_vars [(string)entry.Key] = (string)entry.Value;
383
384                         return env_vars;
385                 }
386
387                 protected virtual StringDictionary EnvironmentOverride
388                 {
389                         get { return null; }
390                 }
391
392                 // Ignore EnvironmentOverride if this is set
393                 public string[] EnvironmentVariables { get; set; }
394
395                 [Output]
396                 public int ExitCode {
397                         get { return exitCode; }
398                 }
399
400                 protected virtual Encoding ResponseFileEncoding
401                 {
402                         get { return responseFileEncoding; }
403                 }
404
405                 protected virtual Encoding StandardErrorEncoding
406                 {
407                         get { return Console.Error.Encoding; }
408                 }
409
410                 protected virtual MessageImportance StandardErrorLoggingImportance {
411                         get { return standardErrorLoggingImportance; }
412                 }
413
414                 protected virtual Encoding StandardOutputEncoding
415                 {
416                         get { return Console.Out.Encoding; }
417                 }
418
419                 protected virtual MessageImportance StandardOutputLoggingImportance {
420                         get { return standardOutputLoggingImportance; }
421                 }
422
423                 protected virtual bool HasLoggedErrors {
424                         get { return Log.HasLoggedErrors; }
425                 }
426
427                 public virtual int Timeout
428                 {
429                         get { return timeout; }
430                         set { timeout = value; }
431                 }
432
433                 public virtual string ToolExe
434                 {
435                         get {
436                                 if (toolExe == null)
437                                         return ToolName;
438                                 else
439                                         return toolExe;
440                         }
441                         set {
442                                 if (!String.IsNullOrEmpty (value))
443                                         toolExe = value;
444                         }
445                 }
446
447                 protected abstract string ToolName
448                 {
449                         get;
450                 }
451
452                 public string ToolPath
453                 {
454                         get { return toolPath; }
455                         set {
456                                 if (!String.IsNullOrEmpty (value))
457                                         toolPath  = value;
458                         }
459                 }
460
461                 // Snatched from our codedom code, with some changes to make it compatible with csc
462                 // (the line+column group is optional is csc)
463                 static Regex errorRegex;
464                 static Regex CscErrorRegex {
465                         get {
466                                 if (errorRegex == null)
467                                         errorRegex = new Regex (@"^(\s*(?<file>[^\(]+)(\((?<line>\d*)(,(?<column>\d*[\+]*))?\))?:\s+)*(?<level>\w+)\s+(?<number>.*\d):\s*(?<message>.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
468                                 return errorRegex;
469                         }
470                 }
471         }
472 }
473
474 #endif