From f5698a3f4119354dda704ae1f98522adeb206c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Behrmann?= Date: Thu, 10 Oct 2024 18:23:35 +0200 Subject: [PATCH 1/6] ukify: Add missing typing for iterator --- src/ukify/ukify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index e78c6119cb..8d122aaa42 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -504,7 +504,7 @@ def combine_signatures(pcrsigs: list[dict[str, str]]) -> str: return json.dumps(combined) -def key_path_groups(opts: argparse.Namespace) -> Iterator: +def key_path_groups(opts: argparse.Namespace) -> Iterator[tuple[str, Optional[Path], Optional[str]]]: if not opts.pcr_private_keys: return From 9ba53499b97d59cc57ce763b8ba7f272a876dd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Behrmann?= Date: Fri, 11 Oct 2024 14:34:15 +0200 Subject: [PATCH 2/6] ukify: Require both key and cert be set in generate_keys If either is None the write in this branch of the code will fail. --- src/ukify/ukify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 8d122aaa42..80d4bfd7e7 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -1176,7 +1176,7 @@ def generate_keys(opts: argparse.Namespace) -> None: # This will generate keys and certificates and write them to the paths that # are specified as input paths. - if opts.sb_key or opts.sb_cert: + if opts.sb_key and opts.sb_cert: fqdn = socket.getfqdn() cn = f'SecureBoot signing key on host {fqdn}' key_pem, cert_pem = generate_key_cert_pair( From 929d6225b221a20d22dc0a0ee1f03696916ed9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Behrmann?= Date: Sat, 12 Oct 2024 15:50:57 +0200 Subject: [PATCH 3/6] ukify: Fix type of UKI.executable --- src/ukify/ukify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 80d4bfd7e7..94fe16db27 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -387,7 +387,7 @@ class Section: @dataclasses.dataclass class UKI: - executable: list[Union[Path, str]] + executable: Path sections: list[Section] = dataclasses.field(default_factory=list, init=False) def add_section(self, section: Section) -> None: From 02eabaffe98c9a3b5dec1c4837968a4d3e2ff7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Behrmann?= Date: Thu, 10 Oct 2024 11:57:12 +0200 Subject: [PATCH 4/6] ukify: Add a unified interface for signing tools --- src/ukify/test/test_ukify.py | 2 +- src/ukify/ukify.py | 199 ++++++++++++++++++----------------- 2 files changed, 105 insertions(+), 96 deletions(-) diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py index 48182483c2..c547e7caee 100755 --- a/src/ukify/test/test_ukify.py +++ b/src/ukify/test/test_ukify.py @@ -364,7 +364,7 @@ def test_config_priority(tmp_path): pathlib.Path('some/path8')] assert opts.pcr_banks == ['SHA1', 'SHA256'] assert opts.signing_engine == 'ENGINE' - assert opts.signtool == 'sbsign' # from args + assert opts.signtool == ukify.SbSign # from args assert opts.sb_key == 'SBKEY' # from args assert opts.sb_cert == 'SBCERT' # from args assert opts.sb_certdir == 'some/path5' # from config diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 94fe16db27..accc977ac2 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -404,6 +404,81 @@ class UKI: self.sections += [section] +class SignTool: + @staticmethod + def sign(input_f: str, output_f: str, opts: argparse.Namespace) -> None: + raise NotImplementedError() + + @staticmethod + def verify(opts: argparse.Namespace) -> bool: + raise NotImplementedError() + + +class PeSign(SignTool): + @staticmethod + def sign(input_f: str, output_f: str, opts: argparse.Namespace) -> None: + assert opts.sb_certdir is not None + assert opts.sb_cert_name is not None + + tool = find_tool('pesign', opts=opts, msg='pesign, required for signing, is not installed') + cmd = [ + tool, + '-s', + '--force', + '-n', opts.sb_certdir, + '-c', opts.sb_cert_name, + '-i', input_f, + '-o', output_f, + ] # fmt: skip + + print('+', shell_join(cmd)) + subprocess.check_call(cmd) + + @staticmethod + def verify(opts: argparse.Namespace) -> bool: + assert opts.linux is not None + + tool = find_tool('pesign', opts=opts) + cmd = [tool, '-i', opts.linux, '-S'] + + print('+', shell_join(cmd)) + info = subprocess.check_output(cmd, text=True) + + return 'No signatures found.' in info + + +class SbSign(SignTool): + @staticmethod + def sign(input_f: str, output_f: str, opts: argparse.Namespace) -> None: + assert opts.sb_key is not None + assert opts.sb_cert is not None + + tool = find_tool('sbsign', opts=opts, msg='sbsign, required for signing, is not installed') + cmd = [ + tool, + '--key', opts.sb_key, + '--cert', opts.sb_cert, + *(['--engine', opts.signing_engine] if opts.signing_engine is not None else []), + input_f, + '--output', output_f, + ] # fmt: skip + + print('+', shell_join(cmd)) + subprocess.check_call(cmd) + + @staticmethod + def verify(opts: argparse.Namespace) -> bool: + assert opts.linux is not None + + tool = find_tool('sbverify', opts=opts) + cmd = [tool, '--list', opts.linux] + + print('+', shell_join(cmd)) + info = subprocess.check_output(cmd, text=True) + + return 'No signature table present' in info + + def parse_banks(s: str) -> list[str]: banks = re.split(r',|\s+', s) # TODO: do some sanity checking here @@ -798,79 +873,6 @@ def merge_sbat(input_pe: list[Path], input_text: list[str]) -> str: ) -def signer_sign(cmd: list[Union[str, Path]]) -> None: - print('+', shell_join(cmd)) - subprocess.check_call(cmd) - - -def sbsign_sign( - sbsign_tool: Union[str, Path], - input_f: str, - output_f: str, - opts: argparse.Namespace, -) -> None: - sign_invocation = [ - sbsign_tool, - '--key', opts.sb_key, - '--cert', opts.sb_cert, - ] # fmt: skip - if opts.signing_engine is not None: - sign_invocation += ['--engine', opts.signing_engine] - sign_invocation += [ - input_f, - '--output', output_f, - ] # fmt: skip - signer_sign(sign_invocation) - - -def pesign_sign( - pesign_tool: Union[str, Path], - input_f: str, - output_f: str, - opts: argparse.Namespace, -) -> None: - sign_invocation = [ - pesign_tool, - '-s', - '--force', - '-n', opts.sb_certdir, - '-c', opts.sb_cert_name, - '-i', input_f, - '-o', output_f, - ] # fmt: skip - signer_sign(sign_invocation) - - -SBVERIFY = { - 'name': 'sbverify', - 'option': '--list', - 'output': 'No signature table present', -} - -PESIGCHECK = { - 'name': 'pesign', - 'option': '-i', - 'output': 'No signatures found.', - 'flags': '-S', -} - - -def verify(tool: dict[str, str], opts: argparse.Namespace) -> bool: - verify_tool = find_tool(tool['name'], opts=opts) - cmd = [ - verify_tool, - tool['option'], - opts.linux, - ] - if 'flags' in tool: - cmd.append(tool['flags']) - - print('+', shell_join(cmd)) - info = subprocess.check_output(cmd, text=True) - - return tool['output'] in info - - STUB_SBAT = """\ sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md uki,1,UKI,uki,1,https://uapi-group.org/specifications/specs/unified_kernel_image/ @@ -885,32 +887,21 @@ uki-addon,1,UKI Addon,addon,1,https://www.freedesktop.org/software/systemd/man/l def make_uki(opts: argparse.Namespace) -> None: # kernel payload signing - sign_tool = None sign_args_present = opts.sb_key or opts.sb_cert_name sign_kernel = opts.sign_kernel - sign: Optional[Callable[[Union[str, Path], str, str, argparse.Namespace], None]] = None linux = opts.linux if sign_args_present: - if opts.signtool == 'sbsign': - sign_tool = find_tool('sbsign', opts=opts, msg='sbsign, required for signing, is not installed') - sign = sbsign_sign - verify_tool = SBVERIFY - else: - sign_tool = find_tool('pesign', opts=opts, msg='pesign, required for signing, is not installed') - sign = pesign_sign - verify_tool = PESIGCHECK + assert opts.signtool is not None - if sign_kernel is None and opts.linux is not None: + if not sign_kernel and opts.linux is not None: # figure out if we should sign the kernel - sign_kernel = verify(verify_tool, opts) + sign_kernel = opts.signtool.verify(opts) if sign_kernel: - assert sign is not None - assert sign_tool is not None linux_signed = tempfile.NamedTemporaryFile(prefix='linux-signed') linux = Path(linux_signed.name) - sign(sign_tool, opts.linux, linux, opts=opts) + opts.signtool.sign(opts.linux, linux, opts=opts) if opts.uname is None and opts.linux is not None: print('Kernel version not specified, starting autodetection 😖.') @@ -1067,9 +1058,8 @@ def make_uki(opts: argparse.Namespace) -> None: # UKI signing if sign_args_present: - assert sign is not None - assert sign_tool is not None - sign(sign_tool, unsigned_output, opts.output, opts) + assert opts.signtool is not None + opts.signtool.sign(unsigned_output, opts.output, opts) # We end up with no executable bits, let's reapply them os.umask(umask := os.umask(0)) @@ -1422,6 +1412,24 @@ class ConfigItem: return (section_name, key, value) +class SignToolAction(argparse.Action): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None] = None, + option_string: Optional[str] = None, + ) -> None: + if values is None: + setattr(namespace, 'signtool', None) + elif values == 'sbsign': + setattr(namespace, 'signtool', SbSign) + elif values == 'pesign': + setattr(namespace, 'signtool', PeSign) + else: + raise ValueError(f"Unknon signtool '{values}' (this is unreachable)") + + VERBS = ('build', 'genkey', 'inspect') CONFIG_ITEMS = [ @@ -1566,6 +1574,7 @@ CONFIG_ITEMS = [ ConfigItem( '--signtool', choices=('sbsign', 'pesign'), + action=SignToolAction, dest='signtool', help=( 'whether to use sbsign or pesign. It will also be inferred by the other ' @@ -1880,18 +1889,18 @@ def finalize_options(opts: argparse.Namespace) -> None: ) elif bool(opts.sb_key) and bool(opts.sb_cert): # both param given, infer sbsign and in case it was given, ensure signtool=sbsign - if opts.signtool and opts.signtool != 'sbsign': + if opts.signtool and opts.signtool != SbSign: raise ValueError( f'Cannot provide --signtool={opts.signtool} with --secureboot-private-key= and --secureboot-certificate=' # noqa: E501 ) - opts.signtool = 'sbsign' + opts.signtool = SbSign elif bool(opts.sb_cert_name): # sb_cert_name given, infer pesign and in case it was given, ensure signtool=pesign - if opts.signtool and opts.signtool != 'pesign': + if opts.signtool and opts.signtool != PeSign: raise ValueError( f'Cannot provide --signtool={opts.signtool} with --secureboot-certificate-name=' ) - opts.signtool = 'pesign' + opts.signtool = PeSign if opts.sign_kernel and not opts.sb_key and not opts.sb_cert_name: raise ValueError( From f1b6430ecf947897cbe802abb0598d6423e4841a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Behrmann?= Date: Mon, 14 Oct 2024 09:58:05 +0200 Subject: [PATCH 5/6] ukify: Add UkifyConfig Using a dataclass instead of an argparse namespace to pass around the parsed options allows to track the types properly. --- src/ukify/ukify.py | 122 ++++++++++++++++++++++++++++++++------------- 1 file changed, 86 insertions(+), 36 deletions(-) diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index accc977ac2..ebb4985cfc 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -26,6 +26,7 @@ import contextlib import dataclasses import datetime import fnmatch +import inspect import itertools import json import os @@ -48,6 +49,7 @@ from typing import ( IO, Any, Callable, + Literal, Optional, TypeVar, Union, @@ -230,6 +232,50 @@ def maybe_decompress(filename: Union[str, Path]) -> bytes: raise NotImplementedError(f'unknown file format (starts with {start!r})') +@dataclasses.dataclass +class UkifyConfig: + all: bool + cmdline: Union[str, Path, None] + devicetree: Path + efi_arch: str + initrd: list[Path] + join_profiles: list[Path] + json: Union[Literal['pretty'], Literal['short'], Literal['off']] + linux: Optional[Path] + measure: bool + microcode: Path + os_release: Union[str, Path, None] + output: Optional[str] + pcr_banks: list[str] + pcr_private_keys: list[str] + pcr_public_keys: list[Path] + pcrpkey: Optional[Path] + phase_path_groups: Optional[list[str]] + profile: Union[str, Path, None] + sb_cert: Path + sb_cert_name: Optional[str] + sb_cert_validity: int + sb_certdir: Path + sb_key: Optional[Path] + sbat: Optional[list[str]] + sections: list['Section'] + sections_by_name: dict[str, 'Section'] + sign_kernel: bool + signing_engine: Optional[str] + signtool: Optional[type['SignTool']] + splash: Optional[Path] + stub: Path + summary: bool + tools: list[Path] + uname: Optional[str] + verb: str + files: list[str] = dataclasses.field(default_factory=list) + + @classmethod + def from_namespace(cls, ns: argparse.Namespace) -> 'UkifyConfig': + return cls(**{k: v for k, v in vars(ns).items() if k in inspect.signature(cls).parameters}) + + class Uname: # This class is here purely as a namespace for the functions @@ -243,7 +289,7 @@ class Uname: TEXT_PATTERN = rb'Linux version (?P\d\.\S+) \(' @classmethod - def scrape_x86(cls, filename: Path, opts: Optional[argparse.Namespace] = None) -> str: + def scrape_x86(cls, filename: Path, opts: Optional[UkifyConfig] = None) -> str: # Based on https://gitlab.archlinux.org/archlinux/mkinitcpio/mkinitcpio/-/blob/master/functions#L136 # and https://docs.kernel.org/arch/x86/boot.html#the-real-mode-kernel-header with open(filename, 'rb') as f: @@ -263,7 +309,7 @@ class Uname: return m.group('version') @classmethod - def scrape_elf(cls, filename: Path, opts: Optional[argparse.Namespace] = None) -> str: + def scrape_elf(cls, filename: Path, opts: Optional[UkifyConfig] = None) -> str: readelf = find_tool('readelf', opts=opts) cmd = [ @@ -285,7 +331,7 @@ class Uname: return text.rstrip('\0') @classmethod - def scrape_generic(cls, filename: Path, opts: Optional[argparse.Namespace] = None) -> str: + def scrape_generic(cls, filename: Path, opts: Optional[UkifyConfig] = None) -> str: # import libarchive # libarchive-c fails with # ArchiveError: Unrecognized archive format (errno=84, retcode=-30, archive_p=94705420454656) @@ -299,7 +345,7 @@ class Uname: return m.group('version').decode() @classmethod - def scrape(cls, filename: Path, opts: Optional[argparse.Namespace] = None) -> Optional[str]: + def scrape(cls, filename: Path, opts: Optional[UkifyConfig] = None) -> Optional[str]: for func in (cls.scrape_x86, cls.scrape_elf, cls.scrape_generic): try: version = func(filename, opts=opts) @@ -406,17 +452,17 @@ class UKI: class SignTool: @staticmethod - def sign(input_f: str, output_f: str, opts: argparse.Namespace) -> None: + def sign(input_f: str, output_f: str, opts: UkifyConfig) -> None: raise NotImplementedError() @staticmethod - def verify(opts: argparse.Namespace) -> bool: + def verify(opts: UkifyConfig) -> bool: raise NotImplementedError() class PeSign(SignTool): @staticmethod - def sign(input_f: str, output_f: str, opts: argparse.Namespace) -> None: + def sign(input_f: str, output_f: str, opts: UkifyConfig) -> None: assert opts.sb_certdir is not None assert opts.sb_cert_name is not None @@ -435,7 +481,7 @@ class PeSign(SignTool): subprocess.check_call(cmd) @staticmethod - def verify(opts: argparse.Namespace) -> bool: + def verify(opts: UkifyConfig) -> bool: assert opts.linux is not None tool = find_tool('pesign', opts=opts) @@ -449,7 +495,7 @@ class PeSign(SignTool): class SbSign(SignTool): @staticmethod - def sign(input_f: str, output_f: str, opts: argparse.Namespace) -> None: + def sign(input_f: str, output_f: str, opts: UkifyConfig) -> None: assert opts.sb_key is not None assert opts.sb_cert is not None @@ -467,7 +513,7 @@ class SbSign(SignTool): subprocess.check_call(cmd) @staticmethod - def verify(opts: argparse.Namespace) -> bool: + def verify(opts: UkifyConfig) -> bool: assert opts.linux is not None tool = find_tool('sbverify', opts=opts) @@ -507,7 +553,7 @@ def parse_phase_paths(s: str) -> list[str]: return paths -def check_splash(filename: Optional[str]) -> None: +def check_splash(filename: Optional[Path]) -> None: if filename is None: return @@ -521,7 +567,7 @@ def check_splash(filename: Optional[str]) -> None: print(f'Splash image {filename} is {img.width}×{img.height} pixels') -def check_inputs(opts: argparse.Namespace) -> None: +def check_inputs(opts: UkifyConfig) -> None: for name, value in vars(opts).items(): if name in {'output', 'tools'}: continue @@ -537,9 +583,9 @@ def check_inputs(opts: argparse.Namespace) -> None: check_splash(opts.splash) -def check_cert_and_keys_nonexistent(opts: argparse.Namespace) -> None: +def check_cert_and_keys_nonexistent(opts: UkifyConfig) -> None: # Raise if any of the keys and certs are found on disk - paths = itertools.chain( + paths: Iterator[Union[str, Path, None]] = itertools.chain( (opts.sb_key, opts.sb_cert), *((priv_key, pub_key) for priv_key, pub_key, _ in key_path_groups(opts)), ) @@ -551,14 +597,14 @@ def check_cert_and_keys_nonexistent(opts: argparse.Namespace) -> None: def find_tool( name: str, fallback: Optional[str] = None, - opts: Optional[argparse.Namespace] = None, + opts: Optional[UkifyConfig] = None, msg: str = 'Tool {name} not installed!', ) -> Union[str, Path]: if opts and opts.tools: for d in opts.tools: tool = d / name if tool.exists(): - return cast(Path, tool) + return tool if shutil.which(name) is not None: return name @@ -579,18 +625,19 @@ def combine_signatures(pcrsigs: list[dict[str, str]]) -> str: return json.dumps(combined) -def key_path_groups(opts: argparse.Namespace) -> Iterator[tuple[str, Optional[Path], Optional[str]]]: +def key_path_groups(opts: UkifyConfig) -> Iterator[tuple[str, Optional[Path], Optional[str]]]: if not opts.pcr_private_keys: return n_priv = len(opts.pcr_private_keys) - pub_keys = opts.pcr_public_keys or [None] * n_priv - pp_groups = opts.phase_path_groups or [None] * n_priv + pub_keys = opts.pcr_public_keys or [] + pp_groups = opts.phase_path_groups or [] - yield from zip( + yield from itertools.zip_longest( opts.pcr_private_keys, - pub_keys, - pp_groups, + pub_keys[:n_priv], + pp_groups[:n_priv], + fillvalue=None, ) @@ -598,7 +645,7 @@ def pe_strip_section_name(name: bytes) -> str: return name.rstrip(b'\x00').decode() -def call_systemd_measure(uki: UKI, opts: argparse.Namespace, profile_start: int = 0) -> None: +def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) -> None: measure_tool = find_tool( 'systemd-measure', '/usr/lib/systemd/systemd-measure', @@ -884,7 +931,9 @@ uki-addon,1,UKI Addon,addon,1,https://www.freedesktop.org/software/systemd/man/l """ -def make_uki(opts: argparse.Namespace) -> None: +def make_uki(opts: UkifyConfig) -> None: + assert opts.output is not None + # kernel payload signing sign_args_present = opts.sb_key or opts.sb_cert_name @@ -892,16 +941,17 @@ def make_uki(opts: argparse.Namespace) -> None: linux = opts.linux if sign_args_present: + assert opts.linux is not None assert opts.signtool is not None - if not sign_kernel and opts.linux is not None: + if not sign_kernel: # figure out if we should sign the kernel sign_kernel = opts.signtool.verify(opts) if sign_kernel: linux_signed = tempfile.NamedTemporaryFile(prefix='linux-signed') linux = Path(linux_signed.name) - opts.signtool.sign(opts.linux, linux, opts=opts) + opts.signtool.sign(os.fspath(opts.linux), os.fspath(linux), opts=opts) if opts.uname is None and opts.linux is not None: print('Kernel version not specified, starting autodetection 😖.') @@ -910,7 +960,7 @@ def make_uki(opts: argparse.Namespace) -> None: uki = UKI(opts.stub) initrd = join_initrds(opts.initrd) - pcrpkey = opts.pcrpkey + pcrpkey: Union[bytes, Path, None] = opts.pcrpkey if pcrpkey is None: if opts.pcr_public_keys and len(opts.pcr_public_keys) == 1: pcrpkey = opts.pcr_public_keys[0] @@ -1032,15 +1082,15 @@ def make_uki(opts: argparse.Namespace) -> None: if names.count('.profile') > 1: raise ValueError(f'Profile PE binary {profile} contains multiple .profile sections') - for section in pe.sections: - n = pe_strip_section_name(section.Name) + for pesection in pe.sections: + n = pe_strip_section_name(pesection.Name) if n not in to_import: continue - print(f"Copying section '{n}' from '{profile}': {section.Misc_VirtualSize} bytes") + print(f"Copying section '{n}' from '{profile}': {pesection.Misc_VirtualSize} bytes") uki.add_section( - Section.create(n, section.get_data(length=section.Misc_VirtualSize), measure=True) + Section.create(n, pesection.get_data(length=pesection.Misc_VirtualSize), measure=True) ) call_systemd_measure(uki, opts=opts, profile_start=prev_len) @@ -1059,7 +1109,7 @@ def make_uki(opts: argparse.Namespace) -> None: if sign_args_present: assert opts.signtool is not None - opts.signtool.sign(unsigned_output, opts.output, opts) + opts.signtool.sign(os.fspath(unsigned_output), os.fspath(opts.output), opts) # We end up with no executable bits, let's reapply them os.umask(umask := os.umask(0)) @@ -1161,7 +1211,7 @@ def generate_priv_pub_key_pair(keylength: int = 2048) -> tuple[bytes, bytes]: return priv_key_pem, pub_key_pem -def generate_keys(opts: argparse.Namespace) -> None: +def generate_keys(opts: UkifyConfig) -> None: work = False # This will generate keys and certificates and write them to the paths that @@ -1200,7 +1250,7 @@ def generate_keys(opts: argparse.Namespace) -> None: def inspect_section( - opts: argparse.Namespace, + opts: UkifyConfig, section: pefile.SectionStructure, ) -> tuple[str, Optional[dict[str, Union[int, str]]]]: name = pe_strip_section_name(section.Name) @@ -1243,7 +1293,7 @@ def inspect_section( return name, struct -def inspect_sections(opts: argparse.Namespace) -> None: +def inspect_sections(opts: UkifyConfig) -> None: indent = 4 if opts.json == 'pretty' else None for file in opts.files: @@ -1935,7 +1985,7 @@ def parse_args(args: Optional[list[str]] = None) -> argparse.Namespace: def main() -> None: - opts = parse_args() + opts = UkifyConfig.from_namespace(parse_args()) if opts.summary: # TODO: replace pprint() with some fancy formatting. pprint.pprint(vars(opts)) From 467d21a7104ddbdbe793dbb1493e2237b8b8e905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Behrmann?= Date: Sun, 13 Oct 2024 15:20:44 +0200 Subject: [PATCH 6/6] ukify: Add more mypy options This achieves parity with the mypy config used in mkosi. The import of builtins is needed because otherwise type is attempted to be resolved to the variable in the scope. --- src/ukify/mypy.ini | 3 ++- src/ukify/ukify.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ukify/mypy.ini b/src/ukify/mypy.ini index bae4e70f44..c9836169c1 100644 --- a/src/ukify/mypy.ini +++ b/src/ukify/mypy.ini @@ -1,8 +1,9 @@ [mypy] python_version = 3.9 -allow_redefinition = True # belonging to --strict warn_unused_configs = true +disallow_any_generics = true +disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_defs = true disallow_untyped_decorators = true diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index ebb4985cfc..f0d7ee60b7 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -20,6 +20,7 @@ # pylint: disable=unnecessary-lambda-assignment import argparse +import builtins import collections import configparser import contextlib @@ -1386,9 +1387,9 @@ class ConfigItem: name: Union[str, tuple[str, str]] dest: Optional[str] = None metavar: Optional[str] = None - type: Optional[Callable] = None + type: Optional[Callable[[str], Any]] = None nargs: Optional[str] = None - action: Optional[Union[str, Callable]] = None + action: Optional[Union[str, Callable[[str], Any], builtins.type[argparse.Action]]] = None default: Any = None version: Optional[str] = None choices: Optional[tuple[str, ...]] = None