]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Revert "Bump minimum python version to 3.10" 4169/head
authorDaan De Meyer <daan@amutable.com>
Sat, 14 Feb 2026 13:24:29 +0000 (14:24 +0100)
committerDaan De Meyer <daan@amutable.com>
Sat, 14 Feb 2026 13:29:40 +0000 (14:29 +0100)
This reverts commit 22b2f0bf18ac98f62ef92745fde5dd3f8369d4bf.

Turns out using python3.12 on CentOS causes more issues than
we thought it would, so let's revert the move to python 3.9.
Instead, we'll conditionally import Union in sandbox.py only on
python 3.9 and use the Union operator otherwise.

30 files changed:
bin/mkosi
docs/CODING_STYLE.md
kernel-install/50-mkosi.install
mkosi/__init__.py
mkosi/__main__.py
mkosi/archive.py
mkosi/bootloader.py
mkosi/completion.py
mkosi/config.py
mkosi/context.py
mkosi/curl.py
mkosi/distribution/__init__.py
mkosi/distribution/opensuse.py
mkosi/distribution/rhel.py
mkosi/initrd.py
mkosi/installer/apt.py
mkosi/installer/dnf.py
mkosi/installer/rpm.py
mkosi/log.py
mkosi/manifest.py
mkosi/mounts.py
mkosi/pager.py
mkosi/partition.py
mkosi/qemu.py
mkosi/resources/man/mkosi.1.md
mkosi/run.py
mkosi/util.py
pyproject.toml
tests/__init__.py
tests/test_json.py

index 930b171f37d0762d13d6250ec9d42ede2af77098..9abee8f133ff3c40f51faf6827662299a09b77b1 100755 (executable)
--- a/bin/mkosi
+++ b/bin/mkosi
@@ -8,7 +8,7 @@ command="$(basename "${BASH_SOURCE[0]//-/.}")"
 if [ -z "$MKOSI_INTERPRETER" ]; then
     # Note the check seems to be inverted here because the if branch is
     # executed when the exit status is 0 which is equal to False in Python.
-    if python3 -c 'import sys; sys.exit(sys.version_info < (3, 10))'; then
+    if python3 -c 'import sys; sys.exit(sys.version_info < (3, 9))'; then
         MKOSI_INTERPRETER=python3
     else
         # python3 is not found or too old, search $PATH for the newest interpreter.
@@ -21,7 +21,7 @@ if [ -z "$MKOSI_INTERPRETER" ]; then
             done | sort --unique --version-sort --reverse | head --lines=1
         )"
 
-        if [ -n "$candidate" ] && "$candidate" -c 'import sys; sys.exit(sys.version_info < (3, 10))'; then
+        if [ -n "$candidate" ] && "$candidate" -c 'import sys; sys.exit(sys.version_info < (3, 9))'; then
             MKOSI_INTERPRETER="$candidate"
         fi
     fi
index 2920437cdc01c669c0839a9a9cefc7fd906b4141..ea7b7f9178e4381283673a602e8791e8961e1764 100644 (file)
@@ -9,7 +9,7 @@ SPDX-License-Identifier: LGPL-2.1-or-later
 
 ## Python Version
 
-- The lowest supported Python version is CPython 3.10.
+- The lowest supported Python version is CPython 3.9.
 
 ## Formatting
 
index d51451a2eb34e51d1921d1025e59561a7b81032b..bff9e4c769c6ab228a2b1f6b4f7ed7c0d09a2bf1 100755 (executable)
@@ -6,6 +6,7 @@ import os
 import sys
 import tempfile
 from pathlib import Path
+from typing import Optional
 
 from mkosi import identify_cpu
 from mkosi.archive import make_cpio
@@ -21,7 +22,7 @@ def we_are_wanted(context: KernelInstallContext) -> bool:
     return context.uki_generator == "mkosi" or context.initrd_generator in ("mkosi", "mkosi-initrd")
 
 
-def build_microcode_initrd(output: Path) -> Path | None:
+def build_microcode_initrd(output: Path) -> Optional[Path]:
     vendor, ucode = identify_cpu(Path("/"))
 
     if vendor is None:
index f92ec1b5ec8294178cee07b4f0152bfbd6d483a9..a38ce440c256f0c3018558661778f4cb5999b0df 100644 (file)
@@ -25,7 +25,7 @@ import zipapp
 from collections.abc import Iterator, Mapping, Sequence
 from contextlib import AbstractContextManager
 from pathlib import Path
-from typing import Any, cast
+from typing import Any, Optional, Union, cast
 
 from mkosi.archive import can_extract_tar, extract_tar, make_cpio, make_tar
 from mkosi.bootloader import (
@@ -1186,7 +1186,7 @@ def install_tree(
     src: Path,
     dst: Path,
     *,
-    target: Path | None = None,
+    target: Optional[Path] = None,
     preserve: bool = True,
 ) -> None:
     src = src.resolve()
@@ -1366,7 +1366,7 @@ def gzip_binary(context: Context) -> str:
     return "pigz" if context.config.find_binary("pigz") else "gzip"
 
 
-def kernel_get_ver_from_modules(context: Context) -> str | None:
+def kernel_get_ver_from_modules(context: Context) -> Optional[str]:
     # Try to get version from the first dir under usr/lib/modules but fail if multiple versions are found
     versions = [
         p.name for p in (context.root / "usr/lib/modules").glob("*") if KERNEL_VERSION_PATTERN.match(p.name)
@@ -1403,7 +1403,7 @@ def fixup_vmlinuz_location(context: Context) -> None:
             filename = d.name.removeprefix(f"{type}-")
             match = KERNEL_VERSION_PATTERN.search(filename)
 
-            kver: str | None
+            kver: Optional[str]
             if match:
                 kver = match.group(0)
             else:
@@ -1426,7 +1426,7 @@ def fixup_vmlinuz_location(context: Context) -> None:
                 copyfile2(d, vmlinuz)
 
 
-def identify_cpu(root: Path) -> tuple[Path | None, Path | None]:
+def identify_cpu(root: Path) -> tuple[Optional[Path], Optional[Path]]:
     for entry in Path("/proc/cpuinfo").read_text().split("\n\n"):
         vendor_id = family = model = stepping = None
         for line in entry.splitlines():
@@ -1872,7 +1872,7 @@ def systemd_stub_binary(context: Context) -> Path:
     return stub
 
 
-def systemd_stub_version(context: Context, stub: Path) -> GenericVersion | None:
+def systemd_stub_version(context: Context, stub: Path) -> Optional[GenericVersion]:
     try:
         sdmagic = extract_pe_section(context, stub, ".sdmagic", context.workspace / "sdmagic")
     except KeyError:
@@ -1937,7 +1937,9 @@ def find_entry_token(context: Context) -> str:
     return cast(str, output["EntryToken"])
 
 
-def finalize_cmdline(context: Context, partitions: Sequence[Partition], roothash: str | None) -> list[str]:
+def finalize_cmdline(
+    context: Context, partitions: Sequence[Partition], roothash: Optional[str]
+) -> list[str]:
     if (context.root / "etc/kernel/cmdline").exists():
         cmdline = [(context.root / "etc/kernel/cmdline").read_text().strip()]
     elif (context.root / "usr/lib/kernel/cmdline").exists():
@@ -2369,7 +2371,7 @@ def maybe_compress(
     context: Context,
     compression: Compression,
     src: Path,
-    dst: Path | None = None,
+    dst: Optional[Path] = None,
 ) -> None:
     if not compression or src.is_dir():
         if dst:
@@ -2403,7 +2405,7 @@ def copy_nspawn_settings(context: Context) -> None:
         copyfile2(context.config.nspawn_settings, context.staging / context.config.output_nspawn_settings)
 
 
-def get_uki_path(context: Context) -> Path | None:
+def get_uki_path(context: Context) -> Optional[Path]:
     if not want_efi(context.config) or context.config.unified_kernel_images == UnifiedKernelImage.none:
         return None
 
@@ -2593,7 +2595,7 @@ def calculate_signature_sop(context: Context) -> None:
         )  # fmt: skip
 
 
-def dir_size(path: Path | os.DirEntry[str]) -> int:
+def dir_size(path: Union[Path, os.DirEntry[str]]) -> int:
     dir_sum = 0
     for entry in os.scandir(path):
         if entry.is_symlink():
@@ -2608,7 +2610,7 @@ def dir_size(path: Path | os.DirEntry[str]) -> int:
     return dir_sum
 
 
-def save_manifest(context: Context, manifest: Manifest | None) -> None:
+def save_manifest(context: Context, manifest: Optional[Manifest]) -> None:
     if not manifest:
         return
 
@@ -2787,7 +2789,7 @@ def check_inputs(config: Config) -> None:
         )
 
 
-def check_tool(config: Config, *tools: PathString, reason: str, hint: str | None = None) -> Path:
+def check_tool(config: Config, *tools: PathString, reason: str, hint: Optional[str] = None) -> Path:
     tool = config.find_binary(*tools)
     if not tool:
         die(f"Could not find '{tools[0]}' which is required to {reason}.", hint=hint)
@@ -2800,7 +2802,7 @@ def check_systemd_tool(
     *tools: PathString,
     version: str,
     reason: str,
-    hint: str | None = None,
+    hint: Optional[str] = None,
 ) -> None:
     tool = check_tool(config, *tools, reason=reason, hint=hint)
 
@@ -2816,7 +2818,7 @@ def check_ukify(
     config: Config,
     version: str,
     reason: str,
-    hint: str | None = None,
+    hint: Optional[str] = None,
 ) -> None:
     ukify = check_tool(config, "ukify", "/usr/lib/systemd/ukify", reason=reason, hint=hint)
 
@@ -3371,7 +3373,7 @@ def reuse_cache(context: Context) -> bool:
 
 def save_esp_components(
     context: Context,
-) -> tuple[Path | None, str | None, Path | None, list[Path]]:
+) -> tuple[Optional[Path], Optional[str], Optional[Path], list[Path]]:
     if context.config.output_format == OutputFormat.addon:
         stub = systemd_addon_stub_binary(context)
         if not stub.exists():
