summaryrefslogtreecommitdiff
path: root/src/kernel-install
diff options
context:
space:
mode:
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>2023-04-13 18:07:22 +0200
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>2023-05-05 18:42:37 +0200
commitca1abaa5c4ffbd0f72f5bbbd98a70db925a82503 (patch)
tree0ef7656b8d31d37f79e8044bb189e69e8bb6bd6e /src/kernel-install
parent47a6df4da0a852daae9458b7cb10fb1e43322c2b (diff)
downloadsystemd-ca1abaa5c4ffbd0f72f5bbbd98a70db925a82503.tar.gz
60-ukify: kernel-install plugin that calls ukify to create a UKI
60-ukify.install calls ukify with a config file, so singing and policies and splash will be done through the ukify config file, without 60-ukify.install knowing anything directly. In meson.py, the variable for loaderentry.install.in is used just once, let's drop it. (I guess this approach was copied from kernel_install_in, which is used in another file.) The general idea is based on cvlc12's #27119, but now in Python instead of bash.
Diffstat (limited to 'src/kernel-install')
-rwxr-xr-xsrc/kernel-install/60-ukify.install.in224
-rw-r--r--src/kernel-install/meson.build12
2 files changed, 234 insertions, 2 deletions
diff --git a/src/kernel-install/60-ukify.install.in b/src/kernel-install/60-ukify.install.in
new file mode 100755
index 0000000000..7c29f7e8af
--- /dev/null
+++ b/src/kernel-install/60-ukify.install.in
@@ -0,0 +1,224 @@
+#!/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=missing-docstring,invalid-name,import-outside-toplevel
+# pylint: disable=consider-using-with,unspecified-encoding,line-too-long
+# pylint: disable=too-many-locals,too-many-statements,too-many-return-statements
+# pylint: disable=too-many-branches,redefined-builtin,fixme
+
+import argparse
+import os
+import runpy
+import shlex
+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', '/usr/lib/systemd/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')
+
+ 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 config_file_location() -> Optional[Path]:
+ if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
+ p = Path(root) / 'uki.conf'
+ else:
+ p = Path('/etc/kernel/uki.conf')
+ if p.exists():
+ return p
+ return None
+
+
+def kernel_cmdline_base() -> list[str]:
+ if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
+ return Path(root).joinpath('cmdline').read_text().split()
+
+ for cmdline in ('/etc/kernel/cmdline',
+ '/usr/lib/kernel/cmdline'):
+ try:
+ return Path(cmdline).read_text().split()
+ except FileNotFoundError:
+ continue
+
+ 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 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 duplicate all the fields. We use a hack
+ # to pre-populate the namespace like argparse would, all defaults.
+ # We need to specify the two mandatory arguments to not get an error.
+ opts2 = ukify['create_parser']().parse_args(('A','B'))
+
+ opts2.config = config_file_location()
+ opts2.uname = opts.kernel_version
+ opts2.linux = opts.kernel_image
+ opts2.initrd = opts.initrd
+ # Note that 'uki.efi' is the name required by 90-uki-copy.install.
+ opts2.output = opts.staging_area / 'uki.efi'
+
+ 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/meson.build b/src/kernel-install/meson.build
index f5db4432c9..95aa0d9497 100644
--- a/src/kernel-install/meson.build
+++ b/src/kernel-install/meson.build
@@ -1,11 +1,19 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
kernel_install_in = files('kernel-install.in')
-loaderentry_install_in = files('90-loaderentry.install.in')
+
+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 : loaderentry_install_in,
+ input : '90-loaderentry.install.in',
output : '90-loaderentry.install',
command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'],
install : want_kernel_install,