2 # SPDX-License-Identifier: LGPL-2.1-or-later
3 # -*- mode: python-mode -*-
5 # This file is part of systemd.
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.
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.
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/>.
20 # pylint: disable=import-outside-toplevel,consider-using-with,disable=redefined-builtin
26 from shutil
import which
27 from pathlib
import Path
28 from typing
import Optional
30 __version__
= '{{PROJECT_VERSION}} ({{GIT_VERSION}})'
33 VERBOSE
= int(os
.environ
['KERNEL_INSTALL_VERBOSE']) > 0
34 except (KeyError, ValueError):
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')
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
)
46 def log(*args
, **kwargs
):
48 print(*args
, **kwargs
)
50 def path_is_readable(p
: Path
, dir=False) -> None:
51 """Verify access to a file or directory."""
54 except IsADirectoryError
:
59 def mandatory_variable(name
):
61 return os
.environ
[name
]
63 raise KeyError(f
'${name} must be set in the environment') from e
65 def parse_args(args
=None):
66 p
= argparse
.ArgumentParser(
67 description
='kernel-install plugin to build a Unified Kernel Image',
69 usage
='60-ukify.install COMMAND KERNEL_VERSION ENTRY_DIR KERNEL_IMAGE INITRD…',
72 # Suppress printing of usage synopsis on errors
73 p
.error
= lambda message
: p
.exit(2, f
'{p.prog}: error: {message}\n')
75 p
.add_argument('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',
85 help='Type#1 entry directory (ignored)')
86 p
.add_argument('kernel_image',
87 metavar
='KERNEL_IMAGE',
91 p
.add_argument('initrd',
96 p
.add_argument('--version',
98 version
=f
'systemd {__version__}')
100 opts
= p
.parse_args(args
)
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)
106 opts
.entry_token
= mandatory_variable('KERNEL_INSTALL_ENTRY_TOKEN')
107 opts
.machine_id
= mandatory_variable('KERNEL_INSTALL_MACHINE_ID')
111 def we_are_wanted() -> bool:
112 KERNEL_INSTALL_LAYOUT
= os
.getenv('KERNEL_INSTALL_LAYOUT')
114 if KERNEL_INSTALL_LAYOUT
!= 'uki':
115 log(f
'{KERNEL_INSTALL_LAYOUT=}, quitting.')
118 KERNEL_INSTALL_UKI_GENERATOR
= os
.getenv('KERNEL_INSTALL_UKI_GENERATOR') or 'ukify'
120 if KERNEL_INSTALL_UKI_GENERATOR
!= 'ukify':
121 log(f
'{KERNEL_INSTALL_UKI_GENERATOR=}, quitting.')
124 log('KERNEL_INSTALL_LAYOUT and KERNEL_INSTALL_UKI_GENERATOR are good')
128 def input_file_location(
130 *search_directories
: str) -> Optional
[Path
]:
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',
139 for dir in search_directories
:
140 p
= Path(dir) / filename
146 def uki_conf_location() -> Optional
[Path
]:
147 return input_file_location('uki.conf')
150 def devicetree_config_location() -> Optional
[Path
]:
151 return input_file_location('devicetree')
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:
160 devicetree
= configfile
.read_text().strip()
162 raise ValueError(f
'{configfile!r} is empty')
164 path
= input_file_location(
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',
171 raise FileNotFoundError(f
'DeviceTree file {devicetree} not found')
175 def kernel_cmdline_base() -> list[str]:
176 path
= input_file_location('cmdline')
178 return path
.read_text().split()
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='))]
186 def kernel_cmdline(opts
) -> str:
187 options
= kernel_cmdline_base()
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}']
197 # TODO: we unconditionally set the cmdline here, ignoring the setting in
198 # the config file. Should we not do that?
200 # Prepend a space so that '@' does not get misinterpreted
201 return ' ' + ' '.join(options
)
204 def initrd_list(opts
) -> list[Path
]:
205 microcode
= sorted(opts
.staging_area
.glob('microcode*'))
206 initrd
= sorted(opts
.staging_area
.glob('initrd*'))
208 # Order taken from 90-loaderentry.install
209 return [*microcode
, *opts
.initrd
, *initrd
]
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
__)
219 def call_ukify(opts
) -> None:
220 ukify
= load_module('ukify', UKIFY
)
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'])
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'
234 if devicetree
:= devicetree_file_location(opts
):
235 opts2
.devicetree
= devicetree
237 opts2
.cmdline
= kernel_cmdline(opts
)
239 opts2
.stub
= BOOT_STUB
241 ukify
.apply_config(opts2
)
242 ukify
.finalize_options(opts2
)
243 ukify
.check_inputs(opts2
)
244 ukify
.make_uki(opts2
)
246 log(f
'{opts2.output} has been created')
251 if opts
.command
!= 'add':
253 if not we_are_wanted():
259 if __name__
== '__main__':