@@ -3467,7 +3469,7 @@ def make_image(
         cmdline += ["--definitions", workdir(d)]
         opts += ["--ro-bind", d, workdir(d)]
 
-    def can_orphan_file(distribution: Distribution | str | None, release: str | None) -> bool:
+    def can_orphan_file(distribution: Union[Distribution, str, None], release: Optional[str]) -> bool:
         if not isinstance(distribution, Distribution):
             return True
 
@@ -3741,9 +3743,9 @@ def make_oci(context: Context, root_layer: Path, dst: Path) -> None:
 
 def make_esp(
     context: Context,
-    stub: Path | None,
-    kver: str | None,
-    kimg: Path | None,
+    stub: Optional[Path],
+    kver: Optional[str],
+    kimg: Optional[Path],
     microcode: list[Path],
 ) -> list[Partition]:
     if not context.config.architecture.to_efi():
@@ -3930,7 +3932,7 @@ def clamp_mtime(path: Path, mtime: int) -> None:
         os.utime(path, ns=updated, follow_symlinks=False)
 
 
-def normalize_mtime(root: Path, mtime: int | None, directory: Path = Path("")) -> None:
+def normalize_mtime(root: Path, mtime: Optional[int], directory: Path = Path("")) -> None:
     if mtime is None:
         return
 
@@ -4475,7 +4477,7 @@ def run_coredumpctl(args: Args, config: Config) -> None:
     run_systemd_tool("coredumpctl", args, config)
 
 
-def start_storage_target_mode(config: Config) -> AbstractContextManager[Popen | None]:
+def start_storage_target_mode(config: Config) -> AbstractContextManager[Optional[Popen]]:
     if config.storage_target_mode == ConfigFeature.disabled:
         return contextlib.nullcontext()
 
@@ -4915,7 +4917,7 @@ def run_build(
     resources: Path,
     keyring_dir: Path,
     metadata_dir: Path,
-    package_dir: Path | None = None,
+    package_dir: Optional[Path] = None,
 ) -> None:
     if not have_effective_cap(CAP_SYS_ADMIN):
         acquire_privileges()
@@ -4983,7 +4985,7 @@ def ensure_tools_tree_has_etc_resolv_conf(config: Config) -> None:
         )
 
 
-def run_verb(args: Args, tools: Config | None, images: Sequence[Config], *, resources: Path) -> None:
+def run_verb(args: Args, tools: Optional[Config], images: Sequence[Config], *, resources: Path) -> None:
     images = list(images)
 
     if args.verb == Verb.init:
index fca0ce7d55ce91b17ec53efc69ffc988322f6e5c..57c082b69ec67ac683babf069e8db22d473c98b9 100644 (file)
@@ -5,6 +5,7 @@ import faulthandler
 import signal
 import sys
 from types import FrameType
+from typing import Optional
 
 import mkosi.resources
 from mkosi import run_verb
@@ -16,7 +17,7 @@ from mkosi.util import resource_path
 INTERRUPTED = False
 
 
-def onsignal(signal: int, frame: FrameType | None) -> None:
+def onsignal(signal: int, frame: Optional[FrameType]) -> None:
     global INTERRUPTED
     if INTERRUPTED:
         return
index 89a38bcf5b105fc5f7218fa12c1220436968a3de..f3fbea6108f022d64ffc189e83c9ad41d1ba99f4 100644 (file)
@@ -3,6 +3,7 @@
 import os
 from collections.abc import Iterable, Sequence
 from pathlib import Path
+from typing import Optional
 
 from mkosi.log import complete_step, log_step
 from mkosi.run import SandboxProtocol, finalize_passwd_symlinks, nosandbox, run, workdir
@@ -108,7 +109,7 @@ def make_cpio(
     src: Path,
     dst: Path,
     *,
-    files: Iterable[Path] | None = None,
+    files: Optional[Iterable[Path]] = None,
     sandbox: SandboxProtocol = nosandbox,
 ) -> None:
     if not files:
index e8fafbf8ac561afdd28557f11d28ee3796afa3ab..6e22f5f00bbee09d2578b49df03e1d7d7893d195 100644 (file)
@@ -10,6 +10,7 @@ import tempfile
 import textwrap
 from collections.abc import Iterator, Mapping, Sequence
 from pathlib import Path
