Add TODO file.
[febootstrap.git] / helper / kernel.c
index 3301826..88dc940 100644 (file)
 
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 #include <fnmatch.h>
 #include <unistd.h>
 #include <errno.h>
+#include <sys/utsname.h>
 
 #include "error.h"
 #include "xvasprintf.h"
@@ -35,6 +37,9 @@
 #define KERNELDIR "/boot"
 #define MODULESDIR "/lib/modules"
 
+static char* get_kernel_version (char* filename);
+static const char *create_kernel_from_env (const char *hostcpu, const char *kernel, const char *kernel_env, const char *modpath_env);
+
 static char *
 get_modpath (const char *kernel_name)
 {
@@ -48,6 +53,27 @@ get_modpath (const char *kernel_name)
     exit (EXIT_FAILURE);
   }
 
+  if (! isdir (modpath)) {
+    char* path;
+    char* version;
+    path = xasprintf (KERNELDIR "/%s", kernel_name);
+    if (!path) {
+      perror ("xasprintf");
+      exit (EXIT_FAILURE);
+    }
+    version = get_kernel_version (path);
+    free (path);
+    if (version != NULL) {
+      free (modpath);
+      modpath = xasprintf (MODULESDIR "/%s", version);
+      free (version);
+      if (!path) {
+        perror ("xasprintf");
+        exit (EXIT_FAILURE);
+      }
+    }
+  }
+
   return modpath;
 }
 
@@ -94,7 +120,12 @@ has_modpath (const char *kernel_name)
 const char *
 create_kernel (const char *hostcpu, const char *kernel)
 {
-  char **all_files = read_dir (KERNELDIR);
+  /* Override kernel selection using environment variables? */
+  char *kernel_env = getenv ("FEBOOTSTRAP_KERNEL");
+  if (kernel_env) {
+    char *modpath_env = getenv ("FEBOOTSTRAP_MODULES");
+    return create_kernel_from_env (hostcpu, kernel, kernel_env, modpath_env);
+  }
 
   /* In original: ls -1dvr /boot/vmlinuz-*.$arch* 2>/dev/null | grep -v xen */
   const char *patt;
@@ -104,6 +135,7 @@ create_kernel (const char *hostcpu, const char *kernel)
   else
     patt = xasprintf ("vmlinuz-*.%s*", hostcpu);
 
+  char **all_files = read_dir (KERNELDIR);
   char **candidates;
   candidates = filter_fnmatch (all_files, patt, FNM_NOESCAPE);
   candidates = filter_notmatching_substring (candidates, "xen");
@@ -122,16 +154,18 @@ create_kernel (const char *hostcpu, const char *kernel)
 
   sort (candidates, reverse_filevercmp);
 
-  /* Choose the first candidate. */
-  char *tmp = xasprintf (KERNELDIR "/%s", candidates[0]);
+  if (kernel) {
+    /* Choose the first candidate. */
+    char *tmp = xasprintf (KERNELDIR "/%s", candidates[0]);
 
-  if (verbose)
-    fprintf (stderr, "creating symlink %s -> %s\n", kernel, tmp);
+    if (verbose >= 2)
+      fprintf (stderr, "creating symlink %s -> %s\n", kernel, tmp);
 
-  if (symlink (tmp, kernel) == -1)
-    error (EXIT_FAILURE, errno, "symlink kernel");
+    if (symlink (tmp, kernel) == -1)
+      error (EXIT_FAILURE, errno, "symlink kernel");
 
-  free (tmp);
+    free (tmp);
+  }
 
   return get_modpath (candidates[0]);
 
@@ -146,3 +180,135 @@ create_kernel (const char *hostcpu, const char *kernel)
            "febootstrap use, you shouldn't boot the Xen guest with it).\n");
   exit (EXIT_FAILURE);
 }
