]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/kernel-install/60-ukify.install.in
ukify: move verb mangling to finalize_options()
[thirdparty/systemd.git] / src / kernel-install / 60-ukify.install.in
CommitLineData
ca1abaa5
ZJS
1#!/usr/bin/env python3
2# SPDX-License-Identifier: LGPL-2.1-or-later
3# -*- mode: python-mode -*-
4#
5# This file is part of systemd.
6#
7# systemd is free software; you can redistribute it and/or modify it
8# under the terms of the GNU Lesser General Public License as published by
9# the Free Software Foundation; either version 2.1 of the License, or
10# (at your option) any later version.
11#
12# systemd is distributed in the hope that it will be useful, but
13# WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15# General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public License
18# along with systemd; If not, see <https://www.gnu.org/licenses/>.
19
20# pylint: disable=missing-docstring,invalid-name,import-outside-toplevel
21# pylint: disable=consider-using-with,unspecified-encoding,line-too-long
22# pylint: disable=too-many-locals,too-many-statements,too-many-return-statements
23# pylint: disable=too-many-branches,redefined-builtin,fixme
24
25import argparse
26import os
27import runpy
28import shlex
29from pathlib import Path
30from typing import Optional
31
32__version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
33
34try:
35 VERBOSE = int(os.environ['KERNEL_INSTALL_VERBOSE']) > 0
36except (KeyError, ValueError):
37 VERBOSE = False
38
39# Override location of ukify and the boot stub for testing and debugging.
40UKIFY = os.getenv('KERNEL_INSTALL_UKIFY', '/usr/lib/systemd/ukify')
41BOOT_STUB = os.getenv('KERNEL_INSTALL_BOOT_STUB')
42
43
44def shell_join(cmd):
45 # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path.
46 return ' '.join(shlex.quote(str(x)) for x in cmd)
47
48def log(*args, **kwargs):
49 if VERBOSE:
50 print(*args, **kwargs)
51
52def path_is_readable(p: Path, dir=False) -> None:
53 """Verify access to a file or directory."""
54 try:
55 p.open().close()
56 except IsADirectoryError:
57 if dir:
58 return
59 raise
60
61def mandatory_variable(name):
62 try:
63 return os.environ[name]
64 except KeyError as e:
65 raise KeyError(f'${name} must be set in the environment') from e
66
67def parse_args(args=None):
68 p = argparse.ArgumentParser(
69 description='kernel-install plugin to build a Unified Kernel Image',
70 allow_abbrev=False,
71 usage='60-ukify.install COMMAND KERNEL_VERSION ENTRY_DIR KERNEL_IMAGE INITRD…',
72 )
73
74 # Suppress printing of usage synopsis on errors
75 p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n')
76
77 p.add_argument('command',
78 metavar='COMMAND',
79 help="The action to perform. Only 'add' is supported.")
80 p.add_argument('kernel_version',
81 metavar='KERNEL_VERSION',
82 help='Kernel version string')
83 p.add_argument('entry_dir',
84 metavar='ENTRY_DIR',
85 type=Path,
86 nargs='?',
87 help='Type#1 entry directory (ignored)')
88 p.add_argument('kernel_image',
89 metavar='KERNEL_IMAGE',
90 type=Path,
91 nargs='?',
92 help='Kernel binary')
93 p.add_argument('initrd',
94 metavar='INITRD…',
95 type=Path,
96 nargs='*',
97 help='Initrd files')
98 p.add_argument('--version',
99 action='version',
100 version=f'systemd {__version__}')
101
102 opts = p.parse_args(args)
103
104 if opts.command == 'add':
105 opts.staging_area = Path(mandatory_variable('KERNEL_INSTALL_STAGING_AREA'))
106 path_is_readable(opts.staging_area, dir=True)
107
108 opts.entry_token = mandatory_variable('KERNEL_INSTALL_ENTRY_TOKEN')
109 opts.machine_id = mandatory_variable('KERNEL_INSTALL_MACHINE_ID')
110
111 return opts
112
113def we_are_wanted() -> bool:
114 KERNEL_INSTALL_LAYOUT = os.getenv('KERNEL_INSTALL_LAYOUT')
115
116 if KERNEL_INSTALL_LAYOUT != 'uki':
117 log(f'{KERNEL_INSTALL_LAYOUT=}, quitting.')
118 return False
119
120 KERNEL_INSTALL_UKI_GENERATOR = os.getenv('KERNEL_INSTALL_UKI_GENERATOR')
121
122 if KERNEL_INSTALL_UKI_GENERATOR != 'ukify':
123 log(f'{KERNEL_INSTALL_UKI_GENERATOR=}, quitting.')
124 return False
125
126 log('KERNEL_INSTALL_LAYOUT and KERNEL_INSTALL_UKI_GENERATOR are good')
127 return True
128
129
130def config_file_location() -> Optional[Path]:
131 if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
132 p = Path(root) / 'uki.conf'
133 else:
134 p = Path('/etc/kernel/uki.conf')
135 if p.exists():
136 return p
137 return None
138
139
140def kernel_cmdline_base() -> list[str]:
141 if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
142 return Path(root).joinpath('cmdline').read_text().split()
143
144 for cmdline in ('/etc/kernel/cmdline',
145 '/usr/lib/kernel/cmdline'):
146 try:
147 return Path(cmdline).read_text().split()
148 except FileNotFoundError:
149 continue
150
151 options = Path('/proc/cmdline').read_text().split()
152 return [opt for opt in options
153 if not opt.startswith(('BOOT_IMAGE=', 'initrd='))]
154
155
156def kernel_cmdline(opts) -> str:
157 options = kernel_cmdline_base()
158
159 # If the boot entries are named after the machine ID, then suffix the kernel
160 # command line with the machine ID we use, so that the machine ID remains
161 # stable, even during factory reset, in the initrd (where the system's machine
162 # ID is not directly accessible yet), and if the root file system is volatile.
163 if (opts.entry_token == opts.machine_id and
164 not any(opt.startswith('systemd.machine_id=') for opt in options)):
165 options += [f'systemd.machine_id={opts.machine_id}']
166
167 # TODO: we unconditionally set the cmdline here, ignoring the setting in
168 # the config file. Should we not do that?
169
170 # Prepend a space so that '@' does not get misinterpreted
171 return ' ' + ' '.join(options)
172
173
174def call_ukify(opts):
175 # Punish me harder.
176 # We want this:
177 # ukify = importlib.machinery.SourceFileLoader('ukify', UKIFY).load_module()
178 # but it throws a DeprecationWarning.
179 # https://stackoverflow.com/questions/67631/how-can-i-import-a-module-dynamically-given-the-full-path
180 # https://github.com/python/cpython/issues/65635
181 # offer "explanations", but to actually load a python file without a .py extension,
182 # the "solution" is 4+ incomprehensible lines.
183 # The solution with runpy gives a dictionary, which isn't great, but will do.
184 ukify = runpy.run_path(UKIFY, run_name='ukify')
185
1df35a46
ZJS
186 # Create "empty" namespace. We want to override just a few settings, so it
187 # doesn't make sense to configure everything. We pretend to parse an empty
188 # argument set to prepopulate the namespace with the defaults.
b09a5315 189 opts2 = ukify['create_parser']().parse_args(['build'])
ca1abaa5
ZJS
190
191 opts2.config = config_file_location()
192 opts2.uname = opts.kernel_version
193 opts2.linux = opts.kernel_image
194 opts2.initrd = opts.initrd
195 # Note that 'uki.efi' is the name required by 90-uki-copy.install.
196 opts2.output = opts.staging_area / 'uki.efi'
197
198 opts2.cmdline = kernel_cmdline(opts)
199 if BOOT_STUB:
200 opts2.stub = BOOT_STUB
201
202 # opts2.summary = True
203
204 ukify['apply_config'](opts2)
205 ukify['finalize_options'](opts2)
206 ukify['check_inputs'](opts2)
207 ukify['make_uki'](opts2)
208
209 log(f'{opts2.output} has been created')
210
211
212def main():
213 opts = parse_args()
214 if opts.command != 'add':
215 return
216 if not we_are_wanted():
217 return
218
219 call_ukify(opts)
220
221
222if __name__ == '__main__':
223 main()