]>
Commit | Line | Data |
---|---|---|
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 | ||
25 | import argparse | |
26 | import os | |
27 | import runpy | |
28 | import shlex | |
29 | from pathlib import Path | |
30 | from typing import Optional | |
31 | ||
32 | __version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})' | |
33 | ||
34 | try: | |
35 | VERBOSE = int(os.environ['KERNEL_INSTALL_VERBOSE']) > 0 | |
36 | except (KeyError, ValueError): | |
37 | VERBOSE = False | |
38 | ||
39 | # Override location of ukify and the boot stub for testing and debugging. | |
40 | UKIFY = os.getenv('KERNEL_INSTALL_UKIFY', '/usr/lib/systemd/ukify') | |
41 | BOOT_STUB = os.getenv('KERNEL_INSTALL_BOOT_STUB') | |
42 | ||
43 | ||
44 | def 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 | ||
48 | def log(*args, **kwargs): | |
49 | if VERBOSE: | |
50 | print(*args, **kwargs) | |
51 | ||
52 | def 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 | ||
61 | def 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 | ||
67 | def 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 | ||
113 | def 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 | ||
130 | def 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 | ||
140 | def 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 | ||
156 | def 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 | ||
174 | def 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 | ||
212 | def 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 | ||
222 | if __name__ == '__main__': | |
223 | main() |