]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/kernel-install/60-ukify.install.in
Merge pull request #30232 from keszybz/ukify-imports
[thirdparty/systemd.git] / src / kernel-install / 60-ukify.install.in
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=import-outside-toplevel,consider-using-with,disable=redefined-builtin
21
22 import argparse
23 import os
24 import shlex
25 import types
26 from shutil import which
27 from pathlib import Path
28 from typing import Optional
29
30 __version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
31
32 try:
33 VERBOSE = int(os.environ['KERNEL_INSTALL_VERBOSE']) > 0
34 except (KeyError, ValueError):
35 VERBOSE = False
36
37 # Override location of ukify and the boot stub for testing and debugging.
38 UKIFY = os.getenv('KERNEL_INSTALL_UKIFY', which('ukify'))
39 BOOT_STUB = os.getenv('KERNEL_INSTALL_BOOT_STUB')
40
41
42 def shell_join(cmd):
43 # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path.
44 return ' '.join(shlex.quote(str(x)) for x in cmd)
45
46 def log(*args, **kwargs):
47 if VERBOSE:
48 print(*args, **kwargs)
49
50 def path_is_readable(p: Path, dir=False) -> None:
51 """Verify access to a file or directory."""
52 try:
53 p.open().close()
54 except IsADirectoryError:
55 if dir:
56 return
57 raise
58
59 def mandatory_variable(name):
60 try:
61 return os.environ[name]
62 except KeyError as e:
63 raise KeyError(f'${name} must be set in the environment') from e
64
65 def parse_args(args=None):
66 p = argparse.ArgumentParser(
67 description='kernel-install plugin to build a Unified Kernel Image',
68 allow_abbrev=False,
69 usage='60-ukify.install COMMAND KERNEL_VERSION ENTRY_DIR KERNEL_IMAGE INITRD…',
70 )
71
72 # Suppress printing of usage synopsis on errors
73 p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n')
74
75 p.add_argument('command',
76 metavar='COMMAND',
77 help="The action to perform. Only 'add' is supported.")
78 p.add_argument('kernel_version',
79 metavar='KERNEL_VERSION',
80 help='Kernel version string')
81 p.add_argument('entry_dir',
82 metavar='ENTRY_DIR',
83 type=Path,
84 nargs='?',
85 help='Type#1 entry directory (ignored)')
86 p.add_argument('kernel_image',
87 metavar='KERNEL_IMAGE',
88 type=Path,
89 nargs='?',
90 help='Kernel binary')
91 p.add_argument('initrd',
92 metavar='INITRD…',
93 type=Path,
94 nargs='*',
95 help='Initrd files')
96 p.add_argument('--version',
97 action='version',
98 version=f'systemd {__version__}')
99
100 opts = p.parse_args(args)
101
102 if opts.command == 'add':
103 opts.staging_area = Path(mandatory_variable('KERNEL_INSTALL_STAGING_AREA'))
104 path_is_readable(opts.staging_area, dir=True)
105
106 opts.entry_token = mandatory_variable('KERNEL_INSTALL_ENTRY_TOKEN')
107 opts.machine_id = mandatory_variable('KERNEL_INSTALL_MACHINE_ID')
108
109 return opts
110
111 def we_are_wanted() -> bool:
112 KERNEL_INSTALL_LAYOUT = os.getenv('KERNEL_INSTALL_LAYOUT')
113
114 if KERNEL_INSTALL_LAYOUT != 'uki':
115 log(f'{KERNEL_INSTALL_LAYOUT=}, quitting.')
116 return False
117
118 KERNEL_INSTALL_UKI_GENERATOR = os.getenv('KERNEL_INSTALL_UKI_GENERATOR') or 'ukify'
119
120 if KERNEL_INSTALL_UKI_GENERATOR != 'ukify':
121 log(f'{KERNEL_INSTALL_UKI_GENERATOR=}, quitting.')
122 return False
123
124 log('KERNEL_INSTALL_LAYOUT and KERNEL_INSTALL_UKI_GENERATOR are good')
125 return True
126
127
128 def input_file_location(
129 filename: str,
130 *search_directories: str) -> Optional[Path]:
131
132 if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
133 search_directories = (root,)
134 elif not search_directories:
135 # This is the default search path.
136 search_directories = ('/etc/kernel',
137 '/usr/lib/kernel')
138
139 for dir in search_directories:
140 p = Path(dir) / filename
141 if p.exists():
142 return p
143 return None
144
145
146 def uki_conf_location() -> Optional[Path]:
147 return input_file_location('uki.conf')
148
149
150 def devicetree_config_location() -> Optional[Path]:
151 return input_file_location('devicetree')
152
153
154 def devicetree_file_location(opts) -> Optional[Path]:
155 # This mirrors the logic in 90-loaderentry.install. Keep in sync.
156 configfile = devicetree_config_location()
157 if configfile is None:
158 return None
159
160 devicetree = configfile.read_text().strip()
161 if not devicetree:
162 raise ValueError(f'{configfile!r} is empty')
163
164 path = input_file_location(
165 devicetree,
166 f'/usr/lib/firmware/{opts.kernel_version}/device-tree',
167 f'/usr/lib/linux-image-{opts.kernel_version}',
168 f'/usr/lib/modules/{opts.kernel_version}/dtb',
169 )
170 if path is None:
171 raise FileNotFoundError(f'DeviceTree file {devicetree} not found')
172 return path
173
174
175 def kernel_cmdline_base() -> list[str]:
176 path = input_file_location('cmdline')
177 if path:
178 return path.read_text().split()
179
180 # If we read /proc/cmdline, we need to do some additional filtering.
181 options = Path('/proc/cmdline').read_text().split()
182 return [opt for opt in options
183 if not opt.startswith(('BOOT_IMAGE=', 'initrd='))]
184
185
186 def kernel_cmdline(opts) -> str:
187 options = kernel_cmdline_base()
188
189 # If the boot entries are named after the machine ID, then suffix the kernel
190 # command line with the machine ID we use, so that the machine ID remains
191 # stable, even during factory reset, in the initrd (where the system's machine
192 # ID is not directly accessible yet), and if the root file system is volatile.
193 if (opts.entry_token == opts.machine_id and
194 not any(opt.startswith('systemd.machine_id=') for opt in options)):
195 options += [f'systemd.machine_id={opts.machine_id}']
196
197 # TODO: we unconditionally set the cmdline here, ignoring the setting in
198 # the config file. Should we not do that?
199
200 # Prepend a space so that '@' does not get misinterpreted
201 return ' ' + ' '.join(options)
202
203
204 def initrd_list(opts) -> list[Path]:
205 microcode = sorted(opts.staging_area.glob('microcode*'))
206 initrd = sorted(opts.staging_area.glob('initrd*'))
207
208 # Order taken from 90-loaderentry.install
209 return [*microcode, *opts.initrd, *initrd]
210
211
212 def load_module(name: str, path: str) -> types.ModuleType:
213 module = types.ModuleType(name)
214 text = open(path).read()
215 exec(compile(text, path, 'exec'), module.__dict__)
216 return module
217
218
219 def call_ukify(opts) -> None:
220 ukify = load_module('ukify', UKIFY)
221
222 # Create "empty" namespace. We want to override just a few settings, so it
223 # doesn't make sense to configure everything. We pretend to parse an empty
224 # argument set to prepopulate the namespace with the defaults.
225 opts2 = ukify.create_parser().parse_args(['build'])
226
227 opts2.config = uki_conf_location()
228 opts2.uname = opts.kernel_version
229 opts2.linux = opts.kernel_image
230 opts2.initrd = initrd_list(opts)
231 # Note that 'uki.efi' is the name required by 90-uki-copy.install.
232 opts2.output = opts.staging_area / 'uki.efi'
233
234 if devicetree := devicetree_file_location(opts):
235 opts2.devicetree = devicetree
236
237 opts2.cmdline = kernel_cmdline(opts)
238 if BOOT_STUB:
239 opts2.stub = BOOT_STUB
240
241 ukify.apply_config(opts2)
242 ukify.finalize_options(opts2)
243 ukify.check_inputs(opts2)
244 ukify.make_uki(opts2)
245
246 log(f'{opts2.output} has been created')
247
248
249 def main():
250 opts = parse_args()
251 if opts.command != 'add':
252 return
253 if not we_are_wanted():
254 return
255
256 call_ukify(opts)
257
258
259 if __name__ == '__main__':
260 main()