+from typing import Optional
 
 from mkosi.config import (
     BiosBootloader,
@@ -167,7 +168,7 @@ def want_grub_bios(context: Context, partitions: Sequence[Partition] = ()) -> bo
     return (have and bios and esp and root and installed) if partitions else have
 
 
-def find_grub_directory(context: Context, *, target: str) -> Path | None:
+def find_grub_directory(context: Context, *, target: str) -> Optional[Path]:
     for d in ("usr/lib/grub", "usr/share/grub2"):
         if (p := context.root / d / target).exists() and any(p.iterdir()):
             return p
@@ -175,7 +176,7 @@ def find_grub_directory(context: Context, *, target: str) -> Path | None:
     return None
 
 
-def find_grub_binary(config: Config, binary: str) -> Path | None:
+def find_grub_binary(config: Config, binary: str) -> Optional[Path]:
     assert "grub" not in binary
 
     # Debian has a bespoke setup where if only grub-pc-bin is installed, grub-bios-setup is installed in
@@ -184,7 +185,7 @@ def find_grub_binary(config: Config, binary: str) -> Path | None:
     return config.find_binary(f"grub-{binary}", f"grub2-{binary}", f"/usr/lib/grub/i386-pc/grub-{binary}")
 
 
-def prepare_grub_config(context: Context) -> Path | None:
+def prepare_grub_config(context: Context) -> Optional[Path]:
     config = context.root / "efi" / context.config.distribution.installer.grub_prefix() / "grub.cfg"
     with umask(~0o700):
         config.parent.mkdir(exist_ok=True)
@@ -222,8 +223,8 @@ def grub_mkimage(
     *,
     target: str,
     modules: Sequence[str] = (),
-    output: Path | None = None,
-    sbat: Path | None = None,
+    output: Optional[Path] = None,
+    sbat: Optional[Path] = None,
 ) -> None:
     mkimage = find_grub_binary(context.config, "mkimage")
     assert mkimage
@@ -292,7 +293,7 @@ def grub_mkimage(
         )  # fmt: skip
 
 
-def find_signed_grub_image(context: Context) -> Path | None:
+def find_signed_grub_image(context: Context) -> Optional[Path]:
     arch = context.config.architecture.to_efi()
 
     patterns = [
@@ -475,9 +476,9 @@ def run_systemd_sign_tool(
     *,
     cmdline: Sequence[PathString],
     options: Sequence[PathString],
-    certificate: Path | None,
+    certificate: Optional[Path],
     certificate_source: CertificateSource,
-    key: Path | None,
+    key: Optional[Path],
     key_source: KeySource,
     env: Mapping[str, str] = {},
     stdout: _FILE = None,
index a72421cd53c4f2388edc5c6aa3568d0e5c5794c3..398edb2ca2a0e25bc745de38a25bc33f41474905 100644 (file)
@@ -8,6 +8,7 @@ import shlex
 from collections.abc import Iterable, Mapping
 from pathlib import Path
 from textwrap import indent
+from typing import Optional, Union
 
 from mkosi import config
 from mkosi.log import die
@@ -26,7 +27,7 @@ class CompGen(StrEnum):
                 return CompGen.dirs
             else:
                 return CompGen.files
-        # TODO: the type of action.type is Callable[[str], Any] | FileType
+        # TODO: the type of action.type is Union[Callable[[str], Any], FileType]
         # the type of Path is type, but Path also works in this position,
         # because the constructor is a callable from str -> Path
         elif action.type is not None and (isinstance(action.type, type) and issubclass(action.type, Path)):
@@ -59,9 +60,9 @@ class CompGen(StrEnum):
 
 @dataclasses.dataclass(frozen=True)
 class CompletionItem:
-    short: str | None
-    long: str | None
-    help: str | None
+    short: Optional[str]
+    long: Optional[str]
+    help: Optional[str]
     choices: list[str]
     compgen: CompGen
 
@@ -103,7 +104,7 @@ def finalize_completion_bash(options: list[CompletionItem], resources: Path) ->
     def to_bash_array(name: str, entries: Iterable[str]) -> str:
         return f"{name.replace('-', '_')}=(" + " ".join(shlex.quote(str(e)) for e in entries) + ")"
 
-    def to_bash_hasharray(name: str, entries: Mapping[str, str | int]) -> str:
+    def to_bash_hasharray(name: str, entries: Mapping[str, Union[str, int]]) -> str:
         return (
             f"{name.replace('-', '_')}=("
             + " ".join(f"[{shlex.quote(str(k))}]={shlex.quote(str(v))}" for k, v in entries.items())
index a897b9a42df04bab4483642cda84ec09a52e6343..0916dac4eff158c476086a5886e0e85f4d9fccf0 100644 (file)
@@ -29,7 +29,7 @@ import uuid
 from collections.abc import Collection, Iterable, Iterator, Sequence
 from contextlib import AbstractContextManager
 from pathlib import Path
-from typing import Any, Callable, ClassVar, Generic, Protocol, TypeVar, cast
+from typing import Any, Callable, ClassVar, Generic, Optional, Protocol, TypeVar, Union, cast
 
 from mkosi.distribution import Distribution, detect_distribution
 from mkosi.log import ARG_DEBUG, ARG_DEBUG_SANDBOX, ARG_DEBUG_SHELL, complete_step, die
@@ -60,7 +60,7 @@ T = TypeVar("T")
 D = TypeVar("D", bound=DataclassInstance)
 SE = TypeVar("SE", bound=StrEnum)
 
-ConfigParseCallback = Callable[[str | None, T | None], T | None]
+ConfigParseCallback = Callable[[Optional[str], Optional[T]], Optional[T]]
 ConfigMatchCallback = Callable[[str, T], bool]
 ConfigDefaultCallback = Callable[[dict[str, Any]], T]
 
@@ -161,7 +161,7 @@ class ConfigFeature(StrEnum):
 @dataclasses.dataclass(frozen=True)
 class ConfigTree:
     source: Path
-    target: Path | None
+    target: Optional[Path]
 
     def with_prefix(self, prefix: PathString = "/") -> tuple[Path, Path]:
         return (
@@ -181,8 +181,8 @@ class DriveFlag(StrEnum):
 class Drive:
     id: str
     size: int
-    directory: Path | None
-    options: str | None
+    directory: Optional[Path]
+    options: Optional[str]
     file_id: str
     flags: list[DriveFlag]
 
@@ -497,7 +497,7 @@ class Architecture(StrEnum):
 
         return a
 
-    def to_efi(self) -> str | None:
+    def to_efi(self) -> Optional[str]:
         return {
             Architecture.x86:         "ia32",
             Architecture.x86_64:      "x64",
@@ -508,7 +508,7 @@ class Architecture(StrEnum):
             Architecture.loongarch64: "loongarch64",
         }.get(self)  # fmt: skip
 
-    def to_grub(self) -> str | None:
+    def to_grub(self) -> Optional[str]:
         return {
             Architecture.x86_64: "x86_64",
             Architecture.x86:    "i386",
@@ -686,7 +686,7 @@ def expand_delayed_specifiers(specifiers: dict[str, str], text: str) -> str:
     return re.sub(r"&(?P<specifier>[&a-zA-Z])", replacer, text)
 
 
-def try_parse_boolean(s: str) -> bool | None:
+def try_parse_boolean(s: str) -> Optional[bool]:
     "Parse 1/true/yes/y/t/on as true and 0/false/no/n/f/off/None as false"
 
     s_l = s.lower()
@@ -799,14 +799,14 @@ def parse_paths_from_directory(
     return sorted(parse_path(os.fspath(p), resolve=resolve, secret=secret) for p in path.glob(glob))
 
 
-def config_parse_key(value: str | None, old: str | None) -> Path | None:
+def config_parse_key(value: Optional[str], old: Optional[str]) -> Optional[Path]:
     if not value:
         return None
 
     return parse_path(value, secret=True) if Path(value).exists() else Path(value)
 
 
-def config_parse_certificate(value: str | None, old: str | None) -> Path | None:
+def config_parse_certificate(value: Optional[str], old: Optional[str]) -> Optional[Path]:
     if not value:
         return None
 
@@ -855,7 +855,7 @@ def config_make_list_matcher(parse: Callable[[str], T]) -> ConfigMatchCallback[l
     return config_match_list
 
 
-def config_parse_string(value: str | None, old: str | None) -> str | None:
+def config_parse_string(value: Optional[str], old: Optional[str]) -> Optional[str]:
     return value or None
 
 
@@ -877,7 +877,7 @@ def config_match_key_value(match: str, value: dict[str, str]) -> bool:
     return value.get(k, None) == v
 
 
-def config_parse_boolean(value: str | None, old: bool | None) -> bool | None:
+def config_parse_boolean(value: Optional[str], old: Optional[bool]) -> Optional[bool]:
     if value is None:
         return False
 
@@ -894,7 +894,7 @@ def parse_feature(value: str) -> ConfigFeature:
         return ConfigFeature.enabled if parse_boolean(value) else ConfigFeature.disabled
 
 
-def config_parse_feature(value: str | None, old: ConfigFeature | None) -> ConfigFeature | None:
+def config_parse_feature(value: Optional[str], old: Optional[ConfigFeature]) -> Optional[ConfigFeature]:
     if value is None:
         return ConfigFeature.auto
 
@@ -908,7 +908,7 @@ def config_match_feature(match: str, value: ConfigFeature) -> bool:
     return value == parse_feature(match)
 
 
-def config_parse_compression(value: str | None, old: Compression | None) -> Compression | None:
+def config_parse_compression(value: Optional[str], old: Optional[Compression]) -> Optional[Compression]:
     if not value:
         return None
 
@@ -918,7 +918,7 @@ def config_parse_compression(value: str | None, old: Compression | None) -> Comp
         return Compression.zstd if parse_boolean(value) else Compression.none
 
 
-def config_parse_uuid(value: str | None, old: str | None) -> uuid.UUID | None:
+def config_parse_uuid(value: Optional[str], old: Optional[str]) -> Optional[uuid.UUID]:
     if not value:
         return None
 
@@ -931,7 +931,7 @@ def config_parse_uuid(value: str | None, old: str | None) -> uuid.UUID | None:
         die(f"{value} is not a valid UUID")
 
 
-def config_parse_source_date_epoch(value: str | None, old: int | None) -> int | None:
+def config_parse_source_date_epoch(value: Optional[str], old: Optional[int]) -> Optional[int]:
     if not value:
         return None
 
@@ -946,7 +946,7 @@ def config_parse_source_date_epoch(value: str | None, old: int | None) -> int |
     return timestamp
 
 
-def config_parse_compress_level(value: str | None, old: int | None) -> int | None:
+def config_parse_compress_level(value: Optional[str], old: Optional[int]) -> Optional[int]:
     if not value:
         return None
 
@@ -961,7 +961,7 @@ def config_parse_compress_level(value: str | None, old: int | None) -> int | Non
     return level
 
 
-def config_parse_mode(value: str | None, old: int | None) -> int | None:
+def config_parse_mode(value: Optional[str], old: Optional[int]) -> Optional[int]:
     if not value:
         return None
 
@@ -1023,8 +1023,8 @@ def config_default_distribution(namespace: dict[str, Any]) -> Distribution:
 
 
 def config_default_release(namespace: dict[str, Any]) -> str:
-    hd: Distribution | str | None
-    hr: str | None
+    hd: Union[Distribution, str, None]
+    hr: Optional[str]
 
     if (
         (d := os.getenv("MKOSI_HOST_DISTRIBUTION"))
@@ -1070,7 +1070,7 @@ def config_default_repository_key_fetch(namespace: dict[str, Any]) -> bool:
     )
 
 
-def config_default_source_date_epoch(namespace: dict[str, Any]) -> int | None:
+def config_default_source_date_epoch(namespace: dict[str, Any]) -> Optional[int]:
     for env in namespace["environment"]:
         if s := startswith(env, "SOURCE_DATE_EPOCH="):
             break
@@ -1079,7 +1079,7 @@ def config_default_source_date_epoch(namespace: dict[str, Any]) -> int | None:
     return config_parse_source_date_epoch(s, None)
 
 
-def config_default_proxy_url(namespace: dict[str, Any]) -> str | None:
+def config_default_proxy_url(namespace: dict[str, Any]) -> Optional[str]:
     names = ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY")
 
     for env in namespace["environment"]:
@@ -1094,7 +1094,7 @@ def config_default_proxy_url(namespace: dict[str, Any]) -> str | None:
     return None
 
 
-def config_default_proxy_exclude(namespace: dict[str, Any]) -> list[str] | None:
+def config_default_proxy_exclude(namespace: dict[str, Any]) -> Optional[list[str]]:
     names = ("no_proxy", "NO_PROXY")
 
     for env in namespace["environment"]:
@@ -1109,7 +1109,7 @@ def config_default_proxy_exclude(namespace: dict[str, Any]) -> list[str] | None:
     return None
 
 
-def config_default_proxy_peer_certificate(namespace: dict[str, Any]) -> Path | None:
+def config_default_proxy_peer_certificate(namespace: dict[str, Any]) -> Optional[Path]:
     for p in (Path("/etc/pki/tls/certs/ca-bundle.crt"), Path("/etc/ssl/certs/ca-certificates.crt")):
         if p.exists():
             return p
@@ -1153,14 +1153,14 @@ def make_enum_parser(type: type[SE]) -> Callable[[str], SE]:
 
 
 def config_make_enum_parser(type: type[SE]) -> ConfigParseCallback[SE]:
-    def config_parse_enum(value: str | None, old: SE | None) -> SE | None:
+    def config_parse_enum(value: Optional[str], old: Optional[SE]) -> Optional[SE]:
         return make_enum_parser(type)(value) if value else None
 
     return config_parse_enum
 
 
 def config_make_enum_parser_with_boolean(type: type[SE], *, yes: SE, no: SE) -> ConfigParseCallback[SE]:
-    def config_parse_enum(value: str | None, old: SE | None) -> SE | None:
+    def config_parse_enum(value: Optional[str], old: Optional[SE]) -> Optional[SE]:
         if not value:
             return None
 
@@ -1198,13 +1198,13 @@ def package_sort_key(package: str) -> tuple[int, str]:
 
 def config_make_list_parser(
     *,
-    delimiter: str | None = None,
+    delimiter: Optional[str] = None,
     parse: Callable[[str], T] = str,  # type: ignore # see mypy#3737
     unescape: bool = False,
     reset: bool = True,
-    key: Callable[[T], Any] | None = None,
+    key: Optional[Callable[[T], Any]] = None,
 ) -> ConfigParseCallback[list[T]]:
-    def config_parse_list(value: str | None, old: list[T] | None) -> list[T] | None:
+    def config_parse_list(value: Optional[str], old: Optional[list[T]]) -> Optional[list[T]]:
         new = old.copy() if old else []
 
         if value is None:
@@ -1266,16 +1266,16 @@ def config_match_version(match: str, value: str) -> bool:
 
 def config_make_dict_parser(
     *,
-    delimiter: str | None = None,
+    delimiter: Optional[str] = None,
     parse: Callable[[str], tuple[str, PathString]],
     unescape: bool = False,
     allow_paths: bool = False,
     reset: bool = True,
 ) -> ConfigParseCallback[dict[str, PathString]]:
     def config_parse_dict(
-        value: str | None,
-        old: dict[str, PathString] | None,
-    ) -> dict[str, PathString] | None:
+        value: Optional[str],
+        old: Optional[dict[str, PathString]],
+    ) -> Optional[dict[str, PathString]]:
         new = old.copy() if old else {}
 
         if value is None:
@@ -1358,7 +1358,7 @@ def config_make_path_parser(
     absolute: bool = False,
     constants: Sequence[str] = (),
 ) -> ConfigParseCallback[Path]:
-    def config_parse_path(value: str | None, old: Path | None) -> Path | None:
+    def config_parse_path(value: Optional[str], old: Optional[Path]) -> Optional[Path]:
         if not value:
             return None
 
@@ -1382,7 +1382,7 @@ def is_valid_filename(s: str) -> bool:
 
 
 def config_make_filename_parser(hint: str) -> ConfigParseCallback[str]:
-    def config_parse_filename(value: str | None, old: str | None) -> str | None:
+    def config_parse_filename(value: Optional[str], old: Optional[str]) -> Optional[str]:
         if not value:
             return None
 
@@ -1405,9 +1405,8 @@ def match_path_exists(image: str, value: str) -> bool:
 
 
 def config_parse_root_password(
-    value: str | None,
-    old: tuple[str, bool] | None,
-) -> tuple[str, bool] | None:
+    value: Optional[str], old: Optional[tuple[str, bool]]
+) -> Optional[tuple[str, bool]]:
     if not value:
         return None
 
@@ -1458,14 +1457,14 @@ def parse_bytes(value: str) -> int:
     return result
 
 
-def config_parse_bytes(value: str | None, old: int | None = None) -> int | None:
+def config_parse_bytes(value: Optional[str], old: Optional[int] = None) -> Optional[int]:
     if not value:
         return None
 
     return parse_bytes(value)
 
 
-def config_parse_number(value: str | None, old: int | None = None) -> int | None:
+def config_parse_number(value: Optional[str], old: Optional[int] = None) -> Optional[int]:
     if not value:
         return None
 
@@ -1514,7 +1513,7 @@ def parse_drive(value: str) -> Drive:
     )
 
 
-def config_parse_sector_size(value: str | None, old: int | None) -> int | None:
+def config_parse_sector_size(value: Optional[str], old: Optional[int]) -> Optional[int]:
     if not value:
         return None
 
@@ -1532,7 +1531,7 @@ def config_parse_sector_size(value: str | None, old: int | None) -> int | None:
     return size
 
 
-def config_parse_vsock_cid(value: str | None, old: int | None) -> int | None:
+def config_parse_vsock_cid(value: Optional[str], old: Optional[int]) -> Optional[int]:
     if not value:
         return None
 
@@ -1553,7 +1552,7 @@ def config_parse_vsock_cid(value: str | None, old: int | None) -> int | None:
     return cid
 
 
-def config_parse_minimum_version(value: str | None, old: str | None) -> str | None:
+def config_parse_minimum_version(value: Optional[str], old: Optional[str]) -> Optional[str]:
     if not value:
         return old
 
@@ -1630,7 +1629,7 @@ class KeySource:
         return f"{self.type}:{self.source}" if self.source else str(self.type)
 
 
-def config_parse_key_source(value: str | None, old: KeySource | None) -> KeySource | None:
+def config_parse_key_source(value: Optional[str], old: Optional[KeySource]) -> Optional[KeySource]:
     if not value:
         return KeySource(type=KeySourceType.file)
 
@@ -1658,9 +1657,9 @@ class CertificateSource:
 
 
 def config_parse_certificate_source(
-    value: str | None,
-    old: CertificateSource | None,
-) -> CertificateSource | None:
+    value: Optional[str],
+    old: Optional[CertificateSource],
+) -> Optional[CertificateSource]:
     if not value:
         return CertificateSource(type=CertificateSourceType.file)
 
@@ -1674,8 +1673,8 @@ def config_parse_certificate_source(
 
 
 def config_parse_artifact_output_list(
-    value: str | None, old: list[ArtifactOutput] | None
-) -> list[ArtifactOutput] | None:
+    value: Optional[str], old: Optional[list[ArtifactOutput]]
+) -> Optional[list[ArtifactOutput]]:
     if not value:
         return []
 
@@ -1723,10 +1722,10 @@ class ConfigSetting(Generic[T]):
     dest: str
     section: str
     parse: ConfigParseCallback[T] = config_parse_string  # type: ignore # see mypy#3737
-    match: ConfigMatchCallback[T] | None = None
+    match: Optional[ConfigMatchCallback[T]] = None
     name: str = ""
-    default: T | None = None
-    default_factory: ConfigDefaultCallback[T] | None = None
+    default: Optional[T] = None
+    default_factory: Optional[ConfigDefaultCallback[T]] = None
     default_factory_depends: tuple[str, ...] = tuple()
     path_suffixes: tuple[str, ...] = ()
     recursive_path_suffixes: tuple[str, ...] = ()
@@ -1736,12 +1735,12 @@ class ConfigSetting(Generic[T]):
     scope: SettingScope = SettingScope.local
 
     # settings for argparse
-    short: str | None = None
+    short: Optional[str] = None
     long: str = ""
-    choices: list[str] | None = None
-    metavar: str | None = None
-    const: Any | None = None
-    help: str | None = None
+    choices: Optional[list[str]] = None
+    metavar: Optional[str] = None
+    const: Optional[Any] = None
+    help: Optional[str] = None
 
     # backward compatibility
     compat_names: tuple[str, ...] = ()
@@ -1794,7 +1793,7 @@ class CustomHelpFormatter(argparse.HelpFormatter):
         )
 
 
-def parse_chdir(path: str) -> Path | None:
+def parse_chdir(path: str) -> Optional[Path]:
     if not path:
         # The current directory should be ignored
         return None
@@ -1819,9 +1818,9 @@ class IgnoreAction(argparse.Action):
         self,
         option_strings: Sequence[str],
         dest: str,
-        nargs: int | str | None = None,
+        nargs: Union[int, str, None] = None,
         default: Any = argparse.SUPPRESS,
-        help: str | None = argparse.SUPPRESS,
+        help: Optional[str] = argparse.SUPPRESS,
     ) -> None:
         super().__init__(option_strings, dest, nargs=nargs, default=default, help=help)
 
@@ -1829,8 +1828,8 @@ class IgnoreAction(argparse.Action):
         self,
         parser: argparse.ArgumentParser,
         namespace: argparse.Namespace,
-        values: str | Sequence[Any] | None,
-        option_string: str | None = None,
+        values: Union[str, Sequence[Any], None],
+        option_string: Optional[str] = None,
     ) -> None:
         logging.warning(f"{option_string} is no longer supported")
 
@@ -1840,8 +1839,8 @@ class PagerHelpAction(argparse._HelpAction):
         self,
         parser: argparse.ArgumentParser,
         namespace: argparse.Namespace,
-        values: str | Sequence[Any] | None = None,
-        option_string: str | None = None,
+        values: Union[str, Sequence[Any], None] = None,
+        option_string: Optional[str] = None,
     ) -> None:
         page(parser.format_help(), namespace.pager)
         parser.exit()
@@ -1861,7 +1860,7 @@ class Args:
     verb: Verb
     cmdline: list[str]
     force: int
-    directory: Path | None
+    directory: Optional[Path]
     debug: bool
     debug_shell: bool
     debug_workspace: bool
@@ -1900,7 +1899,7 @@ class Args:
         return dataclasses.asdict(self, dict_factory=dict_with_capitalised_keys_factory)
 
     @classmethod
-    def from_json(cls, s: str | dict[str, Any] | SupportsRead[str] | SupportsRead[bytes]) -> "Args":
+    def from_json(cls, s: Union[str, dict[str, Any], SupportsRead[str], SupportsRead[bytes]]) -> "Args":
         """Instantiate a Args object from a (partial) JSON dump."""
 
         if isinstance(s, str):
@@ -2008,15 +2007,15 @@ class Config:
     profiles: list[str]
     files: list[Path]
     dependencies: list[str]
-    minimum_version: str | None
+    minimum_version: Optional[str]
     pass_environment: list[str]
 
     distribution: Distribution
     release: str
     architecture: Architecture
-    mirror: str | None
-    snapshot: str | None
-    local_mirror: str | None
+    mirror: Optional[str]
+    snapshot: Optional[str]
+    local_mirror: Optional[str]
     repository_key_check: bool
     repository_key_fetch: bool
     repositories: list[str]
@@ -2025,19 +2024,19 @@ class Config:
     manifest_format: list[ManifestFormat]
     output: str
     output_extension: str
-    output_size: int | None
+    output_size: Optional[int]
     compress_output: Compression
     compress_level: int
-    output_dir: Path | None
-    output_mode: int | None
-    image_id: str | None
-    image_version: str | None
+    output_dir: Optional[Path]
+    output_mode: Optional[int]
+    image_id: Optional[str]
+    image_version: Optional[str]
     oci_labels: dict[str, str]
     oci_annotations: dict[str, str]
     split_artifacts: list[ArtifactOutput]
     repart_dirs: list[Path]
-    sysupdate_dir: Path | None
-    sector_size: int | None
+    sysupdate_dir: Optional[Path]
+    sector_size: Optional[int]
     overlay: bool
     seed: uuid.UUID
 
@@ -2056,7 +2055,7 @@ class Config:
     remove_packages: list[str]
     remove_files: list[str]
     clean_package_metadata: ConfigFeature
-    source_date_epoch: int | None
+    source_date_epoch: Optional[int]
 
     configure_scripts: list[Path]
     sync_scripts: list[Path]
@@ -2080,7 +2079,7 @@ class Config:
     initrd_volatile_packages: list[str]
     microcode_host: bool
     devicetrees: list[str]
-    splash: Path | None
+    splash: Optional[Path]
     kernel_command_line: list[str]
     kernel_modules_include: list[str]
     kernel_modules_exclude: list[str]
@@ -2093,14 +2092,14 @@ class Config:
     kernel_modules_initrd_exclude: list[str]
     kernel_modules_initrd_include_host: bool
 
-    locale: str | None
-    locale_messages: str | None
-    keymap: str | None
-    timezone: str | None
-    hostname: str | None
-    root_password: tuple[str, bool] | None
-    root_shell: str | None
-    machine_id: uuid.UUID | None
+    locale: Optional[str]
+    locale_messages: Optional[str]
+    keymap: Optional[str]
+    timezone: Optional[str]
+    hostname: Optional[str]
+    root_password: Optional[tuple[str, bool]]
+    root_shell: Optional[str]
+    machine_id: Optional[uuid.UUID]
 
     autologin: bool
     make_initrd: bool
@@ -2109,38 +2108,38 @@ class Config:
 
     secure_boot: bool
     secure_boot_auto_enroll: bool
-    secure_boot_key: Path | None
+    secure_boot_key: Optional[Path]
     secure_boot_key_source: KeySource
-    secure_boot_certificate: Path | None
+    secure_boot_certificate: Optional[Path]
     secure_boot_certificate_source: CertificateSource
     secure_boot_sign_tool: SecureBootSignTool
     verity: Verity
-    verity_key: Path | None
+    verity_key: Optional[Path]
     verity_key_source: KeySource
-    verity_certificate: Path | None
+    verity_certificate: Optional[Path]
     verity_certificate_source: CertificateSource
     sign_expected_pcr: ConfigFeature
-    sign_expected_pcr_key: Path | None
+    sign_expected_pcr_key: Optional[Path]
     sign_expected_pcr_key_source: KeySource
-    sign_expected_pcr_certificate: Path | None
+    sign_expected_pcr_certificate: Optional[Path]
     sign_expected_pcr_certificate_source: CertificateSource
-    passphrase: Path | None
+    passphrase: Optional[Path]
     checksum: bool
     sign: bool
     openpgp_tool: str
-    key: str | None
+    key: Optional[str]
 
-    tools_tree: Path | None
+    tools_tree: Optional[Path]
     tools_tree_certificates: bool
     extra_search_paths: list[Path]
     incremental: Incremental
     cacheonly: Cacheonly
     sandbox_trees: list[ConfigTree]
-    workspace_dir: Path | None
-    cache_dir: Path | None
+    workspace_dir: Optional[Path]
+    cache_dir: Optional[Path]
     cache_key: str
-    package_cache_dir: Path | None
-    build_dir: Path | None
+    package_cache_dir: Optional[Path]
+    build_dir: Optional[Path]
     build_key: str
     use_subvolumes: ConfigFeature
     repart_offline: bool
@@ -2151,28 +2150,28 @@ class Config:
     environment_files: list[Path]
     with_tests: bool
     with_network: bool
-    proxy_url: str | None
+    proxy_url: Optional[str]
     proxy_exclude: list[str]
-    proxy_peer_certificate: Path | None
-    proxy_client_certificate: Path | None
-    proxy_client_key: Path | None
+    proxy_peer_certificate: Optional[Path]
+    proxy_client_certificate: Optional[Path]
+    proxy_client_key: Optional[Path]
     make_scripts_executable: bool
 
-    nspawn_settings: Path | None
+    nspawn_settings: Optional[Path]
     ephemeral: bool
     credentials: dict[str, PathString]
     kernel_command_line_extra: list[str]
     register: ConfigFeature
     storage_target_mode: ConfigFeature
     runtime_trees: list[ConfigTree]
-    runtime_size: int | None
+    runtime_size: Optional[int]
     runtime_network: Network
     runtime_build_sources: bool
     bind_user: bool
-    ssh_key: Path | None
-    ssh_certificate: Path | None
-    machine: str | None
-    forward_journal: Path | None
+    ssh_key: Optional[Path]
+    ssh_certificate: Optional[Path]
+    machine: Optional[str]
+    forward_journal: Optional[Path]
 
     vmm: Vmm
     console: ConsoleMode
@@ -2186,8 +2185,8 @@ class Config:
     tpm: ConfigFeature
     removable: bool
     firmware: Firmware
-    firmware_variables: Path | None
-    linux: str | None
+    firmware_variables: Optional[Path]
+    linux: Optional[str]
     drives: list[Drive]
     qemu_args: list[str]
 
@@ -2474,7 +2473,7 @@ class Config:
     @classmethod
     def from_partial_json(
         cls,
-        s: str | dict[str, Any] | SupportsRead[str] | SupportsRead[bytes],
+        s: Union[str, dict[str, Any], SupportsRead[str], SupportsRead[bytes]],
     ) -> dict[str, Any]:
         """Instantiate a Config object from a (partial) JSON dump."""
         if isinstance(s, str):
@@ -2508,12 +2507,12 @@ class Config:
         return {(tk := key_transformer(k)): value_transformer(tk, v) for k, v in j.items()}
 
     @classmethod
-    def from_json(cls, s: str | dict[str, Any] | SupportsRead[str] | SupportsRead[bytes]) -> "Config":
+    def from_json(cls, s: Union[str, dict[str, Any], SupportsRead[str], SupportsRead[bytes]]) -> "Config":
         return dataclasses.replace(
             cls.default(), **{k: v for k, v in cls.from_partial_json(s).items() if k in cls.fields()}
         )
 
-    def find_binary(self, *names: PathString, tools: bool = True) -> Path | None:
+    def find_binary(self, *names: PathString, tools: bool = True) -> Optional[Path]:
         return find_binary(*names, root=self.tools() if tools else Path("/"), extra=self.extra_search_paths)
 
     def sandbox(
@@ -2523,8 +2522,8 @@ class Config:
         devices: bool = False,
         relaxed: bool = False,
         tools: bool = True,
-        scripts: Path | None = None,
-        overlay: Path | None = None,
+        scripts: Optional[Path] = None,
+        overlay: Optional[Path] = None,
         options: Sequence[PathString] = (),
     ) -> AbstractContextManager[list[PathString]]:
         opt: list[PathString] = [*options]