+
+/* Select the kernel from environment variables set by the user.
+ * modpath_env may be NULL, in which case we attempt to work it out
+ * from kernel_env.
+ */
+static const char *
+create_kernel_from_env (const char *hostcpu, const char *kernel,
+                        const char *kernel_env, const char *modpath_env)
+{
+  if (verbose) {
+    fprintf (stderr,
+             "febootstrap-supermin-helper: using environment variable(s) FEBOOTSTRAP_* to\n"
+             "select kernel %s", kernel_env);
+    if (modpath_env)
+      fprintf (stderr, " and module path %s", modpath_env);
+    fprintf (stderr, "\n");
+  }
+
+  if (!isfile (kernel_env)) {
+    fprintf (stderr,
+             "febootstrap-supermin-helper: %s: not a regular file\n"
+             "(what is $FEBOOTSTRAP_KERNEL set to?)\n", kernel_env);
+    exit (EXIT_FAILURE);
+  }
+
+  if (!modpath_env) {
+    /* Try to guess modpath from kernel path. */
+    const char *p = strrchr (kernel_env, '/');
+    if (p) p++; else p = kernel_env;
+
+    /* NB: We need the extra test to ensure calling get_modpath is safe. */
+    if (strncmp (p, "vmlinuz-", 8) != 0) {
+      fprintf (stderr,
+               "febootstrap-supermin-helper: cannot guess module path.\n"
+               "Set $FEBOOTSTRAP_MODULES to the modules directory corresponding to\n"
+               "kernel %s, or unset $FEBOOTSTRAP_KERNEL to autoselect a kernel.\n",
+               kernel_env);
+      exit (EXIT_FAILURE);
+    }
+
+    modpath_env = get_modpath (p);
+  }
+
+  if (!isdir (modpath_env)) {
+    fprintf (stderr,
+             "febootstrap-supermin-helper: %s: not a directory\n"
+             "(what is $FEBOOTSTRAP_MODULES set to?)\n", modpath_env);
+    exit (EXIT_FAILURE);
+  }
+
+  /* Create the symlink. */
+  if (kernel) {
+    if (verbose >= 2)
+      fprintf (stderr, "creating symlink %s -> %s\n", kernel_env, kernel);
+
+    if (symlink (kernel_env, kernel) == -1)
+      error (EXIT_FAILURE, errno, "symlink kernel");
+  }
+
+  return modpath_env;
+}
+
+/* Read an unsigned little endian short at a specified offset in a file.
+ * Returns a non-negative int on success or -1 on failure.
+ */
+static int
+read_leshort (FILE* fp, int offset)
+{
+  char buf[2];
+  if (fseek (fp, offset, SEEK_SET) != 0 ||
+      fread (buf, sizeof(char), 2, fp) != 2)
+  {
+    return -1;
+  }
+  return ((buf[1] & 0xFF) << 8) | (buf[0] & 0xFF);
+}
+
+/* Extract the kernel version from a Linux kernel file.
+ * Returns a malloc'd string containing the version or NULL if the
+ * file can't be read, is not a Linux kernel, or the version can't
+ * be found.
+ *
+ * See ftp://ftp.astron.com/pub/file/file-<ver>.tar.gz
+ * (file-<ver>/magic/Magdir/linux) for the rules used to find the
+ * version number:
+ *   514             string  HdrS     Linux kernel
+ *   >518            leshort >0x1ff
+ *   >>(526.s+0x200) string  >\0      version %s,
+ *
+ * Bugs: probably limited to x86 kernels.
+ */
+static char*
+get_kernel_version (char* filename)
+{
+  FILE* fp;
+  int size = 132;
+  char buf[size];
+  int offset;
+
+  fp = fopen (filename, "rb");
+
+  if (fseek (fp, 514, SEEK_SET) != 0 ||
+      fgets (buf, size, fp) == NULL ||
+      strncmp (buf, "HdrS", 4) != 0 ||
+      read_leshort (fp, 518) < 0x1FF)
+  {
+    /* not a Linux kernel */
+    fclose (fp);
+    return NULL;
+  }
+
+  offset = read_leshort (fp, 526);
+  if (offset == -1)
+  {
+    /* can't read version offset */
+    fclose (fp);
+    return NULL;
+  }
+
+  if (fseek (fp, offset + 0x200, SEEK_SET) != 0 ||
+      fgets (buf, size, fp) == NULL)
+  {
+    /* can't read version string */
+    fclose (fp);
+    return NULL;
+  }
+
+  fclose (fp);
+
+  buf[strcspn (buf, " \t\n")] = '\0';
+  return strdup (buf);
+}