The csharpexec, csharpcomp modules allow to compile and run C# programs. So far, they support - Mono, - the (20 years old, proprietary) SSCLI implementation from Microsoft.
Nowadays, Mono is in minimal maintenance, and the common C# implementation is .NET (from Microsoft, under MIT license). This implementation can be obtained - either from source, - or as Linux binaries from https://versionsof.net/, - or as binary packages for some distros such as Ubuntu, CentOS (cf. https://repology.org/project/dotnet/versions ). This series of patches adds support for this implementation to the csharpexec, csharpcomp modules. Tested with - dotnet 6, 7, 8 on Ubuntu, - MSVC (dotnet 6) on Windows and Cygwin. 2024-10-08 Bruno Haible <br...@clisp.org> csharpcomp: Add support for dotnet. * lib/csharpcomp.c: Include <dirent.h>, concat-filename.h, xvasprintf.h. (name_is_dll): New function, from lib/csharpexec.c. (compile_csharp_using_dotnet): New function. (compile_csharp_class): Invoke compile_csharp_using_dotnet. * modules/csharpcomp (Depends-on): Add xconcat-filename, scandir, alphasort, xvasprintf. csharpcomp-script: Add support for dotnet. * m4/csharpcomp.m4 (gt_CSHARPCOMP): Support 'dotnet' as implementation. Set HAVE_DOTNET_SDK, HAVE_DOTNET_CSC. * build-aux/csharpcomp.sh.in: Add implementations for the cases $HAVE_DOTNET_SDK = 1 and $HAVE_DOTNET_CSC = 1. 2024-10-08 Bruno Haible <br...@clisp.org> csharpexec: Add support for dotnet. * lib/csharpexec.c: Include <dirent.h>, <errno.h>, <sys/stat.h>, dirname.h, concat-filename.h, canonicalize.h, spawn-pipe.h, wait-process.h, xalloc.h, copy-file.h, clean-temp-simple.h, clean-temp.h. (name_is_dll, execute_csharp_using_dotnet): New functions. (execute_csharp_program): Invoke execute_csharp_using_dotnet. * modules/csharpexec (Depends-on): Add stat, dirname, xconcat-filename, canonicalize, spawn-pipe, wait-process, scandir, alphasort, copy-file, clean-temp-simple, clean-temp. csharpexec-script: Add support for dotnet. * m4/csharpexec.m4 (gt_CSHARPEXEC): Support 'dotnet' as implementation. Set HAVE_DOTNET. * build-aux/csharpexec.sh.in (func_tmpdir): New function, copied from build-aux/csharpcomp.sh.in. Add implementation for the case $HAVE_DOTNET = 1. 2024-10-08 Bruno Haible <br...@clisp.org> csharpexec-script, csharpcomp-script: Prepare support for dotnet. * m4/csharp.m4 (gt_CSHARP_CHOICE): Recognize 'dotnet' as value of --enable-csharp. 2024-10-08 Bruno Haible <br...@clisp.org> csharpcomp: Behave like csharpcomp-script. * lib/csharpcomp.c (compile_csharp_using_sscli): Pass the option '-nologo' to csc. 2024-10-08 Bruno Haible <br...@clisp.org> csharpcomp: Fix memory management bug (regression yesterday). * lib/csharpcomp.c (compile_csharp_using_sscli): Allocate the source options with malloc() always, not sometimes with malloca() and sometimes with malloc().
>From 5949b7855aff36e7e084d7f2c15b62575b258171 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Wed, 9 Oct 2024 03:05:33 +0200 Subject: [PATCH 1/7] csharpcomp: Fix memory management bug (regression yesterday). * lib/csharpcomp.c (compile_csharp_using_sscli): Allocate the source options with malloc() always, not sometimes with malloca() and sometimes with malloc(). --- ChangeLog | 7 +++++++ lib/csharpcomp.c | 9 +++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ChangeLog b/ChangeLog index 27e1dca18c..1ddf1e11b6 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +2024-10-08 Bruno Haible <br...@clisp.org> + + csharpcomp: Fix memory management bug (regression yesterday). + * lib/csharpcomp.c (compile_csharp_using_sscli): Allocate the source + options with malloc() always, not sometimes with malloca() and sometimes + with malloc(). + 2024-10-07 Bruno Haible <br...@clisp.org> csharpcomp: Improve Cygwin support. diff --git a/lib/csharpcomp.c b/lib/csharpcomp.c index a0139c5b85..71e9633355 100644 --- a/lib/csharpcomp.c +++ b/lib/csharpcomp.c @@ -324,7 +324,7 @@ compile_csharp_using_sscli (const char * const *sources, we need to use cygpath_w. */ malloced = (char **) - xmalloca ((1 + libdirs_count + sources_count) * sizeof (char *)); + xmalloca ((1 + libdirs_count + sources_count * 2) * sizeof (char *)); mallocedp = malloced; argc = @@ -377,10 +377,10 @@ compile_csharp_using_sscli (const char * const *sources, 10) == 0) { char *option = - (char *) xmalloca (10 + strlen (source_file_converted) + 1); - + (char *) xmalloc (10 + strlen (source_file_converted) + 1); memcpy (option, "-resource:", 10); strcpy (option + 10, source_file_converted); + *mallocedp++ = option; *argp++ = option; } else @@ -404,9 +404,6 @@ compile_csharp_using_sscli (const char * const *sources, for (i = 2; i < 3 + libdirs_count + libraries_count; i++) freea ((char *) argv[i]); - for (i = 0; i < sources_count; i++) - if (argv[argc - sources_count + i] != sources[i]) - freea ((char *) argv[argc - sources_count + i]); while (mallocedp > malloced) free (*--mallocedp); freea (argv); -- 2.34.1
>From 5b3286f6bb69abfbeebc693aa75b08205ed739ac Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Wed, 9 Oct 2024 03:09:15 +0200 Subject: [PATCH 2/7] csharpcomp: Behave like csharpcomp-script. * lib/csharpcomp.c (compile_csharp_using_sscli): Pass the option '-nologo' to csc. --- ChangeLog | 6 ++++++ lib/csharpcomp.c | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ChangeLog b/ChangeLog index 1ddf1e11b6..20c091f007 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2024-10-08 Bruno Haible <br...@clisp.org> + + csharpcomp: Behave like csharpcomp-script. + * lib/csharpcomp.c (compile_csharp_using_sscli): Pass the option + '-nologo' to csc. + 2024-10-08 Bruno Haible <br...@clisp.org> csharpcomp: Fix memory management bug (regression yesterday). diff --git a/lib/csharpcomp.c b/lib/csharpcomp.c index 71e9633355..28247ed1a7 100644 --- a/lib/csharpcomp.c +++ b/lib/csharpcomp.c @@ -328,12 +328,13 @@ compile_csharp_using_sscli (const char * const *sources, mallocedp = malloced; argc = - 1 + 1 + 1 + libdirs_count + libraries_count + 2 + 1 + 1 + libdirs_count + libraries_count + (optimize ? 1 : 0) + (debug ? 1 : 0) + sources_count; argv = (const char **) xmalloca ((argc + 1) * sizeof (const char *)); argp = argv; *argp++ = "csc"; + *argp++ = "-nologo"; *argp++ = (output_is_library ? "-target:library" : "-target:exe"); { char *output_file_converted = cygpath_w (output_file); @@ -402,7 +403,7 @@ compile_csharp_using_sscli (const char * const *sources, false, false, false, false, true, true, NULL); - for (i = 2; i < 3 + libdirs_count + libraries_count; i++) + for (i = 3; i < 4 + libdirs_count + libraries_count; i++) freea ((char *) argv[i]); while (mallocedp > malloced) free (*--mallocedp); -- 2.34.1
>From 9949a97e8b3962b49354770f85c4e18a67706572 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Wed, 9 Oct 2024 03:11:32 +0200 Subject: [PATCH 3/7] csharpexec-script, csharpcomp-script: Prepare support for dotnet. * m4/csharp.m4 (gt_CSHARP_CHOICE): Recognize 'dotnet' as value of --enable-csharp. --- ChangeLog | 6 ++++++ m4/csharp.m4 | 14 +++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/ChangeLog b/ChangeLog index 20c091f007..af1b11f8d9 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2024-10-08 Bruno Haible <br...@clisp.org> + + csharpexec-script, csharpcomp-script: Prepare support for dotnet. + * m4/csharp.m4 (gt_CSHARP_CHOICE): Recognize 'dotnet' as value of + --enable-csharp. + 2024-10-08 Bruno Haible <br...@clisp.org> csharpcomp: Behave like csharpcomp-script. diff --git a/m4/csharp.m4 b/m4/csharp.m4 index 06a71c5f32..8b30a4ad4d 100644 --- a/m4/csharp.m4 +++ b/m4/csharp.m4 @@ -1,17 +1,21 @@ # csharp.m4 -# serial 4 +# serial 5 dnl Copyright (C) 2004-2005, 2009-2024 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, dnl with or without modifications, as long as this notice is preserved. # Sets CSHARP_CHOICE to the preferred C# implementation: -# 'mono' or 'any' or 'no'. +# 'mono' or 'dotnet' or 'any' or 'no'. +# Here +# - 'mono' means <https://en.wikipedia.org/wiki/Mono_(software)>. +# - 'dotnet' means the (newer) .NET <https://en.wikipedia.org/wiki/.NET>, +# *not* the .NET framework <https://en.wikipedia.org/wiki/.NET_Framework>. AC_DEFUN([gt_CSHARP_CHOICE], [ AC_MSG_CHECKING([for preferred C[#] implementation]) AC_ARG_ENABLE([csharp], - [ --enable-csharp[[=IMPL]] choose preferred C[#] implementation (mono)], + [ --enable-csharp[[=IMPL]] choose preferred C[#] implementation (mono, dotnet)], [CSHARP_CHOICE="$enableval"], CSHARP_CHOICE=any) AC_SUBST([CSHARP_CHOICE]) @@ -21,5 +25,9 @@ AC_DEFUN([gt_CSHARP_CHOICE] AC_DEFINE([CSHARP_CHOICE_MONO], [1], [Define if mono is the preferred C# implementation.]) ;; + dotnet) + AC_DEFINE([CSHARP_CHOICE_DOTNET], [1], + [Define if dotnet is the preferred C# implementation.]) + ;; esac ]) -- 2.34.1
>From d81eb1e88b7493d35d85c2f9fc17c68a8e9b50f9 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Wed, 9 Oct 2024 03:12:42 +0200 Subject: [PATCH 4/7] csharpexec-script: Add support for dotnet. * m4/csharpexec.m4 (gt_CSHARPEXEC): Support 'dotnet' as implementation. Set HAVE_DOTNET. * build-aux/csharpexec.sh.in (func_tmpdir): New function, copied from build-aux/csharpcomp.sh.in. Add implementation for the case $HAVE_DOTNET = 1. --- ChangeLog | 9 +++ build-aux/csharpexec.sh.in | 148 +++++++++++++++++++++++++++++++++---- m4/csharpexec.m4 | 16 +++- 3 files changed, 155 insertions(+), 18 deletions(-) diff --git a/ChangeLog b/ChangeLog index af1b11f8d9..441bdbb7fb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +2024-10-08 Bruno Haible <br...@clisp.org> + + csharpexec-script: Add support for dotnet. + * m4/csharpexec.m4 (gt_CSHARPEXEC): Support 'dotnet' as implementation. + Set HAVE_DOTNET. + * build-aux/csharpexec.sh.in (func_tmpdir): New function, copied from + build-aux/csharpcomp.sh.in. + Add implementation for the case $HAVE_DOTNET = 1. + 2024-10-08 Bruno Haible <br...@clisp.org> csharpexec-script, csharpcomp-script: Prepare support for dotnet. diff --git a/build-aux/csharpexec.sh.in b/build-aux/csharpexec.sh.in index 83d60ca870..45c95b4b5f 100644 --- a/build-aux/csharpexec.sh.in +++ b/build-aux/csharpexec.sh.in @@ -28,13 +28,46 @@ # Options: # -L DIRECTORY search for C# libraries also in DIRECTORY +# 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' libdirs_mono= +libdirs_dotnet= prog= while test $# != 0; do case "$1" in -L) libdirs_mono="${libdirs_mono:+$libdirs_mono@MONO_PATH_SEPARATOR@}$2" + libdirs_dotnet="${libdirs_dotnet:+$libdirs_dotnet|}$2" shift ;; -*) @@ -71,26 +104,111 @@ if test -n "@HAVE_MONO@"; then 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 - @CLIX_PATH_VAR@="$libdirs_mono${CONF_CLIX_PATH:+@MONO_PATH_SEPARATOR@$CONF_CLIX_PATH}" - else - @CLIX_PATH_VAR@="$CONF_CLIX_PATH" - fi - export @CLIX_PATH_VAR@ + if test -n "@HAVE_DOTNET@"; then + # Invoke 'dotnet $prog ...'. + # Documentation: + # <https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet#options-for-running-an-application> + # This could be optimized on Windows platforms, because C# .exe files are + # directly executable there. But there's no point in optimizing specifically + # a non-free platform, especially since it would increase the test matrix. shift - # On Windows, assume that 'clix' is a native Windows program, - # not a Cygwin program. + # On Windows, assume that 'dotnet' is a native Windows program, not a Cygwin program. + prog_arg="$prog" case "@build_os@" in cygwin*) - prog=`cygpath -w "$prog"` + prog_arg=`cygpath -w "$prog"` ;; esac - test -z "$CSHARP_VERBOSE" || echo clix "$prog" "$@" - exec clix "$prog" "$@" + # Handle the -L options. + # The way this works is that we have to copy (or symlink) the DLLs into the + # directory where FOO.exe resides. + # Maybe there is another way to do this, but I haven't found it, trying + # - the --additionalprobingpath command-line option, + # - the additionalProbingPaths property in runtimeconfig.json, + # - adding a --deps deps.json option, + # cf. <https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet#options-for-running-an-application> + # and <https://github.com/dotnet/runtime/blob/main/docs/design/features/host-probing.md>. + tmpfiles= + if test -n "$libdirs_dotnet"; then + # Make sure the added DLLs are removed when this script terminates. + func_cleanup_tmpfiles() + { + saved_IFS="$IFS" + IFS='|' + for file in $tmpfiles; do + IFS="$saved_IFS" + rm -f "$file" + done + IFS="$saved_IFS" + } + trap func_cleanup_tmpfiles HUP INT QUIT PIPE TERM + trap 'exit_status=$?; func_cleanup_tmpfiles; exit $exit_status' EXIT + # Copy the DLLs. + prog_dir=`dirname "$prog"` + saved_IFS="$IFS" + IFS='|' + for dir in $libdirs_dotnet; do + IFS="$saved_IFS" + for file in `cd "$dir" && echo *.dll`; do + if test -f "$prog_dir/$file"; then + # A DLL of this name is already at the expected location. + : + else + tmpfiles="${tmpfiles:+$tmpfiles|}$prog_dir/$file" + cp "$dir/$file" "$prog_dir/$file" || exit 1 + fi + done + done + IFS="$saved_IFS" + fi + if test -f "${prog%.exe}.runtimeconfig.json"; then + # There is already a FOO.runtimeconfig.json alongside FOO.exe. + dotnet exec "$prog_arg" "$@" + result=$? + else + # dotnet needs a FOO.runtimeconfig.json alongside FOO.exe in order to + # execute FOO.exe. Create a dummy one in a temporary directory + # (because the directory where FOO.exe sits is not necessarily writable). + # Documentation of this file format: + # <https://learn.microsoft.com/en-us/dotnet/core/runtime-config/> + func_tmpdir + runtimeconfig="$tmp"/runtimeconfig.json + dotnet --list-runtimes | sed -n -e 's/Microsoft.NETCore.App \([^ ]*\) .*/{ "runtimeOptions": { "framework": { "name": "Microsoft.NETCore.App", "version": "\1" } } }/p' > "$runtimeconfig" + runtimeconfig_arg="$runtimeconfig" + case "@build_os@" in + cygwin*) + runtimeconfig_arg=`cygpath -w "$runtimeconfig"` + ;; + esac + test -z "$CSHARP_VERBOSE" || echo dotnet exec --runtimeconfig "$runtimeconfig_arg" "$prog_arg" "$@" + dotnet exec --runtimeconfig "$runtimeconfig_arg" "$prog_arg" "$@" + result=$? + rm -f "$runtimeconfig" + rmdir "$tmp" + fi + exit $result else - echo 'C# virtual machine not found, try installing mono, then reconfigure' 1>&2 - exit 1 + if test -n "@HAVE_CLIX@"; then + CONF_CLIX_PATH='@CLIX_PATH@' + if test -n "$libdirs_mono"; then + @CLIX_PATH_VAR@="$libdirs_mono${CONF_CLIX_PATH:+@MONO_PATH_SEPARATOR@$CONF_CLIX_PATH}" + else + @CLIX_PATH_VAR@="$CONF_CLIX_PATH" + fi + export @CLIX_PATH_VAR@ + shift + # On Windows, assume that 'clix' is a native Windows program, + # not a Cygwin program. + case "@build_os@" in + cygwin*) + prog=`cygpath -w "$prog"` + ;; + esac + test -z "$CSHARP_VERBOSE" || echo clix "$prog" "$@" + exec clix "$prog" "$@" + else + echo 'C# virtual machine not found, try installing mono or dotnet, then reconfigure' 1>&2 + exit 1 + fi fi fi diff --git a/m4/csharpexec.m4 b/m4/csharpexec.m4 index 8191579f52..a47ad7de29 100644 --- a/m4/csharpexec.m4 +++ b/m4/csharpexec.m4 @@ -1,5 +1,5 @@ # csharpexec.m4 -# serial 9 +# serial 10 dnl Copyright (C) 2003-2005, 2009-2024 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -8,7 +8,7 @@ # Prerequisites of csharpexec.sh. # Checks for a C# execution engine. # gt_CSHARPEXEC or gt_CSHARPEXEC(testexecutable, its-directory) -# Sets at most one of HAVE_MONO, HAVE_CLIX. +# Sets at most one of HAVE_MONO, HAVE_DOTNET, HAVE_CLIX. # Sets HAVE_CSHARPEXEC to nonempty if csharpexec.sh will work. AC_DEFUN([gt_CSHARPEXEC], [ @@ -25,11 +25,12 @@ AC_DEFUN([gt_CSHARPEXEC] pushdef([AC_CHECKING],[:])dnl pushdef([AC_MSG_RESULT],[:])dnl AC_CHECK_PROG([HAVE_MONO_IN_PATH], [mono], [yes]) + AC_CHECK_PROG([HAVE_DOTNET_IN_PATH], [dotnet], [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" mono no; do + for impl in "$CSHARP_CHOICE" mono dotnet no; do case "$impl" in mono) if test -n "$HAVE_MONO_IN_PATH" \ @@ -40,6 +41,14 @@ AC_DEFUN([gt_CSHARPEXEC] break fi ;; + dotnet) + if test -n "$HAVE_DOTNET_IN_PATH" \ + && dotnet --list-runtimes >/dev/null 2>/dev/null; then + HAVE_DOTNET=1 + ac_result="dotnet" + break + fi + ;; sscli) if test -n "$HAVE_CLIX_IN_PATH" \ m4_if([$1], , , [&& clix $2/$1 >/dev/null 2>/dev/null]); then @@ -73,5 +82,6 @@ AC_DEFUN([gt_CSHARPEXEC] AC_SUBST([CLIX_PATH_VAR]) AC_SUBST([CLIX_PATH]) AC_SUBST([HAVE_MONO]) + AC_SUBST([HAVE_DOTNET]) AC_SUBST([HAVE_CLIX]) ]) -- 2.34.1
>From a300496564ad2abcd9b65b98d2f8f474ee2a4b24 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Wed, 9 Oct 2024 03:14:04 +0200 Subject: [PATCH 5/7] csharpexec: Add support for dotnet. * lib/csharpexec.c: Include <dirent.h>, <errno.h>, <sys/stat.h>, dirname.h, concat-filename.h, canonicalize.h, spawn-pipe.h, wait-process.h, xalloc.h, copy-file.h, clean-temp-simple.h, clean-temp.h. (name_is_dll, execute_csharp_using_dotnet): New functions. (execute_csharp_program): Invoke execute_csharp_using_dotnet. * modules/csharpexec (Depends-on): Add stat, dirname, xconcat-filename, canonicalize, spawn-pipe, wait-process, scandir, alphasort, copy-file, clean-temp-simple, clean-temp. --- ChangeLog | 11 ++ lib/csharpexec.c | 427 ++++++++++++++++++++++++++++++++++++++++++++- modules/csharpexec | 13 +- 3 files changed, 445 insertions(+), 6 deletions(-) diff --git a/ChangeLog b/ChangeLog index 441bdbb7fb..8e3a411649 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,16 @@ 2024-10-08 Bruno Haible <br...@clisp.org> + csharpexec: Add support for dotnet. + * lib/csharpexec.c: Include <dirent.h>, <errno.h>, <sys/stat.h>, + dirname.h, concat-filename.h, canonicalize.h, spawn-pipe.h, + wait-process.h, xalloc.h, copy-file.h, clean-temp-simple.h, + clean-temp.h. + (name_is_dll, execute_csharp_using_dotnet): New functions. + (execute_csharp_program): Invoke execute_csharp_using_dotnet. + * modules/csharpexec (Depends-on): Add stat, dirname, xconcat-filename, + canonicalize, spawn-pipe, wait-process, scandir, alphasort, copy-file, + clean-temp-simple, clean-temp. + csharpexec-script: Add support for dotnet. * m4/csharpexec.m4 (gt_CSHARPEXEC): Support 'dotnet' as implementation. Set HAVE_DOTNET. diff --git a/lib/csharpexec.c b/lib/csharpexec.c index c7e3fe5ae5..79f2a3fd91 100644 --- a/lib/csharpexec.c +++ b/lib/csharpexec.c @@ -21,14 +21,26 @@ /* Specification. */ #include "csharpexec.h" +#include <dirent.h> +#include <errno.h> #include <stdio.h> #include <stdlib.h> +#include <sys/stat.h> #include <error.h> +#include "dirname.h" +#include "concat-filename.h" +#include "canonicalize.h" #include "cygpath.h" #include "execute.h" +#include "spawn-pipe.h" +#include "wait-process.h" #include "sh-quote.h" +#include "xalloc.h" #include "xmalloca.h" +#include "copy-file.h" +#include "clean-temp-simple.h" +#include "clean-temp.h" #include "gettext.h" /* Handling of MONO_PATH is just like Java CLASSPATH. */ @@ -72,20 +84,33 @@ Program from mono mono - clix sscli + dotnet dotnet or MSVC + clix sscli (non-free) With Mono, the MONO_PATH is a colon separated list of pathnames. (On Windows: semicolon separated list of pathnames.) We try the CIL interpreters in the following order: - 1. "mono", because it is a partially free system but doesn't integrate - well with Unix. - 2. "clix", although it is not free, because it is a kind of "reference + 1. "mono", because it is nowadays the "traditional" C# system on Unix. + 2. "dotnet", because it is another (newer) free C# system on Unix. + 3. "clix", although it is not free, because it is a kind of "reference implementation" of C#. But the order can be changed through the --enable-csharp configuration option. */ +static int +name_is_dll (const struct dirent *d) +{ + if (d->d_name[0] != '.') + { + size_t d_name_len = strlen (d->d_name); + if (d_name_len > 4 && memcmp (d->d_name + d_name_len - 4, ".dll", 4) == 0) + return 1; + } + return 0; +} + static int execute_csharp_using_mono (const char *assembly_path, const char * const *libdirs, @@ -150,6 +175,383 @@ execute_csharp_using_mono (const char *assembly_path, return -1; } +static int +execute_csharp_using_dotnet (const char *assembly_path, + const char * const *libdirs, + unsigned int libdirs_count, + const char * const *args, unsigned int nargs, + bool verbose, bool quiet, + execute_fn *executer, void *private_data) +{ + static bool dotnet_tested; + static bool dotnet_present; + + if (!dotnet_tested) + { + /* Test for presence of dotnet: + "dotnet --list-runtimes >/dev/null 2>/dev/null" */ + const char *argv[3]; + int exitstatus; + + argv[0] = "dotnet"; + argv[1] = "--list-runtimes"; + argv[2] = NULL; + exitstatus = execute ("dotnet", "dotnet", argv, NULL, + false, false, true, true, + true, false, NULL); + dotnet_present = (exitstatus == 0); + dotnet_tested = true; + } + + if (dotnet_present) + { + bool err = false; + + char *assembly_path_converted = cygpath_w (assembly_path); + + /* Handle the -L options. + The way this works is that we have to copy (or symlink) the DLLs into + the directory where FOO.exe resides. + Maybe there is another way to do this, but I haven't found it, trying + - the --additionalprobingpath command-line option, + - the additionalProbingPaths property in runtimeconfig.json, + - adding a --deps deps.json option, + cf. <https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet#options-for-running-an-application> + and <https://github.com/dotnet/runtime/blob/main/docs/design/features/host-probing.md>. */ + char **tmpfiles = NULL; + size_t tmpfiles_alloc = 0; + size_t tmpfiles_count = 0; + if (libdirs_count > 0) + { + /* Copy the DLLs. */ + char *assembly_dir = dir_name (assembly_path); + + unsigned int l; + for (l = 0; l < libdirs_count; l++) + { + const char *libdir = libdirs[l]; + + struct dirent **dlls; + int num_dlls; + + /* Get a list of all *.dll files in libdir. */ + num_dlls = scandir (libdir, &dlls, name_is_dll, alphasort); + if (num_dlls < 0 && errno == ENOMEM) + xalloc_die (); + if (num_dlls <= 0) + { + dlls = NULL; + num_dlls = 0; + } + + int i; + for (i = 0; i < num_dlls; i++) + { + char *target_dll = + xconcatenated_filename (assembly_dir, dlls[i]->d_name, + NULL); + struct stat statbuf; + if (stat (target_dll, &statbuf) == 0 || errno == EOVERFLOW) + { + /* A DLL of this name is already at the expected + location. */ + } + else + { + char *absolute_target_dll = + canonicalize_filename_mode (target_dll, CAN_ALL_BUT_LAST); + if (absolute_target_dll == NULL) + { + if (errno == ENOMEM) + xalloc_die (); + } + else + { + char *libdir_dll = + xconcatenated_filename (libdir, dlls[i]->d_name, + NULL); + + /* Create absolute_target_dll as a temporary file. */ + if (register_temporary_file (absolute_target_dll) < 0) + xalloc_die (); + + if (tmpfiles_count >= tmpfiles_alloc) + { + tmpfiles_alloc = 2 * tmpfiles_alloc + 1; + tmpfiles = + (char **) + xrealloc (tmpfiles, tmpfiles_alloc * sizeof (char *)); + } + tmpfiles[tmpfiles_count++] = absolute_target_dll; + + if (copy_file_to (libdir_dll, absolute_target_dll) != 0) + { + remove (absolute_target_dll); + unregister_temporary_file (absolute_target_dll); + error (0, 0, _("failed to copy '%s' to '%s'"), + libdir_dll, absolute_target_dll); + free (libdir_dll); + free (target_dll); + for (i = 0; i < num_dlls; i++) + free (dlls[i]); + free (dlls); + free (assembly_dir); + while (tmpfiles_count > 0) + { + char *tmpfile = tmpfiles[--tmpfiles_count]; + remove (tmpfile); + unregister_temporary_file (tmpfile); + free (tmpfile); + } + free (tmpfiles); + return 1; + } + free (libdir_dll); + } + } + free (target_dll); + } + + for (i = 0; i < num_dlls; i++) + free (dlls[i]); + free (dlls); + } + + free (assembly_dir); + } + + /* Test whether alongside FOO.exe, a file FOO.runtimeconfig.json already + exists. */ + char *runtimeconfig_filename = + (char *) xmalloca (strlen (assembly_path) + 19 + 1); + { + size_t assembly_path_len = strlen (assembly_path); + if (assembly_path_len > 4 + && memcmp (assembly_path + (assembly_path_len - 4), ".exe", 4) == 0) + assembly_path_len -= 4; + char *p = runtimeconfig_filename; + memcpy (p, assembly_path, assembly_path_len); + p += assembly_path_len; + strcpy (p, ".runtimeconfig.json"); + } + struct stat runtimeconfig_statbuf; + if (stat (runtimeconfig_filename, &runtimeconfig_statbuf) == 0 + || errno == EOVERFLOW) + { + const char **argv = + (const char **) xmalloca ((3 + nargs + 1) * sizeof (const char *)); + unsigned int i; + + argv[0] = "dotnet"; + argv[1] = "exec"; + argv[2] = assembly_path_converted; + for (i = 0; i <= nargs; i++) + argv[3 + i] = args[i]; + + if (verbose) + { + char *command = shell_quote_argv (argv); + printf ("%s\n", command); + free (command); + } + + err = executer ("dotnet", "dotnet", argv, private_data); + + freea (argv); + } + else + { + /* dotnet needs a FOO.runtimeconfig.json alongside FOO.exe in order to + execute FOO.exe. Create a dummy one in a temporary directory + (because the directory where FOO.exe sits is not necessarily + writable). + Documentation of this file format: + <https://learn.microsoft.com/en-us/dotnet/core/runtime-config/> */ + + char *netcore_version; + + /* Invoke 'dotnet --list-runtimes' and extract the .NET Core version + from its output. */ + { + netcore_version = NULL; + + const char *argv[3]; + argv[0] = "dotnet"; + argv[1] = "--list-runtimes"; + argv[2] = NULL; + + /* Open a pipe to the program. */ + int fd[1]; + pid_t child = create_pipe_in ("dotnet", "dotnet", argv, NULL, + DEV_NULL, false, true, false, fd); + if (child == -1) + { + error (0, 0, _("%s subprocess I/O error"), "dotnet"); + err = true; + } + else + { + /* Retrieve its result. */ + FILE *fp = fdopen (fd[0], "r"); + if (fp == NULL) + error (EXIT_FAILURE, errno, _("fdopen() failed")); + + char *line = NULL; + size_t linesize = 0; + + for (;;) + { + size_t linelen = getline (&line, &linesize, fp); + if (linelen == (size_t)(-1)) + { + error (0, 0, _("%s subprocess I/O error"), "dotnet"); + err = true; + break; + } + if (linelen > 0 && line[linelen - 1] == '\n') + { + line[linelen - 1] = '\0'; + linelen--; + if (linelen > 0 && line[linelen - 1] == '\r') + { + line[linelen - 1] = '\0'; + linelen--; + } + } + /* The line has the structure + Microsoft.SUBSYSTEM VERSION [DIRECTORY] */ + if (linelen > 22 + && memcmp (line, "Microsoft.NETCore.App ", 22) == 0) + { + char *version = line + 22; + char *version_end = strchr (version, ' '); + if (version_end != NULL) + { + *version_end = '\0'; + netcore_version = xstrdup (version); + break; + } + } + } + + free (line); + + /* Read until EOF (otherwise the child process may get a + SIGPIPE signal). */ + while (getc (fp) != EOF) + ; + + fclose (fp); + + /* Remove zombie process from process list, and retrieve + exit status. */ + int exitstatus = + wait_subprocess (child, "dotnet", true, false, true, false, + NULL); + if (exitstatus != 0) + { + error (0, 0, _("%s subprocess failed"), "dotnet"); + err = true; + } + } + } + + if (!err) + { + if (netcore_version == NULL) + { + error (0, 0, _("could not determine %s version"), "dotnet"); + err = true; + } + } + + if (!err) + { + /* Create the runtimeconfig.json file. */ + struct temp_dir *tmpdir = create_temp_dir ("csharp", NULL, false); + if (tmpdir == NULL) + err = true; + else + { + char *runtimeconfig = + xconcatenated_filename (tmpdir->dir_name, + "runtimeconfig.json", NULL); + register_temp_file (tmpdir, runtimeconfig); + FILE *fp = fopen_temp (runtimeconfig, "w", false); + if (fp == NULL) + { + error (0, errno, _("failed to create \"%s\""), runtimeconfig); + unregister_temp_file (tmpdir, runtimeconfig); + err = true; + } + else + { + fprintf (fp, + "{ \"runtimeOptions\":\n" + " { \"framework\":\n" + " { \"name\": \"Microsoft.NETCore.App\", \"version\": \"%s\" }\n" + " }\n" + "}", + netcore_version); + if (fwriteerror_temp (fp)) + { + error (0, errno, _("error while writing \"%s\" file"), + runtimeconfig); + err = true; + } + else + { + char *runtimeconfig_converted = + cygpath_w (runtimeconfig); + const char **argv = + (const char **) + xmalloca ((5 + nargs + 1) * sizeof (const char *)); + unsigned int i; + + argv[0] = "dotnet"; + argv[1] = "exec"; + argv[2] = "--runtimeconfig"; + argv[3] = runtimeconfig_converted; + argv[4] = assembly_path_converted; + for (i = 0; i <= nargs; i++) + argv[5 + i] = args[i]; + + if (verbose) + { + char *command = shell_quote_argv (argv); + printf ("%s\n", command); + free (command); + } + + err = executer ("dotnet", "dotnet", argv, + private_data); + + freea (argv); + free (runtimeconfig_converted); + } + } + free (runtimeconfig); + cleanup_temp_dir (tmpdir); + } + } + } + + freea (runtimeconfig_filename); + while (tmpfiles_count > 0) + { + char *tmpfile = tmpfiles[--tmpfiles_count]; + remove (tmpfile); + unregister_temporary_file (tmpfile); + free (tmpfile); + } + free (tmpfiles); + free (assembly_path_converted); + return err; + } + else + return -1; +} + static int execute_csharp_using_sscli (const char *assembly_path, const char * const *libdirs, @@ -244,6 +646,13 @@ execute_csharp_program (const char *assembly_path, if (result >= 0) return (bool) result; #endif +#if CSHARP_CHOICE_DOTNET + result = execute_csharp_using_dotnet (assembly_path, libdirs, libdirs_count, + args, nargs, verbose, quiet, + executer, private_data); + if (result >= 0) + return (bool) result; +#endif /* Then try the remaining C# implementations in our standard order. */ #if !CSHARP_CHOICE_MONO @@ -254,6 +663,14 @@ execute_csharp_program (const char *assembly_path, return (bool) result; #endif +#if !CSHARP_CHOICE_DOTNET + result = execute_csharp_using_dotnet (assembly_path, libdirs, libdirs_count, + args, nargs, verbose, quiet, + executer, private_data); + if (result >= 0) + return (bool) result; +#endif + result = execute_csharp_using_sscli (assembly_path, libdirs, libdirs_count, args, nargs, verbose, quiet, executer, private_data); @@ -261,6 +678,6 @@ execute_csharp_program (const char *assembly_path, return (bool) result; if (!quiet) - error (0, 0, _("C# virtual machine not found, try installing mono")); + error (0, 0, _("C# virtual machine not found, try installing mono or dotnet")); return true; } diff --git a/modules/csharpexec b/modules/csharpexec index 11dcf49edb..6559c0e195 100644 --- a/modules/csharpexec +++ b/modules/csharpexec @@ -9,13 +9,24 @@ lib/classpath.c Depends-on: stdbool +stat +error +dirname +xconcat-filename +canonicalize cygpath execute +spawn-pipe +wait-process xsetenv +scandir +alphasort sh-quote xalloc xmalloca -error +copy-file +clean-temp-simple +clean-temp gettext-h csharpexec-script -- 2.34.1
>From 4d69eebfa32552c71922579feaf702e7896f2532 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Wed, 9 Oct 2024 03:14:26 +0200 Subject: [PATCH 6/7] csharpcomp-script: Add support for dotnet. * m4/csharpcomp.m4 (gt_CSHARPCOMP): Support 'dotnet' as implementation. Set HAVE_DOTNET_SDK, HAVE_DOTNET_CSC. * build-aux/csharpcomp.sh.in: Add implementations for the cases $HAVE_DOTNET_SDK = 1 and $HAVE_DOTNET_CSC = 1. --- ChangeLog | 8 ++++++ build-aux/csharpcomp.sh.in | 50 ++++++++++++++++++++++++++++---------- m4/csharpcomp.m4 | 35 ++++++++++++++++++++++++-- 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8e3a411649..6c1bf09679 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2024-10-08 Bruno Haible <br...@clisp.org> + + csharpcomp-script: Add support for dotnet. + * m4/csharpcomp.m4 (gt_CSHARPCOMP): Support 'dotnet' as implementation. + Set HAVE_DOTNET_SDK, HAVE_DOTNET_CSC. + * build-aux/csharpcomp.sh.in: Add implementations for the cases + $HAVE_DOTNET_SDK = 1 and $HAVE_DOTNET_CSC = 1. + 2024-10-08 Bruno Haible <br...@clisp.org> csharpexec: Add support for dotnet. diff --git a/build-aux/csharpcomp.sh.in b/build-aux/csharpcomp.sh.in index 9febcddd72..85ea5aaf22 100644 --- a/build-aux/csharpcomp.sh.in +++ b/build-aux/csharpcomp.sh.in @@ -82,8 +82,8 @@ while test $# != 0; do ;; esac options_mcs="$options_mcs -out:"`echo "$2" | sed -e "$sed_quote_subst"` - # On Windows, assume that 'csc' is a native Windows program, - # not a Cygwin program. + # On Windows, assume that 'dotnet' and 'csc' are native Windows programs, + # not Cygwin programs. arg="$2" case "@build_os@" in cygwin*) @@ -95,8 +95,8 @@ while test $# != 0; do ;; -L) options_mcs="$options_mcs -lib:"`echo "$2" | sed -e "$sed_quote_subst"` - # On Windows, assume that 'csc' is a native Windows program, - # not a Cygwin program. + # On Windows, assume that 'dotnet' and 'csc' are native Windows programs, + # not Cygwin programs. arg="$2" case "@build_os@" in cygwin*) @@ -124,8 +124,8 @@ while test $# != 0; do ;; *.resources) options_mcs="$options_mcs -resource:"`echo "$1" | sed -e "$sed_quote_subst"` - # On Windows, assume that 'csc' is a native Windows program, - # not a Cygwin program. + # On Windows, assume that 'dotnet' and 'csc' are native Windows programs, + # not Cygwin programs. arg="$1" case "@build_os@" in cygwin*) @@ -136,8 +136,8 @@ while test $# != 0; do ;; *.cs) sources="$sources "`echo "$1" | sed -e "$sed_quote_subst"` - # On Windows, assume that 'csc' is a native Windows program, - # not a Cygwin program. + # On Windows, assume that 'dotnet' and 'csc' are native Windows programs, + # not Cygwin programs. arg="$1" case "@build_os@" in cygwin*) @@ -169,11 +169,35 @@ if test -n "@HAVE_MCS@"; then rm -rf "$tmp" exit $result else - if test -n "@HAVE_CSC@"; then - test -z "$CSHARP_VERBOSE" || echo csc $options_csc $sources_csc - exec csc $options_csc $sources_csc + if test -n "@HAVE_DOTNET_SDK@"; then + dotnet_runtime_dir=`dotnet --list-runtimes | sed -n -e 's/Microsoft.NETCore.App \([^ ]*\) \[\(.*\)\].*/\2\/\1/p' | sed -e 1q` + dotnet_sdk_dir=`dotnet --list-sdks | sed -e 's/\([^ ]*\) \[\(.*\)\].*/\2\/\1/p' | sed -e 1q` + # Add -lib and -reference options, so that the compiler finds Object, Console, String, etc. + arg="$dotnet_runtime_dir" + case "@build_os@" in + cygwin*) + arg=`cygpath -w "$arg"` + ;; + esac + options_csc="$options_csc -lib:"`echo "$arg" | sed -e "$sed_quote_subst"` + for file in `cd "$dotnet_runtime_dir" && echo *.dll`; do + options_csc="$options_csc -reference:"`echo "$file" | sed -e "$sed_quote_subst"` + done + csc="$dotnet_sdk_dir/Roslyn/bincore/csc.dll" + case "@build_os@" in + cygwin*) + csc=`cygpath -w "$csc"` + ;; + esac + test -z "$CSHARP_VERBOSE" || echo dotnet "$csc" $options_csc $sources_csc + exec dotnet "$csc" $options_csc $sources_csc else - echo 'C# compiler not found, try installing mono, then reconfigure' 1>&2 - exit 1 + if test -n "@HAVE_DOTNET_CSC@" || test -n "@HAVE_CSC@"; then + test -z "$CSHARP_VERBOSE" || echo csc $options_csc $sources_csc + exec csc $options_csc $sources_csc + else + echo 'C# compiler not found, try installing mono or dotnet, then reconfigure' 1>&2 + exit 1 + fi fi fi diff --git a/m4/csharpcomp.m4 b/m4/csharpcomp.m4 index d9ebd53f6b..8465e85d77 100644 --- a/m4/csharpcomp.m4 +++ b/m4/csharpcomp.m4 @@ -1,5 +1,5 @@ # csharpcomp.m4 -# serial 9 +# serial 10 dnl Copyright (C) 2003-2005, 2007, 2009-2024 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -7,7 +7,7 @@ # Prerequisites of csharpcomp.sh. # Checks for a C# compiler. -# Sets at most one of HAVE_MCS, HAVE_CSC. +# Sets at most one of HAVE_MCS, HAVE_DOTNET_SDK, HAVE_DOTNET_CSC, HAVE_CSC. # Sets HAVE_CSHARPCOMP to nonempty if csharpcomp.sh will work. # Also sets CSHARPCOMPFLAGS. AC_DEFUN([gt_CSHARPCOMP], @@ -19,6 +19,7 @@ AC_DEFUN([gt_CSHARPCOMP] pushdef([AC_CHECKING],[:])dnl pushdef([AC_MSG_RESULT],[:])dnl AC_CHECK_PROG([HAVE_MCS_IN_PATH], [mcs], [yes]) + AC_CHECK_PROG([HAVE_DOTNET_IN_PATH], [dotnet], [yes]) AC_CHECK_PROG([HAVE_CSC_IN_PATH], [csc], [yes]) popdef([AC_MSG_RESULT])dnl popdef([AC_CHECKING])dnl @@ -34,6 +35,34 @@ AC_DEFUN([gt_CSHARPCOMP] break fi ;; + dotnet) + # The dotnet compiler is called "Roslyn". + # <https://en.wikipedia.org/wiki/Roslyn_(compiler)> + # There are two situations: + # - A dotnet SDK, that contains a 'dotnet' program and the Roslyn + # compiler as csc.dll. + # - An MSVC installation, that contains the Roslyn compiler as csc.exe + # (e.g. in C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\Roslyn\csc.exe). + # In the first case, the user only has to make sure that 'dotnet' is + # found in $PATH. + # In the second case, they need to make sure that both 'dotnet' and + # 'csc' are found in $PATH. + if test -n "$HAVE_DOTNET_IN_PATH" \ + && dotnet --list-runtimes >/dev/null 2>/dev/null \ + && test -n "`dotnet --list-sdks 2>/dev/null`"; then + HAVE_DOTNET_SDK=1 + ac_result="dotnet" + break + else + if test -n "$HAVE_CSC_IN_PATH" \ + && csc -help 2>/dev/null | grep analyzer >/dev/null \ + && { if csc -help 2>/dev/null | grep -i chicken > /dev/null; then false; else true; fi; }; then + HAVE_DOTNET_CSC=1 + ac_result="dotnet" + break + fi + fi + ;; sscli) if test -n "$HAVE_CSC_IN_PATH" \ && csc -help >/dev/null 2>/dev/null \ @@ -52,6 +81,8 @@ AC_DEFUN([gt_CSHARPCOMP] done AC_MSG_RESULT([$ac_result]) AC_SUBST([HAVE_MCS]) + AC_SUBST([HAVE_DOTNET_SDK]) + AC_SUBST([HAVE_DOTNET_CSC]) AC_SUBST([HAVE_CSC]) dnl Provide a default for CSHARPCOMPFLAGS. if test -z "${CSHARPCOMPFLAGS+set}"; then -- 2.34.1
>From efa78d8905e1a0260930692fd888d1917a837cd6 Mon Sep 17 00:00:00 2001 From: Bruno Haible <br...@clisp.org> Date: Wed, 9 Oct 2024 03:20:00 +0200 Subject: [PATCH 7/7] csharpcomp: Add support for dotnet. * lib/csharpcomp.c: Include <dirent.h>, concat-filename.h, xvasprintf.h. (name_is_dll): New function, from lib/csharpexec.c. (compile_csharp_using_dotnet): New function. (compile_csharp_class): Invoke compile_csharp_using_dotnet. * modules/csharpcomp (Depends-on): Add xconcat-filename, scandir, alphasort, xvasprintf. --- ChangeLog | 8 + lib/csharpcomp.c | 632 ++++++++++++++++++++++++++++++++++++++++++++- modules/csharpcomp | 8 +- 3 files changed, 636 insertions(+), 12 deletions(-) diff --git a/ChangeLog b/ChangeLog index 6c1bf09679..5e36a3f4be 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,13 @@ 2024-10-08 Bruno Haible <br...@clisp.org> + csharpcomp: Add support for dotnet. + * lib/csharpcomp.c: Include <dirent.h>, concat-filename.h, xvasprintf.h. + (name_is_dll): New function, from lib/csharpexec.c. + (compile_csharp_using_dotnet): New function. + (compile_csharp_class): Invoke compile_csharp_using_dotnet. + * modules/csharpcomp (Depends-on): Add xconcat-filename, scandir, + alphasort, xvasprintf. + csharpcomp-script: Add support for dotnet. * m4/csharpcomp.m4 (gt_CSHARPCOMP): Support 'dotnet' as implementation. Set HAVE_DOTNET_SDK, HAVE_DOTNET_CSC. diff --git a/lib/csharpcomp.c b/lib/csharpcomp.c index 28247ed1a7..4caed49533 100644 --- a/lib/csharpcomp.c +++ b/lib/csharpcomp.c @@ -21,12 +21,14 @@ /* Specification. */ #include "csharpcomp.h" +#include <dirent.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <error.h> +#include "concat-filename.h" #include "cygpath.h" #include "execute.h" #include "spawn-pipe.h" @@ -34,6 +36,7 @@ #include "sh-quote.h" #include "safe-read.h" #include "xmalloca.h" +#include "xvasprintf.h" #include "gettext.h" #define _(str) gettext (str) @@ -41,22 +44,35 @@ /* Survey of C# compilers. - Program from + Program from - mcs mono - csc sscli + mcs mono + dotnet csc.dll dotnet + csc MSVC + csc sscli (non-free) - We try the CIL interpreters in the following order: - 1. "mcs", because it is a free system but doesn't integrate so well - with Unix. (Command line options start with / instead of -. Errors go - to stdout instead of stderr. Source references are printed as - "file(lineno)" instead of "file:lineno:".) - 2. "csc", although it is not free, because it is a kind of "reference + We try the C# compilers in the following order: + 1. "mcs", because it is nowadays the "traditional" C# system on Unix. + 2. "dotnet" or "csc" from MSVC, because it is another (newer) free C# + system. + 3. "csc", although it is not free, because it is a kind of "reference implementation" of C#. But the order can be changed through the --enable-csharp configuration option. */ +static int +name_is_dll (const struct dirent *d) +{ + if (d->d_name[0] != '.') + { + size_t d_name_len = strlen (d->d_name); + if (d_name_len > 4 && memcmp (d->d_name + d_name_len - 4, ".dll", 4) == 0) + return 1; + } + return 0; +} + static int compile_csharp_using_mono (const char * const *sources, unsigned int sources_count, @@ -245,6 +261,583 @@ compile_csharp_using_mono (const char * const *sources, return -1; } +static int +compile_csharp_using_dotnet (const char * const *sources, + unsigned int sources_count, + const char * const *libdirs, + unsigned int libdirs_count, + const char * const *libraries, + unsigned int libraries_count, + const char *output_file, bool output_is_library, + bool optimize, bool debug, + bool verbose) +{ + static bool dotnet_tested; + static bool dotnet_present; + + if (!dotnet_tested) + { + /* Test for presence of dotnet: + dotnet --list-runtimes >/dev/null 2>/dev/null + && test -n "`dotnet --list-sdks 2>/dev/null`" */ + int exitstatus; + { + const char *argv[3]; + + argv[0] = "dotnet"; + argv[1] = "--list-runtimes"; + argv[2] = NULL; + exitstatus = execute ("dotnet", "dotnet", argv, NULL, + false, false, true, true, + true, false, NULL); + } + if (exitstatus == 0) + { + const char *argv[3]; + pid_t child; + int fd[1]; + + argv[0] = "dotnet"; + argv[1] = "--list-sdks"; + argv[2] = NULL; + child = create_pipe_in ("dotnet", "dotnet", argv, NULL, + DEV_NULL, true, true, false, fd); + if (child != -1) + { + /* Read the subprocess output, and test whether it is + non-empty. */ + size_t count = 0; + char c; + + while (safe_read (fd[0], &c, 1) > 0) + count++; + + close (fd[0]); + + /* Remove zombie process from process list, and retrieve exit + status. */ + exitstatus = + wait_subprocess (child, "dotnet", false, true, true, false, + NULL); + dotnet_present = (exitstatus == 0 && count > 0); + } + else + dotnet_present = false; + } + else + dotnet_present = false; + dotnet_tested = true; + } + + if (dotnet_present) + { + bool err = false; + + char *dotnet_runtime_dir; + char *dotnet_sdk_dir; + + /* Invoke 'dotnet --list-runtimes' and extract the .NET runtime dir + from its output. */ + { + dotnet_runtime_dir = NULL; + + const char *argv[3]; + argv[0] = "dotnet"; + argv[1] = "--list-runtimes"; + argv[2] = NULL; + + /* Open a pipe to the program. */ + int fd[1]; + pid_t child = create_pipe_in ("dotnet", "dotnet", argv, NULL, + DEV_NULL, false, true, false, fd); + if (child == -1) + { + error (0, 0, _("%s subprocess I/O error"), "dotnet"); + err = true; + } + else + { + /* Retrieve its result. */ + FILE *fp = fdopen (fd[0], "r"); + if (fp == NULL) + error (EXIT_FAILURE, errno, _("fdopen() failed")); + + char *line = NULL; + size_t linesize = 0; + + for (;;) + { + size_t linelen = getline (&line, &linesize, fp); + if (linelen == (size_t)(-1)) + { + error (0, 0, _("%s subprocess I/O error"), "dotnet"); + err = true; + break; + } + if (linelen > 0 && line[linelen - 1] == '\n') + { + line[linelen - 1] = '\0'; + linelen--; + if (linelen > 0 && line[linelen - 1] == '\r') + { + line[linelen - 1] = '\0'; + linelen--; + } + } + /* The line has the structure + Microsoft.SUBSYSTEM VERSION [DIRECTORY] */ + if (linelen > 22 + && memcmp (line, "Microsoft.NETCore.App ", 22) == 0) + { + char *version = line + 22; + char *version_end = strchr (version, ' '); + if (version_end != NULL && version_end[1] == '[') + { + *version_end = '\0'; + char *dir = version_end + 2; + char *dir_end = strchr (dir, ']'); + if (dir_end != NULL) + { + *dir_end = '\0'; + dotnet_runtime_dir = + xasprintf ("%s/%s", dir, version); + break; + } + } + } + } + + free (line); + + /* Read until EOF (otherwise the child process may get a + SIGPIPE signal). */ + while (getc (fp) != EOF) + ; + + fclose (fp); + + /* Remove zombie process from process list, and retrieve + exit status. */ + int exitstatus = + wait_subprocess (child, "dotnet", true, false, true, false, NULL); + if (exitstatus != 0) + { + error (0, 0, _("%s subprocess failed"), "dotnet"); + err = true; + } + } + } + + /* Invoke 'dotnet --list-sdks' and extract the .NET SDK dir + from its output. */ + { + dotnet_sdk_dir = NULL; + + const char *argv[3]; + argv[0] = "dotnet"; + argv[1] = "--list-sdks"; + argv[2] = NULL; + + /* Open a pipe to the program. */ + int fd[1]; + pid_t child = create_pipe_in ("dotnet", "dotnet", argv, NULL, + DEV_NULL, false, true, false, fd); + if (child == -1) + { + error (0, 0, _("%s subprocess I/O error"), "dotnet"); + err = true; + } + else + { + /* Retrieve its result. */ + FILE *fp = fdopen (fd[0], "r"); + if (fp == NULL) + error (EXIT_FAILURE, errno, _("fdopen() failed")); + + char *line = NULL; + size_t linesize = 0; + + for (;;) + { + size_t linelen = getline (&line, &linesize, fp); + if (linelen == (size_t)(-1)) + { + error (0, 0, _("%s subprocess I/O error"), "dotnet"); + err = true; + break; + } + if (linelen > 0 && line[linelen - 1] == '\n') + { + line[linelen - 1] = '\0'; + linelen--; + if (linelen > 0 && line[linelen - 1] == '\r') + { + line[linelen - 1] = '\0'; + linelen--; + } + } + /* The line has the structure + VERSION [DIRECTORY] */ + char *version = line; + char *version_end = strchr (version, ' '); + if (version_end != NULL && version_end[1] == '[') + { + *version_end = '\0'; + char *dir = version_end + 2; + char *dir_end = strchr (dir, ']'); + if (dir_end != NULL) + { + *dir_end = '\0'; + dotnet_sdk_dir = xasprintf ("%s/%s", dir, version); + break; + } + } + } + + free (line); + + /* Read until EOF (otherwise the child process may get a + SIGPIPE signal). */ + while (getc (fp) != EOF) + ; + + fclose (fp); + + /* Remove zombie process from process list, and retrieve + exit status. */ + int exitstatus = + wait_subprocess (child, "dotnet", true, false, true, false, NULL); + if (exitstatus != 0) + { + error (0, 0, _("%s subprocess failed"), "dotnet"); + err = true; + } + } + } + + if (err) + return 1; + + struct dirent **dlls; + int num_dlls; + char *roslyn_dir; + char *roslyn_bin_dir; + char *csc; + char *csc_converted; + char **malloced; + char **mallocedp; + unsigned int argc; + const char **argv; + const char **argp; + int exitstatus; + unsigned int i; + + /* Get a list of all *.dll files in dotnet_runtime_dir. */ + num_dlls = scandir (dotnet_runtime_dir, &dlls, name_is_dll, alphasort); + if (num_dlls < 0 && errno == ENOMEM) + xalloc_die (); + if (num_dlls <= 0) + { + dlls = NULL; + num_dlls = 0; + } + + /* Construct the file name of the 'csc' compiler. */ + roslyn_dir = xconcatenated_filename (dotnet_sdk_dir, "Roslyn", NULL); + roslyn_bin_dir = xconcatenated_filename (roslyn_dir, "bincore", NULL); + csc = xconcatenated_filename (roslyn_bin_dir, "csc.dll", NULL); + csc_converted = cygpath_w (csc); + + /* Here, we assume that 'csc' is a native Windows program, therefore + we need to use cygpath_w. */ + malloced = + (char **) + xmalloca ((1 + libdirs_count + sources_count * 2 + 1) * sizeof (char *)); + mallocedp = malloced; + + argc = + 3 + 1 + 1 + libdirs_count + libraries_count + + (optimize ? 1 : 0) + (debug ? 1 : 0) + sources_count + 1 + num_dlls; + argv = (const char **) xmalloca ((argc + 1) * sizeof (const char *)); + + argp = argv; + *argp++ = "dotnet"; + *argp++ = csc_converted; + *argp++ = "-nologo"; + *argp++ = (output_is_library ? "-target:library" : "-target:exe"); + { + char *output_file_converted = cygpath_w (output_file); + *mallocedp++ = output_file_converted; + char *option = (char *) xmalloca (5 + strlen (output_file_converted) + 1); + memcpy (option, "-out:", 5); + strcpy (option + 5, output_file_converted); + *argp++ = option; + } + for (i = 0; i < libdirs_count; i++) + { + const char *libdir = libdirs[i]; + char *libdir_converted = cygpath_w (libdir); + *mallocedp++ = libdir_converted; + char *option = (char *) xmalloca (5 + strlen (libdir_converted) + 1); + memcpy (option, "-lib:", 5); + strcpy (option + 5, libdir_converted); + *argp++ = option; + } + for (i = 0; i < libraries_count; i++) + { + char *option = (char *) xmalloca (11 + strlen (libraries[i]) + 4 + 1); + memcpy (option, "-reference:", 11); + memcpy (option + 11, libraries[i], strlen (libraries[i])); + strcpy (option + 11 + strlen (libraries[i]), ".dll"); + *argp++ = option; + } + if (optimize) + *argp++ = "-optimize+"; + if (debug) + *argp++ = "-debug+"; + for (i = 0; i < sources_count; i++) + { + const char *source_file = sources[i]; + char *source_file_converted = cygpath_w (source_file); + *mallocedp++ = source_file_converted; + if (strlen (source_file_converted) >= 10 + && memcmp (source_file_converted + + strlen (source_file_converted) - 10, + ".resources", + 10) == 0) + { + char *option = + (char *) xmalloc (10 + strlen (source_file_converted) + 1); + memcpy (option, "-resource:", 10); + strcpy (option + 10, source_file_converted); + *mallocedp++ = option; + *argp++ = option; + } + else + *argp++ = source_file_converted; + } + /* Add -lib and -reference options, so that the compiler finds + Object, Console, String, etc. */ + { + char *dotnet_runtime_dir_converted = cygpath_w (dotnet_runtime_dir); + *mallocedp++ = dotnet_runtime_dir_converted; + char *option = + (char *) xmalloca (5 + strlen (dotnet_runtime_dir_converted) + 1); + memcpy (option, "-lib:", 5); + strcpy (option + 5, dotnet_runtime_dir_converted); + *argp++ = option; + } + for (i = 0; i < num_dlls; i++) + { + char *option = (char *) xmalloca (11 + strlen (dlls[i]->d_name) + 1); + memcpy (option, "-reference:", 11); + strcpy (option + 11, dlls[i]->d_name); + *argp++ = option; + } + *argp = NULL; + /* Ensure argv length was correctly calculated. */ + if (argp - argv != argc) + abort (); + + if (verbose) + { + char *command = shell_quote_argv (argv); + printf ("%s\n", command); + free (command); + } + + exitstatus = execute ("dotnet", "dotnet", argv, NULL, + false, false, false, false, + true, true, NULL); + + for (i = 4; i < 5 + libdirs_count + libraries_count; i++) + freea ((char *) argv[i]); + for (i = 0; i < 1 + num_dlls; i++) + freea ((char *) argv[argc - (1 + num_dlls) + i]); + while (mallocedp > malloced) + free (*--mallocedp); + freea (argv); + freea (malloced); + free (csc_converted); + free (csc); + free (roslyn_bin_dir); + free (roslyn_dir); + for (i = 0; i < num_dlls; i++) + free (dlls[i]); + free (dlls); + + return (exitstatus != 0); + } + else + { + static bool csc_tested; + static bool csc_present; + + if (!csc_tested) + { + /* Test for presence of csc: + "csc -help 2>/dev/null | grep -i analyzer >/dev/null \ + && ! { csc -help 2>/dev/null | grep -i chicken > /dev/null; }" */ + const char *argv[3]; + pid_t child; + int fd[1]; + int exitstatus; + + argv[0] = "csc"; + argv[1] = "-help"; + argv[2] = NULL; + child = create_pipe_in ("csc", "csc", argv, NULL, + DEV_NULL, true, true, false, fd); + if (child != -1) + { + /* Read the subprocess output, and test whether it contains the + string "analyzer" or the string "chicken". */ + char c[8]; + size_t count = 0; + bool seen_analyzer = false; + bool seen_chicken = false; + + csc_present = true; + while (safe_read (fd[0], &c[count], 1) > 0) + { + if (c[count] >= 'A' && c[count] <= 'Z') + c[count] += 'a' - 'A'; + count++; + if (count >= 7) + { + if (memcmp (c, "chicken", 7) == 0) + seen_chicken = true; + } + if (count == 8) + { + if (memcmp (c, "analyzer", 8) == 0) + seen_analyzer = true; + c[0] = c[1]; c[1] = c[2]; c[2] = c[3]; c[3] = c[4]; + c[4] = c[5]; c[5] = c[6]; c[6] = c[7]; + count--; + } + } + + close (fd[0]); + + /* Remove zombie process from process list, and retrieve exit + status. */ + exitstatus = + wait_subprocess (child, "csc", false, true, true, false, NULL); + csc_present = (exitstatus == 0 && seen_analyzer && !seen_chicken); + } + else + csc_present = false; + csc_tested = true; + } + + if (csc_present) + { + char **malloced; + char **mallocedp; + unsigned int argc; + const char **argv; + const char **argp; + int exitstatus; + unsigned int i; + + /* Here, we assume that 'csc' is a native Windows program, therefore + we need to use cygpath_w. */ + malloced = + (char **) + xmalloca ((1 + libdirs_count + sources_count * 2) * sizeof (char *)); + mallocedp = malloced; + + argc = + 2 + 1 + 1 + libdirs_count + libraries_count + + (optimize ? 1 : 0) + (debug ? 1 : 0) + sources_count; + argv = (const char **) xmalloca ((argc + 1) * sizeof (const char *)); + + argp = argv; + *argp++ = "csc"; + *argp++ = "-nologo"; + *argp++ = (output_is_library ? "-target:library" : "-target:exe"); + { + char *output_file_converted = cygpath_w (output_file); + *mallocedp++ = output_file_converted; + char *option = (char *) xmalloca (5 + strlen (output_file_converted) + 1); + memcpy (option, "-out:", 5); + strcpy (option + 5, output_file_converted); + *argp++ = option; + } + for (i = 0; i < libdirs_count; i++) + { + const char *libdir = libdirs[i]; + char *libdir_converted = cygpath_w (libdir); + *mallocedp++ = libdir_converted; + char *option = (char *) xmalloca (5 + strlen (libdir_converted) + 1); + memcpy (option, "-lib:", 5); + strcpy (option + 5, libdir_converted); + *argp++ = option; + } + for (i = 0; i < libraries_count; i++) + { + char *option = (char *) xmalloca (11 + strlen (libraries[i]) + 4 + 1); + memcpy (option, "-reference:", 11); + memcpy (option + 11, libraries[i], strlen (libraries[i])); + strcpy (option + 11 + strlen (libraries[i]), ".dll"); + *argp++ = option; + } + if (optimize) + *argp++ = "-optimize+"; + if (debug) + *argp++ = "-debug+"; + for (i = 0; i < sources_count; i++) + { + const char *source_file = sources[i]; + char *source_file_converted = cygpath_w (source_file); + *mallocedp++ = source_file_converted; + if (strlen (source_file_converted) >= 10 + && memcmp (source_file_converted + + strlen (source_file_converted) - 10, + ".resources", + 10) == 0) + { + char *option = + (char *) xmalloc (10 + strlen (source_file_converted) + 1); + memcpy (option, "-resource:", 10); + strcpy (option + 10, source_file_converted); + *mallocedp++ = option; + *argp++ = option; + } + else + *argp++ = source_file_converted; + } + *argp = NULL; + /* Ensure argv length was correctly calculated. */ + if (argp - argv != argc) + abort (); + + if (verbose) + { + char *command = shell_quote_argv (argv); + printf ("%s\n", command); + free (command); + } + + exitstatus = execute ("csc", "csc", argv, NULL, + false, false, false, false, + true, true, NULL); + + for (i = 3; i < 4 + libdirs_count + libraries_count; i++) + freea ((char *) argv[i]); + while (mallocedp > malloced) + free (*--mallocedp); + freea (argv); + freea (malloced); + + return (exitstatus != 0); + } + else + return -1; + } +} + static int compile_csharp_using_sscli (const char * const *sources, unsigned int sources_count, @@ -442,6 +1035,15 @@ compile_csharp_class (const char * const *sources, if (result >= 0) return (bool) result; #endif +#if CSHARP_CHOICE_DOTNET + result = compile_csharp_using_dotnet (sources, sources_count, + libdirs, libdirs_count, + libraries, libraries_count, + output_file, output_is_library, + optimize, debug, verbose); + if (result >= 0) + return (bool) result; +#endif /* Then try the remaining C# implementations in our standard order. */ #if !CSHARP_CHOICE_MONO @@ -454,6 +1056,16 @@ compile_csharp_class (const char * const *sources, return (bool) result; #endif +#if !CSHARP_CHOICE_DOTNET + result = compile_csharp_using_dotnet (sources, sources_count, + libdirs, libdirs_count, + libraries, libraries_count, + output_file, output_is_library, + optimize, debug, verbose); + if (result >= 0) + return (bool) result; +#endif + result = compile_csharp_using_sscli (sources, sources_count, libdirs, libdirs_count, libraries, libraries_count, @@ -462,6 +1074,6 @@ compile_csharp_class (const char * const *sources, if (result >= 0) return (bool) result; - error (0, 0, _("C# compiler not found, try installing mono")); + error (0, 0, _("C# compiler not found, try installing mono or dotnet")); return true; } diff --git a/modules/csharpcomp b/modules/csharpcomp index ea996fe81d..4b0a177fbb 100644 --- a/modules/csharpcomp +++ b/modules/csharpcomp @@ -7,15 +7,19 @@ lib/csharpcomp.c Depends-on: stdbool -xmalloca +error +xconcat-filename cygpath execute spawn-pipe wait-process getline +scandir +alphasort sh-quote safe-read -error +xmalloca +xvasprintf gettext-h memcmp csharpcomp-script -- 2.34.1