List Info

Thread: A tool to have MSBuild use the Mono compilers




A tool to have MSBuild use the Mono compilers
country flaguser name
United Kingdom
2007-10-12 06:18:43
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/0
8/31/458658.aspx
   -->

  <!-- Use with:
        msbuild
/p:CustomAfterMicrosoftCommonTargets="D:TempMonoMsbui
ldMono.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:TempMonoMsbuild</CscToolPath>
   
<VbcToolPath>D:TempMonoMsbuild</VbcToolPath>
   
<UseHostCompilerIfAvailable>false</UseHostCompilerI
fAvailable>
  </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:TempMono.Comp
ilers.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:
//
//  "absolutepathcompiler.exe"  /noconfig "absolutepathrspfile.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 "absolutepathrspfile.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/defaul
t.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
FilesMono-1.2.5bingmcs.bat";
    //const string VbncPath = "D:Program
FilesMono-1.2.5binvbnc.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&q
uot;" >>
        // 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:WINDOWSMicrosoft.NETFrameworkv2.0.50727
Csc.exe"  /noconfig "C:Documents and
SettingsandyLocal SettingsTemptmpEE.tmp">>
    //
<<"C:WINDOWSMicrosoft.NETFrameworkv2.0.50727
Vbc.exe"  /noconfig "C:Documents and
SettingsandyLocal SettingsTemptmp76.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(errorC
ode));
       
System.Diagnostics.Debug.Assert(!String.IsNullOrEmpty(errorC
ode.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(originCategoryCodeTextExpres
sion.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: ", 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: ,
clX:", 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.NETFrameworkv2.0.50727");
        // Remove this assert if your windows directory is
not C:WINDOWS.
        System.Diagnostics.Trace.Assert(fclPath
          == "C:WINDOWSMicrosoft.NETFrameworkv2.0.50727&
quot;);
        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\&q
uot;,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(" :  ",
            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
]]

_______________________________________________
Mono-vb mailing list
Mono-vblists.ximian.com
http
://lists.ximian.com/mailman/listinfo/mono-vb

[1]

about | contact  Other archives ( Real Estate discussion Medical topics )