2 // ToolTask.cs: Base class for command line tool tasks.
5 // Marek Sieradzki (marek.sieradzki@gmail.com)
6 // Ankit Jain (jankit@novell.com)
8 // (C) 2005 Marek Sieradzki
9 // Copyright 2009 Novell, Inc (http://www.novell.com)
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:
19 // The above copyright notice and this permission notice shall be
20 // included in all copies or substantial portions of the Software.
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.
33 using System.Diagnostics;
34 using System.Collections;
35 using System.Collections.Specialized;
37 using System.Resources;
39 using System.Text.RegularExpressions;
40 using Microsoft.Build.Framework;
41 using Mono.XBuild.Utilities;
43 using SCS = System.Collections.Specialized;
45 namespace Microsoft.Build.Utilities
47 public abstract class ToolTask : Task
51 string toolPath, toolExe;
52 Encoding responseFileEncoding;
53 MessageImportance standardErrorLoggingImportance;
54 MessageImportance standardOutputLoggingImportance;
55 StringBuilder toolOutput;
56 bool typeLoadException;
61 this.standardErrorLoggingImportance = MessageImportance.High;
62 this.standardOutputLoggingImportance = MessageImportance.Normal;
65 protected ToolTask (ResourceManager taskResources)
66 : this (taskResources, null)
70 protected ToolTask (ResourceManager taskResources,
71 string helpKeywordPrefix)
73 this.TaskResources = taskResources;
74 this.HelpKeywordPrefix = helpKeywordPrefix;
75 this.responseFileEncoding = Encoding.UTF8;
76 this.timeout = Int32.MaxValue;
80 protected virtual bool CallHostObjectToExecute ()
85 public override bool Execute ()
87 if (SkipTaskExecution ())
90 exitCode = ExecuteTool (GenerateFullPathToTool (), GenerateResponseFileCommands (),
91 GenerateCommandLineCommands ());
93 // HandleTaskExecutionErrors is called only if exitCode != 0
94 return exitCode == 0 || HandleTaskExecutionErrors ();
98 protected virtual string GetWorkingDirectory ()
103 protected virtual int ExecuteTool (string pathToTool,
104 string responseFileCommands,
105 string commandLineCommands)
108 if (pathToTool == null)
109 throw new ArgumentNullException ("pathToTool");
111 string responseFileName;
112 responseFileName = null;
113 toolOutput = new StringBuilder ();
116 string responseFileSwitch = String.Empty;
117 if (!String.IsNullOrEmpty (responseFileCommands)) {
118 responseFileName = Path.GetTempFileName ();
119 File.WriteAllText (responseFileName, responseFileCommands);
120 responseFileSwitch = GetResponseFileSwitch (responseFileName);
123 var pinfo = GetProcessStartInfo (pathToTool, commandLineCommands, responseFileSwitch);
124 LogToolCommand (String.Format ("Tool {0} execution started with arguments: {1} {2}",
125 pinfo.FileName, commandLineCommands, responseFileCommands));
127 var pendingLineFragmentOutput = new StringBuilder ();
128 var pendingLineFragmentError = new StringBuilder ();
129 var environmentOverride = GetAndLogEnvironmentVariables ();
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
135 ProcessWrapper pw = ProcessService.StartProcess (pinfo,
136 (_, msg) => ProcessLine (pendingLineFragmentOutput, msg, StandardOutputLoggingImportance),
137 (_, msg) => ProcessLine (pendingLineFragmentError, msg, StandardErrorLoggingImportance),
139 environmentOverride);
141 pw.WaitForOutput (timeout == Int32.MaxValue ? Int32.MaxValue : timeout);
143 // Process any remaining line
144 ProcessLine (pendingLineFragmentOutput, StandardOutputLoggingImportance, true);
145 ProcessLine (pendingLineFragmentError, StandardErrorLoggingImportance, true);
147 exitCode = pw.ExitCode;
149 } catch (System.ComponentModel.Win32Exception e) {
150 Log.LogError ("Error executing tool '{0}': {1}", pathToTool, e.Message);
154 if (typeLoadException)
155 ProcessTypeLoadException ();
157 pendingLineFragmentOutput.Length = 0;
158 pendingLineFragmentError.Length = 0;
160 Log.LogMessage (MessageImportance.Low, "Tool {0} execution finished.", pathToTool);
163 DeleteTempFile (responseFileName);
167 void ProcessTypeLoadException ()
169 string output_str = toolOutput.ToString ();
170 Regex reg = new Regex (@".*WARNING.*used in (mscorlib|System),.*",
171 RegexOptions.Multiline);
173 if (reg.Match (output_str).Success)
175 "Error: A referenced assembly may be built with an incompatible " +
176 "CLR version. See the compilation output for more details.");
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.");
183 Log.LogError (output_str);
186 void ProcessLine (StringBuilder outputBuilder, MessageImportance importance, bool isLastLine)
188 if (outputBuilder.Length == 0)
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);
195 ProcessLine (outputBuilder, null, importance);
198 void ProcessLine (StringBuilder outputBuilder, string line, MessageImportance importance)
200 // Add to any line fragment from previous call
202 outputBuilder.Append (line);
204 // Don't remove empty lines!
205 var lines = outputBuilder.ToString ().Split (new string [] {Environment.NewLine}, StringSplitOptions.None);
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);
219 toolOutput.AppendLine (singleLine);
221 // in case of typeLoadException, collect all the output
222 // and then handle in ProcessTypeLoadException
223 if (!typeLoadException)
224 LogEventsFromTextOutput (singleLine, importance);
228 protected virtual void LogEventsFromTextOutput (string singleLine, MessageImportance importance)
230 if (singleLine.Length == 0) {
231 Log.LogMessage (singleLine, importance);
235 if (singleLine.StartsWith ("Unhandled Exception: System.TypeLoadException") ||
236 singleLine.StartsWith ("Unhandled Exception: System.IO.FileNotFoundException")) {
237 typeLoadException = true;
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"))
247 Match match = CscErrorRegex.Match (singleLine);
248 if (!match.Success) {
249 Log.LogMessage (importance, singleLine);
253 string filename = match.Result ("${file}") ?? "";
254 string line = match.Result ("${line}");
255 int lineNumber = !string.IsNullOrEmpty (line) ? Int32.Parse (line) : 0;
257 string col = match.Result ("${column}");
258 int columnNumber = 0;
259 if (!string.IsNullOrEmpty (col))
260 columnNumber = col.IndexOf ("+") >= 0 ? -1 : Int32.Parse (col);
262 string category = match.Result ("${level}");
263 string code = match.Result ("${number}");
264 string text = match.Result ("${message}");
266 if (String.Compare (category, "warning", StringComparison.OrdinalIgnoreCase) == 0) {
267 Log.LogWarning (null, code, null, filename, lineNumber, columnNumber, -1,
269 } else if (String.Compare (category, "error", StringComparison.OrdinalIgnoreCase) == 0) {
270 Log.LogError (null, code, null, filename, lineNumber, columnNumber, -1,
273 Log.LogMessage (importance, singleLine);
277 protected virtual string GenerateCommandLineCommands ()
282 protected abstract string GenerateFullPathToTool ();
284 protected virtual string GenerateResponseFileCommands ()
289 protected virtual string GetResponseFileSwitch (string responseFilePath)
291 return String.Format ("@{0}", responseFilePath);
294 protected virtual ProcessStartInfo GetProcessStartInfo (string pathToTool, string commandLineCommands, string responseFileSwitch)
296 var pinfo = new ProcessStartInfo (pathToTool, String.Format ("{0} {1}", commandLineCommands, responseFileSwitch));
298 pinfo.WorkingDirectory = GetWorkingDirectory () ?? Environment.CurrentDirectory;
299 pinfo.UseShellExecute = false;
300 pinfo.CreateNoWindow = true;
301 pinfo.RedirectStandardOutput = true;
302 pinfo.RedirectStandardError = true;
307 protected virtual bool HandleTaskExecutionErrors ()
309 if (!Log.HasLoggedErrors && exitCode != 0)
310 Log.LogError ("Tool exited with code: {0}. Output: {1}", exitCode,
311 toolOutput != null ? toolOutput.ToString () : String.Empty);
314 return ExitCode == 0 && !Log.HasLoggedErrors;
317 protected virtual HostObjectInitializationStatus InitializeHostObject ()
319 return HostObjectInitializationStatus.NoActionReturnSuccess;
322 protected virtual void LogToolCommand (string message)
324 Log.LogMessage (MessageImportance.Normal, message);
328 protected virtual void LogPathToTool (string toolName,
333 protected virtual bool SkipTaskExecution()
335 return !ValidateParameters ();
338 protected virtual bool ValidateParameters()
343 protected void DeleteTempFile (string fileName)
345 if (String.IsNullOrEmpty (fileName))
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);
357 // If EnvironmentVariables is defined, then merge EnvironmentOverride
358 // EnvironmentOverride is Obsolete'd in 4.0
360 // Returns the final set of environment variables and logs them
361 SCS.StringDictionary GetAndLogEnvironmentVariables ()
363 var env_vars = GetEnvironmentVariables ();
364 if (env_vars == null)
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);
374 SCS.StringDictionary GetEnvironmentVariables ()
376 if (EnvironmentVariables == null || EnvironmentVariables.Length == 0)
377 return EnvironmentOverride;
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;
386 if (EnvironmentOverride != null)
387 foreach (DictionaryEntry entry in EnvironmentOverride)
388 env_vars [(string)entry.Key] = (string)entry.Value;
393 protected virtual StringDictionary EnvironmentOverride
398 // Ignore EnvironmentOverride if this is set
399 public string[] EnvironmentVariables { get; set; }
402 public int ExitCode {
403 get { return exitCode; }
406 protected virtual Encoding ResponseFileEncoding
408 get { return responseFileEncoding; }
411 protected virtual Encoding StandardErrorEncoding
413 get { return Console.Error.Encoding; }
416 protected virtual MessageImportance StandardErrorLoggingImportance {
417 get { return standardErrorLoggingImportance; }
420 protected virtual Encoding StandardOutputEncoding
422 get { return Console.Out.Encoding; }
425 protected virtual MessageImportance StandardOutputLoggingImportance {
426 get { return standardOutputLoggingImportance; }
429 protected virtual bool HasLoggedErrors {
430 get { return Log.HasLoggedErrors; }
433 public virtual int Timeout
435 get { return timeout; }
436 set { timeout = value; }
439 public virtual string ToolExe
442 if (string.IsNullOrEmpty (toolExe))
447 set { toolExe = value; }
450 protected abstract string ToolName
455 public string ToolPath
457 get { return toolPath; }
458 set { toolPath = value; }
461 // Keep in sync with mcs/class/System/Microsoft.CSharp/CSharpCodeCompiler.cs
462 const string ErrorRegexPattern = @"
464 (\s*(?<file>[^\(]+) # filename (optional)
465 (\((?<line>\d*)(,(?<column>\d*[\+]*))?\))? # line+column (optional)
467 (?<level>\w+) # error|warning
469 (?<number>[^:]*\d) # CS1234
474 static Regex errorRegex;
475 static Regex CscErrorRegex {
477 if (errorRegex == null)
478 errorRegex = new Regex (ErrorRegexPattern,
479 RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnorePatternWhitespace);