diff --git a/man/ukify.xml b/man/ukify.xml
index 4531ac89b2..098dacfb99 100644
--- a/man/ukify.xml
+++ b/man/ukify.xml
@@ -23,9 +23,8 @@
/usr/lib/systemd/ukify
- LINUX
- INITRD
OPTIONS
+ build
@@ -35,13 +34,18 @@
Note: this command is experimental for now. While it is intended to become a regular component of
systemd, it might still change in behaviour and interface.
- ukify is a tool that combines components (e.g.: a kernel and an initrd with
- a UEFI boot stub) to create a
+ ukify is a tool that combines components (usually a kernel, an initrd, and a
+ UEFI boot stub) to create a
Unified Kernel Image (UKI)
— a PE binary that can be executed by the firmware to start the embedded linux kernel.
See systemd-stub7
for details about the stub.
+ The two primary options that should be specified for the build verb are
+ Linux=/, and
+ Initrd=/. Initrd= accepts multiple
+ whitespace-separated paths and can be specified multiple times.
+
Additional sections will be inserted into the UKI, either automatically or only if a specific
option is provided. See the discussions of
Cmdline=/,
@@ -173,14 +177,14 @@
Linux=LINUX
- positional argument LINUX
+
A path to the kernel binary.
Initrd=INITRD...
- positional argument INITRD
+
Zero or more initrd paths. In the configuration file, items are separated by
whitespace. The initrds are combined in the order of specification, with the initrds specified in
@@ -399,9 +403,9 @@
Minimal invocation
- $ ukify \
- /lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
- /some/path/initramfs-6.0.9-300.fc37.x86_64.img \
+ $ ukify build \
+ --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
+ --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
--cmdline='quiet rw'
@@ -411,10 +415,10 @@
All the bells and whistles
- # /usr/lib/systemd/ukify \
- /lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
- early_cpio \
- /some/path/initramfs-6.0.9-300.fc37.x86_64.img \
+ # /usr/lib/systemd/ukify build \
+ --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
+ --initrd=early_cpio \
+ --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
--pcr-private-key=pcr-private-initrd-key.pem \
--pcr-public-key=pcr-public-initrd-key.pem \
--phases='enter-initrd' \
@@ -468,9 +472,9 @@ Phases=enter-initrd:leave-initrd
enter-initrd:leave-initrd:sysinit
enter-initrd:leave-initrd:sysinit:ready
-# /usr/lib/systemd/ukify -c ukify.conf \
- /lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
- /some/path/initramfs-6.0.9-300.fc37.x86_64.img
+# /usr/lib/systemd/ukify -c ukify.conf build \
+ --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
+ --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img
One "initrd" (early_cpio) is specified in the config file, and
@@ -482,7 +486,7 @@ Phases=enter-initrd:leave-initrd
Kernel command line auxiliary PE
- ukify \
+ ukify build \
--secureboot-private-key=sb.key \
--secureboot-certificate=sb.cert \
--cmdline='debug' \
diff --git a/src/boot/efi/meson.build b/src/boot/efi/meson.build
index b573e6996d..68c19fbcc9 100644
--- a/src/boot/efi/meson.build
+++ b/src/boot/efi/meson.build
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
efi_config_h_dir = meson.current_build_dir()
+efi_addon = ''
if efi_arch != ''
libefitest = static_library(
@@ -376,6 +377,11 @@ foreach efi_elf_binary : efi_elf_binaries
if name.startswith('linux')
boot_stubs += exe
endif
+
+ # This is supposed to match exactly one time
+ if name == 'addon@0@.efi.stub'.format(efi_arch)
+ efi_addon = exe.full_path()
+ endif
endforeach
alias_target('systemd-boot', boot_targets)
diff --git a/src/kernel-install/60-ukify.install.in b/src/kernel-install/60-ukify.install.in
index 7c29f7e8af..0927bd7a2e 100755
--- a/src/kernel-install/60-ukify.install.in
+++ b/src/kernel-install/60-ukify.install.in
@@ -183,11 +183,10 @@ def call_ukify(opts):
# The solution with runpy gives a dictionary, which isn't great, but will do.
ukify = runpy.run_path(UKIFY, run_name='ukify')
- # Create "empty" namespace. We want to override just a few settings,
- # so it doesn't make sense to duplicate all the fields. We use a hack
- # to pre-populate the namespace like argparse would, all defaults.
- # We need to specify the two mandatory arguments to not get an error.
- opts2 = ukify['create_parser']().parse_args(('A','B'))
+ # Create "empty" namespace. We want to override just a few settings, so it
+ # doesn't make sense to configure everything. We pretend to parse an empty
+ # argument set to prepopulate the namespace with the defaults.
+ opts2 = ukify['create_parser']().parse_args(())
opts2.config = config_file_location()
opts2.uname = opts.kernel_version
diff --git a/src/test/meson.build b/src/test/meson.build
index 84d642bf6e..fe1a0fec21 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -15,6 +15,10 @@ test_env.set('SYSTEMD_LANGUAGE_FALLBACK_MAP', language_fallback_map)
test_env.set('PATH', project_build_root + ':' + path)
test_env.set('PROJECT_BUILD_ROOT', project_build_root)
+if efi_addon != ''
+ test_env.set('EFI_ADDON', efi_addon)
+endif
+
############################################################
generate_sym_test_py = find_program('generate-sym-test.py')
diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py
index 692b7a384b..eae82c7f88 100755
--- a/src/ukify/test/test_ukify.py
+++ b/src/ukify/test/test_ukify.py
@@ -50,9 +50,9 @@ def test_round_up():
assert ukify.round_up(4097) == 8192
def test_namespace_creation():
- ns = ukify.create_parser().parse_args(('A','B'))
- assert ns.linux == pathlib.Path('A')
- assert ns.initrd == [pathlib.Path('B')]
+ ns = ukify.create_parser().parse_args(())
+ assert ns.linux is None
+ assert ns.initrd is None
def test_config_example():
ex = ukify.config_example()
@@ -87,7 +87,7 @@ def test_apply_config(tmp_path):
Phases = {':'.join(ukify.KNOWN_PHASES)}
'''))
- ns = ukify.create_parser().parse_args(('A','B'))
+ ns = ukify.create_parser().parse_args(())
ns.linux = None
ns.initrd = []
ukify.apply_config(ns, config)
@@ -143,7 +143,7 @@ def test_parse_args_minimal():
assert opts.os_release in (pathlib.Path('/etc/os-release'),
pathlib.Path('/usr/lib/os-release'))
-def test_parse_args_many():
+def test_parse_args_many_deprecated():
opts = ukify.parse_args(
['/ARG1', '///ARG2', '/ARG3 WITH SPACE',
'--cmdline=a b c',
@@ -186,9 +186,57 @@ def test_parse_args_many():
assert opts.output == pathlib.Path('OUTPUT')
assert opts.measure is False
+def test_parse_args_many():
+ opts = ukify.parse_args(
+ ['build',
+ '--linux=/ARG1',
+ '--initrd=///ARG2',
+ '--initrd=/ARG3 WITH SPACE',
+ '--cmdline=a b c',
+ '--os-release=K1=V1\nK2=V2',
+ '--devicetree=DDDDTTTT',
+ '--splash=splash',
+ '--pcrpkey=PATH',
+ '--uname=1.2.3',
+ '--stub=STUBPATH',
+ '--pcr-private-key=PKEY1',
+ '--pcr-public-key=PKEY2',
+ '--pcr-banks=SHA1,SHA256',
+ '--signing-engine=ENGINE',
+ '--secureboot-private-key=SBKEY',
+ '--secureboot-certificate=SBCERT',
+ '--sign-kernel',
+ '--no-sign-kernel',
+ '--tools=TOOLZ///',
+ '--output=OUTPUT',
+ '--measure',
+ '--no-measure',
+ ])
+ assert opts.linux == pathlib.Path('/ARG1')
+ assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')]
+ assert opts.cmdline == 'a b c'
+ assert opts.os_release == 'K1=V1\nK2=V2'
+ assert opts.devicetree == pathlib.Path('DDDDTTTT')
+ assert opts.splash == pathlib.Path('splash')
+ assert opts.pcrpkey == pathlib.Path('PATH')
+ assert opts.uname == '1.2.3'
+ assert opts.stub == pathlib.Path('STUBPATH')
+ assert opts.pcr_private_keys == [pathlib.Path('PKEY1')]
+ assert opts.pcr_public_keys == [pathlib.Path('PKEY2')]
+ assert opts.pcr_banks == ['SHA1', 'SHA256']
+ assert opts.signing_engine == 'ENGINE'
+ assert opts.sb_key == 'SBKEY'
+ assert opts.sb_cert == 'SBCERT'
+ assert opts.sign_kernel is False
+ assert opts.tools == [pathlib.Path('TOOLZ/')]
+ assert opts.output == pathlib.Path('OUTPUT')
+ assert opts.measure is False
+
def test_parse_sections():
opts = ukify.parse_args(
- ['/ARG1', '/ARG2',
+ ['build',
+ '--linux=/ARG1',
+ '--initrd=/ARG2',
'--section=test:TESTTESTTEST',
'--section=test2:@FILE',
])
@@ -239,7 +287,10 @@ def test_config_priority(tmp_path):
'''))
opts = ukify.parse_args(
- ['/ARG1', '///ARG2', '/ARG3 WITH SPACE',
+ ['build',
+ '--linux=/ARG1',
+ '--initrd=///ARG2',
+ '--initrd=/ARG3 WITH SPACE',
'--cmdline= a b c ',
'--os-release=K1=V1\nK2=V2',
'--devicetree=DDDDTTTT',
@@ -302,7 +353,7 @@ def test_help(capsys):
assert '--section' in out.out
assert not out.err
-def test_help_error(capsys):
+def test_help_error_deprecated(capsys):
with pytest.raises(SystemExit):
ukify.parse_args(['a', 'b', '--no-such-option'])
out = capsys.readouterr()
@@ -310,6 +361,14 @@ def test_help_error(capsys):
assert '--no-such-option' in out.err
assert len(out.err.splitlines()) == 1
+def test_help_error(capsys):
+ with pytest.raises(SystemExit):
+ ukify.parse_args(['build', '--no-such-option'])
+ out = capsys.readouterr()
+ assert not out.out
+ assert '--no-such-option' in out.err
+ assert len(out.err.splitlines()) == 1
+
@pytest.fixture(scope='session')
def kernel_initrd():
try:
@@ -326,7 +385,7 @@ def kernel_initrd():
initrd = f"{item['root']}{item['initrd'][0].split(' ')[0]}"
except (KeyError, IndexError):
continue
- return [linux, initrd]
+ return ['--linux', linux, '--initrd', initrd]
else:
return None
@@ -345,7 +404,11 @@ def test_basic_operation(kernel_initrd, tmpdir):
pytest.skip('linux+initrd not found')
output = f'{tmpdir}/basic.efi'
- opts = ukify.parse_args(kernel_initrd + [f'--output={output}'])
+ opts = ukify.parse_args([
+ 'build',
+ *kernel_initrd,
+ f'--output={output}',
+ ])
try:
ukify.check_inputs(opts)
except OSError as e:
@@ -362,6 +425,7 @@ def test_sections(kernel_initrd, tmpdir):
output = f'{tmpdir}/basic.efi'
opts = ukify.parse_args([
+ 'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@@ -385,15 +449,22 @@ def test_sections(kernel_initrd, tmpdir):
def test_addon(kernel_initrd, tmpdir):
output = f'{tmpdir}/addon.efi'
- opts = ukify.parse_args([
+ args = [
+ 'build',
f'--output={output}',
'--cmdline=ARG1 ARG2 ARG3',
'--section=.test:CONTENTZ',
- ])
+ ]
+ if stub := os.getenv('EFI_ADDON'):
+ args += [f'--stub={stub}']
+ expected_exceptions = ()
+ else:
+ expected_exceptions = FileNotFoundError,
+ opts = ukify.parse_args(args)
try:
ukify.check_inputs(opts)
- except OSError as e:
+ except expected_exceptions as e:
pytest.skip(str(e))
ukify.make_uki(opts)
@@ -416,7 +487,8 @@ def test_uname_scraping(kernel_initrd):
if kernel_initrd is None:
pytest.skip('linux+initrd not found')
- uname = ukify.Uname.scrape(kernel_initrd[0])
+ assert kernel_initrd[0] == '--linux'
+ uname = ukify.Uname.scrape(kernel_initrd[1])
assert re.match(r'\d+\.\d+\.\d+', uname)
def test_efi_signing_sbsign(kernel_initrd, tmpdir):
@@ -431,6 +503,7 @@ def test_efi_signing_sbsign(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
+ 'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@@ -474,6 +547,7 @@ def test_efi_signing_pesign(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
+ 'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@@ -501,10 +575,6 @@ def test_efi_signing_pesign(kernel_initrd, tmpdir):
def test_pcr_signing(kernel_initrd, tmpdir):
if kernel_initrd is None:
pytest.skip('linux+initrd not found')
- if os.getuid() != 0:
- pytest.skip('must be root to access tpm2')
- if subprocess.call(['systemd-creds', 'has-tpm2', '-q']) != 0:
- pytest.skip('tpm2 is not available')
ourdir = pathlib.Path(__file__).parent
pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
@@ -512,6 +582,7 @@ def test_pcr_signing(kernel_initrd, tmpdir):
output = f'{tmpdir}/signed.efi'
opts = ukify.parse_args([
+ 'build',
*kernel_initrd,
f'--output={output}',
'--uname=1.2.3',
@@ -562,10 +633,6 @@ def test_pcr_signing(kernel_initrd, tmpdir):
def test_pcr_signing2(kernel_initrd, tmpdir):
if kernel_initrd is None:
pytest.skip('linux+initrd not found')
- if os.getuid() != 0:
- pytest.skip('must be root to access tpm2')
- if subprocess.call(['systemd-creds', 'has-tpm2', '-q']) != 0:
- pytest.skip('tpm2 is not available')
ourdir = pathlib.Path(__file__).parent
pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
@@ -578,8 +645,12 @@ def test_pcr_signing2(kernel_initrd, tmpdir):
microcode.write(b'1234567890')
output = f'{tmpdir}/signed.efi'
+ assert kernel_initrd[0] == '--linux'
opts = ukify.parse_args([
- kernel_initrd[0], microcode.name, kernel_initrd[1],
+ 'build',
+ *kernel_initrd[:2],
+ f'--initrd={microcode.name}',
+ *kernel_initrd[2:],
f'--output={output}',
'--uname=1.2.3',
'--cmdline=ARG1 ARG2 ARG3',
diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py
index 19896afac3..a9c21601df 100755
--- a/src/ukify/ukify.py
+++ b/src/ukify/ukify.py
@@ -19,7 +19,9 @@
# pylint: disable=missing-docstring,invalid-name,import-outside-toplevel
# pylint: disable=consider-using-with,unspecified-encoding,line-too-long
# pylint: disable=too-many-locals,too-many-statements,too-many-return-statements
-# pylint: disable=too-many-branches,fixme
+# pylint: disable=too-many-branches,too-many-lines,too-many-instance-attributes
+# pylint: disable=too-many-arguments,unnecessary-lambda-assignment,fixme
+# pylint: disable=unused-argument
import argparse
import configparser
@@ -436,9 +438,9 @@ def call_systemd_measure(uki, linux, opts):
def join_initrds(initrds):
- if len(initrds) == 0:
+ if not initrds:
return None
- elif len(initrds) == 1:
+ if len(initrds) == 1:
return initrds[0]
seq = []
@@ -478,6 +480,9 @@ def pe_add_sections(uki: UKI, output: str):
pe.FILE_HEADER.IMAGE_FILE_LOCAL_SYMS_STRIPPED = True
# Old stubs might have been stripped, leading to unaligned raw data values, so let's fix them up here.
+ # pylint thinks that Structure doesn't have various members that it has…
+ # pylint: disable=no-member
+
for i, section in enumerate(pe.sections):
oldp = section.PointerToRawData
oldsz = section.SizeOfRawData
@@ -745,6 +750,7 @@ class ConfigItem:
) -> None:
"Set namespace.[idx] to value, with idx derived from group"
+ # pylint: disable=protected-access
if group not in namespace._groups:
namespace._groups += [group]
idx = namespace._groups.index(group)
@@ -814,7 +820,10 @@ class ConfigItem:
else:
conv = lambda s:s
- if self.nargs == '*':
+ # This is a bit ugly, but --initrd is the only option which is specified
+ # with multiple args on the command line and a space-separated list in the
+ # config file.
+ if self.name == '--initrd':
value = [conv(v) for v in value.split()]
else:
value = conv(value)
@@ -834,7 +843,16 @@ class ConfigItem:
return (section_name, key, value)
+VERBS = ('build',)
+
CONFIG_ITEMS = [
+ ConfigItem(
+ 'positional',
+ metavar = 'VERB',
+ nargs = '*',
+ help = f"operation to perform ({','.join(VERBS)})",
+ ),
+
ConfigItem(
'--version',
action = 'version',
@@ -848,20 +866,18 @@ CONFIG_ITEMS = [
),
ConfigItem(
- 'linux',
- metavar = 'LINUX',
+ '--linux',
type = pathlib.Path,
- nargs = '?',
help = 'vmlinuz file [.linux section]',
config_key = 'UKI/Linux',
),
ConfigItem(
- 'initrd',
- metavar = 'INITRD…',
+ '--initrd',
+ metavar = 'INITRD',
type = pathlib.Path,
- nargs = '*',
- help = 'initrd files [.initrd section]',
+ action = 'append',
+ help = 'initrd file [part of .initrd section]',
config_key = 'UKI/Initrd',
config_push = ConfigItem.config_list_prepend,
),
@@ -1068,7 +1084,7 @@ def apply_config(namespace, filename=None):
# Fill in ._groups based on --pcr-public-key=, --pcr-private-key=, and --phases=.
assert '_groups' not in namespace
n_pcr_priv = len(namespace.pcr_private_keys or ())
- namespace._groups = list(range(n_pcr_priv))
+ namespace._groups = list(range(n_pcr_priv)) # pylint: disable=protected-access
cp = configparser.ConfigParser(
comment_prefixes='#',
@@ -1193,6 +1209,20 @@ def parse_args(args=None):
p = create_parser()
opts = p.parse_args(args)
+ # Figure out which syntax is being used, one of:
+ # ukify verb --arg --arg --arg
+ # ukify linux initrd…
+ if len(opts.positional) == 1 and opts.positional[0] in VERBS:
+ opts.verb = opts.positional[0]
+ elif opts.linux or opts.initrd:
+ raise ValueError('--linux/--initrd options cannot be used with positional arguments')
+ else:
+ print("Assuming obsolete commandline syntax with no verb. Please use 'build'.")
+ if opts.positional:
+ opts.linux = pathlib.Path(opts.positional[0])
+ opts.initrd = [pathlib.Path(arg) for arg in opts.positional[1:]]
+ opts.verb = 'build'
+
# Check that --pcr-public-key=, --pcr-private-key=, and --phases=
# have either the same number of arguments are are not specified at all.
n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys)
@@ -1213,6 +1243,7 @@ def parse_args(args=None):
def main():
opts = parse_args()
check_inputs(opts)
+ assert opts.verb == 'build'
make_uki(opts)