]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
ukify: add 'build' verb 27938/head
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Tue, 6 Jun 2023 11:23:49 +0000 (13:23 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Tue, 6 Jun 2023 13:45:57 +0000 (15:45 +0200)
The old syntax with linux + initrds as positional arguments is still accepted,
but a warning is emitted. We should remove the support for this after the
next release or so.

Adding a single verb by itself is not very useful, but opens the door to adding
other verbs.

man/ukify.xml
src/ukify/test/test_ukify.py
src/ukify/ukify.py

index 4531ac89b2877f53bc0724a7ddd392e06967d5af..098dacfb99fa99a192e732ebd31c707d8bb8edbe 100644 (file)
@@ -23,9 +23,8 @@
   <refsynopsisdiv>
     <cmdsynopsis>
       <command>/usr/lib/systemd/ukify</command>
-      <arg choice="opt"><replaceable>LINUX</replaceable></arg>
-      <arg choice="opt" rep="repeat"><replaceable>INITRD</replaceable></arg>
       <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain">build</arg>
     </cmdsynopsis>
   </refsynopsisdiv>
 
     <para>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.</para>
 
-    <para><command>ukify</command> is a tool that combines components (e.g.: a kernel and an initrd with
-    UEFI boot stub) to create a
+    <para><command>ukify</command> is a tool that combines components (usually a kernel, an initrd, and a
+    UEFI boot stub) to create a
     <ulink url="https://uapi-group.org/specifications/specs/unified_kernel_image/">Unified Kernel Image (UKI)</ulink>
     — a PE binary that can be executed by the firmware to start the embedded linux kernel.
     See <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>
     for details about the stub.</para>
 
+    <para>The two primary options that should be specified for the <command>build</command> verb are
+    <varname>Linux=</varname>/<option>--linux=</option>, and
+    <varname>Initrd=</varname>/<option>--initrd=</option>. <varname>Initrd=</varname> accepts multiple
+    whitespace-separated paths and <option>--initrd=</option> can be specified multiple times.</para>
+
     <para>Additional sections will be inserted into the UKI, either automatically or only if a specific
     option is provided. See the discussions of
     <varname>Cmdline=</varname>/<option>--cmdline=</option>,
       <variablelist>
         <varlistentry>
           <term><varname>Linux=<replaceable>LINUX</replaceable></varname></term>
-          <term>positional argument <replaceable>LINUX</replaceable></term>
+          <term><option>--linux=<replaceable>LINUX</replaceable></option></term>
 
           <listitem><para>A path to the kernel binary.</para></listitem>
         </varlistentry>
 
         <varlistentry>
           <term><varname>Initrd=<replaceable>INITRD</replaceable>...</varname></term>
-          <term>positional argument <replaceable>INITRD</replaceable></term>
+          <term><option>--initrd=<replaceable>LINUX</replaceable></option></term>
 
           <listitem><para>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
     <example>
       <title>Minimal invocation</title>
 
-      <programlisting>$ ukify \
-      /lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
-      /some/path/initramfs-6.0.9-300.fc37.x86_64.img \
+      <programlisting>$ 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'
       </programlisting>
 
     <example>
       <title>All the bells and whistles</title>
 
-      <programlisting># /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 \
+      <programlisting># /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
       </programlisting>
 
       <para>One "initrd" (<filename index='false'>early_cpio</filename>) is specified in the config file, and
@@ -482,7 +486,7 @@ Phases=enter-initrd:leave-initrd
     <example>
       <title>Kernel command line auxiliary PE</title>
 
-      <programlisting>ukify \
+      <programlisting>ukify build \
       --secureboot-private-key=sb.key \
       --secureboot-certificate=sb.cert \
       --cmdline='debug' \
index ac25c71e9e0986a1a002b0fa7e6bd7b2077f6ae1..eae82c7f88fe4eee86a6fc1c650966a48e48c776 100755 (executable)
@@ -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',
index 88189d272d9e11cbeabb537b7ac08980c632319e..a9c21601df9827363ba1a71360953fede2b8f250 100755 (executable)
@@ -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)