Hi, At least two GNU packages (gettext and libidn) now have parts of their source code written in C#. Simon Josefsson, author of libidn, suggests that C# support be added to automake. Let me here present the basic facts about C# compilation, and a few macros and scripts that are in use in GNU gettext for a few years and that make be useful in their solution.
The situation ============= C# is a general-purpose programming language, and is basically a mix of 70% Java and 30% C++. C# source files always have the extension ".cs". Source files are often named hierarchically in correspondence to the class(es) defined in the source files, i.e. class Foo.Bar.Baz could be defined in Foo/Bar/Baz.cs. This convention is optional; some programmers follow it, some don't. The three main implementations of C# are: - The GNU pnet and pnetlib package. - Novell's mono package and its class library. - Microsoft's implementation. The first two are open-source, share some of their code but are architected with different goals: pnet is more GNU style, whereas mono is more complete and more Microsoft-compatible. Microsoft's implementation is available in different versions; one of them, called 'sscli', is available with source code (but not open source). I believe that the other Microsoft implementations are invoked in the same way as the sscli implementation. For all implementations, C# source code has to be compiled into object code libraries and object code executables, in order to be executable. The names of the compilers are different, and their command line arguments are different as well: Implementation | Compiler ---------------+--------- pnet | cscc mono | mcs sscli | csc Executables are run via a launcher program, whose name is again different: Implementation | Executor ---------------+--------- pnet | ilrun mono | mono sscli | clix (don't know for non-sscli versions) The object code generated from C# source code is platform-dependent. Although most (or all?) of the file format of C# libraries and executables is platform-independent, C# source code is processed by a preprocessor that understands #if conditionals. For this reason we must consider that object code generated for, say, x86_64, is different from object code generated for i586. Object code libraries are files with suffix ".dll". They are installed under $prefix/lib. Object code executables are files with suffix ".exe". There are two ways to install them: a) Install the executable under $prefix/lib. Let the user invoke it through an explicit "ilrun $prefix/lib/foo.exe" or "mono $prefix/lib/foo.exe". b) Install the executable under $prefix/bin. Additionally install a one-line invocation script $prefix/bin/foo: #!/bin/sh exec mono $prefix/bin/foo.exe "$@" I don't know which of these conventions should be preferred. Things that are quite different from the C/C++ world: - There are no object code files for individual source files. The mcs manual page makes this clear; it is probably also related to the fact that in C# 2.0, parts of the source code can be moved to separate .cs files. - There is no equivalent for include files. All .cs files share equal rights. - A second kind of source files, called resources, files ending in ".resources", can be embedded in object code libraries and executables. - The compiler flags for different purposes (specifying optimization or debugging settings or lookup directories) differ between compilers. But a script "csharpcomp.sh" exists that provides a unified interface for the different compilers. (See below.) Proposed syntax in Makefile.am files ==================================== How could a programmer declare his/her project's structure in a Makefile.am file? I would propose that for a library foobar.dll that combines module1.cs, module2.cs and libres.resources, the programmer would write ================================================================ lib_LIBRARIES = foobar.dll foobar_dll_SOURCES = module1.cs module2.cs libres.resources ================================================================ and for an executable proggie.exe that combines main1.cs, main2.cs, mainres.resources and foobar.dll, the programmer would write ================================================================ bin_PROGRAMS = proggie.exe proggie_exe_SOURCES = main1.cs main2.cs mainres.resources proggie_exe_LDADD = -lfoobar ================================================================ Proposed macros and scripts =========================== Automake should provide two macros that check for a C# compiler and two that provide for a C# execution engines: AM_CSHARPCOMP for the compiler, AM_CSHARPEXEC for the execution engine, AM_CSHARPCOMP_OPTIONAL for the compiler, without aborting the configure script if it is missing, AM_CSHARPEXEC_OPTIONAL for the execution engine, without aborting the configure script if missing. Normal projects would use AM_CSHARPCOMP, AM_CSHARPEXEC. For GNU gettext, however, the C# part is optional (because C# is not widely available), therefore it should be able to use the same logic for finding the C# compiler and executor, but without an AC_FATAL call. AM_CSHARPEXEC should check for the common mistake of installing pnet without pnetlib. (Like with earlier versions of GCC, many people installed g++ without a libstdc++ or libg++, leading to many stupid bug reports to various package maintainers. This kind of thing needs to be caught early in the configure script.) Here are proposed implementations of these macros, derived from those in GNU gettext and gnulib (already owned by the FSF). ========================================================================== # Sets CSHARP_CHOICE to the preferred C# implementation: # 'pnet' or 'mono' or 'any' or 'no'. AC_DEFUN([AM_CSHARP_CHOICE], [ AC_MSG_CHECKING([for preferred C[#] implementation]) AC_ARG_ENABLE(csharp, [ --enable-csharp[[=IMPL]] choose preferred C[#] implementation (pnet or mono)], [CSHARP_CHOICE="$enableval"], CSHARP_CHOICE=any) AC_SUBST(CSHARP_CHOICE) AC_MSG_RESULT([$CSHARP_CHOICE]) case "$CSHARP_CHOICE" in pnet) AC_DEFINE([CSHARP_CHOICE_PNET], 1, [Define if pnet is the preferred C# implementation.]) ;; mono) AC_DEFINE([CSHARP_CHOICE_MONO], 1, [Define if mono is the preferred C# implementation.]) ;; esac ]) # Prerequisites of csharpcomp.sh. # Checks for a C# compiler. # Sets at most one of HAVE_CSCC, HAVE_MCS, HAVE_CSC. # Sets HAVE_CSHARPCOMP to nonempty if csharpcomp.sh will work. # Also sets CSHARPCOMPFLAGS. AC_DEFUN([AM_CSHARPCOMP_OPTIONAL], [ AC_REQUIRE([AM_CSHARP_CHOICE]) AC_MSG_CHECKING([for C[#] compiler]) HAVE_CSHARPCOMP=1 pushdef([AC_MSG_CHECKING],[:])dnl pushdef([AC_CHECKING],[:])dnl pushdef([AC_MSG_RESULT],[:])dnl AC_CHECK_PROG(HAVE_CSCC_IN_PATH, cscc, yes) AC_CHECK_PROG(HAVE_MCS_IN_PATH, mcs, yes) AC_CHECK_PROG(HAVE_CSC_IN_PATH, csc, yes) popdef([AC_MSG_RESULT])dnl popdef([AC_CHECKING])dnl popdef([AC_MSG_CHECKING])dnl for impl in "$CSHARP_CHOICE" pnet mono sscli no; do case "$impl" in pnet) if test -n "$HAVE_CSCC_IN_PATH" \ && cscc --version >/dev/null 2>/dev/null \ && ( # See if pnetlib is well installed. echo 'class ConfTest { static void Main() { } }' > conftest.cs cscc -o conftest.exe conftest.cs 2>/dev/null error=$? rm -f conftest.cs conftest.exe exit $error ); then HAVE_CSCC=1 ac_result="cscc" break fi ;; mono) if test -n "$HAVE_MCS_IN_PATH" \ && mcs --version >/dev/null 2>/dev/null; then HAVE_MCS=1 ac_result="mcs" break fi ;; sscli) if test -n "$HAVE_CSC_IN_PATH" \ && csc -help >/dev/null 2>/dev/null \ && { if csc -help 2>/dev/null | grep -i chicken > /dev/null; then false; else true; fi; }; then HAVE_CSC=1 ac_result="csc" break fi ;; no) HAVE_CSHARPCOMP= ac_result="no" break ;; esac done AC_MSG_RESULT([$ac_result]) AC_SUBST(HAVE_CSCC) AC_SUBST(HAVE_MCS) AC_SUBST(HAVE_CSC) dnl Provide a default for CSHARPCOMPFLAGS. if test -z "${CSHARPCOMPFLAGS+set}"; then CSHARPCOMPFLAGS="-O -g" fi AC_SUBST(CSHARPCOMPFLAGS) ]) AC_DEFUN([AM_CSHARPCOMP], [ AC_REQUIRE([AM_CSHARPCOMP_OPTIONAL]) if test -z "$HAVE_CSHARPCOMP"; then AC_FATAL([No C# compiler found.]) fi ]) # Prerequisites of csharpexec.sh. # Checks for a C# execution engine. # AM_CSHARPEXEC or AM_CSHARPEXEC(testexecutable, its-directory) # Sets at most one of HAVE_ILRUN, HAVE_MONO, HAVE_CLIX. # Sets HAVE_CSHARPEXEC to nonempty if csharpexec.sh will work. AC_DEFUN([AM_CSHARPEXEC_OPTIONAL], [ AC_REQUIRE([AM_CSHARP_CHOICE]) AC_REQUIRE([AC_CANONICAL_HOST]) AC_MSG_CHECKING([for C[#] program execution engine]) AC_EGREP_CPP(yes, [ #if defined _WIN32 || defined __WIN32__ || defined __EMX__ || defined __DJGPP__ yes #endif ], MONO_PATH_SEPARATOR=';', MONO_PATH_SEPARATOR=':') HAVE_CSHARPEXEC=1 pushdef([AC_MSG_CHECKING],[:])dnl pushdef([AC_CHECKING],[:])dnl pushdef([AC_MSG_RESULT],[:])dnl AC_CHECK_PROG(HAVE_ILRUN_IN_PATH, ilrun, yes) AC_CHECK_PROG(HAVE_MONO_IN_PATH, mono, yes) AC_CHECK_PROG(HAVE_CLIX_IN_PATH, clix, yes) popdef([AC_MSG_RESULT])dnl popdef([AC_CHECKING])dnl popdef([AC_MSG_CHECKING])dnl for impl in "$CSHARP_CHOICE" pnet mono sscli no; do case "$impl" in pnet) if test -n "$HAVE_ILRUN_IN_PATH" \ && ilrun --version >/dev/null 2>/dev/null \ ifelse([$1], , , [&& ilrun $2/$1 >/dev/null 2>/dev/null]); then HAVE_ILRUN=1 ac_result="ilrun" break fi ;; mono) if test -n "$HAVE_MONO_IN_PATH" \ && mono --version >/dev/null 2>/dev/null \ ifelse([$1], , , [&& mono $2/$1 >/dev/null 2>/dev/null]); then HAVE_MONO=1 ac_result="mono" break fi ;; sscli) if test -n "$HAVE_CLIX_IN_PATH" \ ifelse([$1], , , [&& clix $2/$1 >/dev/null 2>/dev/null]); then HAVE_CLIX=1 case $host_os in cygwin* | mingw* | pw32*) CLIX_PATH_VAR=PATH ;; darwin* | rhapsody*) CLIX_PATH_VAR=DYLD_LIBRARY_PATH ;; *) CLIX_PATH_VAR=LD_LIBRARY_PATH ;; esac eval CLIX_PATH=\"\$CLIX_PATH_VAR\" ac_result="clix" break fi ;; no) HAVE_CSHARPEXEC= ac_result="no" break ;; esac done AC_MSG_RESULT([$ac_result]) AC_SUBST(MONO_PATH) AC_SUBST(MONO_PATH_SEPARATOR) AC_SUBST(CLIX_PATH_VAR) AC_SUBST(CLIX_PATH) AC_SUBST(HAVE_ILRUN) AC_SUBST(HAVE_MONO) AC_SUBST(HAVE_CLIX) ]) AC_DEFUN([AM_CSHARPEXEC], [ AM_CSHARPEXEC_OPTIONAL([$1],[$2]) if test -z "$HAVE_CSHARPEXEC"; then AC_FATAL([No C# execution engine found.]) fi ]) ========================================================================== The test executable serves to protect against incomplete pnet installations; I suggest to take the trivial csharpexec-test.exe from GNU gettext and install that in the aux-dir. Here is the compiler wrapper: ====================== build-aux/csharpcomp.sh.in ====================== #!/bin/sh # Compile a C# program. # Copyright (C) 2003-2005 Free Software Foundation, Inc. # Written by Bruno Haible <[EMAIL PROTECTED]>, 2003. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # This uses the same choices as csharpcomp.c, but instead of relying on the # environment settings at run time, it uses the environment variables # present at configuration time. # # This is a separate shell script, because the various C# compilers have # different command line options. # # Usage: /bin/sh csharpcomp.sh [OPTION] SOURCE.cs ... RES.resources ... # Options: # -o PROGRAM.exe or -o LIBRARY.dll # set the output assembly name # -L DIRECTORY search for C# libraries also in DIRECTORY # -l LIBRARY reference the C# library LIBRARY.dll # -O optimize # -g generate debugging information # func_tmpdir # creates a temporary directory. # Sets variable # - tmp pathname of freshly created temporary directory func_tmpdir () { # Use the environment variable TMPDIR, falling back to /tmp. This allows # users to specify a different temporary directory, for example, if their # /tmp is filled up or too small. : ${TMPDIR=/tmp} { # Use the mktemp program if available. If not available, hide the error # message. tmp=`(umask 077 && mktemp -d -q "$TMPDIR/gtXXXXXX") 2>/dev/null` && test -n "$tmp" && test -d "$tmp" } || { # Use a simple mkdir command. It is guaranteed to fail if the directory # already exists. $RANDOM is bash specific and expands to empty in shells # other than bash, ksh and zsh. Its use does not increase security; # rather, it minimizes the probability of failure in a very cluttered /tmp # directory. tmp=$TMPDIR/gt$$-$RANDOM (umask 077 && mkdir "$tmp") } || { echo "$0: cannot create a temporary directory in $TMPDIR" >&2 { (exit 1); exit 1; } } } sed_quote_subst='s/\([|&;<>()$`"'"'"'*?[#~=% \\]\)/\\\1/g' options_cscc= options_mcs= options_csc="-nologo" sources= while test $# != 0; do case "$1" in -o) case "$2" in *.dll) options_cscc="$options_cscc -shared" options_mcs="$options_mcs -target:library" options_csc="$options_csc -target:library" ;; *.exe) options_csc="$options_csc -target:exe" ;; esac options_cscc="$options_cscc -o "`echo "$2" | sed -e "$sed_quote_subst"` options_mcs="$options_mcs -o "`echo "$2" | sed -e "$sed_quote_subst"` options_csc="$options_csc -out:"`echo "$2" | sed -e "$sed_quote_subst"` shift ;; -L) options_cscc="$options_cscc -L "`echo "$2" | sed -e "$sed_quote_subst"` options_mcs="$options_mcs -L "`echo "$2" | sed -e "$sed_quote_subst"` options_csc="$options_csc -lib:"`echo "$2" | sed -e "$sed_quote_subst"` shift ;; -l) options_cscc="$options_cscc -l "`echo "$2" | sed -e "$sed_quote_subst"` options_mcs="$options_mcs -r "`echo "$2" | sed -e "$sed_quote_subst"` options_csc="$options_csc -reference:"`echo "$2" | sed -e "$sed_quote_subst"`".dll" shift ;; -O) options_cscc="$options_cscc -O" options_csc="$options_csc -optimize+" ;; -g) options_cscc="$options_cscc -g" options_mcs="$options_mcs -g" options_csc="$options_csc -debug+" ;; -*) echo "csharpcomp: unknown option '$1'" 1>&2 exit 1 ;; *.resources) options_cscc="$options_cscc -fresources="`echo "$1" | sed -e "$sed_quote_subst"` options_mcs="$options_mcs -resource:"`echo "$1" | sed -e "$sed_quote_subst"` options_csc="$options_csc -resource:"`echo "$1" | sed -e "$sed_quote_subst"` ;; *.cs) sources="$sources "`echo "$1" | sed -e "$sed_quote_subst"` ;; *) echo "csharpcomp: unknown type of argument '$1'" 1>&2 exit 1 ;; esac shift done if test -n "@HAVE_CSCC@"; then test -z "$CSHARP_VERBOSE" || echo cscc $options_cscc $sources exec cscc $options_cscc $sources else if test -n "@HAVE_MCS@"; then # mcs prints its errors and warnings to stdout, not stderr. Furthermore it # adds a useless line "Compilation succeeded..." at the end. Correct both. sed_drop_success_line='${ /^Compilation succeeded/d }' func_tmpdir trap 'rm -rf "$tmp"' 1 2 3 15 test -z "$CSHARP_VERBOSE" || echo mcs $options_mcs $sources mcs $options_mcs $sources > "$tmp"/mcs.err result=$? sed -e "$sed_drop_success_line" < "$tmp"/mcs.err >&2 rm -rf "$tmp" exit $result else if test -n "@HAVE_CSC@"; then test -z "$CSHARP_VERBOSE" || echo csc $options_csc $sources exec csc $options_csc $sources else echo 'C# compiler not found, try installing pnet, then reconfigure' 1>&2 exit 1 fi fi fi =========================================================================== Here is the execution engine wrapper. ====================== build-aux/csharpexec.sh.in ====================== #!/bin/sh # Execute a C# program. # Copyright (C) 2003, 2005 Free Software Foundation, Inc. # Written by Bruno Haible <[EMAIL PROTECTED]>, 2003. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # This uses the same choices as csharpexec.c, but instead of relying on the # environment settings at run time, it uses the environment variables # present at configuration time. # # This is a separate shell script, because the various C# interpreters have # different command line options. # # Usage: /bin/sh csharpexec.sh [OPTION] program.exe [ARGUMENTS] # Options: # -L DIRECTORY search for C# libraries also in DIRECTORY sed_quote_subst='s/\([|&;<>()$`"'"'"'*?[#~=% \\]\)/\\\1/g' options_ilrun= libdirs_mono= prog= while test $# != 0; do case "$1" in -L) options_ilrun="$options_ilrun -L "`echo "$2" | sed -e "$sed_quote_subst"` libdirs_mono="${libdirs_mono:[EMAIL PROTECTED]@}$2" shift ;; -*) echo "csharpexec: unknown option '$1'" 1>&2 exit 1 ;; *) prog="$1" break ;; esac shift done if test -z "$prog"; then echo "csharpexec: no program specified" 1>&2 exit 1 fi case "$prog" in *.exe) ;; *) echo "csharpexec: program is not a .exe" 1>&2 exit 1 ;; esac if test -n "@HAVE_ILRUN@"; then test -z "$CSHARP_VERBOSE" || echo ilrun $options_ilrun "$@" exec ilrun $options_ilrun "$@" else if test -n "@HAVE_MONO@"; then CONF_MONO_PATH='@MONO_PATH@' if test -n "$libdirs_mono"; then MONO_PATH="$libdirs_mono${CONF_MONO_PATH:[EMAIL PROTECTED]@$CONF_MONO_PATH}" else MONO_PATH="$CONF_MONO_PATH" fi export MONO_PATH test -z "$CSHARP_VERBOSE" || echo mono "$@" exec mono "$@" else if test -n "@HAVE_CLIX@"; then CONF_CLIX_PATH='@CLIX_PATH@' if test -n "$libdirs_mono"; then @[EMAIL PROTECTED]"$libdirs_mono${CONF_CLIX_PATH:[EMAIL PROTECTED]@$CONF_CLIX_PATH}" else @[EMAIL PROTECTED]"$CONF_CLIX_PATH" fi export @CLIX_PATH_VAR@ test -z "$CSHARP_VERBOSE" || echo clix "$@" exec clix "$@" else echo 'C# virtual machine not found, try installing pnet, then reconfigure' 1>&2 exit 1 fi fi fi =========================================================================== Proposed actions in Makefile.in =============================== Let's go back to the sample Makefile.am from above: ================================================================ lib_LIBRARIES = foobar.dll foobar_dll_SOURCES = module1.cs module2.cs libres.resources bin_PROGRAMS = proggie.exe proggie_exe_SOURCES = main1.cs main2.cs mainres.resources proggie_exe_LDADD = -lfoobar ================================================================ What actions should automake put into the appropriate Makefile.in targets? 0) Generally: A variable CSHARPCOMP should be defined: CSHARPCOMP = $(SHELL) $(top_builddir)/csharpcomp.sh Also, a variable CSHARPCOMPFLAGS should be defined, with default value "-O -g". CSHARPCOMPFLAGS = @CSHARPCOMPFLAGS@ 1) For the library: - Target "all" should depend on foobar.dll. - A target foobar.dll should be added: foobar.dll : module1.cs module2.cs libres.resources $(CSHARPCOMP) $(CSHARPCOMPFLAGS) -o $@ $(srcdir)/module1.cs $(srcdir)/module2.cs $(srcdir)/libres.resources - Target "clean" should remove foobar.dll. - Target "install" should install foobar.dll into $(libdir), using $(INSTALL_DATA). - Target "uninstall" and "installdirs" accordingly. 2) For the executable. - Target "all" should depend on proggie.exe. - A target "proggie.exe" should be added: proggie.exe : main1.cs main2.cs mainres.resources foobar.dll $(CSHARPCOMP) $(CSHARPCOMPFLAGS) -o $@ $(srcdir)/main1.cs $(srcdir)/main2.cs $(srcdir)/mainres.resources -lfoobar - Target "clean" should remove proggie.exe. - Target "install" should install proggie.exe into $(bindir), using $(INSTALL_DATA). (Not using $(INSTALL_PROGRAM), because INSTALL_PROGRAM sometimes contains an "-s" options, instructing 'install' to invoke 'strip' - which does not make sense here.) Optionally (or always?), on platforms where $EXEEXT is not ".exe", also install a script 'foobar' into the same directory, using $(INSTALL_SCRIPT), that invokes proggie.exe via the interpreter found by AM_CSHARPEXEC_OPTIONAL. What actions could "automake" perform? - If it has rules that need $(CSHARPCOMP) and $(CSHARPCOMPFLAGS), it should verify that configure.ac directly or indirectly invokes AM_CSHARPCOMP_OPTIONAL. - If it has rules that use $(CSHARPEXEC), it should verify that configure.ac directly or indirectly invokes AM_CSHARPEXEC_OPTIONAL. - If configure.ac directly or indirectly uses AM_CSHARPCOMP_OPTIONAL, it installs csharpcomp.sh.in in the aux-dir, and verifies that a AC_CONFIG_FILES([build-aux/csharpcomp.sh:csharpcomp.sh]) setting exists. - If configure.ac directly or indirectly uses AM_CSHARPEXEC_OPTIONAL, it install csharpexec.sh.in in the aux-dir, and verifies that a AC_CONFIG_FILES([build-aux/csharpexec.sh:csharpexec.sh]) setting exists. Bruno