[mono-vb] A tool to have MSBuild use the Mono compilers

Andy Hume andyhume32 at yahoo.co.uk
Fri Oct 12 07:18:43 EDT 2007


I've seen requests in the past for how to have MSBuild use the Mono compilers.  I attach a tool to allow this.  It can be useful in troubleshooting to find whether a problem is with xbuild/monodevelop or with the compiler etc.


[[
//
// Copyright (c) 2007 Andy Hume.
// No restrictions, free for any use.
//

//==============================================================================

//
// This tool configures MSBuild to use the Mono compilers.  This can be useful 
// in some situations.  For instance I had an issue where neither xbuild nor 
// monodevelop would compile a project, but on using this tool the issue was shown 
// to be a compiler issue.
// 
// Two parts are needed, firstly a .targets file to configure MSBuild to look in 
// a new location for the compilers.  The second part is an executable that fulfills 
// two purposes, firstly MSBuild still looks for compiler filenames csc.exe and 
// vbc.exe.  But secondly, neither of the Mono compilers support all the command-
// line options used by the MSFT tools, nor are all the assemblies supported, so 
// we strip or convert both these -- for details see the code below.
//
// Note the MSFT versions of the other compiler utilities are still used e.g. resgen
// etc.
//


//
// So to use this tool, compile this program creating csc.exe and vbc.exe (e.g. 
// compile it to csc.exe and copy it to vbc.exe).  Then take the xml content below, 
// update the paths and save it to e.g. Mono.Compilers.targets.
//
/*
<Project 
    DefaultTargets="Build" 
    xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <!-- Based on:
	http://blogs.msdn.com/jomo_fisher/archive/2005/08/31/458658.aspx
   -->

  <!-- Use with:
        msbuild /p:CustomAfterMicrosoftCommonTargets="D:\Temp\MonoMsbuild\Mono.Compilers.targets"  MySolution.sln
     Note: the targets file path must be absolute and there is no warning if the 
     file isn't found.
  -->

  <PropertyGroup>
    <CscToolPath>D:\Temp\MonoMsbuild</CscToolPath>
    <VbcToolPath>D:\Temp\MonoMsbuild</VbcToolPath>
    <UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>
  </PropertyGroup>

</Project>
 */
//
// That targets file can then be included in a MSBuild project to have it use the 
// Mono compilers.  One way is to use a command-line of the following form:
//
//   Msbuild /p:CustomAfterMicrosoftCommonTargets="D:\Temp\Mono.Compilers.targets"  MySolution.sln
//
// Note: the targets file path must be absolute and there is no warning if the 
// file isn't found.
//
// It is also possible to reference the file from the project file.  It is further 
// possible to always include that .targets file and make the content of the .targets 
// file itself conditional and thus the Mono compilers are used only when a certain 
// command-line flag is set -- this is how MSBee, the MSBuild FX1.1 builder from 
// http://www.codeplex.com/MSBee, is configured.
//
//
// * How do I know it's working?
// The tool logs the changes it makes to the compiler options, so see warnings 
// in the MSBuild output like:
//    MsBMono : warning XXX999: Was: <<.........>>
//    MsBMono : warning XXX999: Is now: <<.........>>
// And if using the up-to-date vbnc case, see below, see also:
//    VBC : warning : VBNC2009: the option doc was not recognized - ignored
//

//==============================================================================

//
// * Changes
// The three VBNC bugs mentioned below are all fixed (>=r86687), so those pieces of
// functionality are unnecessary in the tool, so if using an up-to-date version
// of vbnc, then in UnsupportedOptionsVbnc you must comment-out "define" and may
// comment-out "doc".
//



//
// What this program does.  Firstly, as noted above the compilers must be files 
// named csc.exe and vbc.exe.  Secondly it handles mapping command-line options 
// and assemblies to forms supported by Mono.
//
// The command-line passed from MSBuild is always of the simple form:
//
//  "absolute\path\compiler.exe"  /noconfig @"absolute\path\rspfile.tmp"
//
// From that we parse the compiler name, "csc" or "vbc", and also the path to the 
// response file, and in the end I we execute e.g.
//
//  "gmcs.bat"  /noconfig @"absolute\path\rspfile.tmp"
//
// However, as noted above we need to modify the command-line options which are 
// passed in the rsp file.  For example gmcs doesn't support /errorprompt and vbnc 
// doesn't support /doc (though it says it ignores it (bug 325332).  For options 
// in this category we just cut them to the first space character, but leave them 
// in place it any quote is found.
//
// However there is also one option that needs special-case handling.  VB's "Compiler 
// Constants" (#defines) support values possibly including spaces, thus the command-
// line option used by MSBuild is doubly-quoted, eg:
//
//  /define:"AA=\"aaa\",BB=-1,CC=\"ccc\"
//
// However vbnc doesn't support this form (bug 325976), so we map this into a supported 
// form where possible.
//
// As well as options, we also support removing unsupprted assemblies.  The assembly 
// System.Deployment.dll is referenced by Visual Studio Windows Forms projects 
// however Mono doesn't include it, by default it isn't used and we can thus remove 
// its reference.
//
// There is one remaining feature that could be added.  In its current state vbnc 
// reports many of its errors in raw for or just as exception output and thus because 
// these are not reported in the standard error format xbuild, monodevelop, and 
// MSBuild all discard these errors and don't report them to the user (bug 328106).
// The tool could be enhanced by detecting such raw errors and prefixing them with 
// a correctly formatted error prefix such as:
//
//     MonoMsbuild: unhandled error MMB000: 
//
// See http://channel9.msdn.com/wiki/default.aspx/MSBuild.CanonicalErrorWarningSpec
// and http://blogs.msdn.com/msbuild/archive/2006/11/03/msbuild-visual-studio-aware-error-messages-and-message-formats.aspx 
//



//==============================================================================

using System;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using System.Text.RegularExpressions;


static class CscVbcCallMonoEquivalent
{
    //
    // The paths to the Mono compilers, if they're in the path then no path is needed
    // for them here just "gmcs.bat" etc will suffice.
    //
    const string GmcsPath = @"gmcs.bat";
    const string VbncPath = @"vbnc.bat";
    // TODO ?Read these from somewhere.
    //const string GmcsPath = @"D:\Program Files\Mono-1.2.5\bin\gmcs.bat";
    //const string VbncPath = @"D:\Program Files\Mono-1.2.5\bin\vbnc.bat";

    //
    // The command-line options not supported by each of the Mono compilers.
    //
    static readonly String[] UnsupportedOptionsGmcs = {
        // <<error CS2007: Unrecognized command-line option: `/errorreport:prompt'>>
        "errorreport",
    };

    static readonly String[] UnsupportedOptionsVbnc = {
        // vbnc can't handle quoted /define values in rsp files.  With this content:
        // <</define:"CONFIG=\"Release\",TRACE=-1,_MyType=\"WindowsForms\",PLATFORM=\"AnyCPU\"" >>
        // it produces
        // <<An error message should have been shown: 'Invalid string constant: "AnyCPU"" /reference:..\MyAsmbly.dll>>
        // We strip all the quotes here.  Hope there's no spaces etc!
        "define",
        // <<Error : VBNC2009: the option doc was not recognized - ignored>>
        // Says ignored but isn't!
        "doc",
        // Is supported "errorreport",
    };

    //
    // Any assembly references we should remove.  For example System.Deployment.dll
    // seems to be added by default for WinForms apps created in VS but it isn't used
    // by default, Mono doesn't include it so we need to remove its reference.
    //
    static readonly String[] UnsupportedAssemblies = {
        "System.Deployment.dll"
    };

    //----
    // The format of the command-line used by MSBuild when running the compiler.
    // Examples are
    // <<"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Csc.exe"  /noconfig @"C:\Documents and Settings\andy\Local Settings\Temp\tmpEE.tmp">>
    // <<"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Vbc.exe"  /noconfig @"C:\Documents and Settings\andy\Local Settings\Temp\tmp76.tmp">>
    // The real compiler options are passed in the rsp file.
    const string MsBuildExecRxStr = "\".*\\\\([^\\\\]+).exe\"?  /noconfig @\"([^\\\"]+)\"";

    //----
    static string s_dbgDescr;

    //----
    static int Main()
    {
        try {
            //Info(   "Iaaaaaaaa");
            //Warning("Waaaaaaaa");
            //RegexPlaying(RxStr);
            //TestCountInstances();
            //Console.WriteLine("----");

            //----
            // Parse the compiler (csc/vbc) and the rsp file passed.
            string cl = Environment.CommandLine;
            //Info("<<cl: " + cl + ">>");
            string compiler, rspFilePath;
            ParseCmdLine(cl, out compiler, out rspFilePath);
            s_dbgDescr = compiler + "->" + rspFilePath;

            // Use that information to configure ourselves.
            string compilerPath;
            string[] badOptions;
            switch (compiler.ToLowerInvariant()) {
                case "csc":
                    compilerPath = GmcsPath;
                    badOptions = UnsupportedOptionsGmcs;
                    break;
                case "vbc":
                    compilerPath = VbncPath;
                    badOptions = UnsupportedOptionsVbnc;
                    break;
                default:
                    Error("Unknown compiler '" + compiler + "'.");
                    throw new ArgumentException();
            }

            //----
            // Recreate the rsp file with options to suit the equivalent Mono compiler.
            CleanOptionsFile(badOptions, rspFilePath);

            //----
            // Now run the Mono compiler.
            string argsString = GetArgsString();
            ProcessStartInfo psi = new ProcessStartInfo(compilerPath, argsString);
            psi.UseShellExecute = false;
            try {
                using (Process proc = Process.Start(psi)) {
                    proc.WaitForExit();
                    return proc.ExitCode;
                }//using
            } catch (System.ComponentModel.Win32Exception winex) {
                // Make a better message...
                throw new System.ComponentModel.Win32Exception("Failed to run the compiler, probably exe/bat file not found.", winex);
            }
        } catch (Exception ex) {
            //ErrorNoExit(ex.ToString());
            //throw;
            Error(ExceptionToStringMessage(ex));
            throw new InvalidOperationException("Internal error -- Reached end of catch");
        }
    }
    
    /// <summary>
    /// Get the first line of the exception ToString -- i.e. containing the type and message,
    /// and the same for all 'inner' exceptions.
    /// </summary>
    static string ExceptionToStringMessage(Exception ex)
    {
        System.Diagnostics.Debug.Assert(ex != null, "ExceptionToStringMessage--ArgNullEx");
        int end = -1;
        string message = ex.ToString();
        end = message.IndexOf('\n');
        int tmp = message.IndexOf('\r');
        if (tmp != -1 && tmp < end) { end = tmp; }
        if (end > -1) {
            message = message.Substring(0, end);
        }
        System.Diagnostics.Debug.Assert(message.IndexOfAny(new char[]{'\n','\r'}) == -1);
        return message;
    }

    enum MessageLevel
    {
        None = 0,
        // ToString'd on output, so nicer if lower case
        error, 
        warning,
        info
    }

    static void ErrorNoExit(string message)
    {
        WriteErrorMessage(MessageLevel.error, message);
    }

    static void Error(string message)
    {
        ErrorNoExit(message);
        Environment.Exit(1);
    }

    static void Warning(string message)
    {
        MessageBox.Show(message, s_dbgDescr);
        WriteErrorMessage(MessageLevel.warning, message);
    }

    static void Info(string message)
    {
        // Info level messages are not displayed, so write as Warning.
        WriteErrorMessage(MessageLevel.warning, message);
    }

    const string ToolName = "MsBMono";

    static void WriteErrorMessage(MessageLevel category, string text)
    {
        WriteErrorMessage(ToolName, null, category, "XXX999", text);
    }
    
    static void WriteErrorMessage(string origin, string subcategory, MessageLevel category, 
        string errorCode, string text)
    {
        System.Text.StringBuilder bldr = new System.Text.StringBuilder();
        if(!String.IsNullOrEmpty(origin)) {
            bldr.Append(origin).Append(": ");
        }
        if(!String.IsNullOrEmpty(subcategory)) {
            bldr.Append(subcategory).Append(" ");
        }
        bldr.Append(category.ToString()).Append(" ");
        System.Diagnostics.Debug.Assert(!String.IsNullOrEmpty(errorCode));
        System.Diagnostics.Debug.Assert(!String.IsNullOrEmpty(errorCode.Trim()));
        bldr.Append(errorCode).Append(":");
        if(!String.IsNullOrEmpty(text)) {
            bldr.Append(" ").Append(text);
        }
        string msg = bldr.ToString();
        // Test for validity against MSBuild's regex.
        // (It doesn't allow 'info' etc, so only test for certain levels).
        if(category == MessageLevel.warning || category == MessageLevel.error) {
            System.Diagnostics.Debug.Assert(originCategoryCodeTextExpression.Match(msg).Success);
        }
        Console.WriteLine(msg);
    }
    
    // From the MSBuild wiki at channel9.  This is apparently the pattern MSBuild
    // uses to check if a compiler output line is a well-formed error message.
    // Defines the main pattern for matching messages.
    static private Regex originCategoryCodeTextExpression = new Regex(
        // Beginning of line and any amount of whitespace.
        @"^\s*"
        // Match a [optional project number prefix 'ddd>'], single letter + colon + remaining filename, or
        // string with no colon followed by a colon.
        +@"(((?<ORIGIN>(((\d+>)?[a-zA-Z]?:[^:]*)|([^:]*))):)"
        // Origin may also be empty. In this case there's no trailing colon.
        +"|())"
        // Match the empty string or a string without a colon that ends with a space
        +"(?<SUBCATEGORY>(()|([^:]*? )))"
        // Match 'error' or 'warning' followed by a space.
        +"(?<CATEGORY>(error|warning)) "
        // Match anything without a colon, followed by a colon
        +"(?<CODE>[^:]*):"
        // Whatever's left on this line, including colons.
        +"(?<TEXT>.*)$",
        RegexOptions.IgnoreCase);


    static void CleanOptionsFile(string[] badOptionsNames, string rspPath)
    {
#if false  // test exception handling
        try {
            throw new RankException("Iiiii iiii.");
        } catch(Exception ex) {
            throw new InvalidOperationException("Eee eeee eeee.", ex);
        }
#endif
    
        string content;
        using (StreamReader rdr = File.OpenText(rspPath)) {
            content = rdr.ReadToEnd();
        }
        Info(   "Was: <<" + content + ">>");
        content = RemoveUnsupportedArgsEtc(badOptionsNames, content);
        Info("Is now: <<" + content + ">>");
        using (StreamWriter wtr = File.CreateText(rspPath)) {
            wtr.Write(content);
        }
    }


    static void ParseCmdLine(string cmdLine, out string compiler,
        out string rspFilePath)
    {
        //compiler = "csc";
        //rspFilePath = "tmpB8.tmp.txt";
        Regex rx = new Regex(MsBuildExecRxStr);
        Match m = rx.Match(cmdLine);
        if (!m.Success) {
            Error(@"Command-line not in expected "
               + @"{""<path>\<CCC>.exe""  /noconfig @""<rspFilePath>""} format.");
        }
        //DiagPrintMatch(m);
        if (m.Groups.Count != 3) {
            Error("Command-line regex didn't find two groups.");
        }
        //CaptureCollection cc = g.Captures;
        compiler = m.Groups[1].Captures[0].Value;
        rspFilePath = m.Groups[2].Captures[0].Value;
    }


    /// <summary>Gets the command-line argument as the original string.
    /// Uses <see cref="P:System.Environment.CommandLine"/> but removes
    /// the program name from the front.
    /// </summary>
    static string GetArgsString()
    {
        string[] cmdArray = Environment.GetCommandLineArgs();
        string cl = Environment.CommandLine;
        string argsString;
        string cmd = cmdArray[0];
        int idx = cl.IndexOf(cmd);
        //Console.WriteLine("idx: {0}", idx);
        if (idx == -1) {
            Error("cmd not in command-line");
            throw new ArgumentException();
        }
        int posArgs;
        if (idx == 0) {
            posArgs = cmd.Length + 0 + 1;
        } else if (idx == 1) {
            //Console.WriteLine("cl0: {0}, clX:{1}", cl[0], cl[cmd.Length + 2 - 1]);
            if (cl[0] == '"' && cl[cmd.Length + 2 - 1] == '"'
                   && cl[cmd.Length + 2 - 1 + 1] == ' ') {
                posArgs = cmd.Length + 2 + 2;
            } else {
                Error("GetArgsString unsupported#1");
                throw new ArgumentException();
            }
        } else {
            Error("GetArgsString unsupported#2");
            throw new ArgumentException();
        }
        // Remove the program and leave only the arguments.
        argsString = cl.Substring(posArgs) + " ";
        return argsString;
    }


    static string RemoveUnsupportedArgsEtc(string[] badOptionsNames,
       String argsString)
    {
        argsString = RemoveUnsupportedOptions(badOptionsNames, argsString);

        //
        // Remove absolute path to FCL assemblies...
        //
        string Windir = Environment.GetEnvironmentVariable("windir"); // @"C:\WINDOWS";
        string fclPath = Path.Combine(Windir,
            @"Microsoft.NET\Framework\v2.0.50727\");
        // Remove this assert if your windows directory is not C:\WINDOWS.
        System.Diagnostics.Trace.Assert(fclPath
          == @"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\");
        argsString = argsString.Replace(fclPath, null);

        //
        // Remove unsupported assemblies -- not crucial hopefully...
        //
        foreach (string badAsmbly in UnsupportedAssemblies) {
            // csc: one ref per /reference: option
            argsString = argsString.Replace("/reference:" + badAsmbly, null);
            // vbc: list of refs in one /reference: option
            argsString = argsString.Replace("," + badAsmbly + ",", ",");
        }
        //
        return argsString;
    }


    static string RemoveUnsupportedOptions(string[] badOptionsNames,
       String argsString)
    {
        // Remove unsupported options
        // First calculate what to do and finally do it.
        if (badOptionsNames != null) {
            foreach (string badOptName in badOptionsNames) {
                // Remove from /optionName to terminating space
                int idxOption = argsString.IndexOf("/" + badOptName);
                if (idxOption == -1) { continue; }
                int idxEnd = argsString.IndexOf(" ", idxOption);
                if (idxEnd == -1) {
                    // ?Either error, or its the last option, so chop to the end.
                    Error("Strip option but no end space.");
                }
                string cut = argsString.Substring(idxOption, idxEnd - idxOption);
                string replace = null;
                //
                // Special case: /define for VB
                if (badOptName == "define") {
                    // Convert /define:"a=\"a\",b=\"b\"" to /define:a=a,b=b
                    // Needs to be in that simple form...
                    // First the inner backslash-quoted quotes.
                    int _countOfValueQuoting = CountInstances(cut, "\\\""); // a=\"aaa\"
                    if ((_countOfValueQuoting & 1) == 1) {
                        Warning("/define, odd number of value quotings.");
                        continue;
                    }
                    replace = cut.Replace("\\\"", null);
                    // Now the other quotes.
                    int countOfQuotes = CountInstances(cut, "\""); // /define:"a=\"aaa\""
                    if ((countOfQuotes & 1) == 1) {
                        Warning("/define, odd number of option quotes.");
                        continue;
                    }
                    replace = replace.Replace("\"", null);
                } else if (cut.Contains("\"")) {
                    // More work to do here?  Do we need to support general 
                    // options with spaces and quoting etc.
                    // /doc doesn't generally, so we're ok so far...
                    // 
                    Warning("Skipping removing option as its value is string delimited");
                    continue;
                }
                //
                // Now actually do the cut/replace.
                Info("gonna cut: <<" + cut + ">>" + (replace == null ? null
                        : " ** and put: <<" + replace + ">>"));
                string join = argsString.Substring(0, idxOption)
                   + replace + argsString.Substring(idxEnd);
                argsString = join;
            }
        }
        return argsString;
    }
    
    /// <summary>
    /// Count the instances of a string in a given string value -- this is not otherwise
    /// supported AFAIK.
    /// </summary>
    static int CountInstances(/*this*/ string thisParam, string value)
    {
        int count = 0;
        int pos = 0;
        while (true) {
            int idx = thisParam.IndexOf(value, pos);
            if (idx == -1) { break; }
            ++count;
            pos = idx + value.Length;
            if (pos >= thisParam.Length) { break; }
        }//while
        return count;
    }
    static void TestCountInstances()
    {
        string[][] values = {
            new string[] { "/define:\"a=\\\"A\\\",b=\\\"B\\\",c=\\\"C\\\"", "\\\"", 6.ToString() },
            new string[] { "/define:\"a=A,b=\\\"B\\\",c=C", "\\\"", 2.ToString() },
            new string[] { "/define:\"aaa\\\"", "\\\"", 1.ToString() },
            new string[] { "/define:\"a=A,b=B,c=C\"", "\\\"", 0.ToString() },
        };
        foreach (string[] cur in values) {
            int count = CountInstances(cur[0], cur[1]);
            Console.WriteLine("{0} {1}: {2} {3}",
            count == int.Parse(cur[2]), count, cur[0], cur[1]
            );
        }//for
    }


    static void RegexPlaying(string rxStr)
    {
        Regex rx = new Regex(rxStr);
        Console.WriteLine("rxStr: " + rxStr);

        string tst = "\"C:\\Temp\\Csc.exe\"  /noconfig @\"C:\\Temp\\tmpEE.tmp\"";
        Console.WriteLine("tst: " + tst);
        Match m = rx.Match(tst);
        Console.WriteLine("m: " + m);
        DiagPrintMatch(m);

        Console.WriteLine();
        tst = "\"C:\\Temp\\XXXX YYY\\Csc.exe\"  /noconfig @\"C:\\Temp\\tmpEE.tmp\"";
        Console.WriteLine("tst: " + tst);
        m = rx.Match(tst);
        Console.WriteLine("m: " + m);
        DiagPrintMatch(m);

        Console.WriteLine();
        tst = "Csc.exe  /noconfig @\"C:\\Temp\\tmpEE.tmp\"";
        Console.WriteLine("tst: " + tst);
        m = rx.Match(tst);
        Console.WriteLine("m: " + m);
        DiagPrintMatch(m);

        Console.WriteLine();
        tst = "Csc.exe  /noconfig @\"C:\\Temp\\tmpEE.tmp\"";
        Console.WriteLine("tst: " + tst);
        m = rx.Match(tst);
        Console.WriteLine("m: " + m);
        DiagPrintMatch(m);
    }

    static void DiagPrintMatch(Match m)
    {
        Console.WriteLine("success: " + m.Success);
        GroupCollection grps = m.Groups;

        for (int i = 0; i < m.Groups.Count; i++) {
            Group g = m.Groups[i];
            Console.WriteLine("Group" + i + "='" + g + "'");
            CaptureCollection cc = g.Captures;
            for (int j = 0; j < cc.Count; j++) {
                Capture c = cc[j];
                System.Console.WriteLine("Capture" + j + "='" + c + "', Position=" + c.Index);
            }
        }
    }

}//class
]]



More information about the Mono-vb mailing list