From: Bruno Haible Date: Wed, 9 Oct 2024 01:14:04 +0000 (+0200) Subject: csharpexec: Add support for dotnet. X-Git-Url: https://gitweb.git.savannah.gnu.org/gitweb/?a=commitdiff_plain;h=09fa6722a6e4285659276055d592ad5c003867a3;p=gnulib.git csharpexec: Add support for dotnet. * lib/csharpexec.c: Include , , , 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. --- diff --git a/ChangeLog b/ChangeLog index a1e92dd257..48b6bc549e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,16 @@ 2024-10-08 Bruno Haible + csharpexec: Add support for dotnet. + * lib/csharpexec.c: Include , , , + 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 +#include #include #include +#include #include +#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. + and . */ + 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: + */ + + 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