]> Savannah Git Hosting - gnulib.git/commitdiff
csharpexec: Add support for dotnet.
authorBruno Haible <bruno@clisp.org>
Wed, 9 Oct 2024 01:14:04 +0000 (03:14 +0200)
committerBruno Haible <bruno@clisp.org>
Wed, 16 Oct 2024 12:06:38 +0000 (14:06 +0200)
* 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
lib/csharpexec.c
modules/csharpexec

index a1e92dd25713d4b7c0fc6d8a228c3db0c7197f52..48b6bc549eb0e9732cea9de8851c452f9ff912e4 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,16 @@
 2024-10-08  Bruno Haible  <bruno@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.
index c7e3fe5ae59decaae3c725367fb0f6444c9350cc..79f2a3fd9189ad06550f0f7fd2d46d5725f5aab9 100644 (file)
 /* 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.  */
    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;
 }
index 11dcf49edb7e41cb1205fb3d5dec251861129628..6559c0e1953a3e862d85461954f0700fce6e5e79 100644 (file)
@@ -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