]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/kernel-install/60-ukify.install.in
kernel-install/60-ukify: use exec() instead of runpy
[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
79f902eb 20# pylint: disable=import-outside-toplevel,consider-using-with,disable=redefined-builtin
ca1abaa5
ZJS
21
22import argparse
23import os
ca1abaa5 24import shlex
2060509d 25import types
b99e4922 26from shutil import which
ca1abaa5
ZJS
27from pathlib import Path
28from typing import Optional
29
30__version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
31
32try:
33 VERBOSE = int(os.environ['KERNEL_INSTALL_VERBOSE']) > 0
34except (KeyError, ValueError):
35 VERBOSE = False
36
37# Override location of ukify and the boot stub for testing and debugging.
b99e4922 38UKIFY = os.getenv('KERNEL_INSTALL_UKIFY', which('ukify'))
ca1abaa5
ZJS
39BOOT_STUB = os.getenv('KERNEL_INSTALL_BOOT_STUB')
40
41
42def 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
46def log(*args, **kwargs):
47 if VERBOSE:
48 print(*args, **kwargs)
49
50def 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
59def 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
65def 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
111def 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
c82cc370 118 KERNEL_INSTALL_UKI_GENERATOR = os.getenv('KERNEL_INSTALL_UKI_GENERATOR') or 'ukify'
ca1abaa5
ZJS
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
40c0c9d4
ZJS
128def input_file_location(
129 filename: str,
130 *search_directories: str) -> Optional[Path]:
131
ca1abaa5 132 if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'):
40c0c9d4
ZJS
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
ca1abaa5
ZJS
143 return None
144
145
40c0c9d4
ZJS
146def uki_conf_location() -> Optional[Path]:
147 return input_file_location('uki.conf',
148 '/etc/kernel')
ca1abaa5 149
ca1abaa5 150
02d8d787
ZJS
151def devicetree_config_location() -> Optional[Path]:
152 return input_file_location('devicetree')
153
154
155def devicetree_file_location(opts) -> Optional[Path]:
156 # This mirrors the logic in 90-loaderentry.install. Keep in sync.
157 configfile = devicetree_config_location()
158 if configfile is None:
159 return None
160
161 devicetree = configfile.read_text().strip()
162 if not devicetree:
163 raise ValueError(f'{configfile!r} is empty')
164
165 path = input_file_location(
166 devicetree,
167 f'/usr/lib/firmware/{opts.kernel_version}/device-tree',
168 f'/usr/lib/linux-image-{opts.kernel_version}',
169 f'/usr/lib/modules/{opts.kernel_version}/dtb',
170 )
171 if path is None:
172 raise FileNotFoundError(f'DeviceTree file {devicetree} not found')
173 return path
174
175
40c0c9d4
ZJS
176def kernel_cmdline_base() -> list[str]:
177 path = input_file_location('cmdline')
178 if path:
179 return path.read_text().split()
180
181 # If we read /proc/cmdline, we need to do some additional filtering.
ca1abaa5
ZJS
182 options = Path('/proc/cmdline').read_text().split()
183 return [opt for opt in options
184 if not opt.startswith(('BOOT_IMAGE=', 'initrd='))]
185
186
187def kernel_cmdline(opts) -> str:
188 options = kernel_cmdline_base()
189
190 # If the boot entries are named after the machine ID, then suffix the kernel
191 # command line with the machine ID we use, so that the machine ID remains
192 # stable, even during factory reset, in the initrd (where the system's machine
193 # ID is not directly accessible yet), and if the root file system is volatile.
194 if (opts.entry_token == opts.machine_id and
195 not any(opt.startswith('systemd.machine_id=') for opt in options)):
196 options += [f'systemd.machine_id={opts.machine_id}']
197
198 # TODO: we unconditionally set the cmdline here, ignoring the setting in
199 # the config file. Should we not do that?
200
201 # Prepend a space so that '@' does not get misinterpreted
202 return ' ' + ' '.join(options)
203
204
f3d1d760 205def initrd_list(opts) -> list[Path]:
abca74cb 206 microcode = sorted(opts.staging_area.glob('microcode*'))
f3d1d760
MPB
207 initrd = sorted(opts.staging_area.glob('initrd*'))
208
288cc268 209 # Order taken from 90-loaderentry.install
f3d1d760
MPB
210 return [*microcode, *opts.initrd, *initrd]
211
212
2060509d
ZJS
213def load_module(name: str, path: str) -> types.ModuleType:
214 module = types.ModuleType(name)
215 text = open(path).read()
216 exec(compile(text, path, 'exec'), module.__dict__)
217 return module
218
219
220def call_ukify(opts) -> None:
221 ukify = load_module('ukify', UKIFY)
ca1abaa5 222
1df35a46
ZJS
223 # Create "empty" namespace. We want to override just a few settings, so it
224 # doesn't make sense to configure everything. We pretend to parse an empty
225 # argument set to prepopulate the namespace with the defaults.
2060509d 226 opts2 = ukify.create_parser().parse_args(['build'])
ca1abaa5 227
40c0c9d4 228 opts2.config = uki_conf_location()
ca1abaa5
ZJS
229 opts2.uname = opts.kernel_version
230 opts2.linux = opts.kernel_image
f3d1d760 231 opts2.initrd = initrd_list(opts)
ca1abaa5
ZJS
232 # Note that 'uki.efi' is the name required by 90-uki-copy.install.
233 opts2.output = opts.staging_area / 'uki.efi'
234
02d8d787
ZJS
235 if devicetree := devicetree_file_location(opts):
236 opts2.devicetree = devicetree
237
ca1abaa5
ZJS
238 opts2.cmdline = kernel_cmdline(opts)
239 if BOOT_STUB:
240 opts2.stub = BOOT_STUB
241
2060509d
ZJS
242 ukify.apply_config(opts2)
243 ukify.finalize_options(opts2)
244 ukify.check_inputs(opts2)
245 ukify.make_uki(opts2)
ca1abaa5
ZJS
246
247 log(f'{opts2.output} has been created')
248
249
250def main():
251 opts = parse_args()
252 if opts.command != 'add':
253 return
254 if not we_are_wanted():
255 return
256
257 call_ukify(opts)
258
259
260if __name__ == '__main__':
261 main()