@@ -2556,9 +2555,9 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
     We have our own parser instead of using configparser as the latter does not support specifying the same
     setting multiple times in the same configuration file.
     """
-    section: str | None = None
-    setting: str | None = None
-    value: str | None = None
+    section: Optional[str] = None
+    setting: Optional[str] = None
+    value: Optional[str] = None
 
     for line in textwrap.dedent(path.read_text()).splitlines():
         comment = line.find("#")
@@ -4599,7 +4598,7 @@ def create_argument_parser(chdir: bool = True) -> argparse.ArgumentParser:
         help=argparse.SUPPRESS,
     )
 
-    last_section: str | None = None
+    last_section: Optional[str] = None
 
     for s in SETTINGS:
         if s.section != last_section:
@@ -4666,8 +4665,8 @@ class ConfigAction(argparse.Action):
         self,
         parser: argparse.ArgumentParser,
         namespace: argparse.Namespace,
-        values: str | Sequence[Any] | None,
-        option_string: str | None = None,
+        values: Union[str, Sequence[Any], None],
+        option_string: Optional[str] = None,
     ) -> None:
         assert option_string is not None
 
@@ -4802,11 +4801,11 @@ class ParseContext:
             with chdir(path if path.is_dir() else Path.cwd()):
                 self.parse_config_one(path if path.is_file() else Path.cwd(), parse_profiles=path.is_dir())
 
-    def finalize_value(self, setting: ConfigSetting[T]) -> T | None:
+    def finalize_value(self, setting: ConfigSetting[T]) -> Optional[T]:
         # If a value was specified on the CLI, it always takes priority. If the setting is a collection of
         # values, we merge the value from the CLI with the value from the configuration, making sure that the
         # value from the CLI always takes priority.
-        if (v := cast(T | None, self.cli.get(setting.dest))) is not None:
+        if (v := cast(Optional[T], self.cli.get(setting.dest))) is not None:
             cfg_value = self.config.get(setting.dest)
             # We either have no corresponding value in the config files
             # or the values was assigned the empty string on the CLI
@@ -4836,7 +4835,7 @@ class ParseContext:
         if (
             setting.dest not in self.cli
             and setting.dest in self.config
-            and (v := cast(T | None, self.config[setting.dest])) is not None
+            and (v := cast(Optional[T], self.config[setting.dest])) is not None
         ):
             return v
 
@@ -4879,8 +4878,8 @@ class ParseContext:
         return default
 
     def match_config(self, path: Path, asserts: bool = False) -> bool:
-        condition_triggered: bool | None = None
-        match_triggered: bool | None = None
+        condition_triggered: Optional[bool] = None
+        match_triggered: Optional[bool] = None
         skip = False
 
         # If the config file does not exist, we assume it matches so that we look at the other files in the
@@ -4961,7 +4960,7 @@ class ParseContext:
         return match_triggered is not False
 
     def parse_config_one(self, path: Path, parse_profiles: bool = False, parse_local: bool = False) -> bool:
-        s: ConfigSetting[object] | None  # Hint to mypy that we might assign None
+        s: Optional[ConfigSetting[object]]  # Hint to mypy that we might assign None
         assert path.is_absolute()
 
         extras = path.is_dir()
@@ -5147,7 +5146,7 @@ def finalize_default_tools(
     main: ParseContext,
     finalized: dict[str, Any],
     *,
-    configdir: Path | None,
+    configdir: Optional[Path],
     resources: Path,
 ) -> Config:
     context = ParseContext(resources)
@@ -5198,7 +5197,7 @@ def finalize_default_initrd(
     main: ParseContext,
     finalized: dict[str, Any],
     *,
-    configdir: Path | None,
+    configdir: Optional[Path],
     resources: Path,
 ) -> Config:
     context = ParseContext(resources)
@@ -5245,7 +5244,7 @@ def finalize_default_initrd(
     return Config.from_dict(context.finalize())
 
 
-def finalize_configdir(directory: Path | None) -> Path | None:
+def finalize_configdir(directory: Optional[Path]) -> Optional[Path]:
     """Allow locating all mkosi configuration in a mkosi/ subdirectory
     instead of in the top-level directory of a git repository.
     """
@@ -5315,7 +5314,7 @@ def parse_config(
     argv: Sequence[str] = (),
     *,
     resources: Path = Path("/"),
-) -> tuple[Args, Config | None, tuple[Config, ...]]:
+) -> tuple[Args, Optional[Config], tuple[Config, ...]]:
     argv = list(argv)
 
     context = ParseContext(resources)
@@ -5434,7 +5433,7 @@ def parse_config(
     # To make this work, we can't use default_factory as it is evaluated too early, so
     # we check here to see if dependencies were explicitly provided and if not we gather
     # the list of default dependencies while we parse the subimages.
-    dependencies: list[str] | None = (
+    dependencies: Optional[list[str]] = (
         None if "dependencies" in context.cli or "dependencies" in context.config else []
     )
 
@@ -5549,7 +5548,7 @@ def finalize_term() -> str:
     return term if sys.stderr.isatty() else "dumb"
 
 
-def finalize_git_config(proxy_url: str | None, env: dict[str, str]) -> dict[str, str]:
+def finalize_git_config(proxy_url: Optional[str], env: dict[str, str]) -> dict[str, str]:
     if proxy_url is None:
         return {}
 
@@ -5574,19 +5573,19 @@ def yes_no(b: bool) -> str:
     return "yes" if b else "no"
 
 
-def none_to_na(s: object | None) -> str:
+def none_to_na(s: Optional[object]) -> str:
     return "n/a" if s is None else str(s)
 
 
-def none_to_random(s: object | None) -> str:
+def none_to_random(s: Optional[object]) -> str:
     return "random" if s is None else str(s)
 
 
-def none_to_none(s: object | None) -> str:
+def none_to_none(s: Optional[object]) -> str:
     return "none" if s is None else str(s)
 
 
-def none_to_default(s: object | None) -> str:
+def none_to_default(s: Optional[object]) -> str:
     return "default" if s is None else str(s)
 
 
@@ -5605,7 +5604,7 @@ def format_bytes(num_bytes: int) -> str:
     return f"{num_bytes}B"
 
 
-def format_bytes_or_none(num_bytes: int | None) -> str:
+def format_bytes_or_none(num_bytes: Optional[int]) -> str:
     return format_bytes(num_bytes) if num_bytes is not None else "none"
 
 
@@ -5613,7 +5612,7 @@ def format_octal(oct_value: int) -> str:
     return f"{oct_value:>04o}"
 
 
-def format_octal_or_default(oct_value: int | None) -> str:
+def format_octal_or_default(oct_value: Optional[int]) -> str:
     return format_octal(oct_value) if oct_value is not None else "default"
 
 
@@ -5892,20 +5891,20 @@ class JsonEncoder(json.JSONEncoder):
         return super().default(o)
 
 
-def dump_json(dict: dict[str, Any], indent: int | None = 4) -> str:
+def dump_json(dict: dict[str, Any], indent: Optional[int] = 4) -> str:
     return json.dumps(dict, cls=JsonEncoder, indent=indent, sort_keys=True)
 
 
 E = TypeVar("E", bound=StrEnum)
 
 
-def json_type_transformer(refcls: type[Args] | type[Config]) -> Callable[[str, Any], Any]:
+def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[str, Any], Any]:
     fields_by_name = {field.name: field for field in dataclasses.fields(refcls)}
 
     def path_transformer(path: str, fieldtype: type[Path]) -> Path:
         return Path(path)
 
-    def optional_path_transformer(path: str | None, fieldtype: type[Path | None]) -> Path | None:
+    def optional_path_transformer(path: Optional[str], fieldtype: type[Optional[Path]]) -> Optional[Path]:
         return Path(path) if path is not None else None
 
     def path_list_transformer(pathlist: list[str], fieldtype: type[list[Path]]) -> list[Path]:
@@ -5915,13 +5914,13 @@ def json_type_transformer(refcls: type[Args] | type[Config]) -> Callable[[str, A
         return uuid.UUID(uuidstr)
 
     def optional_uuid_transformer(
-        uuidstr: str | None, fieldtype: type[uuid.UUID | None]
-    ) -> uuid.UUID | None:
+        uuidstr: Optional[str], fieldtype: type[Optional[uuid.UUID]]
+    ) -> Optional[uuid.UUID]:
         return uuid.UUID(uuidstr) if uuidstr is not None else None
 
     def root_password_transformer(
-        rootpw: list[str | bool] | None, fieldtype: type[tuple[str, bool] | None]
-    ) -> tuple[str, bool] | None:
+        rootpw: Optional[list[Union[str, bool]]], fieldtype: type[Optional[tuple[str, bool]]]
+    ) -> Optional[tuple[str, bool]]:
         if rootpw is None:
             return None
         return (cast(str, rootpw[0]), cast(bool, rootpw[1]))
@@ -5945,7 +5944,7 @@ def json_type_transformer(refcls: type[Args] | type[Config]) -> Callable[[str, A
     def enum_transformer(enumval: str, fieldtype: type[E]) -> E:
         return fieldtype(enumval)
 
-    def optional_enum_transformer(enumval: str | None, fieldtype: type[E | None]) -> E | None:
+    def optional_enum_transformer(enumval: Optional[str], fieldtype: type[Optional[E]]) -> Optional[E]:
         return typing.get_args(fieldtype)[0](enumval) if enumval is not None else None
 
     def enum_list_transformer(enumlist: list[str], fieldtype: type[list[E]]) -> list[E]:
@@ -5973,9 +5972,9 @@ def json_type_transformer(refcls: type[Args] | type[Config]) -> Callable[[str, A
         return ret
 
     def generic_version_transformer(
-        version: str | None,
-        fieldtype: type[GenericVersion | None],
-    ) -> GenericVersion | None:
+        version: Optional[str],
+        fieldtype: type[Optional[GenericVersion]],
+    ) -> Optional[GenericVersion]:
         return GenericVersion(version) if version is not None else None
 
     def certificate_source_transformer(
@@ -6016,11 +6015,11 @@ def json_type_transformer(refcls: type[Args] | type[Config]) -> Callable[[str, A
     # to shut up the type checkers and rely on the tests.
     transformers: dict[Any, Callable[[Any, Any], Any]] = {
         Path: path_transformer,
-        Path | None: optional_path_transformer,
+        Optional[Path]: optional_path_transformer,
         list[Path]: path_list_transformer,
         uuid.UUID: uuid_transformer,
-        uuid.UUID | None: optional_uuid_transformer,
-        tuple[str, bool] | None: root_password_transformer,
+        Optional[uuid.UUID]: optional_uuid_transformer,
+        Optional[tuple[str, bool]]: root_password_transformer,
         list[ConfigTree]: config_tree_transformer,
         Architecture: enum_transformer,
         BiosBootloader: enum_transformer,
@@ -6035,7 +6034,7 @@ def json_type_transformer(refcls: type[Args] | type[Config]) -> Callable[[str, A
         SecureBootSignTool: enum_transformer,
         Incremental: enum_transformer,
         BuildSourcesEphemeral: enum_transformer,
-        Distribution | None: optional_enum_transformer,
+        Optional[Distribution]: optional_enum_transformer,
         list[ManifestFormat]: enum_list_transformer,
         Verb: enum_transformer,
         DocFormat: enum_transformer,
@@ -6054,7 +6053,7 @@ def json_type_transformer(refcls: type[Args] | type[Config]) -> Callable[[str, A
     }
 
     def json_transformer(key: str, val: Any) -> Any:
-        fieldtype: dataclasses.Field[Any] | None = fields_by_name.get(key)
+        fieldtype: Optional[dataclasses.Field[Any]] = fields_by_name.get(key)
         # It is unlikely that the type of a field will be None only, so let's not bother with a different
         # sentinel value
         if fieldtype is None:
@@ -6078,7 +6077,7 @@ def want_selinux_relabel(
     config: Config,
     root: Path,
     fatal: bool = True,
-) -> tuple[Path, str, Path, Path] | None:
+) -> Optional[tuple[Path, str, Path, Path]]:
     if config.selinux_relabel == ConfigFeature.disabled:
         return None
 
@@ -6165,8 +6164,8 @@ def systemd_tool_version(*tool: PathString, sandbox: SandboxProtocol = nosandbox
 def systemd_pty_forward(
     config: Config,
     *,
-    background: str | None = None,
-    title: str | None = None,
+    background: Optional[str] = None,
+    title: Optional[str] = None,
 ) -> list[str]:
     tint_bg = parse_boolean(config.environment.get("SYSTEMD_TINT_BACKGROUND", "1")) and parse_boolean(
         os.environ.get("SYSTEMD_TINT_BACKGROUND", "1")
index 5263bf87c87df1e3841a94299f4adc30be7b3750..665b75ec863404c67d881eab0f3ee0048d395b58 100644 (file)
@@ -4,6 +4,7 @@ import os
 from collections.abc import Sequence
 from contextlib import AbstractContextManager
 from pathlib import Path
+from typing import Optional
 
 from mkosi.config import Args, Config
 from mkosi.util import PathString, flatten
@@ -21,7 +22,7 @@ class Context:
         resources: Path,
         keyring_dir: Path,
         metadata_dir: Path,
-        package_dir: Path | None = None,
+        package_dir: Optional[Path] = None,
     ) -> None:
         self.args = args
         self.config = config
@@ -31,8 +32,8 @@ class Context:
         self.metadata_dir = metadata_dir
         self.package_dir = package_dir or (self.workspace / "packages")
         self.lowerdirs: list[PathString] = []
-        self.upperdir: PathString | None = None
-        self.workdir: PathString | None = None
+        self.upperdir: Optional[PathString] = None
+        self.workdir: Optional[PathString] = None
 
         self.package_dir.mkdir(exist_ok=True)
         self.staging.mkdir()
@@ -86,7 +87,7 @@ class Context:
         *,
         network: bool = False,
         devices: bool = False,
-        scripts: Path | None = None,
+        scripts: Optional[Path] = None,
         options: Sequence[PathString] = (),
     ) -> AbstractContextManager[list[PathString]]:
         return self.config.sandbox(
index 0a225a8afd4750de764d60a6fab55f08a8001d28..0db38229af278aeff134cfd7d4a97b97f723fc81 100644 (file)
@@ -3,7 +3,7 @@
 import os
 import subprocess
 from pathlib import Path
-from typing import overload
+from typing import Optional, overload
 
 from mkosi.config import Config
 from mkosi.mounts import finalize_certificate_mounts
@@ -15,7 +15,7 @@ def curl(
     config: Config,
     url: str,
     *,
-    output_dir: Path | None,
+    output_dir: Optional[Path],
     log: bool = True,
 ) -> None: ...
 
@@ -30,7 +30,7 @@ def curl(
 ) -> str: ...
 
 
-def curl(config: Config, url: str, *, output_dir: Path | None = None, log: bool = True) -> str | None:
+def curl(config: Config, url: str, *, output_dir: Optional[Path] = None, log: bool = True) -> Optional[str]:
     result = run(
         [
             "curl",
index b9cb1c4ae447d894299a0471ece69d42551abdfe..5d42b8f5b3e07f6b83c8961d18106d12b1ae3d0b 100644 (file)
@@ -5,7 +5,7 @@ import importlib
 import urllib.parse
 from collections.abc import Sequence
 from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional, Union
 
 from mkosi.log import die
 from mkosi.util import StrEnum, read_env_file
@@ -135,7 +135,7 @@ class DistributionInstaller:
         return ""
 
     @classmethod
-    def default_tools_tree_distribution(cls) -> Distribution | None:
+    def default_tools_tree_distribution(cls) -> Optional[Distribution]:
         return None
 
     @classmethod
@@ -151,7 +151,7 @@ class DistributionInstaller:
         return False
 
 
-def detect_distribution(root: Path = Path("/")) -> tuple[Distribution | str | None, str | None]:
+def detect_distribution(root: Path = Path("/")) -> tuple[Union[Distribution, str, None], Optional[str]]:
     try:
         os_release = read_env_file(root / "etc/os-release")
     except FileNotFoundError:
@@ -169,7 +169,7 @@ def detect_distribution(root: Path = Path("/")) -> tuple[Distribution | str | No
         "azurelinux": "azure",
     }
 
-    d: Distribution | None = None
+    d: Optional[Distribution] = None
     for the_id in [dist_id, *dist_id_like]:
         if not the_id:
             continue
index 2a929c1b344d8c3f08e478f4eb01dc656b0bb4a7..428c9b27a775efb4bfdc7265dda37598799f74df 100644 (file)
@@ -4,6 +4,7 @@ import os
 import tempfile
 from collections.abc import Iterable, Sequence
 from pathlib import Path
+from typing import Union
 from xml.etree import ElementTree
 
 from mkosi.config import Architecture, Config, parse_ini
@@ -41,7 +42,7 @@ class Installer(DistributionInstaller, distribution=Distribution.opensuse):
         return "grub2"
 
     @classmethod
-    def package_manager(cls, config: Config) -> type[Dnf] | type[Zypper]:
+    def package_manager(cls, config: Config) -> Union[type[Dnf], type[Zypper]]:
         if config.find_binary("zypper"):
             return Zypper
         else:
index 2b22b9b8e8cdfeb8289f39e451199694357c85eb..1e7ff294b3d53a805a0d2971a63c57a9d6f026f8 100644 (file)
@@ -2,7 +2,7 @@
 
 from collections.abc import Iterable
 from pathlib import Path
-from typing import Any
+from typing import Any, Optional
 
 from mkosi.context import Context
 from mkosi.distribution import Distribution, centos, join_mirror
@@ -27,7 +27,7 @@ class Installer(centos.Installer, distribution=Distribution.rhel):
         )
 
     @staticmethod
-    def sslcacert(context: Context) -> Path | None:
+    def sslcacert(context: Context) -> Optional[Path]:
         if context.config.mirror:
             return None
 
@@ -41,7 +41,7 @@ class Installer(centos.Installer, distribution=Distribution.rhel):
         return path
 
     @staticmethod
-    def sslclientkey(context: Context) -> Path | None:
+    def sslclientkey(context: Context) -> Optional[Path]:
         if context.config.mirror:
             return None
 
@@ -56,7 +56,7 @@ class Installer(centos.Installer, distribution=Distribution.rhel):
         return paths[0]
 
     @staticmethod
-    def sslclientcert(context: Context) -> Path | None:
+    def sslclientcert(context: Context) -> Optional[Path]:
         if context.config.mirror:
             return None
 
index fbdf2056c6a27afe4dfe7c173c904ffa8a2d498a..5343a055dfbb1ae57fc3b41d9973de1314ff0557 100644 (file)
@@ -11,7 +11,7 @@ import subprocess
 import sys
 import tempfile
 from pathlib import Path
-from typing import cast
+from typing import Optional, cast
 
 import mkosi.resources
 from mkosi.config import DocFormat, InitrdProfile, OutputFormat
@@ -33,8 +33,8 @@ class KernelInstallContext:
     staging_area: Path
     layout: str
     image_type: str
-    initrd_generator: str | None
-    uki_generator: str | None
+    initrd_generator: Optional[str]
+    uki_generator: Optional[str]
     verbose: bool
 
     @staticmethod
@@ -228,7 +228,7 @@ def vconsole_config() -> list[str]:
     ]
 
 
-def initrd_finalize(staging_dir: Path, output: str, output_dir: Path | None) -> None:
+def initrd_finalize(staging_dir: Path, output: str, output_dir: Optional[Path]) -> None:
     if output_dir:
         with umask(~0o700) if os.getuid() == 0 else cast(umask, contextlib.nullcontext()):
             Path(output_dir).mkdir(parents=True, exist_ok=True)
index 0c5903e7318df075a62b5d561ac2914167cb26cf..a6338dfd27f671b091252cd8274c94f4e8d0319f 100644 (file)
@@ -4,7 +4,7 @@ import dataclasses
 import textwrap
 from collections.abc import Sequence
 from pathlib import Path
-from typing import Final
+from typing import Final, Optional
 
 from mkosi.config import Config, ConfigFeature
 from mkosi.context import Context
@@ -21,8 +21,8 @@ class AptRepository:
     url: str
     suite: str
     components: tuple[str, ...]
-    signedby: Path | None
-    snapshot: str | None = None
+    signedby: Optional[Path]
+    snapshot: Optional[str] = None
 
     def __str__(self) -> str:
         return textwrap.dedent(
index 5490505ceddd8b4b548f7f83d77051e1ad87c642..670a77822e42c79eb491d03ada00d0f096be7967 100644 (file)
@@ -3,6 +3,7 @@
 import textwrap
 from collections.abc import Sequence
 from pathlib import Path
+from typing import Optional
 
 from mkosi.config import Cacheonly, Config
 from mkosi.context import Context
@@ -62,7 +63,7 @@ class Dnf(PackageManager):
         context: Context,
         repositories: Sequence[RpmRepository],
         filelists: bool = True,
-        metadata_expire: str | None = None,
+        metadata_expire: Optional[str] = None,
     ) -> None:
         (context.sandbox_tree / "etc/dnf/vars").mkdir(parents=True, exist_ok=True)
         (context.sandbox_tree / "etc/yum.repos.d").mkdir(parents=True, exist_ok=True)
index f2942ed668043868512045629d22fef83ef2e4cc..f2ca93b639c33b0ac66502bd271293c7a904a82b 100644 (file)
@@ -3,7 +3,7 @@
 import dataclasses
 import textwrap
 from pathlib import Path
-from typing import Literal, overload
+from typing import Literal, Optional, overload
 
 from mkosi.context import Context
 from mkosi.distribution import Distribution
@@ -18,17 +18,17 @@ class RpmRepository:
     url: str
     gpgurls: tuple[str, ...]
     enabled: bool = True
-    sslcacert: Path | None = None
-    sslclientkey: Path | None = None
-    sslclientcert: Path | None = None
-    priority: int | None = None
+    sslcacert: Optional[Path] = None
+    sslclientkey: Optional[Path] = None
+    sslclientcert: Optional[Path] = None
+    priority: Optional[int] = None
 
 
 @overload
 def find_rpm_gpgkey(
     context: Context,
     key: str,
-    fallback: str | None = None,
+    fallback: Optional[str] = None,
     *,
     required: Literal[True] = True,
 ) -> str: ...
@@ -38,19 +38,19 @@ def find_rpm_gpgkey(
 def find_rpm_gpgkey(
     context: Context,
     key: str,
-    fallback: str | None = None,
+    fallback: Optional[str] = None,
     *,
     required: bool,
-) -> str | None: ...
+) -> Optional[str]: ...
 
 
 def find_rpm_gpgkey(
     context: Context,
     key: str,
-    fallback: str | None = None,
+    fallback: Optional[str] = None,
     *,
     required: bool = True,
-) -> str | None:
+) -> Optional[str]:
     # We assume here that GPG keys will only ever be relative symlinks and never absolute symlinks.
 
     paths = glob_in_sandbox(
@@ -78,7 +78,7 @@ def setup_rpm(
     context: Context,
     *,
     dbpath: str = "/usr/lib/sysimage/rpm",
-    dbbackend: str | None = None,
+    dbbackend: Optional[str] = None,
 ) -> None:
     confdir = context.sandbox_tree / "etc/rpm"
     confdir.mkdir(parents=True, exist_ok=True)
index cffa9bdb8cdf364056f642de33f4c635f07c6f1d..ddfd185b2e8a75bb47fbe42a63cf69cde89a7d0b 100644 (file)
@@ -7,7 +7,7 @@ import os
 import sys
 import time
 from collections.abc import Iterator
-from typing import Any, NoReturn
+from typing import Any, NoReturn, Optional
 
 from mkosi.sandbox import ANSI_BOLD, ANSI_GRAY, ANSI_RED, ANSI_RESET, ANSI_YELLOW, terminal_is_dumb
 
@@ -18,7 +18,7 @@ ARG_DEBUG_SANDBOX = contextvars.ContextVar("debug-sandbox", default=False)
 LEVEL = 0
 
 
-def die(message: str, *, hint: str | None = None) -> NoReturn:
+def die(message: str, *, hint: Optional[str] = None) -> NoReturn:
     logging.error(f"{message}")
     if hint:
         logging.info(f"({hint})")
@@ -95,7 +95,7 @@ def log_notice(text: str) -> None:
 
 
 @contextlib.contextmanager
-def complete_step(text: str, text2: str | None = None) -> Iterator[list[Any]]:
+def complete_step(text: str, text2: Optional[str] = None) -> Iterator[list[Any]]:
     global LEVEL
 
     log_step(text)
@@ -116,7 +116,7 @@ def complete_step(text: str, text2: str | None = None) -> Iterator[list[Any]]:
 
 
 class Formatter(logging.Formatter):
-    def __init__(self, fmt: str | None = None, *args: Any, **kwargs: Any) -> None:
+    def __init__(self, fmt: Optional[str] = None, *args: Any, **kwargs: Any) -> None:
         fmt = fmt or "%(message)s"
 
         self.formatters = {
index 41e4c40c4b91f2eba0550d4c02d3bb67db7fcdb9..df363938614b7111020c2a34a8974bb6bcc8e07d 100644 (file)
@@ -5,7 +5,7 @@ import datetime
 import json
 import subprocess
 import textwrap
-from typing import IO, Any
+from typing import IO, Any, Optional
 
 from mkosi.config import ManifestFormat, OutputFormat
 from mkosi.context import Context
@@ -43,7 +43,7 @@ class PackageManifest:
 @dataclasses.dataclass
 class SourcePackageManifest:
     name: str
-    changelog: str | None
+    changelog: Optional[str]
     packages: list[PackageManifest] = dataclasses.field(default_factory=list)
 
     def add(self, package: PackageManifest) -> None:
index 88ab5608acef8e8cb390d44771c4364fb6c39c7e..f15791890347a1b234d334dce4f199d51467666e 100644 (file)
@@ -6,6 +6,7 @@ import stat
 import tempfile
 from collections.abc import Iterator, Sequence
 from pathlib import Path
+from typing import Optional, Union
 
 from mkosi.config import BuildSourcesEphemeral, Config
 from mkosi.log import die
@@ -48,7 +49,7 @@ def mount_overlay(
     lowerdirs: Sequence[Path],
     dst: Path,
     *,
-    upperdir: Path | None = None,
+    upperdir: Optional[Path] = None,
 ) -> Iterator[Path]:
     with contextlib.ExitStack() as stack:
         if upperdir is None:
@@ -85,7 +86,7 @@ def mount_overlay(
 def finalize_source_mounts(
     config: Config,
     *,
-    ephemeral: BuildSourcesEphemeral | bool,
+    ephemeral: Union[BuildSourcesEphemeral, bool],
 ) -> Iterator[list[PathString]]:
     with contextlib.ExitStack() as stack:
         options: list[PathString] = []
index 84de95983be47f2ecaada2f4c3f23a57dec3187a..7077fb0a4ccce3a299a07babedb5e8cc216f5e61 100644 (file)
@@ -2,9 +2,10 @@
 
 import os
 import pydoc
+from typing import Optional
 
 
-def page(text: str, enabled: bool | None) -> None:
+def page(text: str, enabled: Optional[bool]) -> None:
     if enabled:
         # Initialize less options from $MKOSI_LESS or provide a suitable fallback.
         # F: don't page if one screen
index 397022179f3ab04ae02cdc7c56a6752bc00c5dd8..535b6cdd527db22df9a042558bffea35904ae07d 100644 (file)
@@ -5,7 +5,7 @@ import json
 import subprocess
 from collections.abc import Mapping, Sequence
 from pathlib import Path
-from typing import Any, Final
+from typing import Any, Final, Optional
 
 from mkosi.log import die
 from mkosi.run import SandboxProtocol, nosandbox, run, workdir
@@ -15,9 +15,9 @@ from mkosi.run import SandboxProtocol, nosandbox, run, workdir
 class Partition:
     type: str
     uuid: str
-    partno: int | None
-    split_path: Path | None
-    roothash: str | None
+    partno: Optional[int]
+    split_path: Optional[Path]
+    roothash: Optional[str]
 
     @classmethod
     def from_dict(cls, dict: Mapping[str, Any]) -> "Partition":
@@ -47,9 +47,9 @@ def find_partitions(image: Path, *, sandbox: SandboxProtocol = nosandbox) -> lis
     return [Partition.from_dict(d) for d in output]
 
 
-def finalize_roothash(partitions: Sequence[Partition]) -> str | None:
-    roothash: str | None = None
-    usrhash: str | None = None
+def finalize_roothash(partitions: Sequence[Partition]) -> Optional[str]:
+    roothash: Optional[str] = None
+    usrhash: Optional[str] = None
 
     for p in partitions:
         if (h := p.roothash) is None:
@@ -71,7 +71,7 @@ def finalize_roothash(partitions: Sequence[Partition]) -> str | None:
     return f"roothash={roothash}" if roothash else f"usrhash={usrhash}" if usrhash else None
 
 
-def finalize_root(partitions: Sequence[Partition]) -> str | None:
+def finalize_root(partitions: Sequence[Partition]) -> Optional[str]:
     root = finalize_roothash(partitions)
     if not root:
         root = next((f"root=PARTUUID={p.uuid}" for p in partitions if p.type.startswith("root")), None)
index 8eac9a30939722f9992a333f84c69381390677ea..6617677bfd5eaa5f3fb2446553737e13d91e93a9 100644 (file)
@@ -25,6 +25,7 @@ import textwrap
 import uuid
 from collections.abc import Iterator, Sequence
 from pathlib import Path
+from typing import Optional
 
 from mkosi.bootloader import KernelType
 from mkosi.config import (
@@ -183,7 +184,7 @@ class OvmfConfig:
     vars_format: str
 
 
-def find_ovmf_firmware(config: Config, firmware: Firmware) -> OvmfConfig | None:
+def find_ovmf_firmware(config: Config, firmware: Firmware) -> Optional[OvmfConfig]:
     if not firmware.is_uefi():
         return None
 
@@ -300,7 +301,7 @@ def start_swtpm(config: Config) -> Iterator[Path]:
                 proc.terminate()
 
 
-def find_virtiofsd(*, root: Path = Path("/"), extra: Sequence[Path] = ()) -> Path | None:
+def find_virtiofsd(*, root: Path = Path("/"), extra: Sequence[Path] = ()) -> Optional[Path]:
     if p := find_binary("virtiofsd", root=root, extra=extra):
         return p
 
@@ -604,8 +605,8 @@ def qemu_version(config: Config, binary: Path) -> GenericVersion:
 
 def finalize_firmware(
     config: Config,
-    kernel: Path | None,
-    kerneltype: KernelType | None = None,
+    kernel: Optional[Path],
+    kerneltype: Optional[KernelType] = None,
 ) -> Firmware:
     if config.firmware != Firmware.auto:
         return config.firmware
@@ -731,7 +732,7 @@ def finalize_drive(config: Config, drive: Drive) -> Iterator[Path]:
 
 
 @contextlib.contextmanager
-def finalize_initrd(config: Config) -> Iterator[Path | None]:
+def finalize_initrd(config: Config) -> Iterator[Optional[Path]]:
     with contextlib.ExitStack() as stack:
         if (config.output_dir_or_cwd() / config.output_split_initrd).exists():
             yield config.output_dir_or_cwd() / config.output_split_initrd
@@ -901,7 +902,7 @@ def finalize_register(config: Config) -> bool:
     return True
 
 
-def register_machine(config: Config, pid: int, fname: Path, cid: int | None) -> None:
+def register_machine(config: Config, pid: int, fname: Path, cid: Optional[int]) -> None:
     if not finalize_register(config):
         return
 
@@ -1103,7 +1104,7 @@ def run_qemu(args: Args, config: Config) -> None:
 
     cmdline += ["-accel", accel]
 
-    cid: int | None = None
+    cid: Optional[int] = None
     if QemuDeviceNode.vhost_vsock in qemu_device_fds:
         if config.vsock_cid == VsockCID.auto:
             cid = find_unused_vsock_cid(config, qemu_device_fds[QemuDeviceNode.vhost_vsock])
@@ -1151,8 +1152,8 @@ def run_qemu(args: Args, config: Config) -> None:
         assert ovmf
         cmdline += ["-drive", f"if=pflash,format={ovmf.format},readonly=on,file={ovmf.firmware}"]
 
-    vsock: socket.socket | None = None
-    notify: AsyncioThread[tuple[str, str]] | None = None
+    vsock: Optional[socket.socket] = None
+    notify: Optional[AsyncioThread[tuple[str, str]]] = None
 
     with contextlib.ExitStack() as stack:
         if firmware.is_uefi():
index 906b6abdba2941f285c64fcae4e8a24b6f50f7c5..cb3fcd53b962fd667dbbaaafbc5a2810ba2e69cc 100644 (file)
@@ -3309,7 +3309,7 @@ In this scenario, the kernel is loaded from the ESP in the image by **systemd-bo
   `ubuntu-archive-keyring`, `kali-archive-keyring` and/or `debian-archive-keyring`
   packages explicitly, in addition to **apt**, depending on what kind of distribution
   images you want to build.
-- The minimum required Python version is 3.10.
+- The minimum required Python version is 3.9.
 
 ## Unprivileged User Namespaces
 
index 6ddc98f5aab2b6f5932344a72b8f5e0d702026f1..6a1f664f760a56d8759d0cd0c11ce68d3eaaf221 100644 (file)
@@ -20,7 +20,7 @@ from collections.abc import Awaitable, Collection, Iterator, Mapping, Sequence
 from contextlib import AbstractContextManager
 from pathlib import Path
 from types import FrameType, TracebackType
-from typing import TYPE_CHECKING, Any, Callable, Generic, NoReturn, Protocol, TypeVar, cast
+from typing import TYPE_CHECKING, Any, Callable, Generic, NoReturn, Optional, Protocol, TypeVar, cast
 
 import mkosi
 import mkosi.sandbox
@@ -229,7 +229,7 @@ def run(
     stdin: _FILE = None,
     stdout: _FILE = None,
     stderr: _FILE = None,
-    input: str | None = None,
+    input: Optional[str] = None,
     env: Mapping[str, str] = {},
     log: bool = True,
     success_exit_status: Sequence[int] = (0,),
@@ -261,7 +261,7 @@ def _preexec(
     cmd: list[str],
     env: dict[str, Any],
     sandbox: list[str],
-    preexec: Callable[[], None] | None,
+    preexec: Optional[Callable[[], None]],
 ) -> None:
     with uncaught_exception_handler(exit=os._exit, fork=True, proceed=True):
         if preexec:
@@ -301,12 +301,12 @@ def spawn(
     stdin: _FILE = None,
     stdout: _FILE = None,
     stderr: _FILE = None,
-    user: int | None = None,
-    group: int | None = None,
+    user: Optional[int] = None,
+    group: Optional[int] = None,
     pass_fds: Collection[int] = (),
     env: Mapping[str, str] = {},
     log: bool = True,
-    preexec: Callable[[], None] | None = None,
+    preexec: Optional[Callable[[], None]] = None,
     success_exit_status: Sequence[int] = (0,),
     setup: Sequence[PathString] = (),
     sandbox: AbstractContextManager[Sequence[PathString]] = nosandbox(),
@@ -414,7 +414,7 @@ def spawn(
 
 
 def finalize_path(
-    root: Path | None = None,
+    root: Optional[Path] = None,
     extra: Sequence[Path] = (),
     prefix_usr: bool = False,
     relaxed: bool = False,
@@ -446,9 +446,9 @@ def finalize_path(
 
 def find_binary(
     *names: PathString,
-    root: Path | None = None,
+    root: Optional[Path] = None,
     extra: Sequence[Path] = (),
-) -> Path | None:
+) -> Optional[Path]:
     root = root or Path("/")
     path = finalize_path(root=root, extra=extra, prefix_usr=True)
 
@@ -528,9 +528,9 @@ class AsyncioThread(threading.Thread, Generic[T]):
 
     def __exit__(
         self,
-        type: type[BaseException] | None,
-        value: BaseException | None,
-        traceback: TracebackType | None,
+        type: Optional[type[BaseException]],
+        value: Optional[BaseException],
+        traceback: Optional[TracebackType],
     ) -> None:
         self.cancel()
         self.join()
@@ -543,7 +543,7 @@ class AsyncioThread(threading.Thread, Generic[T]):
                 pass
 
 
-def workdir(path: Path, sandbox: SandboxProtocol | None = None) -> str:
+def workdir(path: Path, sandbox: Optional[SandboxProtocol] = None) -> str:
     subdir = "/" if sandbox and sandbox == nosandbox else "/work"
     return joinpath(subdir, os.fspath(path))
 
@@ -606,10 +606,10 @@ def sandbox_cmd(
     *,
     network: bool = False,
     devices: bool = False,
-    scripts: Path | None = None,
+    scripts: Optional[Path] = None,
     tools: Path = Path("/"),
     relaxed: bool = False,
-    overlay: Path | None = None,
+    overlay: Optional[Path] = None,
     options: Sequence[PathString] = (),
     extra: Sequence[Path] = (),
 ) -> Iterator[list[PathString]]:
@@ -715,7 +715,7 @@ def sandbox_cmd(
         if scripts:
             cmdline += ["--ro-bind", scripts, "/scripts"]
 
-        tmp: Path | None
+        tmp: Optional[Path]
 
         if not overlay and not relaxed:
             tmp = stack.enter_context(vartmpdir())
index 01bf394ac0935970b16fb383a53664a32e8d1a2f..19f0b4c7900889a677384deb70953307a1b46e7d 100644 (file)
@@ -21,7 +21,7 @@ import tempfile
 from collections.abc import Hashable, Iterable, Iterator, Mapping, Sequence
 from pathlib import Path
 from types import ModuleType
-from typing import IO, Any, Callable, Protocol, TypeVar
+from typing import IO, Any, Callable, Optional, Protocol, TypeVar, Union
 
 from mkosi.log import die
 from mkosi.resources import as_file
@@ -32,8 +32,8 @@ V = TypeVar("V")
 S = TypeVar("S", bound=Hashable)
 
 # Borrowed from https://github.com/python/typeshed/blob/3d14016085aed8bcf0cf67e9e5a70790ce1ad8ea/stdlib/3/subprocess.pyi#L24
-_FILE = None | int | IO[Any]
-PathString = Path | str
+_FILE = Union[None, int, IO[Any]]
+PathString = Union[Path, str]
 
 # Borrowed from
 # https://github.com/python/typeshed/blob/ec52bf1adde1d3183d0595d2ba982589df48dff1/stdlib/_typeshed/__init__.pyi#L19
@@ -72,7 +72,7 @@ def round_up(x: int, blocksize: int = 4096) -> int:
     return (x + blocksize - 1) // blocksize * blocksize
 
 
-def startswith(s: str, prefix: str) -> str | None:
+def startswith(s: str, prefix: str) -> Optional[str]:
     if s.startswith(prefix):
         return s.removeprefix(prefix)
     return None
index bbad8a58690a00b8e7319964dcceaab050997304..379825e412bf05d9a52786d4309b0ebc203ea7b8 100644 (file)
@@ -10,7 +10,7 @@ authors = [
 version = "26"
 description = "Build Bespoke OS Images"
 readme = "README.md"
-requires-python = ">=3.10"
+requires-python = ">=3.9"
 license = {text = "LGPL-2.1-or-later"}
 
 [project.optional-dependencies]
@@ -53,7 +53,7 @@ multi_line_output = 3
 py_version = "39"
 
 [tool.pyright]
-pythonVersion = "3.10"
+pythonVersion = "3.9"
 include = [
     "mkosi/**/*.py",
     "tests/**/*.py",
@@ -61,7 +61,7 @@ include = [
 ]
 
 [tool.mypy]
-python_version = "3.10"
+python_version = 3.9
 # belonging to --strict
 warn_unused_configs = true
 disallow_any_generics = true
index 96f06552f7c43c773fa2a4db4626a15dd6f9e6c3..3134a7a28e8dd71f9dd4c1e1787c1dcd8cc55b88 100644 (file)
@@ -9,7 +9,7 @@ import uuid
 from collections.abc import Iterator, Mapping, Sequence
 from pathlib import Path
 from types import TracebackType
-from typing import Any
+from typing import Any, Optional
 
 import pytest
 
@@ -44,9 +44,9 @@ class Image:
 
     def __exit__(
         self,
-        type: type[BaseException] | None,
-        value: BaseException | None,
-        traceback: TracebackType | None,
+        type: Optional[type[BaseException]],
+        value: Optional[BaseException],
+        traceback: Optional[TracebackType],
     ) -> None:
         def clean() -> None:
             acquire_privileges()
index 6d4b9d0d8976e3a332c3e776be1e3787649a0f0d..d1f367c7142c8d2059d7dfceb3879ff0bcce21f4 100644 (file)
@@ -4,6 +4,7 @@ import os
 import textwrap
 import uuid
 from pathlib import Path
+from typing import Optional
 
 import pytest
 
@@ -48,7 +49,7 @@ from mkosi.distribution import Distribution
 
 
 @pytest.mark.parametrize("path", [None, "/baz/qux"])
-def test_args(path: Path | None) -> None:
+def test_args(path: Optional[Path]) -> None:
     dump = textwrap.dedent(
         f"""\
         {{