Merge pull request #39 from marcosps/mpdesouza_mkinitramfs

Make virtme-mkinitramfs great again
diff --git a/bin/virtme-prep-kdir-mods b/bin/virtme-prep-kdir-mods
new file mode 100755
index 0000000..be28fb6
--- /dev/null
+++ b/bin/virtme-prep-kdir-mods
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+# This is still a bit of an experiment.
+
+if ! [ -d .tmp_versions ]; then
+    echo 'virtme-prep-kdir-mods must be run from a kernel build directory' >&2
+    exit 1
+fi
+
+FAKEVER=0.0.0
+MODDIR=".virtme_mods/lib/modules/$FAKEVER"
+
+if ! [ -f "modules.order" ]; then
+    echo "modules.order is missing.  Your kernel may be too old or you didn't make modules." >&2
+    exit 1
+fi
+
+# Set up .virtme_mods/lib/modules/0.0.0 as a module directory for this kernel,
+# but fill it with symlinks instead of actual modules.
+
+mkdir -p "$MODDIR/kernel"
+ln -srfT . "$MODDIR/build"
+
+# Remove all preexisting symlinks and add symlinks to all modules that belong
+# to the build kenrnel.
+find "$MODDIR/kernel" -type l -print0 |xargs -0 rm -f --
+while read -r i; do
+    [ ! -e "$i" ] && i=$(echo "$i" | sed s:^kernel/::)
+    mkdir -p "$MODDIR/kernel/$(dirname "$i")"
+    ln -sr "$i" "$MODDIR/kernel/$i"
+done < modules.order
+
+
+# Link in the files that make modules_install would copy
+ln -srf modules.builtin modules.builtin.modinfo modules.order "$MODDIR/"
+
+# Now run depmod to collect dependencies
+depmod -ae -F System.map -b .virtme_mods "$FAKEVER"
diff --git a/setup.py b/setup.py
index 33b8a27..a0b804e 100755
--- a/setup.py
+++ b/setup.py
@@ -30,6 +30,9 @@
             'virtme-mkinitramfs = virtme.commands.mkinitramfs:main',
         ]
     },
