[xbuild] ToolTask - make error column check a little non-specific.
[mono.git] / mcs / class / Microsoft.Build.Utilities / Microsoft.Build.Utilities / ToolTask.cs
index 9d4a75f2b96205bb67f9065fb36e98cf52599e90..4094c122e3b9e9afc86574ece4e1ea98afbd9cb9 100644 (file)
@@ -3,8 +3,10 @@
 //
 // Author:
 //   Marek Sieradzki (marek.sieradzki@gmail.com)
+//   Ankit Jain (jankit@novell.com)
 //
 // (C) 2005 Marek Sieradzki
+// Copyright 2009 Novell, Inc (http://www.novell.com)
 //
 // Permission is hereby granted, free of charge, to any person obtaining
 // a copy of this software and associated documentation files (the
@@ -29,6 +31,7 @@
 
 using System;
 using System.Diagnostics;
+using System.Collections;
 using System.Collections.Specialized;
 using System.IO;
 using System.Resources;
@@ -37,21 +40,21 @@ using System.Text.RegularExpressions;
 using Microsoft.Build.Framework;
 using Mono.XBuild.Utilities;
 
+using SCS = System.Collections.Specialized;
+
 namespace Microsoft.Build.Utilities
 {
        public abstract class ToolTask : Task
        {
-               StringDictionary        environmentOverride;
                int                     exitCode;
                int                     timeout;
-               string                  toolPath;
-               Process                 process;
+               string                  toolPath, toolExe;
                Encoding                responseFileEncoding;
                MessageImportance       standardErrorLoggingImportance;
                MessageImportance       standardOutputLoggingImportance;
-               
-               static Regex            regex;
-               
+               StringBuilder toolOutput;
+               bool typeLoadException;
+
                protected ToolTask ()
                        : this (null, null)
                {
@@ -71,19 +74,7 @@ namespace Microsoft.Build.Utilities
                        this.HelpKeywordPrefix = helpKeywordPrefix;
                        this.toolPath = MonoLocationHelper.GetBinDir ();
                        this.responseFileEncoding = Encoding.UTF8;
-               }
-
-               static ToolTask ()
-               {
-                       regex = new Regex (
-                               @"^\s*"
-                               + @"(((?<ORIGIN>(((\d+>)?[a-zA-Z]?:[^:]*)|([^:]*))):)"
-                               + "|())"
-                               + "(?<SUBCATEGORY>(()|([^:]*? )))"
-                               + "(?<CATEGORY>(error|warning)) "
-                               + "(?<CODE>[^:]*):"
-                               + "(?<TEXT>.*)$",
-                               RegexOptions.IgnoreCase);
+                       this.timeout = Int32.MaxValue;
                }
 
                [MonoTODO]
@@ -92,36 +83,16 @@ namespace Microsoft.Build.Utilities
                        return true;
                }
 
-               [MonoTODO]
-               // FIXME: it should write responseFileCommands to temporary response file
-               protected virtual int ExecuteTool (string pathToTool,
-                                                  string responseFileCommands,
-                                                  string commandLineCommands)
-               {
-                       string arguments;
-                       bool success;
-                       
-                       arguments = String.Concat (commandLineCommands, " ", responseFileCommands);
-                       
-                       success  = RealExecute (pathToTool, arguments);
-                       
-                       if (success)
-                               return 0;
-                       else
-                               return -1;
-               }
-               
                public override bool Execute ()
                {
-                       int result;
-                       
-                       result = ExecuteTool (GenerateFullPathToTool (), GenerateResponseFileCommands (),
-                               GenerateCommandLineCommands ());
-                       
-                       if (result == 0)
+                       if (SkipTaskExecution ())
                                return true;
-                       else
-                               return false;
+
+                       exitCode = ExecuteTool (GenerateFullPathToTool (), GenerateResponseFileCommands (),
+                               GenerateCommandLineCommands ());
+
+                       // HandleTaskExecutionErrors is called only if exitCode != 0
+                       return exitCode == 0 || HandleTaskExecutionErrors ();
                }
                
                [MonoTODO]
@@ -130,122 +101,180 @@ namespace Microsoft.Build.Utilities
                        return null;
                }
                
