[Top][All Lists]
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
C# support for automake
From: |
Bruno Haible |
Subject: |
C# support for automake |
Date: |
Mon, 5 Dec 2005 13:02:10 +0100 |
User-agent: |
KMail/1.5 |
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 <address@hidden>, 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 <address@hidden>, 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:address@hidden@}$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:address@hidden@$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
@address@hidden"$libdirs_mono${CONF_CLIX_PATH:address@hidden@$CONF_CLIX_PATH}"
else
@address@hidden"$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
- C# support for automake,
Bruno Haible <=