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)