From ca1abaa5c4ffbd0f72f5bbbd98a70db925a82503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Thu, 13 Apr 2023 18:07:22 +0200 Subject: 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. --- src/kernel-install/60-ukify.install.in | 224 +++++++++++++++++++++++++++++++++ src/kernel-install/meson.build | 12 +- 2 files changed, 234 insertions(+), 2 deletions(-) create mode 100755 src/kernel-install/60-ukify.install.in (limited to 'src/kernel-install') 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 . + +# 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, -- cgit v1.2.1