diff --git a/man/ukify.xml b/man/ukify.xml
index 4531ac89b28..098dacfb99f 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/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py
index ac25c71e9e0..eae82c7f88f 100755
--- a/src/ukify/test/test_ukify.py
+++ b/src/ukify/test/test_ukify.py
@@ -52,7 +52,7 @@ def test_round_up():
def test_namespace_creation():
ns = ukify.create_parser().parse_args(())
assert ns.linux is None
- assert ns.initrd == []
+ assert ns.initrd is None
def test_config_example():
ex = ukify.config_example()
@@ -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',
@@ -386,6 +450,7 @@ def test_sections(kernel_initrd, tmpdir):
def test_addon(kernel_initrd, tmpdir):
output = f'{tmpdir}/addon.efi'
args = [
+ 'build',
f'--output={output}',
'--cmdline=ARG1 ARG2 ARG3',
'--section=.test:CONTENTZ',
@@ -422,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):
@@ -437,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',
@@ -480,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',
@@ -514,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',
@@ -576,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 88189d272d9..a9c21601df9 100755
--- a/src/ukify/ukify.py
+++ b/src/ukify/ukify.py
@@ -438,7 +438,7 @@ def call_systemd_measure(uki, linux, opts):
def join_initrds(initrds):
- if len(initrds) == 0:
+ if not initrds:
return None
if len(initrds) == 1:
return initrds[0]
@@ -820,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)
@@ -840,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',
@@ -854,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,
),
@@ -1199,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)
@@ -1219,6 +1243,7 @@ def parse_args(args=None):
def main():
opts = parse_args()
check_inputs(opts)
+ assert opts.verb == 'build'
make_uki(opts)