[Mono-list] 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-list
mailing list