+    scripts = [
+        'bin/virtme-prep-kdir-mods',
+        ],
     package_data = {
         'virtme.guest': [
             'virtme-init',
diff --git a/virtme/commands/configkernel.py b/virtme/commands/configkernel.py
index 0da6fab..bd3d42f 100644
--- a/virtme/commands/configkernel.py
+++ b/virtme/commands/configkernel.py
@@ -121,7 +121,14 @@
     if maketarget:
         subprocess.check_call(['make', 'ARCH=%s' % arch.linuxname, maketarget])
 
-    with open('.config', 'ab') as conffile:
+    config = '.config'
+
+    # Check if KBUILD_OUTPUT is defined and if it's a directory
+    config_dir = os.environ.get('KBUILD_OUTPUT', '')
+    if config_dir and os.path.isdir(config_dir):
+        config = os.path.join(config_dir, config)
+
+    with open(config, 'ab') as conffile:
         conffile.write('\n'.join(conf).encode('utf-8'))
 
     subprocess.check_call(['make', 'ARCH=%s' % arch.linuxname, updatetarget])
diff --git a/virtme/commands/run.py b/virtme/commands/run.py
index c40e225..e0a00eb 100644
--- a/virtme/commands/run.py
+++ b/virtme/commands/run.py
@@ -15,6 +15,7 @@
 import shlex
 import re
 import itertools
+import pkg_resources
 from .. import virtmods
 from .. import modfinder
 from .. import mkinitramfs
@@ -41,6 +42,9 @@
                    help='Use a compiled kernel source directory')
 
     g = parser.add_argument_group(title='Kernel options')
+    g.add_argument('--mods', action='store', metavar='none|use|auto', default='use',
+                   help='Setup loadable kernel modules inside a compiled kernel source directory (used in conjunction with --kdir); none: ignore kernel modules, use: asks user to refresh virtme\'s kernel modules directory, auto: automatically refreshes virtme\'s kernel modules directory')
+
     g.add_argument('-a', '--kopt', action='append', default=[],
                    help='Add a kernel option.  You can specify this more than once.')
 
@@ -113,12 +117,18 @@
 
 _ARGPARSER = make_parser()
 
-def arg_fail(message):
+def arg_fail(message, show_usage=True):
     print(message)
-    _ARGPARSER.print_usage()
+    if show_usage:
+        _ARGPARSER.print_usage()
     sys.exit(1)
 
+def is_file_more_recent(a, b):
+    return os.stat(a).st_mtime > os.stat(b).st_mtime
+
 def find_kernel_and_mods(arch, args):
+    use_root_mods = False
+
     if args.installed_kernel is not None:
         kver = args.installed_kernel
         modfiles = modfinder.find_modules_from_install(
@@ -128,21 +138,47 @@
         if not os.path.exists(kimg):
             kimg = '/boot/vmlinuz-%s' % kver
         dtb = None  # For now
+        use_root_mods = True
     elif args.kdir is not None:
         kimg = os.path.join(args.kdir, arch.kimg_path())
-        modfiles = []
-        moddir = None
+        virtme_mods = os.path.join(args.kdir, '.virtme_mods')
+        mod_file = os.path.join(args.kdir, 'modules.order')
+        virtme_mod_file = os.path.join(virtme_mods, 'lib/modules/0.0.0/modules.dep')
 
-        # Once kmod gets fixed (if ever), we can do something like:
-        # modfiles = modfinder.find_modules_from_install(
-        #     virtmods.MODALIASES,
-        #     moddir=os.path.join(args.kernel_build_dir, '.tmp_moddir'))
+        # Kernel modules support
+        kver = None
+        moddir = None
+        modfiles = []
+        if args.mods == 'none':
+            pass
+        elif args.mods == 'use' or args.mods == 'auto':
+            # Check if modules.order exists, otherwise it's not possible to use
+            # this option
+            if not os.path.exists(mod_file):
+                arg_fail('%s not found: kernel modules not enabled or kernel not compiled properly' % mod_file, show_usage=False)
+            # Check if virtme's kernel modules directory needs to be updated
+            if not os.path.exists(virtme_mods) or \
+               is_file_more_recent(mod_file, virtme_mod_file):
+                if args.mods == 'use':
+                    # Inform user to manually refresh virtme's kernel modules
+                    # directory
+                    arg_fail("please run virtme-prep-kdir-mods to update virtme's kernel modules directory or use --mods=auto", show_usage=False)
+                else:
+                    # Auto-refresh virtme's kernel modules directory
+                    guest_tools.run_script('virtme-prep-kdir-mods')
+            moddir = os.path.join(virtme_mods, 'lib/modules', '0.0.0')
+            modfiles = modfinder.find_modules_from_install(
+                               virtmods.MODALIASES, root=virtme_mods, kver='0.0.0')
+        else:
+            arg_fail("invalid argument '%s', please use --mods=none|use|auto" % args.mods)
 
         dtb_path = arch.dtb_path()
         if dtb_path is None:
             dtb = None
         else:
             dtb = os.path.join(args.kdir, dtb_path)
+    elif args.mods is not None:
+        arg_fail("--mods must be used together with --kdir")
     elif args.kimg is not None:
         kimg = args.kimg
         modfiles = []
@@ -151,7 +187,7 @@
     else:
         arg_fail('You must specify a kernel to use.')
 
-    return kimg,dtb,modfiles,moddir
+    return kimg,dtb,modfiles,moddir,use_root_mods
 
 def export_virtfs(qemu, arch, qemuargs, path, mount_tag, security_model='none', readonly=True):
     # NB: We can't use -virtfs for this, because it can't handle a mount_tag
@@ -189,7 +225,7 @@
 
     config = mkinitramfs.Config()
 
-    kimg,dtb,modfiles,moddir = find_kernel_and_mods(arch, args)
+    kimg,dtb,modfiles,moddir,use_root_mods = find_kernel_and_mods(arch, args)
     config.modfiles = modfiles
     if config.modfiles:
         need_initramfs = True
@@ -216,9 +252,20 @@
                 '/bin/mount -n -t 9p -o ro,version=9p2000.L,trans=virtio,access=any virtme.guesttools /run/virtme/guesttools',
                 'exec /run/virtme/guesttools/virtme-init']
 
-    # Map modules
+    # Arrange for modules to end up in the right place
     if moddir is not None:
-        export_virtfs(qemu, arch, qemuargs, moddir, 'virtme.moddir')
+        if use_root_mods:
+            # Tell virtme-init to use the root /lib/modules
+            kernelargs.append("virtme_root_mods=1")
+        else:
+            # We're grabbing modules from somewhere other than /lib/modules.
+            # Rather than mounting it separately, symlink it in the guest.
+            # This allows symlinks within the module directory to resolve
+            # correctly in the guest.
+            kernelargs.append("virtme_link_mods=/%s" % qemu.quote_optarg(os.path.relpath(moddir, args.root)))
+    else:
+        # No modules are available.  virtme-init will hide /lib/modules/KVER
+        pass
 
     # Set up mounts
     mount_index = 0
@@ -306,7 +353,7 @@
             name,fn = namefile
             if '=' in fn or ',' in fn:
                 arg_fail("--disk filenames cannot contain '=' or ','")
-            if '=' in fn or ',' in name:
+            if '=' in name or ',' in name:
                 arg_fail("--disk device names cannot contain '=' or ','")
             driveid = 'disk%d' % i
             qemuargs.extend(['-drive', 'if=none,id=%s,file=%s' % (driveid, fn),
diff --git a/virtme/guest/virtme-init b/virtme/guest/virtme-init
index cd3caa4..f60faa5 100755
--- a/virtme/guest/virtme-init
+++ b/virtme/guest/virtme-init
@@ -27,10 +27,12 @@
 
 kver="`uname -r`"
 
-if [[ -n "${mount_tags[virtme.moddir]}" ]]; then
-    mount -t tmpfs none /lib/modules
-    mkdir /lib/modules/"$kver"
-    mount -n -t 9p -o ro,version=9p2000.L,trans=virtio,access=any virtme.moddir /lib/modules/"$kver"
+if [[ -n "$virtme_root_mods" ]]; then
+    # /lib/modules is already set up
+    true
+elif [[ -n "$virtme_link_mods" ]]; then
+    mount -n -t tmpfs none /lib/modules
+    ln -s "$virtme_link_mods" "/lib/modules/$kver"
 elif [[ -d "/lib/modules/$kver" ]]; then
     # We may have mismatched modules.  Mask them off.
     mount -n -t tmpfs -o ro,mode=0000 disallow_modules "/lib/modules/$kver"
diff --git a/virtme/guest_tools.py b/virtme/guest_tools.py
index 96c1cc2..b84f761 100644
--- a/virtme/guest_tools.py
+++ b/virtme/guest_tools.py
@@ -1,14 +1,16 @@
 # -*- mode: python -*-
 # resources.py: Find virtme's resources
-# Copyright © 2014 Andy Lutomirski
+# Copyright © 2014-2019 Andy Lutomirski
 # Licensed under the GPLv2, which is available in the virtme distribution
 # as a file called LICENSE with SHA-256 hash:
 # 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643
 
-"""Helpers to find virtme's guest tools."""
+"""Helpers to find virtme's guest tools and host scripts."""
 
 import os
+import shutil
 import pkg_resources
+import subprocess
 
 def find_guest_tools():
     """Return the path of the guest tools installed with the running virtme.
@@ -19,3 +21,22 @@
 
     # No luck.  This is somewhat surprising.
     return None
+
+def find_script(name):
+    # If we're running out of a source checkout, we can find scripts through
+    # the 'virtme/scripts' symlink.
+    fn = pkg_resources.resource_filename(__name__, 'scripts/%s' % name)
+    if os.path.isfile(fn):
+        return fn
+
+    # Otherwise assume we're actually installed and in PATH.
+    fn = shutil.which(name)
+    if fn is not None:
+        return fn
+
+    # No luck.  This is somewhat surprising.
+    raise Exception('could not find script %s' % name)
+
+def run_script(name):
+    fn = find_script(name)
+    subprocess.check_call(executable=fn, args=[fn])
diff --git a/virtme/mkinitramfs.py b/virtme/mkinitramfs.py
index baaefe9..dde504d 100644
--- a/virtme/mkinitramfs.py
+++ b/virtme/mkinitramfs.py
@@ -9,6 +9,7 @@
 import io
 import os.path
 import shlex
+import itertools
 from . import cpiowriter
 from . import modfinder
 from . import virtmods
@@ -40,6 +41,7 @@
     cw.write_file(name=b'bin/modprobe', body=b'\n'.join([
         b'#!/bin/sh',
         b'echo "virtme: initramfs does not have module $3" >/dev/console',
+        b'exit 1',
     ]), mode=0o755)
 
 _LOGFUNC = """log() {
@@ -165,12 +167,12 @@
     cw.write_trailer()
 
 def find_busybox(root, is_native):
-    for path in ('usr/local/bin/busybox', 'usr/local/sbin/busybox',
-                 'usr/bin/busybox-static',
-                 'usr/bin/busybox', 'usr/sbin/busybox',
-                 'bin/busybox', 'sbin/busybox'):
-        if os.path.isfile(os.path.join(root, path)):
-            return os.path.join(root, path)
+    for p in itertools.product(['usr/local', 'usr', ''],
+                               ['bin', 'sbin'],
+                               ['', '-static', '.static']):
+        path = os.path.join(root, p[0], p[1], 'busybox' + p[2])
+        if os.path.isfile(path):
+            return path
 
     if is_native:
         # Try the host's busybox, if any
diff --git a/virtme/scripts b/virtme/scripts
new file mode 120000
index 0000000..19f285a
--- /dev/null
+++ b/virtme/scripts
@@ -0,0 +1 @@
+../bin
\ No newline at end of file