-               private bool RealExecute (string filename, string arguments)
+               protected virtual int ExecuteTool (string pathToTool,
+                                                  string responseFileCommands,
+                                                  string commandLineCommands)
+
                {
-                       string line;
-               
-                       if (filename == null)
-                               throw new ArgumentNullException ("filename");
-                       if (arguments == null)
-                               throw new ArgumentNullException ("arguments");
-                       
-                       process = new Process ();
-                       process.StartInfo.Arguments = arguments;
-                       process.StartInfo.CreateNoWindow = true;
-                       process.StartInfo.FileName = filename;
-                       process.StartInfo.RedirectStandardError = true;
-                       process.StartInfo.RedirectStandardOutput = true;
-                       process.StartInfo.UseShellExecute = false;
-                       
-                       Log.LogMessage (MessageImportance.Low, String.Format ("Tool {0} execution started with arguments: {1}",
-                               filename, arguments));
-                       
-                       process.Start ();
-                       process.WaitForExit ();
-                       
-                       exitCode = process.ExitCode;
-                       
-                       while ((line = process.StandardError.ReadLine ()) != null) {
-                               LogEventsFromTextOutput (line, MessageImportance.Normal);
+                       if (pathToTool == null)
+                               throw new ArgumentNullException ("pathToTool");
+
+                       string responseFileName;
+                       responseFileName = null;
+                       toolOutput = new StringBuilder ();
+
+                       try {
+                               string responseFileSwitch = String.Empty;
+                               if (!String.IsNullOrEmpty (responseFileCommands)) {
+                                       responseFileName = Path.GetTempFileName ();
+                                       File.WriteAllText (responseFileName, responseFileCommands);
+                                       responseFileSwitch = GetResponseFileSwitch (responseFileName);
+                               }
+
+                               var pinfo = GetProcessStartInfo (pathToTool, commandLineCommands, responseFileSwitch);
+                               LogToolCommand (String.Format ("Tool {0} execution started with arguments: {1} {2}",
+                                               pinfo.FileName, commandLineCommands, responseFileCommands));
+
+                               var pendingLineFragmentOutput = new StringBuilder ();
+                               var pendingLineFragmentError = new StringBuilder ();
+                               var environmentOverride = GetAndLogEnvironmentVariables ();
+                               try {
+                                       // When StartProcess returns, the process has already .Start()'ed
+                                       // If we subscribe to the events after that, then for processes that
+                                       // finish executing before we can subscribe, we won't get the output/err
+                                       // events at all!
+                                       ProcessWrapper pw = ProcessService.StartProcess (pinfo,
+                                                       (_, msg) => ProcessLine (pendingLineFragmentOutput, msg, StandardOutputLoggingImportance),
+                                                       (_, msg) => ProcessLine (pendingLineFragmentError, msg, StandardErrorLoggingImportance),
+                                                       null,
+                                                       environmentOverride);
+
+                                       pw.WaitForOutput (timeout == Int32.MaxValue ? Int32.MaxValue : timeout);
+
+                                       // Process any remaining line
+                                       ProcessLine (pendingLineFragmentOutput, StandardOutputLoggingImportance, true);
+                                       ProcessLine (pendingLineFragmentError, StandardErrorLoggingImportance, true);
+
+                                       exitCode = pw.ExitCode;
+                                       pw.Dispose ();
+                               } catch (System.ComponentModel.Win32Exception e) {
+                                       Log.LogError ("Error executing tool '{0}': {1}", pathToTool, e.Message);
+                                       return -1;
+                               }
+
+                               if (typeLoadException)
+                                       ProcessTypeLoadException ();
+
+                               pendingLineFragmentOutput.Length = 0;
+                               pendingLineFragmentError.Length = 0;
+
+                               Log.LogMessage (MessageImportance.Low, "Tool {0} execution finished.", pathToTool);
+                               return exitCode;
+                       } finally {
+                               DeleteTempFile (responseFileName);
                        }
-                       
-                       Log.LogMessage (MessageImportance.Low, String.Format ("Tool {0} execution finished.", filename));
-                       
-                       return !Log.HasLoggedErrors;
                }
-               
-               
-               // FIXME: use importance
-               [MonoTODO]
-               protected virtual void LogEventsFromTextOutput (string singleLine,
-                                                               MessageImportance importance)
+
+               void ProcessTypeLoadException ()
                {
-                       string filename, origin, category, code, subcategory, text;
-                       int lineNumber, columnNumber, endLineNumber, endColumnNumber;
-               
-                       Match m = regex.Match (singleLine);
-                       origin = m.Groups [regex.GroupNumberFromName ("ORIGIN")].Value;
-                       category = m.Groups [regex.GroupNumberFromName ("CATEGORY")].Value;
-                       code = m.Groups [regex.GroupNumberFromName ("CODE")].Value;
-                       subcategory = m.Groups [regex.GroupNumberFromName ("SUBCATEGORY")].Value;
-                       text = m.Groups [regex.GroupNumberFromName ("TEXT")].Value;
-                       
-                       ParseOrigin (origin, out filename, out lineNumber, out columnNumber, out endLineNumber, out endColumnNumber);
-                       
-                       if (category == "warning") {
-                               Log.LogWarning (subcategory, code, null, filename, lineNumber, columnNumber, endLineNumber,
-                                       endColumnNumber, text, null);
-                       } else if (category == "error") {
-                               Log.LogError (subcategory, code, null, filename, lineNumber, columnNumber, endLineNumber,
-                                       endColumnNumber, text, null);
+                       string output_str = toolOutput.ToString ();
+                       Regex reg  = new Regex (@".*WARNING.*used in (mscorlib|System),.*",
+                                       RegexOptions.Multiline);
+
+                       if (reg.Match (output_str).Success)
+                               Log.LogError (
+                                       "Error: A referenced assembly may be built with an incompatible " +
+                                       "CLR version. See the compilation output for more details.");
+                       else
+                               Log.LogError (
+                                       "Error: A dependency of a referenced assembly may be missing, or " +
+                                       "you may be referencing an assembly created with a newer CLR " +
+                                       "version. See the compilation output for more details.");
+
+                       Log.LogError (output_str);
+               }
+
+               void ProcessLine (StringBuilder outputBuilder, MessageImportance importance, bool isLastLine)
+               {
+                       if (outputBuilder.Length == 0)
+                               return;
+
+                       if (isLastLine && !outputBuilder.ToString ().EndsWith (Environment.NewLine))
+                               // last line, but w/o an trailing newline, so add that
+                               outputBuilder.Append (Environment.NewLine);
+
+                       ProcessLine (outputBuilder, null, importance);
+               }
+
+               void ProcessLine (StringBuilder outputBuilder, string line, MessageImportance importance)
+               {
+                       // Add to any line fragment from previous call
+                       if (line != null)
+                               outputBuilder.Append (line);
+
+                       // Don't remove empty lines!
+                       var lines = outputBuilder.ToString ().Split (new string [] {Environment.NewLine}, StringSplitOptions.None);
+
+                       // Clear the builder. If any incomplete line is found,
+                       // then it will get added back
+                       outputBuilder.Length = 0;
+                       for (int i = 0; i < lines.Length; i ++) {
+                               string singleLine = lines [i];
+                               if (i == lines.Length - 1 && !singleLine.EndsWith (Environment.NewLine)) {
+                                       // Last line doesn't end in newline, could be part of
+                                       // a bigger line. Save for later processing
+                                       outputBuilder.Append (singleLine);
+                                       continue;
+                               }
+
+                               toolOutput.AppendLine (singleLine);
+
+                               // in case of typeLoadException, collect all the output
+                               // and then handle in ProcessTypeLoadException
+                               if (!typeLoadException)
+                                       LogEventsFromTextOutput (singleLine, importance);
                        }
                }
-               
-               private void ParseOrigin (string origin, out string filename,
-                                    out int lineNumber, out int columnNumber,
-                                    out int endLineNumber, out int endColumnNumber)
-               {
-                       int lParen;
-                       string[] temp;
-                       string[] left, right;
-                       
-                       if (origin.IndexOf ('(') != -1 ) {
-                               lParen = origin.IndexOf ('(');
-                               filename = origin.Substring (0, lParen);
-                               temp = origin.Substring (lParen + 1, origin.Length - lParen - 2).Split (',');
-                               if (temp.Length == 1) {
-                                       left = temp [0].Split ('-');
-                                       if (left.Length == 1) {
-                                               lineNumber = Int32.Parse (left [0]);
-                                               columnNumber = 0;
-                                               endLineNumber = 0;
-                                               endColumnNumber = 0;
-                                       } else if (left.Length == 2) {
-                                               lineNumber = Int32.Parse (left [0]);
-                                               columnNumber = 0;
-                                               endLineNumber = Int32.Parse (left [1]);
-                                               endColumnNumber = 0;
-                                       } else
-                                               throw new Exception ("Invalid line/column format.");
-                               } else if (temp.Length == 2) {
-                                       right = temp [1].Split ('-');
-                                       lineNumber = Int32.Parse (temp [0]);
-                                       endLineNumber = 0;
-                                       if (right.Length == 1) {
-                                               columnNumber = Int32.Parse (right [0]);
-                                               endColumnNumber = 0;
-                                       } else if (right.Length == 2) {
-                                               columnNumber = Int32.Parse (right [0]);
-                                               endColumnNumber = Int32.Parse (right [0]);
-                                       } else
-                                               throw new Exception ("Invalid line/column format.");
-                               } else if (temp.Length == 4) {
-                                       lineNumber = Int32.Parse (temp [0]);
-                                       endLineNumber = Int32.Parse (temp [2]);
-                                       columnNumber = Int32.Parse (temp [1]);
-                                       endColumnNumber = Int32.Parse (temp [3]);
-                               } else
-                                       throw new Exception ("Invalid line/column format.");
+
+               protected virtual void LogEventsFromTextOutput (string singleLine, MessageImportance importance)
+               {
+                       if (singleLine.Length == 0) {
+                               Log.LogMessage (singleLine, importance);
+                               return;
+                       }
+
+                       if (singleLine.StartsWith ("Unhandled Exception: System.TypeLoadException") ||
+                           singleLine.StartsWith ("Unhandled Exception: System.IO.FileNotFoundException")) {
+                               typeLoadException = true;
+                       }
+
+                       // When IncludeDebugInformation is true, prevents the debug symbols stats from braeking this.
+                       if (singleLine.StartsWith ("WROTE SYMFILE") ||
+                               singleLine.StartsWith ("OffsetTable") ||
+                               singleLine.StartsWith ("Compilation succeeded") ||
+                               singleLine.StartsWith ("Compilation failed"))
+                               return;
+
+                       Match match = CscErrorRegex.Match (singleLine);
+                       if (!match.Success) {
+                               Log.LogMessage (importance, singleLine);
+                               return;
+                       }
+
+                       string filename = match.Result ("${file}") ?? "";
+                       string line = match.Result ("${line}");
+                       int lineNumber = !string.IsNullOrEmpty (line) ? Int32.Parse (line) : 0;
+
+                       string col = match.Result ("${column}");
+                       int columnNumber = 0;
+                       if (!string.IsNullOrEmpty (col))
+                               columnNumber = col.IndexOf ("+") >= 0 ? -1 : Int32.Parse (col);
+
+                       string category = match.Result ("${level}");
+                       string code = match.Result ("${number}");
+                       string text = match.Result ("${message}");
+
+                       if (String.Compare (category, "warning", StringComparison.OrdinalIgnoreCase) == 0) {
+                               Log.LogWarning (null, code, null, filename, lineNumber, columnNumber, -1,
+                                       -1, text, null);
+                       } else if (String.Compare (category, "error", StringComparison.OrdinalIgnoreCase) == 0) {
+                               Log.LogError (null, code, null, filename, lineNumber, columnNumber, -1,
+                                       -1, text, null);
                        } else {
-                               filename = origin;
-                               lineNumber = 0;
-                               columnNumber = 0;
-                               endLineNumber = 0;
-                               endColumnNumber = 0;
+                               Log.LogMessage (importance, singleLine);
                        }
                }
 
-               [MonoTODO]
                protected virtual string GenerateCommandLineCommands ()
                {
                        return null;
@@ -253,22 +282,36 @@ namespace Microsoft.Build.Utilities
 
                protected abstract string GenerateFullPathToTool ();
 
-               [MonoTODO]
                protected virtual string GenerateResponseFileCommands ()
                {
                        return null;
                }
 
-               [MonoTODO]
                protected virtual string GetResponseFileSwitch (string responseFilePath)
                {
                        return String.Format ("@{0}", responseFilePath);
                }
 
-               [MonoTODO]
+               protected virtual ProcessStartInfo GetProcessStartInfo (string pathToTool, string commandLineCommands, string responseFileSwitch)
+               {
+                       var pinfo = new ProcessStartInfo (pathToTool, String.Format ("{0} {1}", commandLineCommands, responseFileSwitch));
+
+                       pinfo.WorkingDirectory = GetWorkingDirectory () ?? Environment.CurrentDirectory;
+                       pinfo.UseShellExecute = false;
+                       pinfo.RedirectStandardOutput = true;
+                       pinfo.RedirectStandardError = true;
+
+                       return pinfo;
+               }
+
                protected virtual bool HandleTaskExecutionErrors ()
                {
-                       return true;
+                       if (!Log.HasLoggedErrors && exitCode != 0)
+                               Log.LogError ("Tool exited with code: {0}. Output: {1}", exitCode,
+                                               toolOutput != null ? toolOutput.ToString () : String.Empty);
+                       toolOutput = null;
+
+                       return ExitCode == 0 && !Log.HasLoggedErrors;
                }
 
                protected virtual HostObjectInitializationStatus InitializeHostObject ()
@@ -276,9 +319,9 @@ namespace Microsoft.Build.Utilities
                        return HostObjectInitializationStatus.NoActionReturnSuccess;
                }
 
-               [MonoTODO]
                protected virtual void LogToolCommand (string message)
                {
+                       Log.LogMessage (MessageImportance.Normal, message);
                }
                
                [MonoTODO]
@@ -289,7 +332,7 @@ namespace Microsoft.Build.Utilities
 
                protected virtual bool SkipTaskExecution()
                {
-                       return false;
+                       return !ValidateParameters ();
                }
 
                protected virtual bool ValidateParameters()
@@ -297,12 +340,64 @@ namespace Microsoft.Build.Utilities
                        return true;
                }
 
+               protected void DeleteTempFile (string fileName)
+               {
+                       if (String.IsNullOrEmpty (fileName))
+                               return;
+
+                       try {
+                               File.Delete (fileName);
+                       } catch (IOException ioe) {
+                               Log.LogWarning ("Unable to delete temporary file '{0}' : {1}", ioe.Message);
+                       } catch (UnauthorizedAccessException uae) {
+                               Log.LogWarning ("Unable to delete temporary file '{0}' : {1}", uae.Message);
+                       }
+               }
+
+               // If EnvironmentVariables is defined, then merge EnvironmentOverride
+               // EnvironmentOverride is Obsolete'd in 4.0
+               //
+               // Returns the final set of environment variables and logs them
+               SCS.StringDictionary GetAndLogEnvironmentVariables ()
+               {
+                       var env_vars = GetEnvironmentVariables ();
+                       if (env_vars == null)
+                               return env_vars;
+
+                       Log.LogMessage (MessageImportance.Low, "Environment variables being passed to the tool:");
+                       foreach (DictionaryEntry entry in env_vars)
+                               Log.LogMessage (MessageImportance.Low, "\t{0}={1}", (string)entry.Key, (string)entry.Value);
+
+                       return env_vars;
+               }
+
+               SCS.StringDictionary GetEnvironmentVariables ()
+               {
+                       if (EnvironmentVariables == null || EnvironmentVariables.Length == 0)
+                               return EnvironmentOverride;
+
+                       var env_vars = new SCS.ProcessStringDictionary ();
+                       foreach (string pair in EnvironmentVariables) {
+                               string [] key_value = pair.Split ('=');
+                               if (!String.IsNullOrEmpty (key_value [0]))
+                                       env_vars [key_value [0]] = key_value.Length > 1 ? key_value [1] : String.Empty;
+                       }
+
+                       if (EnvironmentOverride != null)
+                               foreach (DictionaryEntry entry in EnvironmentOverride)
+                                       env_vars [(string)entry.Key] = (string)entry.Value;
+
+                       return env_vars;
+               }
+
                protected virtual StringDictionary EnvironmentOverride
                {
-                       get { return environmentOverride; }
+                       get { return null; }
                }
-               
-               [MonoTODO]
+
+               // Ignore EnvironmentOverride if this is set
+               public string[] EnvironmentVariables { get; set; }
+
                [Output]
                public int ExitCode {
                        get { return exitCode; }
@@ -331,12 +426,30 @@ namespace Microsoft.Build.Utilities
                        get { return standardOutputLoggingImportance; }
                }
 
+               protected virtual bool HasLoggedErrors {
+                       get { return Log.HasLoggedErrors; }
+               }
+
                public virtual int Timeout
                {
                        get { return timeout; }
                        set { timeout = value; }
                }
 
+               public virtual string ToolExe
+               {
+                       get {
+                               if (toolExe == null)
+                                       return ToolName;
+                               else
+                                       return toolExe;
+                       }
+                       set {
+                               if (!String.IsNullOrEmpty (value))
+                                       toolExe = value;
+                       }
+               }
+
                protected abstract string ToolName
                {
                        get;
@@ -345,7 +458,21 @@ namespace Microsoft.Build.Utilities
                public string ToolPath
                {
                        get { return toolPath; }
-                       set { toolPath  = value; }
+                       set {
+                               if (!String.IsNullOrEmpty (value))
+                                       toolPath  = value;
+                       }
+               }
+
+               // Snatched from our codedom code, with some changes to make it compatible with csc
+               // (the line+column group is optional is csc)
+               static Regex errorRegex;
+               static Regex CscErrorRegex {
+                       get {
+                               if (errorRegex == null)
+                                       errorRegex = new Regex (@"^(\s*(?<file>[^\(]+)(\((?<line>\d*)(,(?<column>\d*[\+]*))?\))?:\s+)*(?<level>\w+)\s+(?<number>.*\d):\s*(?<message>.*)", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
+                               return errorRegex;
+                       }
                }
        }
 }