summaryrefslogtreecommitdiffstats
path: root/src/kernel-install
diff options
context:
space:
mode:
Diffstat (limited to 'src/kernel-install')
-rwxr-xr-xsrc/kernel-install/50-depmod.install53
-rwxr-xr-xsrc/kernel-install/60-ukify.install.in265
-rwxr-xr-xsrc/kernel-install/90-loaderentry.install.in210
-rwxr-xr-xsrc/kernel-install/90-uki-copy.install97
-rw-r--r--src/kernel-install/install.conf12
-rw-r--r--src/kernel-install/kernel-install.c1743
-rw-r--r--src/kernel-install/meson.build51
-rwxr-xr-xsrc/kernel-install/test-kernel-install.sh333
8 files changed, 2764 insertions, 0 deletions
diff --git a/src/kernel-install/50-depmod.install b/src/kernel-install/50-depmod.install
new file mode 100755
index 0000000..88f858f
--- /dev/null
+++ b/src/kernel-install/50-depmod.install
@@ -0,0 +1,53 @@
+#!/bin/sh
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see <https://www.gnu.org/licenses/>.
+
+set -e
+
+COMMAND="${1:?}"
+KERNEL_VERSION="${2:?}"
+
+[ -w "/lib/modules" ] || exit 0
+
+case "$COMMAND" in
+ add)
+ [ -d "/lib/modules/$KERNEL_VERSION/kernel" ] || exit 0
+ command -v depmod >/dev/null || exit 0
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "+depmod -a $KERNEL_VERSION"
+ exec depmod -a "$KERNEL_VERSION"
+ ;;
+ remove)
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \
+ echo "Removing /lib/modules/${KERNEL_VERSION}/modules.dep and associated files"
+ exec rm -f \
+ "/lib/modules/$KERNEL_VERSION/modules.alias" \
+ "/lib/modules/$KERNEL_VERSION/modules.alias.bin" \
+ "/lib/modules/$KERNEL_VERSION/modules.builtin.bin" \
+ "/lib/modules/$KERNEL_VERSION/modules.builtin.alias.bin" \
+ "/lib/modules/$KERNEL_VERSION/modules.dep" \
+ "/lib/modules/$KERNEL_VERSION/modules.dep.bin" \
+ "/lib/modules/$KERNEL_VERSION/modules.devname" \
+ "/lib/modules/$KERNEL_VERSION/modules.softdep" \
+ "/lib/modules/$KERNEL_VERSION/modules.symbols" \
+ "/lib/modules/$KERNEL_VERSION/modules.symbols.bin"
+ ;;
+ *)
+ exit 0
+ ;;
+esac
diff --git a/src/kernel-install/60-ukify.install.in b/src/kernel-install/60-ukify.install.in
new file mode 100755
index 0000000..be1e21b
--- /dev/null
+++ b/src/kernel-install/60-ukify.install.in
@@ -0,0 +1,265 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# -*- mode: python-mode -*-
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see <https://www.gnu.org/licenses/>.
+
+# pylint: disable=import-outside-toplevel,consider-using-with,disable=redefined-builtin
+
+import argparse
+import os
+import runpy
+import shlex
+from shutil import which
+from pathlib import Path
+from typing import Optional
+
+__version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
+
+try:
+ VERBOSE = int(os.environ['KERNEL_INSTALL_VERBOSE']) > 0
+except (KeyError, ValueError):
+ VERBOSE = False
+
+# Override location of ukify and the boot stub for testing and debugging.
+UKIFY = os.getenv('KERNEL_INSTALL_UKIFY', which('ukify'))
+BOOT_STUB = os.getenv('KERNEL_INSTALL_BOOT_STUB')
+
+
+def shell_join(cmd):
+ # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path.
+ return ' '.join(shlex.quote(str(x)) for x in cmd)
+
+def log(*args, **kwargs):
+ if VERBOSE:
+ print(*args, **kwargs)
+
+def path_is_readable(p: Path, dir=False) -> None:
+ """Verify access to a file or directory."""
+ try:
+ p.open().close()
+ except IsADirectoryError:
+ if dir:
+ return
+ raise
+
+def mandatory_variable(name):
+ try:
+ return os.environ[name]
+ except KeyError as e:
+ raise KeyError(f'${name} must be set in the environment') from e
+
+def parse_args(args=None):
+ p = argparse.ArgumentParser(
+ description='kernel-install plugin to build a Unified Kernel Image',
+ allow_abbrev=False,
+ usage='60-ukify.install COMMAND KERNEL_VERSION ENTRY_DIR KERNEL_IMAGE INITRD…',
+ )
+
+ # Suppress printing of usage synopsis on errors
+ p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n')
+
+ p.add_argument('command',
+ metavar='COMMAND',
+ help="The action to perform. Only 'add' is supported.")
+ p.add_argument('kernel_version',
+ metavar='KERNEL_VERSION',
+ help='Kernel version string')
+ p.add_argument('entry_dir',
+ metavar='ENTRY_DIR',
+ type=Path,
+ nargs='?',
+ help='Type#1 entry directory (ignored)')
+ p.add_argument('kernel_image',
+ metavar='KERNEL_IMAGE',
+ type=Path,
+ nargs='?',
+ help='Kernel binary')
+ p.add_argument('initrd',
+ metavar='INITRD…',
+ type=Path,
+ nargs='*',
+ help='Initrd files')
+ p.add_argument('--version',
+ action='version',
+ version=f'systemd {__version__}')
+
+ opts = p.parse_args(args)
+
+ if opts.command == 'add':
+ opts.staging_area = Path(mandatory_variable('KERNEL_INSTALL_STAGING_AREA'))
+ path_is_readable(opts.staging_area, dir=True)
+
+ opts.entry_token = mandatory_variable('KERNEL_INSTALL_ENTRY_TOKEN')
+ opts.machine_id = mandatory_variable('KERNEL_INSTALL_MACHINE_ID')
+
+ return opts
+
+def we_are_wanted() -> bool:
+ KERNEL_INSTALL_LAYOUT = os.getenv('KERNEL_INSTALL_LAYOUT')
+
+ if KERNEL_INSTALL_LAYOUT != 'uki':
+ log(f'{KERNEL_INSTALL_LAYOUT=}, quitting.')
+ return False
+
+ KERNEL_INSTALL_UKI_GENERATOR = os.getenv('KERNEL_INSTALL_UKI_GENERATOR') or 'ukify'
+
+ if KERNEL_INSTALL_UKI_GENERATOR != 'ukify':
+ log(f'{KERNEL_INSTALL_UKI_GENERATOR=}, quitting.')
+ return False
+
+ log('KERNEL_INSTALL_LAYOUT and KERNEL_INSTALL_UKI_GENERATOR are good')
+ return True
+
+
+def input_file_location(
+ filename: str,
+ *search_directories: str) -> Optional[Path]:
+
+ if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
+ search_directories = (root,)
+ elif not search_directories:
+ # This is the default search path.
+ search_directories = ('/etc/kernel',
+ '/usr/lib/kernel')
+
+ for dir in search_directories:
+ p = Path(dir) / filename
+ if p.exists():
+ return p
+ return None
+
+
+def uki_conf_location() -> Optional[Path]:
+ return input_file_location('uki.conf',
+ '/etc/kernel')
+
+
+def devicetree_config_location() -> Optional[Path]:
+ return input_file_location('devicetree')
+
+
+def devicetree_file_location(opts) -> Optional[Path]:
+ # This mirrors the logic in 90-loaderentry.install. Keep in sync.
+ configfile = devicetree_config_location()
+ if configfile is None:
+ return None
+
+ devicetree = configfile.read_text().strip()
+ if not devicetree:
+ raise ValueError(f'{configfile!r} is empty')
+
+ path = input_file_location(
+ devicetree,
+ f'/usr/lib/firmware/{opts.kernel_version}/device-tree',
+ f'/usr/lib/linux-image-{opts.kernel_version}',
+ f'/usr/lib/modules/{opts.kernel_version}/dtb',
+ )
+ if path is None:
+ raise FileNotFoundError(f'DeviceTree file {devicetree} not found')
+ return path
+
+
+def kernel_cmdline_base() -> list[str]:
+ path = input_file_location('cmdline')
+ if path:
+ return path.read_text().split()
+
+ # If we read /proc/cmdline, we need to do some additional filtering.
+ options = Path('/proc/cmdline').read_text().split()
+ return [opt for opt in options
+ if not opt.startswith(('BOOT_IMAGE=', 'initrd='))]
+
+
+def kernel_cmdline(opts) -> str:
+ options = kernel_cmdline_base()
+
+ # If the boot entries are named after the machine ID, then suffix the kernel
+ # command line with the machine ID we use, so that the machine ID remains
+ # stable, even during factory reset, in the initrd (where the system's machine
+ # ID is not directly accessible yet), and if the root file system is volatile.
+ if (opts.entry_token == opts.machine_id and
+ not any(opt.startswith('systemd.machine_id=') for opt in options)):
+ options += [f'systemd.machine_id={opts.machine_id}']
+
+ # TODO: we unconditionally set the cmdline here, ignoring the setting in
+ # the config file. Should we not do that?
+
+ # Prepend a space so that '@' does not get misinterpreted
+ return ' ' + ' '.join(options)
+
+
+def initrd_list(opts) -> list[Path]:
+ microcode = sorted(opts.staging_area.glob('microcode*'))
+ initrd = sorted(opts.staging_area.glob('initrd*'))
+
+ # Order taken from 90-loaderentry.install
+ return [*microcode, *opts.initrd, *initrd]
+
+
+def call_ukify(opts):
+ # Punish me harder.
+ # We want this:
+ # ukify = importlib.machinery.SourceFileLoader('ukify', UKIFY).load_module()
+ # but it throws a DeprecationWarning.
+ # https://stackoverflow.com/questions/67631/how-can-i-import-a-module-dynamically-given-the-full-path
+ # https://github.com/python/cpython/issues/65635
+ # offer "explanations", but to actually load a python file without a .py extension,
+ # the "solution" is 4+ incomprehensible lines.
+ # The solution with runpy gives a dictionary, which isn't great, but will do.
+ ukify = runpy.run_path(UKIFY, run_name='ukify')
+
+ # Create "empty" namespace. We want to override just a few settings, so it
+ # doesn't make sense to configure everything. We pretend to parse an empty
+ # argument set to prepopulate the namespace with the defaults.
+ opts2 = ukify['create_parser']().parse_args(['build'])
+
+ opts2.config = uki_conf_location()
+ opts2.uname = opts.kernel_version
+ opts2.linux = opts.kernel_image
+ opts2.initrd = initrd_list(opts)
+ # Note that 'uki.efi' is the name required by 90-uki-copy.install.
+ opts2.output = opts.staging_area / 'uki.efi'
+
+ if devicetree := devicetree_file_location(opts):
+ opts2.devicetree = devicetree
+
+ opts2.cmdline = kernel_cmdline(opts)
+ if BOOT_STUB:
+ opts2.stub = BOOT_STUB
+
+ # opts2.summary = True
+
+ ukify['apply_config'](opts2)
+ ukify['finalize_options'](opts2)
+ ukify['check_inputs'](opts2)
+ ukify['make_uki'](opts2)
+
+ log(f'{opts2.output} has been created')
+
+
+def main():
+ opts = parse_args()
+ if opts.command != 'add':
+ return
+ if not we_are_wanted():
+ return
+
+ call_ukify(opts)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/kernel-install/90-loaderentry.install.in b/src/kernel-install/90-loaderentry.install.in
new file mode 100755
index 0000000..a52dd81
--- /dev/null
+++ b/src/kernel-install/90-loaderentry.install.in
@@ -0,0 +1,210 @@
+#!/bin/sh
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see <https://www.gnu.org/licenses/>.
+
+set -e
+
+COMMAND="${1:?}"
+KERNEL_VERSION="${2:?}"
+ENTRY_DIR_ABS="${3:?}"
+KERNEL_IMAGE="$4"
+INITRD_OPTIONS_SHIFT=4
+
+[ "$KERNEL_INSTALL_LAYOUT" = "bls" ] || exit 0
+
+MACHINE_ID="${KERNEL_INSTALL_MACHINE_ID:?}"
+ENTRY_TOKEN="${KERNEL_INSTALL_ENTRY_TOKEN:?}"
+BOOT_ROOT="${KERNEL_INSTALL_BOOT_ROOT:?}"
+
+[ -n "$BOOT_MNT" ] || BOOT_MNT="$(stat -c %m "$BOOT_ROOT")"
+if [ "$BOOT_MNT" = '/' ]; then
+ ENTRY_DIR="$ENTRY_DIR_ABS"
+else
+ ENTRY_DIR="${ENTRY_DIR_ABS#"$BOOT_MNT"}"
+fi
+
+KERNEL_DEST="$ENTRY_DIR_ABS/linux"
+KERNEL_ENTRY="$ENTRY_DIR/linux"
+LOADER_ENTRY="$BOOT_ROOT/loader/entries/$ENTRY_TOKEN-$KERNEL_VERSION.conf"
+
+case "$COMMAND" in
+ remove)
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \
+ echo "Removing ${LOADER_ENTRY%.conf}*.conf"
+ exec rm -f \
+ "$LOADER_ENTRY" \
+ "${LOADER_ENTRY%.conf}"*".conf"
+ ;;
+ add)
+ ;;
+ *)
+ exit 0
+ ;;
+esac
+
+if [ -f /etc/os-release ]; then
+ # shellcheck source=/dev/null
+ . /etc/os-release
+elif [ -f /usr/lib/os-release ]; then
+ # shellcheck source=/dev/null
+ . /usr/lib/os-release
+fi
+
+[ -n "$PRETTY_NAME" ] || PRETTY_NAME="Linux $KERNEL_VERSION"
+
+SORT_KEY="$IMAGE_ID"
+[ -z "$SORT_KEY" ] && SORT_KEY="$ID"
+
+if [ -n "$KERNEL_INSTALL_CONF_ROOT" ]; then
+ if [ -f "$KERNEL_INSTALL_CONF_ROOT/cmdline" ]; then
+ BOOT_OPTIONS="$(tr -s "$IFS" ' ' <"$KERNEL_INSTALL_CONF_ROOT/cmdline")"
+ fi
+elif [ -f /etc/kernel/cmdline ]; then
+ BOOT_OPTIONS="$(tr -s "$IFS" ' ' </etc/kernel/cmdline)"
+elif [ -f /usr/lib/kernel/cmdline ]; then
+ BOOT_OPTIONS="$(tr -s "$IFS" ' ' </usr/lib/kernel/cmdline)"
+else
+ BOOT_OPTIONS="$(tr -s "$IFS" '\n' </proc/cmdline | grep -ve '^BOOT_IMAGE=' -e '^initrd=' | tr '\n' ' ')"
+fi
+
+BOOT_OPTIONS="${BOOT_OPTIONS% }"
+
+# If the boot entries are named after the machine ID, then suffix the kernel
+# command line with the machine ID we use, so that the machine ID remains
+# stable, even during factory reset, in the initrd (where the system's machine
+# ID is not directly accessible yet), and if the root file system is volatile.
+if [ "$ENTRY_TOKEN" = "$MACHINE_ID" ] && ! echo "$BOOT_OPTIONS" | grep -q "systemd.machine_id="; then
+ BOOT_OPTIONS="$BOOT_OPTIONS systemd.machine_id=$MACHINE_ID"
+fi
+
+TRIES_FILE="${KERNEL_INSTALL_CONF_ROOT:-/etc/kernel}/tries"
+
+if [ -f "$TRIES_FILE" ]; then
+ read -r TRIES <"$TRIES_FILE"
+ if ! echo "$TRIES" | grep -q '^[0-9][0-9]*$'; then
+ echo "$TRIES_FILE does not contain an integer." >&2
+ exit 1
+ fi
+ LOADER_ENTRY="${LOADER_ENTRY%.conf}+$TRIES.conf"
+fi
+
+if ! [ -d "$ENTRY_DIR_ABS" ]; then
+ echo "Error: entry directory '$ENTRY_DIR_ABS' does not exist" >&2
+ exit 1
+fi
+
+install -m 0644 "$KERNEL_IMAGE" "$KERNEL_DEST" || {
+ echo "Error: could not copy '$KERNEL_IMAGE' to '$KERNEL_DEST'." >&2
+ exit 1
+}
+chown root:root "$KERNEL_DEST" || :
+
+if [ -n "$KERNEL_INSTALL_CONF_ROOT" ]; then
+ if [ -f "$KERNEL_INSTALL_CONF_ROOT/devicetree" ]; then
+ read -r DEVICETREE <"$KERNEL_INSTALL_CONF_ROOT/devicetree"
+ fi
+elif [ -f /etc/kernel/devicetree ]; then
+ read -r DEVICETREE </etc/kernel/devicetree
+elif [ -f /usr/lib/kernel/devicetree ]; then
+ read -r DEVICETREE </usr/lib/kernel/devicetree
+fi
+if [ -n "$DEVICETREE" ]; then
+ for prefix in \
+ "$KERNEL_INSTALL_CONF_ROOT" \
+ "/usr/lib/firmware/$KERNEL_VERSION/device-tree" \
+ "/usr/lib/linux-image-$KERNEL_VERSION" \
+ "/usr/lib/modules/$KERNEL_VERSION/dtb"
+ do
+ [ -n "$prefix" ] || continue
+ [ -f "$prefix/$DEVICETREE" ] || continue
+ DEVICETREE_SRC="$prefix/$DEVICETREE"
+ break
+ done
+
+ [ -n "$DEVICETREE_SRC" ] || {
+ echo "Error: could not find device tree blob '$DEVICETREE'." >&2
+ exit 1
+ }
+
+ DEVICETREE_DEST="$ENTRY_DIR_ABS/${DEVICETREE##*/}"
+ DEVICETREE_ENTRY="$ENTRY_DIR/${DEVICETREE##*/}"
+
+ install -m 0644 "$DEVICETREE_SRC" "$DEVICETREE_DEST" || {
+ echo "Error: could not copy '$DEVICETREE_SRC' to '$DEVICETREE_DEST'." >&2
+ exit 1
+ }
+ chown root:root "$DEVICETREE_DEST" || :
+fi
+
+shift "$INITRD_OPTIONS_SHIFT"
+# All files listed as arguments, and staged files starting with "initrd" are installed as initrds.
+for initrd in "${KERNEL_INSTALL_STAGING_AREA}"/microcode* "${@}" "${KERNEL_INSTALL_STAGING_AREA}"/initrd*; do
+ [ -f "$initrd" ] || {
+ case "$initrd" in
+ "${KERNEL_INSTALL_STAGING_AREA}/initrd*" | "${KERNEL_INSTALL_STAGING_AREA}/microcode*")
+ continue ;;
+ esac
+ echo "Error: '$initrd' is not a file." >&2
+ exit 1
+ }
+
+ initrd_basename="${initrd##*/}"
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "Installing $ENTRY_DIR_ABS/$initrd_basename"
+ install -m 0644 "$initrd" "$ENTRY_DIR_ABS/$initrd_basename" || {
+ echo "Error: could not copy '$initrd' to '$ENTRY_DIR_ABS/$initrd_basename'." >&2
+ exit 1
+ }
+ chown root:root "$ENTRY_DIR_ABS/$initrd_basename" || :
+done
+
+mkdir -p "${LOADER_ENTRY%/*}" || {
+ echo "Error: could not create loader entry directory '${LOADER_ENTRY%/*}'." >&2
+ exit 1
+}
+
+[ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "Creating $LOADER_ENTRY"
+{
+ echo "# Boot Loader Specification type#1 entry"
+ echo "# File created by $0 (systemd {{GIT_VERSION}})"
+ echo "title $PRETTY_NAME"
+ echo "version $KERNEL_VERSION"
+ if [ "$ENTRY_TOKEN" = "$MACHINE_ID" ]; then
+ # See similar logic above for the systemd.machine_id= kernel command line option
+ echo "machine-id $MACHINE_ID"
+ fi
+ [ -n "$SORT_KEY" ] && echo "sort-key $SORT_KEY"
+ echo "options $BOOT_OPTIONS"
+ echo "linux $KERNEL_ENTRY"
+ [ -n "$DEVICETREE_ENTRY" ] && echo "devicetree $DEVICETREE_ENTRY"
+
+ have_initrd=
+ for initrd in "${KERNEL_INSTALL_STAGING_AREA}"/microcode* "${@}" "${KERNEL_INSTALL_STAGING_AREA}"/initrd*; do
+ [ -f "$initrd" ] || continue
+ echo "initrd $ENTRY_DIR/${initrd##*/}"
+ have_initrd=yes
+ done
+
+ # Try "initrd", generated by dracut in its kernel-install hook, if no initrds were supplied
+ [ -z "$have_initrd" ] && [ -f "$ENTRY_DIR_ABS/initrd" ] && echo "initrd $ENTRY_DIR/initrd"
+ :
+} >"$LOADER_ENTRY" || {
+ echo "Error: could not create loader entry '$LOADER_ENTRY'." >&2
+ exit 1
+}
+exit 0
diff --git a/src/kernel-install/90-uki-copy.install b/src/kernel-install/90-uki-copy.install
new file mode 100755
index 0000000..c66c097
--- /dev/null
+++ b/src/kernel-install/90-uki-copy.install
@@ -0,0 +1,97 @@
+#!/bin/sh
+# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*-
+# ex: ts=8 sw=4 sts=4 et filetype=sh
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see <https://www.gnu.org/licenses/>.
+
+set -e
+
+COMMAND="${1:?}"
+KERNEL_VERSION="${2:?}"
+# shellcheck disable=SC2034
+ENTRY_DIR_ABS="$3"
+KERNEL_IMAGE="$4"
+
+[ "$KERNEL_INSTALL_LAYOUT" = "uki" ] || exit 0
+
+ENTRY_TOKEN="$KERNEL_INSTALL_ENTRY_TOKEN"
+BOOT_ROOT="$KERNEL_INSTALL_BOOT_ROOT"
+
+UKI_DIR="$BOOT_ROOT/EFI/Linux"
+
+case "$COMMAND" in
+ remove)
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \
+ echo "Removing $UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION*.efi"
+ exec rm -f \
+ "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi" \
+ "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION+"*".efi"
+ ;;
+ add)
+ ;;
+ *)
+ exit 0
+ ;;
+esac
+
+if ! [ -d "$UKI_DIR" ]; then
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "creating $UKI_DIR"
+ mkdir -p "$UKI_DIR"
+fi
+
+TRIES_FILE="${KERNEL_INSTALL_CONF_ROOT:-/etc/kernel}/tries"
+
+if [ -f "$TRIES_FILE" ]; then
+ read -r TRIES <"$TRIES_FILE"
+ if ! echo "$TRIES" | grep -q '^[0-9][0-9]*$'; then
+ echo "$TRIES_FILE does not contain an integer." >&2
+ exit 1
+ fi
+ UKI_FILE="$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION+$TRIES.efi"
+else
+ UKI_FILE="$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi"
+fi
+
+# If there is a UKI named uki.efi on the staging area use that, if not use what
+# was passed in as $KERNEL_IMAGE but insist it has a .efi extension
+if [ -f "$KERNEL_INSTALL_STAGING_AREA/uki.efi" ]; then
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "Installing $KERNEL_INSTALL_STAGING_AREA/uki.efi as $UKI_FILE"
+ install -m 0644 "$KERNEL_INSTALL_STAGING_AREA/uki.efi" "$UKI_FILE" || {
+ echo "Error: could not copy '$KERNEL_INSTALL_STAGING_AREA/uki.efi' to '$UKI_FILE'." >&2
+ exit 1
+ }
+elif [ -n "$KERNEL_IMAGE" ]; then
+ [ -f "$KERNEL_IMAGE" ] || {
+ echo "Error: UKI '$KERNEL_IMAGE' not a file." >&2
+ exit 1
+ }
+ [ "$KERNEL_IMAGE" != "${KERNEL_IMAGE%*.efi}.efi" ] && {
+ echo "Error: $KERNEL_IMAGE is missing .efi suffix." >&2
+ exit 1
+ }
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "Installing $KERNEL_IMAGE as $UKI_FILE"
+ install -m 0644 "$KERNEL_IMAGE" "$UKI_FILE" || {
+ echo "Error: could not copy '$KERNEL_IMAGE' to '$UKI_FILE'." >&2
+ exit 1
+ }
+else
+ [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "No UKI available. Nothing to do."
+ exit 0
+fi
+chown root:root "$UKI_FILE" || :
+
+exit 0
diff --git a/src/kernel-install/install.conf b/src/kernel-install/install.conf
new file mode 100644
index 0000000..4520c01
--- /dev/null
+++ b/src/kernel-install/install.conf
@@ -0,0 +1,12 @@
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# See kernel-install(8) for details.
+
+#layout=bls|other|...
+#initrd_generator=dracut|...
+#uki_generator=ukify|...
diff --git a/src/kernel-install/kernel-install.c b/src/kernel-install/kernel-install.c
new file mode 100644
index 0000000..14ae1a8
--- /dev/null
+++ b/src/kernel-install/kernel-install.c
@@ -0,0 +1,1743 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <stdbool.h>
+#include <sys/utsname.h>
+
+#include "boot-entry.h"
+#include "build.h"
+#include "chase.h"
+#include "conf-files.h"
+#include "dirent-util.h"
+#include "env-file.h"
+#include "env-util.h"
+#include "exec-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "find-esp.h"
+#include "format-table.h"
+#include "fs-util.h"
+#include "id128-util.h"
+#include "image-policy.h"
+#include "kernel-image.h"
+#include "main-func.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "parse-argument.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "recurse-dir.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tmpfile-util.h"
+#include "verbs.h"
+
+static bool arg_verbose = false;
+static char *arg_esp_path = NULL;
+static char *arg_xbootldr_path = NULL;
+static int arg_make_entry_directory = -1; /* tristate */
+static PagerFlags arg_pager_flags = 0;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+static char *arg_root = NULL;
+static char *arg_image = NULL;
+static ImagePolicy *arg_image_policy = NULL;
+static bool arg_legend = true;
+
+STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
+
+typedef enum Action {
+ ACTION_ADD,
+ ACTION_REMOVE,
+ ACTION_INSPECT,
+ _ACTION_MAX,
+ _ACTION_INVALID = -EINVAL,
+} Action;
+
+typedef enum Layout {
+ LAYOUT_AUTO,
+ LAYOUT_UKI,
+ LAYOUT_BLS,
+ LAYOUT_OTHER,
+ _LAYOUT_MAX,
+ _LAYOUT_INVALID = -EINVAL,
+} Layout;
+
+static const char * const layout_table[_LAYOUT_MAX] = {
+ [LAYOUT_AUTO] = "auto",
+ [LAYOUT_UKI] = "uki",
+ [LAYOUT_BLS] = "bls",
+ [LAYOUT_OTHER] = "other",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP(layout, Layout);
+
+typedef struct Context {
+ int rfd;
+ Action action;
+ sd_id128_t machine_id;
+ bool machine_id_is_random;
+ KernelImageType kernel_image_type;
+ Layout layout;
+ char *layout_other;
+ char *conf_root;
+ char *boot_root;
+ BootEntryTokenType entry_token_type;
+ char *entry_token;
+ char *entry_dir;
+ char *version;
+ char *kernel;
+ char **initrds;
+ char *initrd_generator;
+ char *uki_generator;
+ char *staging_area;
+ char **plugins;
+ char **argv;
+ char **envp;
+} Context;
+
+#define CONTEXT_NULL (Context) { .rfd = -EBADF }
+
+static void context_done(Context *c) {
+ assert(c);
+
+ free(c->layout_other);
+ free(c->conf_root);
+ free(c->boot_root);
+ free(c->entry_token);
+ free(c->entry_dir);
+ free(c->version);
+ free(c->kernel);
+ strv_free(c->initrds);
+ free(c->initrd_generator);
+ free(c->uki_generator);
+ if (c->action == ACTION_INSPECT)
+ free(c->staging_area);
+ else
+ rm_rf_physical_and_free(c->staging_area);
+ strv_free(c->plugins);
+ strv_free(c->argv);
+ strv_free(c->envp);
+
+ safe_close(c->rfd);
+}
+
+static int context_copy(const Context *source, Context *ret) {
+ int r;
+
+ assert(source);
+ assert(ret);
+ assert(source->rfd >= 0 || source->rfd == AT_FDCWD);
+
+ _cleanup_(context_done) Context copy = (Context) {
+ .rfd = AT_FDCWD,
+ .action = source->action,
+ .machine_id = source->machine_id,
+ .machine_id_is_random = source->machine_id_is_random,
+ .kernel_image_type = source->kernel_image_type,
+ .layout = source->layout,
+ .entry_token_type = source->entry_token_type,
+ };
+
+ if (source->rfd >= 0) {
+ copy.rfd = fd_reopen(source->rfd, O_CLOEXEC|O_DIRECTORY|O_PATH);
+ if (copy.rfd < 0)
+ return copy.rfd;
+ }
+
+ r = strdup_or_null(source->layout_other, &copy.layout_other);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->conf_root, &copy.conf_root);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->boot_root, &copy.boot_root);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->entry_token, &copy.entry_token);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->entry_dir, &copy.entry_dir);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->version, &copy.version);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->kernel, &copy.kernel);
+ if (r < 0)
+ return r;
+ r = strv_copy_unless_empty(source->initrds, &copy.initrds);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->initrd_generator, &copy.initrd_generator);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->uki_generator, &copy.uki_generator);
+ if (r < 0)
+ return r;
+ r = strdup_or_null(source->staging_area, &copy.staging_area);
+ if (r < 0)
+ return r;
+ r = strv_copy_unless_empty(source->plugins, &copy.plugins);
+ if (r < 0)
+ return r;
+ r = strv_copy_unless_empty(source->argv, &copy.argv);
+ if (r < 0)
+ return r;
+ r = strv_copy_unless_empty(source->envp, &copy.envp);
+ if (r < 0)
+ return r;
+
+ *ret = copy;
+ copy = CONTEXT_NULL;
+
+ return 0;
+}
+
+static int context_open_root(Context *c) {
+ int r;
+
+ assert(c);
+ assert(c->rfd < 0);
+
+ if (isempty(arg_root))
+ return 0;
+
+ r = path_is_root(arg_root);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine if '%s' is the root directory: %m", arg_root);
+ if (r > 0)
+ return 0;
+
+ c->rfd = open(empty_to_root(arg_root), O_CLOEXEC | O_DIRECTORY | O_PATH);
+ if (c->rfd < 0)
+ return log_error_errno(errno, "Failed to open root directory '%s': %m", empty_to_root(arg_root));
+
+ return 0;
+}
+
+static const char* context_get_layout(const Context *c) {
+ assert(c);
+ assert(c->layout >= 0);
+
+ return c->layout_other ?: layout_to_string(c->layout);
+}
+
+static int context_set_layout(Context *c, const char *s, const char *source) {
+ Layout t;
+
+ assert(c);
+ assert(source);
+
+ if (c->layout >= 0 || !s)
+ return 0;
+
+ assert(!c->layout_other);
+
+ t = layout_from_string(s);
+ if (t >= 0)
+ c->layout = t;
+ else if (isempty(s))
+ c->layout = LAYOUT_AUTO;
+ else {
+ c->layout_other = strdup(s);
+ if (!c->layout_other)
+ return log_oom();
+
+ c->layout = LAYOUT_OTHER;
+ }
+
+ log_debug("layout=%s set via %s", context_get_layout(c), source);
+ return 1;
+}
+
+static int context_set_machine_id(Context *c, const char *s, const char *source) {
+ int r;
+
+ assert(c);
+ assert(source);
+
+ if (!sd_id128_is_null(c->machine_id) || !s)
+ return 0;
+
+ r = sd_id128_from_string(s, &c->machine_id);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to parse machine ID specified via %s, ignoring.", source);
+
+ if (sd_id128_is_null(c->machine_id))
+ return 0;
+
+ log_debug("MACHINE_ID=%s set via %s.", SD_ID128_TO_STRING(c->machine_id), source);
+ return 1;
+}
+
+static int context_set_string(const char *s, const char *source, const char *name, char **dest) {
+ char *p;
+
+ assert(source);
+ assert(name);
+ assert(dest);
+
+ if (*dest || !s)
+ return 0;
+
+ p = strdup(s);
+ if (!p)
+ return log_oom();
+
+ log_debug("%s (%s) set via %s.", name, p, source);
+
+ *dest = p;
+ return 1;
+}
+
+static int context_set_initrd_generator(Context *c, const char *s, const char *source) {
+ assert(c);
+ return context_set_string(s, source, "INITRD_GENERATOR", &c->initrd_generator);
+}
+
+static int context_set_uki_generator(Context *c, const char *s, const char *source) {
+ assert(c);
+ return context_set_string(s, source, "UKI_GENERATOR", &c->uki_generator);
+}
+
+static int context_set_version(Context *c, const char *s) {
+ assert(c);
+
+ if (s && !filename_is_valid(s))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid version specified: %s", s);
+
+ return context_set_string(s, "command line", "kernel version", &c->version);
+}
+
+static int context_set_path(Context *c, const char *s, const char *source, const char *name, char **dest) {
+ char *p;
+ int r;
+
+ assert(c);
+ assert(source);
+ assert(name);
+ assert(dest);
+
+ if (*dest || !s)
+ return 0;
+
+ if (c->rfd >= 0) {
+ r = chaseat(c->rfd, s, CHASE_AT_RESOLVE_IN_ROOT, &p, /* ret_fd = */ NULL);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to chase path %s for %s specified via %s, ignoring: %m",
+ s, name, source);
+ } else {
+ r = path_make_absolute_cwd(s, &p);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to make path '%s' for %s specified via %s absolute, ignoring: %m",
+ s, name, source);
+ }
+
+ log_debug("%s (%s) set via %s.", name, p, source);
+
+ *dest = p;
+ return 1;
+}
+
+static int context_set_boot_root(Context *c, const char *s, const char *source) {
+ assert(c);
+ return context_set_path(c, s, source, "BOOT_ROOT", &c->boot_root);
+}
+
+static int context_set_conf_root(Context *c, const char *s, const char *source) {
+ assert(c);
+ return context_set_path(c, s, source, "CONF_ROOT", &c->conf_root);
+}
+
+static int context_set_kernel(Context *c, const char *s) {
+ assert(c);
+ return context_set_path(c, s, "command line", "kernel image file", &c->kernel);
+}
+
+static int context_set_path_strv(Context *c, char* const* strv, const char *source, const char *name, char ***dest) {
+ _cleanup_strv_free_ char **w = NULL;
+ int r;
+
+ assert(c);
+ assert(source);
+ assert(name);
+ assert(dest);
+
+ if (*dest)
+ return 0;
+
+ STRV_FOREACH(s, strv) {
+ char *p;
+
+ if (c->rfd >= 0) {
+ r = chaseat(c->rfd, *s, CHASE_AT_RESOLVE_IN_ROOT, &p, /* ret_fd = */ NULL);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to chase path %s for %s specified via %s: %m",
+ *s, name, source);
+ } else {
+ r = path_make_absolute_cwd(*s, &p);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to make path '%s' for %s specified via %s absolute, ignoring: %m",
+ *s, name, source);
+ }
+ r = strv_consume(&w, p);
+ if (r < 0)
+ return log_oom();
+ }
+
+ if (strv_isempty(w))
+ return 0;
+
+ log_debug("%s set via %s", name, source);
+
+ *dest = TAKE_PTR(w);
+ return 1;
+}
+
+static int context_set_plugins(Context *c, const char *s, const char *source) {
+ _cleanup_strv_free_ char **v = NULL;
+
+ assert(c);
+
+ if (c->plugins || !s)
+ return 0;
+
+ v = strv_split(s, NULL);
+ if (!v)
+ return log_oom();
+
+ return context_set_path_strv(c, v, source, "plugins", &c->plugins);
+}
+
+static int context_set_initrds(Context *c, char* const* strv) {
+ assert(c);
+ return context_set_path_strv(c, strv, "command line", "initrds", &c->initrds);
+}
+
+static int context_load_environment(Context *c) {
+ assert(c);
+
+ (void) context_set_machine_id(c, getenv("MACHINE_ID"), "environment");
+ (void) context_set_boot_root(c, getenv("BOOT_ROOT"), "environment");
+ (void) context_set_conf_root(c, getenv("KERNEL_INSTALL_CONF_ROOT"), "environment");
+ (void) context_set_plugins(c, getenv("KERNEL_INSTALL_PLUGINS"), "environment");
+ return 0;
+}
+
+static int context_ensure_conf_root(Context *c) {
+ int r;
+
+ assert(c);
+
+ if (c->conf_root)
+ return 0;
+
+ r = chaseat(c->rfd, "/etc/kernel", CHASE_AT_RESOLVE_IN_ROOT, &c->conf_root, /* ret_fd = */ NULL);
+ if (r < 0)
+ log_debug_errno(r, "Failed to chase /etc/kernel, ignoring: %m");
+
+ return 0;
+}
+
+static int context_load_install_conf_one(Context *c, const char *path) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char
+ *conf = NULL, *machine_id = NULL, *boot_root = NULL, *layout = NULL,
+ *initrd_generator = NULL, *uki_generator = NULL;
+ int r;
+
+ assert(c);
+ assert(path);
+
+ conf = path_join(path, "install.conf");
+ if (!conf)
+ return log_oom();
+
+ r = chase_and_fopenat_unlocked(c->rfd, conf, CHASE_AT_RESOLVE_IN_ROOT, "re", NULL, &f);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to chase %s: %m", conf);
+
+ log_debug("Loading %s…", conf);
+
+ r = parse_env_file(f, conf,
+ "MACHINE_ID", &machine_id,
+ "BOOT_ROOT", &boot_root,
+ "layout", &layout,
+ "initrd_generator", &initrd_generator,
+ "uki_generator", &uki_generator);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse '%s': %m", conf);
+
+ (void) context_set_machine_id(c, machine_id, conf);
+ (void) context_set_boot_root(c, boot_root, conf);
+ (void) context_set_layout(c, layout, conf);
+ (void) context_set_initrd_generator(c, initrd_generator, conf);
+ (void) context_set_uki_generator(c, uki_generator, conf);
+
+ log_debug("Loaded %s.", conf);
+ return 1;
+}
+
+static int context_load_install_conf(Context *c) {
+ int r;
+
+ assert(c);
+
+ if (c->conf_root) {
+ r = context_load_install_conf_one(c, c->conf_root);
+ if (r != 0)
+ return r;
+ }
+
+ STRV_FOREACH(p, STRV_MAKE("/etc/kernel", "/usr/lib/kernel")) {
+ r = context_load_install_conf_one(c, *p);
+ if (r != 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int context_load_machine_info(Context *c) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *machine_id = NULL, *layout = NULL;
+ static const char *path = "/etc/machine-info";
+ int r;
+
+ assert(c);
+
+ /* If the user configured an explicit machine ID in /etc/machine-info to use for our purpose, we'll
+ * use that instead (for compatibility). */
+
+ if (!sd_id128_is_null(c->machine_id) && c->layout >= 0)
+ return 0;
+
+ /* For testing. To make not read host's /etc/machine-info. */
+ r = getenv_bool("KERNEL_INSTALL_READ_MACHINE_INFO");
+ if (r < 0 && r != -ENXIO)
+ log_warning_errno(r, "Failed to read $KERNEL_INSTALL_READ_MACHINE_INFO, assuming yes: %m");
+ if (r == 0) {
+ log_debug("Skipping to read /etc/machine-info.");
+ return 0;
+ }
+
+ r = chase_and_fopenat_unlocked(c->rfd, path, CHASE_AT_RESOLVE_IN_ROOT, "re", NULL, &f);
+ if (r == -ENOENT)
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to chase %s: %m", path);
+
+ log_debug("Loading %s…", path);
+
+ r = parse_env_file(f, path,
+ "KERNEL_INSTALL_MACHINE_ID", &machine_id,
+ "KERNEL_INSTALL_LAYOUT", &layout);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse '%s': %m", path);
+
+ (void) context_set_machine_id(c, machine_id, path);
+ (void) context_set_layout(c, layout, path);
+ return 0;
+}
+
+static int context_load_machine_id(Context *c) {
+ int r;
+
+ assert(c);
+
+ r = id128_get_machine_at(c->rfd, &c->machine_id);
+ if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r))
+ return 0;
+ if (r < 0)
+ return log_error_errno(r, "Failed to load machine ID from /etc/machine-id: %m");
+
+ log_debug("MACHINE_ID=%s set via /etc/machine-id.", SD_ID128_TO_STRING(c->machine_id));
+ return 1; /* loaded */
+}
+
+static int context_ensure_machine_id(Context *c) {
+ int r;
+
+ assert(c);
+
+ if (!sd_id128_is_null(c->machine_id))
+ return 0;
+
+ /* If /etc/machine-id is initialized we'll use it. */
+ r = context_load_machine_id(c);
+ if (r != 0)
+ return r;
+
+ /* Otherwise we'll use a freshly generated one. */
+ r = sd_id128_randomize(&c->machine_id);
+ if (r < 0)
+ return log_error_errno(r, "Failed to generate random ID: %m");
+
+ c->machine_id_is_random = true;
+ log_debug("New machine ID '%s' generated.", SD_ID128_TO_STRING(c->machine_id));
+ return 0;
+}
+
+static int context_acquire_xbootldr(Context *c) {
+ int r;
+
+ assert(c);
+ assert(!c->boot_root);
+
+ r = find_xbootldr_and_warn_at(
+ /* rfd = */ c->rfd,
+ /* path = */ arg_xbootldr_path,
+ /* unprivileged_mode= */ -1,
+ /* ret_path = */ &c->boot_root,
+ /* ret_uuid = */ NULL,
+ /* ret_devid = */ NULL);
+ if (r == -ENOKEY) {
+ log_debug_errno(r, "Couldn't find an XBOOTLDR partition.");
+ return 0;
+ }
+ if (r == -EACCES && geteuid() != 0)
+ return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m");
+ if (r < 0)
+ return r;
+
+ log_debug("Using XBOOTLDR partition at %s as $BOOT_ROOT.", c->boot_root);
+ return 1; /* found */
+}
+
+static int context_acquire_esp(Context *c) {
+ int r;
+
+ assert(c);
+ assert(!c->boot_root);
+
+ r = find_esp_and_warn_at(
+ /* rfd = */ c->rfd,
+ /* path = */ arg_esp_path,
+ /* unprivileged_mode= */ -1,
+ /* ret_path = */ &c->boot_root,
+ /* ret_part = */ NULL,
+ /* ret_pstart = */ NULL,
+ /* ret_psize = */ NULL,
+ /* ret_uuid = */ NULL,
+ /* ret_devid = */ NULL);
+ if (r == -ENOKEY) {
+ log_debug_errno(r, "Couldn't find EFI system partition, ignoring.");
+ return 0;
+ }
+ if (r == -EACCES && geteuid() != 0)
+ return log_error_errno(r, "Failed to determine EFI system partition: %m");
+ if (r < 0)
+ return r;
+
+ log_debug("Using EFI System Partition at %s as $BOOT_ROOT.", c->boot_root);
+ return 1; /* found */
+}
+
+static int context_ensure_boot_root(Context *c) {
+ int r;
+
+ assert(c);
+
+ /* If BOOT_ROOT is specified via environment or install.conf, then use it. */
+ if (c->boot_root)
+ return 0;
+
+ /* Otherwise, use XBOOTLDR partition, if mounted. */
+ r = context_acquire_xbootldr(c);
+ if (r != 0)
+ return r;
+
+ /* Otherwise, use EFI system partition, if mounted. */
+ r = context_acquire_esp(c);
+ if (r != 0)
+ return r;
+
+ /* If all else fails, use /boot. */
+ if (c->rfd >= 0) {
+ r = chaseat(c->rfd, "/boot", CHASE_AT_RESOLVE_IN_ROOT, &c->boot_root, /* ret_fd = */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to chase '/boot': %m");
+ } else {
+ c->boot_root = strdup("/boot");
+ if (!c->boot_root)
+ return log_oom();
+ }
+
+ log_debug("KERNEL_INSTALL_BOOT_ROOT autodetection yielded no candidates, using \"%s\".", c->boot_root);
+ return 0;
+}
+
+static int context_ensure_entry_token(Context *c) {
+ int r;
+
+ assert(c);
+
+ /* Now that we determined the machine ID to use, let's determine the "token" for the boot loader
+ * entry to generate. We use that for naming the directory below $BOOT where we want to place the
+ * kernel/initrd and related resources, as well for naming the .conf boot loader spec entry.
+ * Typically this is just the machine ID, but it can be anything else, too, if we are told so. */
+
+ r = boot_entry_token_ensure_at(
+ c->rfd,
+ c->conf_root,
+ c->machine_id,
+ c->machine_id_is_random,
+ &c->entry_token_type,
+ &c->entry_token);
+ if (r < 0)
+ return r;
+
+ log_debug("Using entry token: %s", c->entry_token);
+ return 0;
+}
+
+static int context_load_plugins(Context *c) {
+ int r;
+
+ assert(c);
+
+ if (c->plugins)
+ return 0;
+
+ r = conf_files_list_strv_at(
+ &c->plugins,
+ ".install",
+ c->rfd,
+ CONF_FILES_EXECUTABLE | CONF_FILES_REGULAR | CONF_FILES_FILTER_MASKED,
+ STRV_MAKE_CONST("/etc/kernel/install.d", "/usr/lib/kernel/install.d"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to find plugins: %m");
+
+ return 0;
+}
+
+static int context_init(Context *c) {
+ int r;
+
+ assert(c);
+
+ r = context_open_root(c);
+ if (r < 0)
+ return r;
+
+ r = context_load_environment(c);
+ if (r < 0)
+ return r;
+
+ r = context_ensure_conf_root(c);
+ if (r < 0)
+ return r;
+
+ r = context_load_install_conf(c);
+ if (r < 0)
+ return r;
+
+ r = context_load_machine_info(c);
+ if (r < 0)
+ return r;
+
+ r = context_ensure_machine_id(c);
+ if (r < 0)
+ return r;
+
+ r = context_ensure_boot_root(c);
+ if (r < 0)
+ return r;
+
+ r = context_ensure_entry_token(c);
+ if (r < 0)
+ return r;
+
+ r = context_load_plugins(c);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int context_inspect_kernel(Context *c) {
+ assert(c);
+
+ if (!c->kernel)
+ return 0;
+
+ return inspect_kernel(c->rfd, c->kernel, &c->kernel_image_type, NULL, NULL, NULL);
+}
+
+static int context_ensure_layout(Context *c) {
+ int r;
+
+ assert(c);
+ assert(c->boot_root);
+ assert(c->entry_token);
+
+ if (c->layout >= 0 && c->layout != LAYOUT_AUTO)
+ return 0;
+
+ /* No layout configured by the administrator. Let's try to figure it out automatically from metadata
+ * already contained in $BOOT_ROOT. */
+
+ if (c->kernel_image_type == KERNEL_IMAGE_TYPE_UKI) {
+ c->layout = LAYOUT_UKI;
+ log_debug("Kernel image type is %s, using layout=%s.",
+ kernel_image_type_to_string(c->kernel_image_type), layout_to_string(c->layout));
+ return 0;
+ }
+
+ _cleanup_free_ char *srel_path = path_join(c->boot_root, "loader/entries.srel");
+ if (!srel_path)
+ return log_oom();
+
+ _cleanup_free_ char *srel = NULL;
+ r = read_one_line_file_at(c->rfd, srel_path, &srel);
+ if (r >= 0) {
+ if (streq(srel, "type1"))
+ /* The loader/entries.srel file clearly indicates that the installed boot loader
+ * implements the proper standard upstream boot loader spec for Type #1 entries.
+ * Let's default to that, then. */
+ c->layout = LAYOUT_BLS;
+ else
+ /* The loader/entries.srel file indicates some other spec is implemented and owns the
+ * /loader/entries/ directory. Since we have no idea what that means, let's stay away
+ * from it by default. */
+ c->layout = LAYOUT_OTHER;
+
+ log_debug("%s with '%s' found, using layout=%s.", srel_path, srel, layout_to_string(c->layout));
+ return 0;
+ } else if (r != -ENOENT)
+ return log_error_errno(r, "Failed to read %s: %m", srel_path);
+
+ _cleanup_free_ char *entry_token_path = path_join(c->boot_root, c->entry_token);
+ if (!entry_token_path)
+ return log_oom();
+
+ r = is_dir_full(c->rfd, entry_token_path, /* follow = */ false);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to check if '%s' is a directory: %m", entry_token_path);
+ if (r > 0) {
+ /* If the metadata in $BOOT_ROOT doesn't tell us anything, then check if the entry token
+ * directory already exists. If so, let's assume it's the standard boot loader spec, too. */
+ c->layout = LAYOUT_BLS;
+ log_debug("%s exists, using layout=%s.", entry_token_path, layout_to_string(c->layout));
+ return 0;
+ }
+
+ /* There's no metadata in $BOOT_ROOT, and apparently no entry token directory installed? Then we
+ * really don't know anything. */
+ c->layout = LAYOUT_OTHER;
+ log_debug("Entry-token directory not found, using layout=%s.", layout_to_string(c->layout));
+ return 0;
+}
+
+static int context_set_up_staging_area(Context *c) {
+ static const char *template = "/tmp/kernel-install.staging.XXXXXX";
+ int r;
+
+ assert(c);
+
+ if (c->staging_area)
+ return 0;
+
+ if (c->action == ACTION_INSPECT) {
+ /* This is only used for display. The directory will not be created. */
+ c->staging_area = strdup(template);
+ if (!c->staging_area)
+ return log_oom();
+ } else {
+ r = mkdtemp_malloc(template, &c->staging_area);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create staging area: %m");
+ }
+
+ return 0;
+}
+
+static int context_build_entry_dir(Context *c) {
+ assert(c);
+ assert(c->boot_root);
+ assert(c->entry_token);
+ assert(c->version || c->action == ACTION_INSPECT);
+
+ if (c->entry_dir)
+ return 0;
+
+ c->entry_dir = path_join(c->boot_root, c->entry_token, c->version ?: "KERNEL_VERSION");
+ if (!c->entry_dir)
+ return log_oom();
+
+ log_debug("Using ENTRY_DIR=%s", c->entry_dir);
+ return 0;
+}
+
+static bool context_should_make_entry_dir(Context *c) {
+ assert(c);
+
+ /* Compatibility with earlier versions that used the presence of $BOOT_ROOT/$ENTRY_TOKEN to signal to
+ * 00-entry-directory to create $ENTRY_DIR to serve as the indication to use or to not use the BLS */
+
+ if (arg_make_entry_directory < 0)
+ return c->layout == LAYOUT_BLS;
+
+ return arg_make_entry_directory;
+}
+
+static int context_make_entry_dir(Context *c) {
+ _cleanup_close_ int fd = -EBADF;
+
+ assert(c);
+ assert(c->entry_dir);
+
+ if (c->action != ACTION_ADD)
+ return 0;
+
+ if (!context_should_make_entry_dir(c))
+ return 0;
+
+ log_debug("mkdir -p %s", c->entry_dir);
+ fd = chase_and_openat(c->rfd, c->entry_dir, CHASE_AT_RESOLVE_IN_ROOT | CHASE_MKDIR_0755,
+ O_CLOEXEC | O_CREAT | O_DIRECTORY | O_PATH, NULL);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to make directory '%s': %m", c->entry_dir);
+
+ return 0;
+}
+
+static int context_remove_entry_dir(Context *c) {
+ _cleanup_free_ char *p = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ struct stat st;
+ int r;
+
+ assert(c);
+ assert(c->entry_dir);
+
+ if (c->action != ACTION_REMOVE)
+ return 0;
+
+ if (!context_should_make_entry_dir(c))
+ return 0;
+
+ log_debug("rm -rf %s", c->entry_dir);
+ fd = chase_and_openat(c->rfd, c->entry_dir, CHASE_AT_RESOLVE_IN_ROOT, O_CLOEXEC | O_DIRECTORY, &p);
+ if (fd < 0) {
+ if (IN_SET(fd, -ENOTDIR, -ENOENT))
+ return 0;
+ return log_debug_errno(fd, "Failed to chase and open %s, ignoring: %m", c->entry_dir);
+ }
+
+ if (fstat(fd, &st) < 0)
+ return log_debug_errno(errno, "Failed to stat %s: %m", p);
+
+ r = rm_rf_children(TAKE_FD(fd), REMOVE_PHYSICAL|REMOVE_MISSING_OK|REMOVE_CHMOD, &st);
+ if (r < 0)
+ log_debug_errno(r, "Failed to remove children of %s, ignoring: %m", p);
+
+ if (unlinkat(c->rfd, p, AT_REMOVEDIR) < 0)
+ log_debug_errno(errno, "Failed to remove %s, ignoring: %m", p);
+
+ return 0;
+}
+
+static int context_build_arguments(Context *c) {
+ _cleanup_strv_free_ char **a = NULL;
+ const char *verb;
+ int r;
+
+ assert(c);
+ assert(c->entry_dir);
+
+ if (c->argv)
+ return 0;
+
+ switch (c->action) {
+ case ACTION_ADD:
+ assert(c->version);
+ assert(c->kernel);
+ verb = "add";
+ break;
+
+ case ACTION_REMOVE:
+ assert(c->version);
+ assert(!c->kernel);
+ assert(!c->initrds);
+ verb = "remove";
+ break;
+
+ case ACTION_INSPECT:
+ verb = "add|remove";
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ a = strv_new("dummy-arg", /* to make strv_free() works for this variable. */
+ verb,
+ c->version ?: "KERNEL_VERSION",
+ c->entry_dir);
+ if (!a)
+ return log_oom();
+
+ if (c->action == ACTION_ADD) {
+ r = strv_extend(&a, c->kernel);
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extend_strv(&a, c->initrds, /* filter_duplicates = */ false);
+ if (r < 0)
+ return log_oom();
+
+ } else if (c->action == ACTION_INSPECT) {
+ r = strv_extend(&a, c->kernel ?: "[KERNEL_IMAGE]");
+ if (r < 0)
+ return log_oom();
+
+ r = strv_extend(&a, "[INITRD...]");
+ if (r < 0)
+ return log_oom();
+ }
+
+ c->argv = TAKE_PTR(a);
+ return 0;
+}
+
+static int context_build_environment(Context *c) {
+ _cleanup_strv_free_ char **e = NULL;
+ int r;
+
+ assert(c);
+
+ if (c->envp)
+ return 0;
+
+ r = strv_env_assign_many(&e,
+ "LC_COLLATE", SYSTEMD_DEFAULT_LOCALE,
+ "KERNEL_INSTALL_VERBOSE", one_zero(arg_verbose),
+ "KERNEL_INSTALL_IMAGE_TYPE", kernel_image_type_to_string(c->kernel_image_type),
+ "KERNEL_INSTALL_MACHINE_ID", SD_ID128_TO_STRING(c->machine_id),
+ "KERNEL_INSTALL_ENTRY_TOKEN", c->entry_token,
+ "KERNEL_INSTALL_BOOT_ROOT", c->boot_root,
+ "KERNEL_INSTALL_LAYOUT", context_get_layout(c),
+ "KERNEL_INSTALL_INITRD_GENERATOR", strempty(c->initrd_generator),
+ "KERNEL_INSTALL_UKI_GENERATOR", strempty(c->uki_generator),
+ "KERNEL_INSTALL_STAGING_AREA", c->staging_area);
+ if (r < 0)
+ return log_error_errno(r, "Failed to build environment variables for plugins: %m");
+
+ c->envp = TAKE_PTR(e);
+ return 0;
+}
+
+static int context_prepare_execution(Context *c) {
+ int r;
+
+ assert(c);
+
+ r = context_inspect_kernel(c);
+ if (r < 0)
+ return r;
+
+ r = context_ensure_layout(c);
+ if (r < 0)
+ return r;
+
+ r = context_set_up_staging_area(c);
+ if (r < 0)
+ return r;
+
+ r = context_build_entry_dir(c);
+ if (r < 0)
+ return r;
+
+ r = context_build_arguments(c);
+ if (r < 0)
+ return r;
+
+ r = context_build_environment(c);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int context_execute(Context *c) {
+ int r, ret;
+
+ assert(c);
+
+ r = context_make_entry_dir(c);
+ if (r < 0)
+ return r;
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *x = strv_join_full(c->plugins, "", "\n ", /* escape_separator = */ false);
+ log_debug("Using plugins: %s", strna(x));
+
+ _cleanup_free_ char *y = strv_join_full(c->envp, "", "\n ", /* escape_separator = */ false);
+ log_debug("Plugin environment: %s", strna(y));
+
+ _cleanup_free_ char *z = strv_join(strv_skip(c->argv, 1), " ");
+ log_debug("Plugin arguments: %s", strna(z));
+ }
+
+ ret = execute_strv(
+ /* name = */ NULL,
+ c->plugins,
+ /* root = */ NULL,
+ USEC_INFINITY,
+ /* callbacks = */ NULL,
+ /* callback_args = */ NULL,
+ c->argv,
+ c->envp,
+ EXEC_DIR_SKIP_REMAINING);
+
+ r = context_remove_entry_dir(c);
+ if (r < 0)
+ return r;
+
+ /* This returns 0 on success, positive exit code on plugin failure, negative errno on other failures. */
+ return ret;
+}
+
+static bool bypass(void) {
+ int r;
+
+ r = getenv_bool("KERNEL_INSTALL_BYPASS");
+ if (r < 0 && r != -ENXIO)
+ log_debug_errno(r, "Failed to parse $KERNEL_INSTALL_BYPASS, assuming no.");
+ if (r <= 0)
+ return false;
+
+ log_debug("$KERNEL_INSTALL_BYPASS is enabled, skipping execution.");
+ return true;
+}
+
+static int do_add(
+ Context *c,
+ const char *version,
+ const char *kernel,
+ char **initrds) {
+
+ int r;
+
+ assert(c);
+ assert(version);
+ assert(kernel);
+
+ r = context_set_version(c, version);
+ if (r < 0)
+ return r;
+
+ r = context_set_kernel(c, kernel);
+ if (r < 0)
+ return r;
+
+ r = context_set_initrds(c, initrds);
+ if (r < 0)
+ return r;
+
+ r = context_prepare_execution(c);
+ if (r < 0)
+ return r;
+
+ return context_execute(c);
+}
+
+static int kernel_from_version(const char *version, char **ret_kernel) {
+ _cleanup_free_ char *vmlinuz = NULL;
+ int r;
+
+ assert(version);
+
+ vmlinuz = path_join("/usr/lib/modules/", version, "/vmlinuz");
+ if (!vmlinuz)
+ return log_oom();
+
+ r = laccess(vmlinuz, F_OK);
+ if (r < 0) {
+ if (r == -ENOENT)
+ return log_error_errno(r, "Kernel image not installed to '%s', requiring manual kernel image path specification.", vmlinuz);
+
+ return log_error_errno(r, "Failed to determine if kernel image is installed to '%s': %m", vmlinuz);
+ }
+
+ *ret_kernel = TAKE_PTR(vmlinuz);
+ return 0;
+}
+
+static int verb_add(int argc, char *argv[], void *userdata) {
+ Context *c = ASSERT_PTR(userdata);
+ _cleanup_free_ char *vmlinuz = NULL;
+ const char *version, *kernel;
+ char **initrds;
+ struct utsname un;
+ int r;
+
+ assert(argv);
+
+ if (arg_root)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "'add' does not support --root=.");
+
+ if (bypass())
+ return 0;
+
+ c->action = ACTION_ADD;
+
+ /* We use the same order of arguments that "inspect" introduced, i.e. if only on argument is
+ * specified we take it as the kernel path, not the version, i.e. it's the first argument that is
+ * optional, not the 2nd. */
+ version = argc > 2 ? empty_or_dash_to_null(argv[1]) : NULL;
+ kernel = argc > 2 ? empty_or_dash_to_null(argv[2]) :
+ (argc > 1 ? empty_or_dash_to_null(argv[1]) : NULL);
+ initrds = strv_skip(argv, 3);
+
+ if (!version) {
+ assert_se(uname(&un) >= 0);
+ version = un.release;
+ }
+
+ if (!kernel) {
+ r = kernel_from_version(version, &vmlinuz);
+ if (r < 0)
+ return r;
+
+ kernel = vmlinuz;
+ }
+
+ return do_add(c, version, kernel, initrds);
+}
+
+static int verb_add_all(int argc, char *argv[], void *userdata) {
+ Context *c = ASSERT_PTR(userdata);
+ _cleanup_close_ int fd = -EBADF;
+ size_t n = 0;
+ int ret = 0, r;
+
+ assert(argv);
+
+ if (bypass())
+ return 0;
+
+ c->action = ACTION_ADD;
+
+ fd = open("/usr/lib/modules", O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to open /usr/lib/modules/: %m");
+
+ _cleanup_free_ DirectoryEntries *de = NULL;
+ r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de);
+ if (r < 0)
+ return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m");
+
+ FOREACH_ARRAY(d, de->entries, de->n_entries) {
+
+ _cleanup_free_ char *j = path_join("/usr/lib/modules/", (*d)->d_name);
+ if (!j)
+ return log_oom();
+
+ r = dirent_ensure_type(fd, *d);
+ if (r < 0) {
+ if (r != -ENOENT) /* don't log if just gone by now */
+ log_debug_errno(r, "Failed to check if '%s' is a directory, ignoring: %m", j);
+ continue;
+ }
+
+ if ((*d)->d_type != DT_DIR)
+ continue;
+
+ _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz");
+ if (!fn)
+ return log_oom();
+
+ if (faccessat(fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) {
+ if (errno != ENOENT)
+ log_debug_errno(errno, "Failed to check if '/usr/lib/modules/%s/vmlinuz' exists, ignoring: %m", (*d)->d_name);
+
+ log_notice("Not adding version '%s', because kernel image not found.", (*d)->d_name);
+ continue;
+ }
+
+ _cleanup_(context_done) Context copy = CONTEXT_NULL;
+
+ r = context_copy(c, &copy);
+ if (r < 0)
+ return log_error_errno(r, "Failed to copy execution context: %m");
+
+ _cleanup_free_ char *full = path_join("/usr/lib/modules/", fn);
+ if (!full)
+ return log_oom();
+
+ r = do_add(&copy,
+ /* version= */ (*d)->d_name,
+ /* kernel= */ full,
+ /* initrds= */ NULL);
+ if (r == 0)
+ n++;
+ else if (ret == 0)
+ ret = r;
+ }
+
+ if (n > 0)
+ log_debug("Installed %zu kernel(s).", n);
+ else if (ret == 0)
+ ret = log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No kernels to install found.");
+
+ return ret;
+}
+
+static int run_as_installkernel(int argc, char *argv[], Context *c) {
+ /* kernel's install.sh invokes us as
+ * /sbin/installkernel <version> <vmlinuz> <map> <installation-dir>
+ * We ignore the last two arguments. */
+ if (optind + 2 > argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "'installkernel' command requires at least two arguments.");
+
+ return verb_add(3, STRV_MAKE("add", argv[optind], argv[optind+1]), c);
+}
+
+static int verb_remove(int argc, char *argv[], void *userdata) {
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ assert(argc >= 2);
+ assert(argv);
+
+ if (arg_root)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "'remove' does not support --root=.");
+
+ if (argc > 2)
+ log_debug("Too many arguments specified. 'kernel-install remove' takes only kernel version. "
+ "Ignoring residual arguments.");
+
+ if (bypass())
+ return 0;
+
+ c->action = ACTION_REMOVE;
+
+ /* Note, we do not automatically derive the kernel version to remove from uname() here (unlike we do
+ * it for the "add" verb), since we don't want to make it too easy to uninstall your running
+ * kernel, as a safety precaution */
+
+ r = context_set_version(c, argv[1]);
+ if (r < 0)
+ return r;
+
+ r = context_prepare_execution(c);
+ if (r < 0)
+ return r;
+
+ return context_execute(c);
+}
+
+static int verb_inspect(int argc, char *argv[], void *userdata) {
+ Context *c = ASSERT_PTR(userdata);
+ _cleanup_(table_unrefp) Table *t = NULL;
+ _cleanup_free_ char *vmlinuz = NULL;
+ const char *version, *kernel;
+ char **initrds;
+ struct utsname un;
+ int r;
+
+ c->action = ACTION_INSPECT;
+
+ /* When only a single parameter is specified 'inspect' it's the kernel image path, and not the kernel
+ * version. i.e. it's the first argument that is optional, not the 2nd. That's a bit unfortunate, but
+ * we keep the behaviour for compatibility. If users want to specify only the version (and have the
+ * kernel image path derived automatically), then they may specify an empty string or "dash" as
+ * kernel image path. */
+ version = argc > 2 ? empty_or_dash_to_null(argv[1]) : NULL;
+ kernel = argc > 2 ? empty_or_dash_to_null(argv[2]) :
+ (argc > 1 ? empty_or_dash_to_null(argv[1]) : NULL);
+ initrds = strv_skip(argv, 3);
+
+ if (!version && !arg_root) {
+ assert_se(uname(&un) >= 0);
+ version = un.release;
+ }
+
+ if (!kernel && version) {
+ r = kernel_from_version(version, &vmlinuz);
+ if (r < 0)
+ return r;
+
+ kernel = vmlinuz;
+ }
+
+ r = context_set_version(c, version);
+ if (r < 0)
+ return r;
+
+ r = context_set_kernel(c, kernel);
+ if (r < 0)
+ return r;
+
+ r = context_set_initrds(c, initrds);
+ if (r < 0)
+ return r;
+
+ r = context_prepare_execution(c);
+ if (r < 0)
+ return r;
+
+ t = table_new_vertical();
+ if (!t)
+ return log_oom();
+
+ r = table_add_many(t,
+ TABLE_FIELD, "Machine ID",
+ TABLE_ID128, c->machine_id,
+ TABLE_FIELD, "Kernel Image Type",
+ TABLE_STRING, kernel_image_type_to_string(c->kernel_image_type),
+ TABLE_FIELD, "Layout",
+ TABLE_STRING, context_get_layout(c),
+ TABLE_FIELD, "Boot Root",
+ TABLE_STRING, c->boot_root,
+ TABLE_FIELD, "Entry Token Type",
+ TABLE_STRING, boot_entry_token_type_to_string(c->entry_token_type),
+ TABLE_FIELD, "Entry Token",
+ TABLE_STRING, c->entry_token,
+ TABLE_FIELD, "Entry Directory",
+ TABLE_STRING, c->entry_dir,
+ TABLE_FIELD, "Kernel Version",
+ TABLE_STRING, c->version,
+ TABLE_FIELD, "Kernel",
+ TABLE_STRING, c->kernel,
+ TABLE_FIELD, "Initrds",
+ TABLE_STRV, c->initrds,
+ TABLE_FIELD, "Initrd Generator",
+ TABLE_STRING, c->initrd_generator,
+ TABLE_FIELD, "UKI Generator",
+ TABLE_STRING, c->uki_generator,
+ TABLE_FIELD, "Plugins",
+ TABLE_STRV, c->plugins,
+ TABLE_FIELD, "Plugin Environment",
+ TABLE_STRV, c->envp);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (arg_json_format_flags & JSON_FORMAT_OFF) {
+ r = table_add_many(t,
+ TABLE_FIELD, "Plugin Arguments",
+ TABLE_STRV, strv_skip(c->argv, 1));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ table_set_ersatz_string(t, TABLE_ERSATZ_UNSET);
+
+ for (size_t row = 1; row < table_get_rows(t); row++) {
+ _cleanup_free_ char *name = NULL;
+
+ name = strdup(table_get_at(t, row, 0));
+ if (!name)
+ return log_oom();
+
+ r = table_set_json_field_name(t, row - 1, delete_chars(name, " "));
+ if (r < 0)
+ return log_error_errno(r, "Failed to set JSON field name: %m");
+ }
+
+ return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, /* show_header= */ false);
+}
+
+static int verb_list(int argc, char *argv[], void *userdata) {
+ _cleanup_close_ int fd = -EBADF;
+ int r;
+
+ fd = open("/usr/lib/modules", O_DIRECTORY|O_RDONLY|O_CLOEXEC);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to open /usr/lib/modules/: %m");
+
+ _cleanup_free_ DirectoryEntries *de = NULL;
+ r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de);
+ if (r < 0)
+ return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m");
+
+ _cleanup_(table_unrefp) Table *table = NULL;
+ table = table_new("version", "has kernel", "path");
+ if (!table)
+ return log_oom();
+
+ table_set_ersatz_string(table, TABLE_ERSATZ_DASH);
+ table_set_align_percent(table, table_get_cell(table, 0, 1), 100);
+
+ FOREACH_ARRAY(d, de->entries, de->n_entries) {
+
+ _cleanup_free_ char *j = path_join("/usr/lib/modules/", (*d)->d_name);
+ if (!j)
+ return log_oom();
+
+ r = dirent_ensure_type(fd, *d);
+ if (r < 0) {
+ if (r != -ENOENT) /* don't log if just gone by now */
+ log_debug_errno(r, "Failed to check if '%s' is a directory, ignoring: %m", j);
+ continue;
+ }
+
+ if ((*d)->d_type != DT_DIR)
+ continue;
+
+ _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz");
+ if (!fn)
+ return log_oom();
+
+ bool exists;
+ if (faccessat(fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) {
+ if (errno != ENOENT)
+ log_debug_errno(errno, "Failed to check if '/usr/lib/modules/%s/vmlinuz' exists, ignoring: %m", (*d)->d_name);
+
+ exists = false;
+ } else
+ exists = true;
+
+ r = table_add_many(table,
+ TABLE_STRING, (*d)->d_name,
+ TABLE_BOOLEAN_CHECKMARK, exists,
+ TABLE_SET_COLOR, ansi_highlight_green_red(exists),
+ TABLE_PATH, j);
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+
+ return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend);
+}
+
+static int help(void) {
+ _cleanup_free_ char *link = NULL;
+ int r;
+
+ r = terminal_urlify_man("kernel-install", "8", &link);
+ if (r < 0)
+ return log_oom();
+
+ printf("%1$s [OPTIONS...] COMMAND ...\n\n"
+ "%5$sAdd and remove kernel and initrd images to and from /boot/%6$s\n"
+ "\n%3$sUsage:%4$s\n"
+ " kernel-install [OPTIONS...] add [[[KERNEL-VERSION] KERNEL-IMAGE] [INITRD ...]]\n"
+ " kernel-install [OPTIONS...] add-all\n"
+ " kernel-install [OPTIONS...] remove KERNEL-VERSION\n"
+ " kernel-install [OPTIONS...] inspect [[[KERNEL-VERSION] KERNEL-IMAGE]\n"
+ " [INITRD ...]]\n"
+ " kernel-install [OPTIONS...] list\n"
+ "\n%3$sOptions:%4$s\n"
+ " -h --help Show this help\n"
+ " --version Show package version\n"
+ " -v --verbose Increase verbosity\n"
+ " --esp-path=PATH Path to the EFI System Partition (ESP)\n"
+ " --boot-path=PATH Path to the $BOOT partition\n"
+ " --make-entry-directory=yes|no|auto\n"
+ " Create $BOOT/ENTRY-TOKEN/ directory\n"
+ " --entry-token=machine-id|os-id|os-image-id|auto|literal:…\n"
+ " Entry token to use for this installation\n"
+ " --no-pager Do not pipe inspect output into a pager\n"
+ " --json=pretty|short|off Generate JSON output\n"
+ " --no-legend Do not show the headers and footers\n"
+ " --root=PATH Operate on an alternate filesystem root\n"
+ " --image=PATH Operate on disk image as filesystem root\n"
+ " --image-policy=POLICY Specify disk image dissection policy\n"
+ "\n"
+ "This program may also be invoked as 'installkernel':\n"
+ " installkernel [OPTIONS...] VERSION VMLINUZ [MAP] [INSTALLATION-DIR]\n"
+ "(The optional arguments are passed by kernel build system, but ignored.)\n"
+ "\n"
+ "See the %2$s for details.\n",
+ program_invocation_short_name,
+ link,
+ ansi_underline(),
+ ansi_normal(),
+ ansi_highlight(),
+ ansi_normal());
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[], Context *c) {
+ enum {
+ ARG_VERSION = 0x100,
+ ARG_NO_LEGEND,
+ ARG_ESP_PATH,
+ ARG_BOOT_PATH,
+ ARG_MAKE_ENTRY_DIRECTORY,
+ ARG_ENTRY_TOKEN,
+ ARG_NO_PAGER,
+ ARG_JSON,
+ ARG_ROOT,
+ ARG_IMAGE,
+ ARG_IMAGE_POLICY,
+ };
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, ARG_VERSION },
+ { "verbose", no_argument, NULL, 'v' },
+ { "esp-path", required_argument, NULL, ARG_ESP_PATH },
+ { "boot-path", required_argument, NULL, ARG_BOOT_PATH },
+ { "make-entry-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY },
+ { "entry-token", required_argument, NULL, ARG_ENTRY_TOKEN },
+ { "no-pager", no_argument, NULL, ARG_NO_PAGER },
+ { "json", required_argument, NULL, ARG_JSON },
+ { "root", required_argument, NULL, ARG_ROOT },
+ { "image", required_argument, NULL, ARG_IMAGE },
+ { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY },
+ { "no-legend", no_argument, NULL, ARG_NO_LEGEND },
+ {}
+ };
+ int t, r;
+
+ assert(argc >= 0);
+ assert(argv);
+ assert(c);
+
+ while ((t = getopt_long(argc, argv, "hv", options, NULL)) >= 0)
+ switch (t) {
+ case 'h':
+ return help();
+
+ case ARG_VERSION:
+ return version();
+
+ case ARG_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ case 'v':
+ log_set_max_level(LOG_DEBUG);
+ arg_verbose = true;
+ break;
+
+ case ARG_ESP_PATH:
+ r = parse_path_argument(optarg, /* suppress_root = */ false, &arg_esp_path);
+ if (r < 0)
+ return log_oom();
+ break;
+
+ case ARG_BOOT_PATH:
+ r = parse_path_argument(optarg, /* suppress_root = */ false, &arg_xbootldr_path);
+ if (r < 0)
+ return log_oom();
+ break;
+
+ case ARG_MAKE_ENTRY_DIRECTORY:
+ if (streq(optarg, "auto"))
+ arg_make_entry_directory = -1;
+ else {
+ r = parse_boolean_argument("--make-entry-directory=", optarg, NULL);
+ if (r < 0)
+ return r;
+
+ arg_make_entry_directory = r;
+ }
+ break;
+
+ case ARG_ENTRY_TOKEN:
+ r = parse_boot_entry_token_type(optarg, &c->entry_token_type, &c->entry_token);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case ARG_JSON:
+ r = parse_json_argument(optarg, &arg_json_format_flags);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_ROOT:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_IMAGE:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image);
+ if (r < 0)
+ return r;
+ break;
+
+ case ARG_IMAGE_POLICY:
+ r = parse_image_policy_argument(optarg, &arg_image_policy);
+ if (r < 0)
+ return r;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (arg_image && arg_root)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported.");
+
+ return 1;
+}
+
+static int run(int argc, char* argv[]) {
+ static const Verb verbs[] = {
+ { "add", 1, VERB_ANY, 0, verb_add },
+ { "add-all", 1, 1, 0, verb_add_all },
+ { "remove", 2, VERB_ANY, 0, verb_remove },
+ { "inspect", 1, VERB_ANY, VERB_DEFAULT, verb_inspect },
+ { "list", 1, 1, 0, verb_list },
+ {}
+ };
+ _cleanup_(context_done) Context c = {
+ .rfd = AT_FDCWD,
+ .action = _ACTION_INVALID,
+ .kernel_image_type = KERNEL_IMAGE_TYPE_UNKNOWN,
+ .layout = _LAYOUT_INVALID,
+ .entry_token_type = BOOT_ENTRY_TOKEN_AUTO,
+ };
+ _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
+ _cleanup_(umount_and_freep) char *mounted_dir = NULL;
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv, &c);
+ if (r <= 0)
+ return r;
+
+ if (arg_image) {
+ assert(!arg_root);
+
+ r = mount_image_privately_interactively(
+ arg_image,
+ arg_image_policy,
+ DISSECT_IMAGE_GENERIC_ROOT |
+ DISSECT_IMAGE_REQUIRE_ROOT |
+ DISSECT_IMAGE_RELAX_VAR_CHECK |
+ DISSECT_IMAGE_VALIDATE_OS,
+ &mounted_dir,
+ /* ret_dir_fd= */ NULL,
+ &loop_device);
+ if (r < 0)
+ return r;
+
+ arg_root = strdup(mounted_dir);
+ if (!arg_root)
+ return log_oom();
+ }
+
+ r = context_init(&c);
+ if (r < 0)
+ return r;
+
+ if (invoked_as(argv, "installkernel"))
+ return run_as_installkernel(argc, argv, &c);
+
+ return dispatch_verb(argc, argv, verbs, &c);
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/kernel-install/meson.build b/src/kernel-install/meson.build
new file mode 100644
index 0000000..7f61fcc
--- /dev/null
+++ b/src/kernel-install/meson.build
@@ -0,0 +1,51 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+want_kernel_install = conf.get('ENABLE_KERNEL_INSTALL') == 1
+
+executables += [
+ executable_template + {
+ 'name' : 'kernel-install',
+ 'public' : true,
+ 'conditions' : ['ENABLE_KERNEL_INSTALL'],
+ 'sources' : files('kernel-install.c'),
+ },
+]
+
+ukify_install = custom_target(
+ '60-ukify.install',
+ input : '60-ukify.install.in',
+ output : '60-ukify.install',
+ command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
+ install : want_kernel_install and want_ukify,
+ install_mode : 'rwxr-xr-x',
+ install_dir : kernelinstalldir)
+
+loaderentry_install = custom_target(
+ '90-loaderentry.install',
+ input : '90-loaderentry.install.in',
+ output : '90-loaderentry.install',
+ command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
+ install : want_kernel_install,
+ install_mode : 'rwxr-xr-x',
+ install_dir : kernelinstalldir)
+
+uki_copy_install = files('90-uki-copy.install')
+
+kernel_install_files = uki_copy_install + files(
+ '50-depmod.install',
+)
+
+if want_kernel_install
+ install_data(kernel_install_files,
+ install_mode : 'rwxr-xr-x',
+ install_dir : kernelinstalldir)
+
+ install_data('install.conf',
+ install_dir : kerneldir)
+
+ if install_sysconfdir
+ install_emptydir(sysconfdir / 'kernel/install.d')
+ endif
+endif
+
+test_kernel_install_sh = find_program('test-kernel-install.sh')
diff --git a/src/kernel-install/test-kernel-install.sh b/src/kernel-install/test-kernel-install.sh
new file mode 100755
index 0000000..338d811
--- /dev/null
+++ b/src/kernel-install/test-kernel-install.sh
@@ -0,0 +1,333 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# shellcheck disable=SC2235
+set -eux
+set -o pipefail
+
+export SYSTEMD_LOG_LEVEL=debug
+
+kernel_install="${1:?}"
+loaderentry_install="${2:?}"
+uki_copy_install="${3:?}"
+ukify="${4:-}"
+ukify_install="${5:-}"
+boot_stub="${6:-}"
+if [[ -d "${PROJECT_BUILD_ROOT:-}" ]]; then
+ bootctl="${PROJECT_BUILD_ROOT}/bootctl"
+else
+ bootctl=
+fi
+
+D="$(mktemp --tmpdir --directory "test-kernel-install.XXXXXXXXXX")"
+
+# shellcheck disable=SC2064
+trap "rm -rf '$D'" EXIT INT QUIT PIPE
+mkdir -p "$D/boot"
+mkdir -p "$D/efi"
+mkdir -p "$D/sources"
+
+echo 'buzy image' >"$D/sources/linux"
+echo 'the initrd' >"$D/sources/initrd"
+echo 'the-token' >"$D/sources/entry-token"
+echo 'opt1 opt2' >"$D/sources/cmdline"
+
+cat >"$D/sources/install.conf" <<EOF
+layout=bls
+initrd_generator=none
+# those are overridden by envvars
+BOOT_ROOT="$D/badboot"
+MACHINE_ID=badbadbadbadbadbad6abadbadbadbad
+EOF
+
+# Create a 'devicetree' config file that points to a fake dtb file
+echo 'subdir/whatever.dtb' >"$D/sources/devicetree"
+mkdir "$D/sources/subdir"
+echo 'DTBDTBDTBDTB' >"$D/sources/subdir/whatever.dtb"
+
+export KERNEL_INSTALL_CONF_ROOT="$D/sources"
+# We "install" multiple plugins, but control which ones will be active via install.conf.
+export KERNEL_INSTALL_PLUGINS="${ukify_install} ${loaderentry_install} ${uki_copy_install}"
+export BOOT_ROOT="$D/boot"
+export BOOT_MNT="$D/boot"
+export MACHINE_ID='3e0484f3634a418b8e6a39e8828b03e3'
+export KERNEL_INSTALL_UKIFY="$ukify"
+export KERNEL_INSTALL_BOOT_STUB="$boot_stub"
+export KERNEL_INSTALL_READ_MACHINE_INFO="no"
+export KERNEL_INSTALL_BYPASS="no"
+
+# Test type#1 installation
+"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+
+entry="$BOOT_ROOT/loader/entries/the-token-1.1.1.conf"
+test -f "$entry"
+grep -qE '^title ' "$entry"
+grep -qE '^version +1.1.1' "$entry"
+grep -qE '^options +opt1 opt2' "$entry"
+grep -qE '^linux .*/the-token/1.1.1/linux' "$entry"
+grep -qE '^initrd .*/the-token/1.1.1/initrd' "$entry"
+grep -qE '^devicetree .*/the-token/1.1.1/whatever.dtb' "$entry"
+
+grep -qE 'image' "$BOOT_ROOT/the-token/1.1.1/linux"
+grep -qE 'initrd' "$BOOT_ROOT/the-token/1.1.1/initrd"
+grep -qE 'DTBDTB' "$BOOT_ROOT/the-token/1.1.1/whatever.dtb"
+
+test -f /usr/lib/modules/"$(uname -r)"/vmlinuz && "$kernel_install" inspect
+"$kernel_install" inspect "$D/sources/linux"
+
+"$kernel_install" -v remove 1.1.1
+test ! -e "$entry"
+test ! -e "$BOOT_ROOT/the-token/1.1.1/linux"
+test ! -e "$BOOT_ROOT/the-token/1.1.1/initrd"
+
+# Test again with too many arguments for 'remove' command. See #28448.
+"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+
+test -f "$entry"
+test -f "$BOOT_ROOT/the-token/1.1.1/linux"
+test -f "$BOOT_ROOT/the-token/1.1.1/initrd"
+
+"$kernel_install" -v remove 1.1.1 hoge foo bar
+test ! -e "$entry"
+test ! -e "$BOOT_ROOT/the-token/1.1.1/linux"
+test ! -e "$BOOT_ROOT/the-token/1.1.1/initrd"
+
+# Invoke kernel-install as installkernel
+ln -s --relative -v "$kernel_install" "$D/sources/installkernel"
+"$D/sources/installkernel" -v 1.1.2 "$D/sources/linux" System.map /somedirignored
+
+entry="$BOOT_ROOT/loader/entries/the-token-1.1.2.conf"
+test -f "$entry"
+grep -qE '^title ' "$entry"
+grep -qE '^version +1.1.2' "$entry"
+grep -qE '^options +opt1 opt2' "$entry"
+grep -qE '^linux .*/the-token/1.1.2/linux' "$entry"
+( ! grep -qE '^initrd' "$entry" )
+grep -qE '^devicetree .*/the-token/1.1.2/whatever.dtb' "$entry"
+
+grep -qE 'image' "$BOOT_ROOT/the-token/1.1.2/linux"
+test ! -e "$BOOT_ROOT/the-token/1.1.2/initrd"
+grep -qE 'DTBDTB' "$BOOT_ROOT/the-token/1.1.2/whatever.dtb"
+
+# Check installation with boot counting
+echo '56' >"$D/sources/tries"
+
+"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+entry="$BOOT_ROOT/loader/entries/the-token-1.1.1+56.conf"
+test -f "$entry"
+grep -qE '^title ' "$entry"
+grep -qE '^version +1.1.1' "$entry"
+grep -qE '^options +opt1 opt2' "$entry"
+grep -qE '^linux .*/the-token/1.1.1/linux' "$entry"
+grep -qE '^initrd .*/the-token/1.1.1/initrd' "$entry"
+
+grep -qE 'image' "$BOOT_ROOT/the-token/1.1.1/linux"
+grep -qE 'initrd' "$BOOT_ROOT/the-token/1.1.1/initrd"
+
+# Install UKI
+if [ -f "$ukify" ]; then
+ cat >>"$D/sources/install.conf" <<EOF
+layout=uki
+uki_generator=ukify
+EOF
+
+ "$kernel_install" -v add 1.1.3 "$D/sources/linux" "$D/sources/initrd"
+ uki="${BOOT_ROOT}/EFI/Linux/the-token-1.1.3+56.efi"
+ test -f "$uki"
+
+ if [ -x "$bootctl" ]; then
+ "$bootctl" kernel-inspect "$uki" | grep -qE 'Kernel Type: +uki$'
+ "$bootctl" kernel-inspect "$uki" | grep -qE 'Version: +1\.1\.3$'
+ "$bootctl" kernel-inspect "$uki" | grep -qE 'Cmdline: +opt1 opt2$'
+ fi
+
+ "$ukify" inspect "$uki" | grep -qE '^.sbat'
+ "$ukify" inspect "$uki" | grep -qE '^.cmdline'
+ "$ukify" inspect "$uki" | grep -qE '^.uname'
+ "$ukify" inspect "$uki" | grep -qE '^.initrd'
+ "$ukify" inspect "$uki" | grep -qE '^.linux'
+ "$ukify" inspect "$uki" | grep -qE '^.dtb'
+fi
+
+# Test bootctl
+if [ -x "$bootctl" ]; then
+ echo "Testing bootctl"
+ e2="${entry%+*}_2.conf"
+ cp "$entry" "$e2"
+ export SYSTEMD_ESP_PATH=/boot
+ # We use --root so strip the root prefix from KERNEL_INSTALL_CONF_ROOT
+ export KERNEL_INSTALL_CONF_ROOT="sources"
+
+ # create file that is not referenced. Check if cleanup removes
+ # it but leaves the rest alone
+ :> "$BOOT_ROOT/the-token/1.1.2/initrd"
+ "$bootctl" --root="$D" cleanup
+ test ! -e "$BOOT_ROOT/the-token/1.1.2/initrd"
+ test -e "$BOOT_ROOT/the-token/1.1.2/linux"
+ test -e "$BOOT_ROOT/the-token/1.1.1/linux"
+ test -e "$BOOT_ROOT/the-token/1.1.1/initrd"
+
+ # now remove duplicated entry and make sure files are left over
+ "$bootctl" --root="$D" unlink "${e2##*/}"
+ test -e "$BOOT_ROOT/the-token/1.1.1/linux"
+ test -e "$BOOT_ROOT/the-token/1.1.1/initrd"
+ test -e "$entry"
+ test ! -e "$e2"
+ # remove last entry referencing those files
+ entry_id="${entry##*/}"
+ entry_id="${entry_id%+*}.conf"
+ "$bootctl" --root="$D" unlink "$entry_id"
+ test ! -e "$entry"
+ test ! -e "$BOOT_ROOT/the-token/1.1.1/linux"
+ test ! -e "$BOOT_ROOT/the-token/1.1.1/initrd"
+fi
+
+###########################################
+# tests for --make-entry-directory=
+###########################################
+
+# disable all dropins
+cat >"$D/00-skip.install" <<EOF
+#!/bin/bash
+exit 77
+EOF
+chmod +x "$D/00-skip.install"
+export KERNEL_INSTALL_PLUGINS="$D/00-skip.install"
+
+# drop layout= from install.conf
+cat >"$D/sources/install.conf" <<EOF
+initrd_generator=none
+# those are overridden by envvars
+BOOT_ROOT="$D/badboot"
+MACHINE_ID=badbadbadbadbadbad6abadbadbadbad
+EOF
+export KERNEL_INSTALL_CONF_ROOT="$D/sources"
+
+rm -rf "$BOOT_ROOT"
+mkdir -p "$BOOT_ROOT"
+
+# 1. defaults to 'auto', and the entry directory is created only when the layout is BLS
+# 1.1 token directory does not exist -> layout is other.
+"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+"$kernel_install" -v remove 1.1.1
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+
+# 1.2 token directory exists -> layout is BLS
+mkdir -p "$BOOT_ROOT/the-token"
+"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+test -d "$BOOT_ROOT/the-token/1.1.1"
+"$kernel_install" -v remove 1.1.1
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+rmdir "$BOOT_ROOT/the-token"
+
+# 2. --make-entry-directory=yes
+# 2.1 token directory does not exist -> layout is other.
+"$kernel_install" -v --make-entry-directory=yes add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+test -d "$BOOT_ROOT/the-token/1.1.1"
+"$kernel_install" -v --make-entry-directory=yes remove 1.1.1
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+test -d "$BOOT_ROOT/the-token"
+
+# 2.2 token directory exists -> layout is BLS
+mkdir -p "$BOOT_ROOT/the-token"
+"$kernel_install" -v --make-entry-directory=yes add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+test -d "$BOOT_ROOT/the-token/1.1.1"
+"$kernel_install" -v --make-entry-directory=yes remove 1.1.1
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+test -d "$BOOT_ROOT/the-token"
+rmdir "$BOOT_ROOT/the-token"
+
+# 3. --make-entry-directory=no
+# 3.1 token directory does not exist -> layout is other.
+"$kernel_install" -v --make-entry-directory=no add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+"$kernel_install" -v --make-entry-directory=no remove 1.1.1
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+
+# 3.2 token directory exists -> layout is BLS
+mkdir -p "$BOOT_ROOT/the-token"
+"$kernel_install" -v --make-entry-directory=no add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+"$kernel_install" -v --make-entry-directory=no remove 1.1.1
+test ! -e "$BOOT_ROOT/the-token/1.1.1"
+test -d "$BOOT_ROOT/the-token"
+rmdir "$BOOT_ROOT/the-token"
+
+###########################################
+# tests for --entry-token=
+###########################################
+"$kernel_install" -v --make-entry-directory=yes --entry-token=machine-id add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+test -d "$BOOT_ROOT/$MACHINE_ID/1.1.1"
+"$kernel_install" -v --make-entry-directory=yes --entry-token=machine-id remove 1.1.1
+test ! -e "$BOOT_ROOT/$MACHINE_ID/1.1.1"
+test -d "$BOOT_ROOT/$MACHINE_ID"
+rmdir "$BOOT_ROOT/$MACHINE_ID"
+
+"$kernel_install" -v --make-entry-directory=yes --entry-token=literal:hoge add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+test -d "$BOOT_ROOT/hoge/1.1.1"
+"$kernel_install" -v --make-entry-directory=yes --entry-token=literal:hoge remove 1.1.1
+test ! -e "$BOOT_ROOT/hoge/1.1.1"
+test -d "$BOOT_ROOT/hoge"
+rmdir "$BOOT_ROOT/hoge"
+
+###########################################
+# tests for --json=
+###########################################
+output="$("$kernel_install" -v --json=pretty inspect 1.1.1 "$D/sources/linux")"
+
+diff -u <(echo "$output") - <<EOF
+{
+ "MachineID" : "3e0484f3634a418b8e6a39e8828b03e3",
+ "KernelImageType" : "unknown",
+ "Layout" : "other",
+ "BootRoot" : "$BOOT_ROOT",
+ "EntryTokenType" : "literal",
+ "EntryToken" : "the-token",
+ "EntryDirectory" : "$BOOT_ROOT/the-token/1.1.1",
+ "KernelVersion" : "1.1.1",
+ "Kernel" : "$D/sources/linux",
+ "Initrds" : null,
+ "InitrdGenerator" : "none",
+ "UKIGenerator" : null,
+ "Plugins" : [
+ "$D/00-skip.install"
+ ],
+ "PluginEnvironment" : [
+ "LC_COLLATE=C.UTF-8",
+ "KERNEL_INSTALL_VERBOSE=1",
+ "KERNEL_INSTALL_IMAGE_TYPE=unknown",
+ "KERNEL_INSTALL_MACHINE_ID=3e0484f3634a418b8e6a39e8828b03e3",
+ "KERNEL_INSTALL_ENTRY_TOKEN=the-token",
+ "KERNEL_INSTALL_BOOT_ROOT=$BOOT_ROOT",
+ "KERNEL_INSTALL_LAYOUT=other",
+ "KERNEL_INSTALL_INITRD_GENERATOR=none",
+ "KERNEL_INSTALL_UKI_GENERATOR=",
+ "KERNEL_INSTALL_STAGING_AREA=/tmp/kernel-install.staging.XXXXXX"
+ ]
+}
+EOF
+
+###########################################
+# tests for propagation of plugin failure (issue #30087)
+###########################################
+cat >"$D/00-plugin-skip" <<EOF
+#!/usr/bin/env bash
+exit 77
+EOF
+chmod +x "$D/00-plugin-skip"
+
+cat >"$D/10-plugin-fail" <<EOF
+#!/usr/bin/env bash
+exit 42
+EOF
+chmod +x "$D/10-plugin-fail"
+
+# Exit code 77 means remaining plugins will be skipped.
+KERNEL_INSTALL_PLUGINS="$D/00-plugin-skip $D/10-plugin-fail" "$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+
+# Other non-zero exit code will be propagated.
+set +e
+KERNEL_INSTALL_PLUGINS="$D/10-plugin-fail" "$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
+ret=$?
+set -e
+test "$ret" -eq "42"