]> git.ipfire.org Git - thirdparty/systemd.git/blame - src/kernel-install/60-ukify.install.in
Use VERSION_TAG instead of GIT_VERSION in kernel-install scripts
[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
76940e0a 30__version__ = '{{PROJECT_VERSION_FULL}} ({{VERSION_TAG}})'
ca1abaa5
ZJS
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 146def uki_conf_location() -> Optional[Path]:
fc7cdb2d 147 return input_file_location('uki.conf')
ca1abaa5 148
ca1abaa5 149
02d8d787
ZJS
150def devicetree_config_location() -> Optional[Path]:
151 return input_file_location('devicetree')
152
153
154def 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
40c0c9d4
ZJS
175def 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.
ca1abaa5
ZJS
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
186def 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
f3d1d760 204def initrd_list(opts) -> list[Path]:
abca74cb 205 microcode = sorted(opts.staging_area.glob('microcode*'))
f3d1d760
MPB
206 initrd = sorted(opts.staging_area.glob('initrd*'))
207
288cc268 208 # Order taken from 90-loaderentry.install
f3d1d760
MPB
209 return [*microcode, *opts.initrd, *initrd]
210
211
2060509d
ZJS
212def 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
219def call_ukify(opts) -> None:
220 ukify = load_module('ukify', UKIFY)
ca1abaa5 221
1df35a46
ZJS
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.
2060509d 225 opts2 = ukify.create_parser().parse_args(['build'])
ca1abaa5 226
40c0c9d4 227 opts2.config = uki_conf_location()
ca1abaa5
ZJS
228 opts2.uname = opts.kernel_version
229 opts2.linux = opts.kernel_image
f3d1d760 230 opts2.initrd = initrd_list(opts)
ca1abaa5
ZJS
231 # Note that 'uki.efi' is the name required by 90-uki-copy.install.
232 opts2.output = opts.staging_area / 'uki.efi'
233
02d8d787
ZJS
234 if devicetree := devicetree_file_location(opts):
235 opts2.devicetree = devicetree
236
ca1abaa5
ZJS
237 opts2.cmdline = kernel_cmdline(opts)
238 if BOOT_STUB:
239 opts2.stub = BOOT_STUB
240
2060509d
ZJS
241 ukify.apply_config(opts2)
242 ukify.finalize_options(opts2)
243 ukify.check_inputs(opts2)
244 ukify.make_uki(opts2)
ca1abaa5
ZJS
245
246 log(f'{opts2.output} has been created')
247
248
249def 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
259if __name__ == '__main__':
260 main()