From 182e2d6e83ee7db44691c4030aafad7beec301ab Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sat, 6 Jun 2026 09:15:27 -0400 Subject: [PATCH 1/2] Arch: single registry, broad aliases, x86_64 canonical Consolidate the seven scattered/duplicated architecture tables into one source of truth and let users write any common spelling for an arch. - New src/penguin/arch_registry.py (pure stdlib): one ArchSpec per arch holding every per-namespace name (arch_subdir, dylib_subdir, kmod_subdir, qemu/panda arch, machine, cpu, kconf, kernel_fmt/kernel_whole, serial, console, endianness) plus accepted aliases. normalize_arch()/spec()/accessors are alias-tolerant. - Flip x86 canonical to x86_64 (intel64 becomes an alias), matching the on-disk asset names. Broad aliases for every arch (arm64->aarch64, ppc64le/powerpc64el ->powerpc64le, amd64->x86_64, ...). - Refactor consumers to read the registry, deleting the duplicate tables: utils.get_arch_subdir/get_driver_kmod_path, arch.get_dylib_subdir, dropin_compile.DYLIB_DIRS (also fixes its missing-intel64 bug), q_config.qemu_configs/load_q_config (now returns a fresh dict and fixes the powerpc64le KeyError + the in-place mutation), abi_info (rekey intel64->x86_64 + arch_abi_info() helper), and the nvram2 dylib-override duplicate. Rewire the serial (config_patchers) and console (penguin_run) inline branches. - Normalize core.arch to canonical at config-load (after merge), so all downstream consumers and the realized config use one spelling; widen the schema Literal to canonical+aliases (with a unit test asserting it equals arch_registry.all_names()); canonicalize {{ arch }} in templating. - Safe transition for the two assets still named by config arch (guesthopper., sysroots/): utils.resolve_arch_asset prefers the canonical name and falls back to an alias filename that exists, so x86 keeps working until the guesthopper sibling artifact is renamed. Dockerfile sysroots stage renamed intel64 -> x86_64. Update plugin arch maps (live_image, unwind, kffi) for the flip. Identifier-namespace code (arch.arch_filter/arch_end, static_analyses) is left emitting intel64 by design; normalization happens at the identifier->config boundary (config_patchers.set_arch_info). --- Dockerfile | 4 +- docs/schema_doc.md | 13 +- pyplugins/apis/kffi.py | 6 +- pyplugins/apis/unwind.py | 11 +- pyplugins/core/live_image.py | 8 +- pyplugins/interventions/nvram2.py | 18 +- src/penguin/abi_info.py | 13 +- src/penguin/arch.py | 26 +-- src/penguin/arch_registry.py | 276 +++++++++++++++++++++++ src/penguin/config_patchers.py | 27 +-- src/penguin/dropin_compile.py | 23 +- src/penguin/penguin_config/__init__.py | 20 +- src/penguin/penguin_config/structure.py | 22 +- src/penguin/penguin_config/templating.py | 7 + src/penguin/penguin_prep.py | 8 +- src/penguin/penguin_run.py | 8 +- src/penguin/q_config.py | 87 ++----- src/penguin/utils.py | 36 ++- tests/unit_tests/test_config.py | 140 +++++++++++- 19 files changed, 584 insertions(+), 169 deletions(-) create mode 100644 src/penguin/arch_registry.py diff --git a/Dockerfile b/Dockerfile index 650edc970..7a5dd4d7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -228,7 +228,7 @@ RUN set -eux; mkdir -p /sysroots; \ stage mipseb mipseb-linux-musl mipseb-linux-musl; \ stage mips64el mips64el-linux-musl-cross mips64el-linux-musl; \ stage mips64eb mips64-linux-musl-cross mips64-linux-musl; \ - stage intel64 x86_64-linux-musl-cross x86_64-linux-musl; \ + stage x86_64 x86_64-linux-musl-cross x86_64-linux-musl; \ du -sh /sysroots/* | sort -h; du -sh /sysroots #### NMAP BUILDER: Build nmap #### @@ -511,7 +511,7 @@ RUN set -eux; \ stage mipseb mips mipseb; \ stage mips64el mips64 mips64el; \ stage mips64eb mips64 mips64eb; \ - stage intel64 x86_64 x86_64 + stage x86_64 x86_64 x86_64 COPY guest-utils /igloo_static/guest-utils COPY --from=rust_builder /root/vhost-device/target/x86_64-unknown-linux-gnu/release/vhost-device-vsock /usr/local/bin/vhost-device-vsock diff --git a/docs/schema_doc.md b/docs/schema_doc.md index ad863d151..80eb3ac67 100644 --- a/docs/schema_doc.md +++ b/docs/schema_doc.md @@ -10,9 +10,14 @@ Core configuration options for this rehosting ||| |-|-| -|__Type__|`"armel"` or `"aarch64"` or `"mipsel"` or `"mipseb"` or `"mips64el"` or `"mips64eb"` or `"powerpc"` or `"powerpc64"` or `"powerpc64le"` or `"riscv64"` or `"loongarch64"` or `"intel64"` or null| +|__Type__|`"armel"` or `"arm"` or `"armle"` or `"aarch64"` or `"arm64"` or `"mipsel"` or `"mipseb"` or `"mipsbe"` or `"mips64el"` or `"mips64eb"` or `"mips64be"` or `"powerpc"` or `"ppc"` or `"powerpc64"` or `"ppc64"` or `"powerpc64le"` or `"ppc64le"` or `"powerpc64el"` or `"ppc64el"` or `"riscv64"` or `"riscv"` or `"rv64"` or `"loongarch64"` or `"loongarch"` or `"la64"` or `"x86_64"` or `"intel64"` or `"amd64"` or `"x86-64"` or `"x64"` or null| |__Default__|`null`| +Canonical name or an accepted alias (normalized at load, e.g. intel64 -> x86_64). + +```yaml +x86_64 +``` ```yaml armel @@ -35,11 +40,7 @@ mips64el ``` ```yaml -mips64eb -``` - -```yaml -intel64 +powerpc64le ``` ### `core.kernel` Path to kernel image diff --git a/pyplugins/apis/kffi.py b/pyplugins/apis/kffi.py index b76485c1c..ebde84f10 100644 --- a/pyplugins/apis/kffi.py +++ b/pyplugins/apis/kffi.py @@ -52,7 +52,7 @@ from wrappers.ptregs_wrap import get_pt_regs_wrapper from penguin import Plugin, getColoredLogger, plugins -from penguin.abi_info import ARCH_ABI_INFO +from penguin.abi_info import arch_abi_info class KFFI(Plugin): @@ -72,6 +72,8 @@ def __init__(self) -> None: self.outdir = self.get_arg("outdir") conf = self.get_arg("conf") kernel = conf["core"]["kernel"] + # cosi files are named by the config arch, except x86_64 and arm64. + # (arch is already canonical post-load; the intel64 case is a safety net.) arch = conf["core"]["arch"] if arch == "intel64": arch = "x86_64" @@ -120,7 +122,7 @@ def cdef(self, source: str) -> None: proj_dir = self.get_arg("proj_dir") arch = conf["core"]["arch"] - arch_info = ARCH_ABI_INFO[arch] + arch_info = arch_abi_info(arch) abi = arch_info.get("default_abi", list(arch_info.get("abis", {}).keys())[0]) abi_info = arch_info["abis"][abi] diff --git a/pyplugins/apis/unwind.py b/pyplugins/apis/unwind.py index 8f9e1950e..a33c23681 100644 --- a/pyplugins/apis/unwind.py +++ b/pyplugins/apis/unwind.py @@ -55,7 +55,7 @@ def __init__(self, name, ptr_size, endian, dwarf_map, cs_arch, cs_mode, call_off RISCV_MAP = {"sp_reg": 2, "ra_reg": 1, "fp_reg": 8} CONFIGS = { - "intel64": ArchInfo("x86_64", 8, "little", X86_64_MAP, CS_ARCH_X86, CS_MODE_64, 5), + "x86_64": ArchInfo("x86_64", 8, "little", X86_64_MAP, CS_ARCH_X86, CS_MODE_64, 5), "armel": ArchInfo("arm", 4, "little", ARM_MAP, CS_ARCH_ARM, CS_MODE_ARM, 4), "aarch64": ArchInfo("arm64", 8, "little", ARM64_MAP, CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN, 4), "mipsel": ArchInfo("mips", 4, "little", MIPS_MAP, CS_ARCH_MIPS, CS_MODE_MIPS32 + CS_MODE_LITTLE_ENDIAN, 8), @@ -100,9 +100,14 @@ def _get_arch_info(self) -> ArchInfo: if self._arch_info: return self._arch_info conf = self.get_arg("conf") - arch_str = conf.get("core", {}).get("arch") if conf else "intel64" + arch_str = conf.get("core", {}).get("arch") if conf else "x86_64" + try: + from penguin.arch_registry import normalize_arch + arch_str = normalize_arch(arch_str) + except Exception: + pass if arch_str not in CONFIGS: - arch_str = "intel64" + arch_str = "x86_64" info = CONFIGS[arch_str] self._init_capstone(info) return info diff --git a/pyplugins/core/live_image.py b/pyplugins/core/live_image.py index 8f674471a..dccf69d3c 100644 --- a/pyplugins/core/live_image.py +++ b/pyplugins/core/live_image.py @@ -68,7 +68,7 @@ def __init__(self) -> None: self.patch_queue = [] self.config = self.get_arg("conf") core_config = self.config.get("core", {}) - self.arch = core_config.get("arch", "intel64") + self.arch = core_config.get("arch", "x86_64") self.ensure_init = lambda *args: None self._init_callbacks = [] @@ -509,9 +509,11 @@ def _gen_asm_patch_bytes(self, asm_code: str, user_mode: Optional[str]) -> Optio return None arch_map = {"armel": keystone.KS_ARCH_ARM, "aarch64": keystone.KS_ARCH_ARM64, - "mipsel": keystone.KS_ARCH_MIPS, "mipseb": keystone.KS_ARCH_MIPS, "intel64": keystone.KS_ARCH_X86} + "mipsel": keystone.KS_ARCH_MIPS, "mipseb": keystone.KS_ARCH_MIPS, + "x86_64": keystone.KS_ARCH_X86, "intel64": keystone.KS_ARCH_X86} mode_map = {"aarch64": keystone.KS_MODE_LITTLE_ENDIAN, "mipsel": keystone.KS_MODE_MIPS32 | keystone.KS_MODE_LITTLE_ENDIAN, - "mipseb": keystone.KS_MODE_MIPS32 | keystone.KS_MODE_BIG_ENDIAN, "intel64": keystone.KS_MODE_64} + "mipseb": keystone.KS_MODE_MIPS32 | keystone.KS_MODE_BIG_ENDIAN, + "x86_64": keystone.KS_MODE_64, "intel64": keystone.KS_MODE_64} ks_arch = arch_map.get(self.arch) if self.arch == "armel": diff --git a/pyplugins/interventions/nvram2.py b/pyplugins/interventions/nvram2.py index 20da7d45a..9ae0e9f04 100644 --- a/pyplugins/interventions/nvram2.py +++ b/pyplugins/interventions/nvram2.py @@ -32,8 +32,8 @@ """ -from penguin import Plugin, plugins, PluginArgs -from penguin.abi_info import ARCH_ABI_INFO +from penguin import Plugin, plugins, PluginArgs, arch_registry +from penguin.abi_info import arch_abi_info from pydantic import Field import subprocess import os @@ -86,7 +86,7 @@ def add_lib_inject_for_abi(config, abi, cache_dir, proj_dir=None, build_log_path arch = config["core"]["arch"] lib_inject = config.get("lib_inject", dict()) - arch_info = ARCH_ABI_INFO[arch] + arch_info = arch_abi_info(arch) abi_info = arch_info["abis"][abi] headers_dir = f"/igloo_static/musl-headers/{abi_info['musl_arch_name']}/include" libnvram_arch_name = abi_info.get( @@ -217,7 +217,7 @@ def add_lib_inject_all_abis(conf, cache_dir, proj_dir=None, build_log_path=None) """Add lib_inject for all supported ABIs to /igloo""" arch = conf["core"]["arch"] - arch_info = ARCH_ABI_INFO[arch] + arch_info = arch_abi_info(arch) for abi in arch_info["abis"].keys(): add_lib_inject_for_abi(conf, abi, cache_dir, proj_dir=proj_dir, build_log_path=build_log_path) @@ -245,15 +245,7 @@ def add_lib_inject_all_abis(conf, cache_dir, proj_dir=None, build_log_path=None) # than deriving it from musl_arch_name, because musl_arch_name reflects the # headers directory (shared between endianness variants) not the loader # filename (which encodes endianness: ld-musl-mipsel.so.1 ≠ ld-musl-mips.so.1). - _dylib_dir_overrides = { - "aarch64": "arm64", - "intel64": "x86_64", - "loongarch64": "loongarch", - "powerpc": "ppc", - "powerpc64": "ppc64", - "powerpc64le": "ppc64el", - } - dylib_dir = _dylib_dir_overrides.get(arch, arch) + dylib_dir = arch_registry.dylib_subdir(arch) canonical_loader = f"ld-musl-{arch}.so.1" actual_loaders = glob.glob(f"/igloo_static/dylibs/{dylib_dir}/ld-musl-*.so.1") if actual_loaders: diff --git a/src/penguin/abi_info.py b/src/penguin/abi_info.py index 6b1555ce2..5ca331116 100644 --- a/src/penguin/abi_info.py +++ b/src/penguin/abi_info.py @@ -162,7 +162,7 @@ ), ), ), - intel64=dict( + x86_64=dict( target_triple="x86_64-unknown-linux-musl", libnvram_arch_name="x86_64", default_abi="default", @@ -192,3 +192,14 @@ ), ) ) + + +def arch_abi_info(arch: str) -> dict: + """ + Look up ABI info for a config arch name or any accepted alias. + + Normalizes via the arch registry so callers may pass e.g. ``intel64`` or + ``x86_64`` (both resolve to the canonical ``x86_64`` entry). + """ + from penguin.arch_registry import spec + return ARCH_ABI_INFO[spec(arch).abi_key] diff --git a/src/penguin/arch.py b/src/penguin/arch.py index 6bcd976da..ed6ddd6e7 100644 --- a/src/penguin/arch.py +++ b/src/penguin/arch.py @@ -8,27 +8,15 @@ def get_dylib_subdir(arch_name: str) -> str: """ - Map a config arch name (e.g. "aarch64", "powerpc64le") to the subdirectory - under ``/dylibs`` holding that arch's prebuilt dynamic libraries. + Map a config arch name (or alias) to the subdirectory under + ``/dylibs`` holding that arch's prebuilt dynamic libraries. - Dylibs are built/published under short, sometimes-distinct names that differ - from both the config arch name and the generic arch subdir, so this mapping - is the single source of truth for both patch generation and config templating - (the ``{{ dylib_dir }}`` template variable). + Thin wrapper over the arch registry (the single source of truth), kept for + backward compatibility with existing callers / the ``{{ dylib_dir }}`` + template variable. """ - if arch_name == "aarch64": - return "arm64" - if arch_name == "intel64": - return "x86_64" - if arch_name == "loongarch64": - return "loongarch" - if arch_name == "powerpc64le": - return "ppc64el" - if "powerpc" in arch_name: - return arch_name.replace("powerpc", "ppc") # dylibs use short names - # Fall back to the generic arch subdir (e.g. intel64 -> x86_64, mips* as-is). - from .utils import get_arch_subdir - return get_arch_subdir({"core": {"arch": arch_name}}) + from .arch_registry import dylib_subdir + return dylib_subdir(arch_name) @dataclasses.dataclass diff --git a/src/penguin/arch_registry.py b/src/penguin/arch_registry.py new file mode 100644 index 000000000..e0e42f8d8 --- /dev/null +++ b/src/penguin/arch_registry.py @@ -0,0 +1,276 @@ +""" +penguin.arch_registry +===================== + +Single source of truth for architecture naming in Penguin. + +Historically arch names were spelled differently in a half-dozen tables +(``q_config.qemu_configs``, ``utils.get_arch_subdir``, ``arch.get_dylib_subdir``, +``dropin_compile.DYLIB_DIRS``, ``abi_info.ARCH_ABI_INFO``, +``qemu_compat._normalize_arch_name``), with drift and bugs between them. This +module defines one :class:`ArchSpec` per architecture holding every per-namespace +name, plus alias handling so a user may write any common spelling. + +The canonical x86-64 name is ``x86_64`` (``intel64`` is an accepted alias), +matching the on-disk asset layout (dylibs/x86_64, kernels vmlinux.x86_64, …). + +This module is intentionally **dependency-free** (stdlib only) so that +``utils``, ``arch``, ``q_config``, ``dropin_compile``, ``abi_info`` and the config +``templating`` module can all import it without import cycles or heavy +dependencies. It must NOT touch the filesystem — the canonical/alias on-disk +fallback resolver lives in ``utils.resolve_arch_asset``. +""" + +import dataclasses +from typing import Optional, Tuple + + +@dataclasses.dataclass(frozen=True) +class ArchSpec: + """All naming/config facts about one architecture.""" + + canonical: str # canonical config arch name, e.g. "x86_64" + aliases: Tuple[str, ...] = () # other accepted spellings -> canonical + # Static asset layout + arch_subdir: str = "" # /igloo_static// (guest utils) + dylib_subdir: str = "" # /igloo_static/dylibs// + _kmod_subdir: Optional[str] = None # igloo.ko. (defaults to arch_subdir) + # QEMU / PANDA + qemu_arch: Optional[str] = None # q_config "arch" / panda arch (defaults to canonical) + qemu_machine: Optional[str] = None # None => not runnable (load_q_config raises) + cpu: Optional[str] = None + _kconf_group: Optional[str] = None # defaults to canonical + kernel_fmt: str = "vmlinux" + _kernel_whole: Optional[str] = None # defaults to f"vmlinux.{qemu_arch}" + # Guest device / boot + serial: Tuple[int, int] = (204, 65) # (major, minor) of /igloo/serial + console_replacement: Optional[Tuple[str, str]] = None # (find, replace) on kernel append + endianness: str = "little" # "little" | "big" + _abi_key: Optional[str] = None # key into ARCH_ABI_INFO (defaults to canonical) + + # ---- resolved accessors (apply the "defaults to canonical" rules) ---- + @property + def kmod_subdir(self) -> str: + return self._kmod_subdir or self.arch_subdir + + @property + def panda_arch(self) -> str: + return self.qemu_arch or self.canonical + + @property + def kconf_group(self) -> str: + return self._kconf_group or self.canonical + + @property + def kernel_whole(self) -> str: + return self._kernel_whole or f"vmlinux.{self.panda_arch}" + + @property + def abi_key(self) -> str: + return self._abi_key or self.canonical + + +# One entry per architecture. Values transcribed from the previous scattered +# tables; parity tests (tests/unit_tests/test_config.py) assert no regression. +_SPECS = ( + ArchSpec( + canonical="armel", + aliases=("arm", "armle"), + arch_subdir="armel", + dylib_subdir="armel", + qemu_arch="arm", + qemu_machine="virt", + _kernel_whole="zImage.armel", + serial=(204, 65), + console_replacement=("console=ttyS0", "console=ttyAMA0"), + endianness="little", + ), + ArchSpec( + canonical="aarch64", + aliases=("arm64",), + arch_subdir="aarch64", + dylib_subdir="arm64", + _kmod_subdir="arm64", + qemu_arch="aarch64", + qemu_machine="virt", + cpu="cortex-a57", + _kconf_group="arm64", + _kernel_whole="zImage.arm64", + serial=(204, 65), + console_replacement=("console=ttyS0", "console=ttyAMA0"), + endianness="little", + ), + ArchSpec( + canonical="mipsel", + arch_subdir="mipsel", + dylib_subdir="mipsel", + qemu_arch="mipsel", + qemu_machine="malta", + serial=(4, 65), + endianness="little", + ), + ArchSpec( + canonical="mipseb", + aliases=("mipsbe",), + arch_subdir="mipseb", + dylib_subdir="mipseb", + qemu_arch="mips", + qemu_machine="malta", + _kernel_whole="vmlinux.mipseb", + serial=(4, 65), + endianness="big", + ), + ArchSpec( + canonical="mips64el", + arch_subdir="mips64el", + dylib_subdir="mips64el", + qemu_arch="mips64el", + qemu_machine="malta", + cpu="MIPS64R2-generic", + _kernel_whole="vmlinux.mips64el", + serial=(4, 65), + endianness="little", + ), + ArchSpec( + canonical="mips64eb", + aliases=("mips64be",), + arch_subdir="mips64eb", + dylib_subdir="mips64eb", + qemu_arch="mips64", + qemu_machine="malta", + cpu="MIPS64R2-generic", + _kernel_whole="vmlinux.mips64eb", + serial=(4, 65), + endianness="big", + ), + ArchSpec( + canonical="powerpc", + aliases=("ppc",), + arch_subdir="powerpc", + dylib_subdir="ppc", + qemu_arch="ppc", + qemu_machine=None, # no QEMU machine was ever configured for 32-bit ppc + serial=(229, 1), + console_replacement=("console=ttyS0", "console=hvc0 console=ttyS0"), + endianness="big", + ), + ArchSpec( + canonical="powerpc64", + aliases=("ppc64",), + arch_subdir="powerpc64", + dylib_subdir="ppc64", + qemu_arch="ppc64", + qemu_machine="pseries", + cpu="power9", + _kconf_group="powerpc64", + _kernel_whole="vmlinux.powerpc64", + serial=(229, 1), + console_replacement=("console=ttyS0", "console=hvc0 console=ttyS0"), + endianness="big", + ), + ArchSpec( + canonical="powerpc64le", + aliases=("ppc64le", "powerpc64el", "ppc64el"), + arch_subdir="powerpc64", + dylib_subdir="ppc64el", + qemu_arch="ppc64", + qemu_machine="pseries", + cpu="power9", + serial=(229, 1), + console_replacement=("console=ttyS0", "console=hvc0 console=ttyS0"), + endianness="little", + ), + ArchSpec( + canonical="riscv64", + aliases=("riscv", "rv64"), + arch_subdir="riscv64", + dylib_subdir="riscv64", + qemu_arch="riscv64", + qemu_machine="virt", + kernel_fmt="Image", + serial=(204, 65), + endianness="little", + ), + ArchSpec( + canonical="loongarch64", + aliases=("loongarch", "la64"), + arch_subdir="loongarch64", + dylib_subdir="loongarch", + qemu_arch="loongarch64", + qemu_machine="virt", + cpu="la464", + kernel_fmt="vmlinuz.efi", + serial=(4, 65), + endianness="little", + ), + ArchSpec( + canonical="x86_64", + aliases=("intel64", "amd64", "x86-64", "x64"), + arch_subdir="x86_64", + dylib_subdir="x86_64", + qemu_arch="x86_64", + qemu_machine="pc", + _kconf_group="x86_64", + kernel_fmt="bzImage", + serial=(4, 65), + endianness="little", + ), +) + + +_BY_CANONICAL = {s.canonical: s for s in _SPECS} +_BY_NAME = {} +for _s in _SPECS: + for _n in (_s.canonical, *_s.aliases): + _key = _n.lower() + assert _key not in _BY_NAME, f"duplicate arch name/alias: {_n!r}" + _BY_NAME[_key] = _s +del _s, _n, _key + + +def spec(name: str) -> ArchSpec: + """Return the :class:`ArchSpec` for a canonical name or any accepted alias.""" + try: + return _BY_NAME[name.lower()] + except (KeyError, AttributeError): + raise KeyError(f"Unknown architecture: {name!r}") + + +def normalize_arch(name: str) -> str: + """Map any accepted spelling to its canonical config arch name.""" + return spec(name).canonical + + +def is_known(name: str) -> bool: + try: + spec(name) + return True + except KeyError: + return False + + +def all_names() -> list: + """Canonical names + every alias — exactly the set the schema Literal accepts.""" + return [n for s in _SPECS for n in (s.canonical, *s.aliases)] + + +def canonical_names() -> list: + """Just the canonical names, in registry order.""" + return [s.canonical for s in _SPECS] + + +# Thin accessors so consumers don't reach into the dataclass directly. +def arch_subdir(name: str) -> str: + return spec(name).arch_subdir + + +def dylib_subdir(name: str) -> str: + return spec(name).dylib_subdir + + +def kmod_subdir(name: str) -> str: + return spec(name).kmod_subdir + + +def qemu_arch(name: str) -> str: + return spec(name).panda_arch diff --git a/src/penguin/config_patchers.py b/src/penguin/config_patchers.py index 9075691ec..0316ce8bb 100644 --- a/src/penguin/config_patchers.py +++ b/src/penguin/config_patchers.py @@ -23,6 +23,7 @@ from penguin import getColoredLogger from .arch import arch_filter, arch_end, get_dylib_subdir +from . import arch_registry from .defaults import ( default_init_script, default_lib_aliases, @@ -420,30 +421,18 @@ def set_arch_info(self, arch_identified: str) -> None: # For architectures like mips with endianness, construct the name self.arch_name = arch + endian + # Normalize the identified arch to its canonical config name so generated + # configs use one spelling (e.g. x86_64, not intel64). + self.arch_name = arch_registry.normalize_arch(self.arch_name) + mock_config = {"core": {"arch": self.arch_name}} self.arch_dir = get_arch_subdir(mock_config) self.dylib_dir = get_dylib_subdir(self.arch_name) def generate(self, patches: dict) -> dict: - # Add serial device in pseudofiles - # This is because arm uses ttyAMA (major 204) and mips uses ttyS (major 4). - # XXX: For mips we use major 4, minor 65. For arm we use major 204, minor 65. - # For powerpc: major 229, minor 1 (hvc1) - if 'mips' in self.arch_name or self.arch_name == "intel64": - igloo_serial_major = 4 - igloo_serial_minor = 65 - elif self.arch_name in ['armel', 'aarch64']: - igloo_serial_major = 204 - igloo_serial_minor = 65 - elif "powerpc" in self.arch_name: - igloo_serial_major = 229 - igloo_serial_minor = 1 - elif self.arch_name == "loongarch64": - igloo_serial_major = 4 - igloo_serial_minor = 65 - else: - igloo_serial_major = 204 - igloo_serial_minor = 65 + # Serial device major/minor varies by arch (arm uses ttyAMA major 204, + # mips uses ttyS major 4, powerpc uses hvc1 major 229) — see arch_registry. + igloo_serial_major, igloo_serial_minor = arch_registry.spec(self.arch_name).serial result = { "core": { diff --git a/src/penguin/dropin_compile.py b/src/penguin/dropin_compile.py index fd48e009e..d7ba7f407 100644 --- a/src/penguin/dropin_compile.py +++ b/src/penguin/dropin_compile.py @@ -6,25 +6,17 @@ from typing import Any, Dict import penguin -from penguin.abi_info import ARCH_ABI_INFO +from penguin import arch_registry +from penguin.abi_info import arch_abi_info from penguin.defaults import static_dir as STATIC_DIR +from penguin.utils import resolve_arch_asset logger = penguin.getColoredLogger("dropin_compile") -DYLIB_DIRS = { - "aarch64": "arm64", - "intel64": "x86_64", - "loongarch64": "loongarch", - "powerpc": "ppc", - "powerpc64": "ppc64", - "powerpc64le": "ppc64el", -} - - def _default_abi_info(arch: str) -> Dict[str, Any]: - arch_info = ARCH_ABI_INFO[arch] + arch_info = arch_abi_info(arch) abi = arch_info["default_abi"] abi_info = arch_info["abis"][abi] return { @@ -35,7 +27,7 @@ def _default_abi_info(arch: str) -> Dict[str, Any]: def _dylib_dir(arch: str) -> str: - return DYLIB_DIRS.get(arch, arch) + return arch_registry.dylib_subdir(arch) def _loader_name(arch: str) -> str: @@ -67,10 +59,11 @@ def _source_signature(init_dir: Path) -> str: def compile_init_c_dropin(proj_dir: str, init_dir: str, source_path: str, config: Dict[str, Any]) -> str: """Compile an init.d/*.c drop-in and return its project-local binary path.""" arch = config["core"]["arch"] - if arch not in ARCH_ABI_INFO: + if not arch_registry.is_known(arch): raise ValueError(f"cannot compile C drop-in {source_path}: unsupported architecture {arch}") - sysroot = Path(STATIC_DIR) / "sysroots" / arch + sysroot_name = resolve_arch_asset(arch, os.path.join(STATIC_DIR, "sysroots")) + sysroot = Path(STATIC_DIR) / "sysroots" / sysroot_name if not sysroot.is_dir(): raise FileNotFoundError( f"cannot compile C drop-in {source_path}: missing {sysroot}. " diff --git a/src/penguin/penguin_config/__init__.py b/src/penguin/penguin_config/__init__.py index 9e306c516..9908d0a8e 100644 --- a/src/penguin/penguin_config/__init__.py +++ b/src/penguin/penguin_config/__init__.py @@ -341,6 +341,15 @@ def load_config(proj_dir, path, validate=True, resolved_kernel=None, verbose=Fal # `vars` is load-time metadata only; drop it so it never reaches the run. config.pop("vars", None) + # Normalize the architecture to its canonical name (e.g. intel64 -> x86_64, + # arm64 -> aarch64) so all downstream consumers and the realized config use a + # single spelling. arch may have been set by a patch, so do this on the merged + # value. Unknown values are left as-is for schema validation to report. + from penguin import arch_registry + _arch = config.get("core", {}).get("arch") + if _arch is not None and arch_registry.is_known(_arch): + config["core"]["arch"] = arch_registry.normalize_arch(_arch) + # Backwards compat: `timeout` used to be a core-plugin arg (plugins.core.timeout). # It is now the top-level core.timeout option. Migrate an old-style value so # existing configs keep working, preferring an explicit core.timeout. @@ -355,9 +364,18 @@ def load_config(proj_dir, path, validate=True, resolved_kernel=None, verbose=Fal config["core"]["timeout"] = legacy_timeout if config["core"].get("guest_cmd", False) is True: + guesthopper_name = arch_registry.spec(config["core"]["arch"]).canonical + guesthopper_dir = "/igloo_static/guesthopper" + try: + from penguin.utils import resolve_arch_asset + guesthopper_name = resolve_arch_asset( + config["core"]["arch"], guesthopper_dir, prefix="guesthopper." + ) + except Exception: + pass config["static_files"]["/igloo/utils/guesthopper"] = dict( type="host_file", - host_path="/igloo_static/guesthopper/guesthopper."+config["core"]["arch"], + host_path=f"{guesthopper_dir}/guesthopper.{guesthopper_name}", mode=0o755, ) config["static_files"]["/igloo/init.d/guesthopper"] = dict( diff --git a/src/penguin/penguin_config/structure.py b/src/penguin/penguin_config/structure.py index 74f9a1c80..b6b82e632 100644 --- a/src/penguin/penguin_config/structure.py +++ b/src/penguin/penguin_config/structure.py @@ -100,11 +100,29 @@ class Core(PartialModelMixin, BaseModel): model_config = ConfigDict(title="Core configuration options", extra="forbid") arch: Annotated[ - Optional[Literal["armel", "aarch64", "mipsel", "mipseb", "mips64el", "mips64eb", "powerpc", "powerpc64", "powerpc64le", "riscv64", "loongarch64", "intel64"]], + # Canonical config arch names plus accepted aliases. Aliases are + # normalized to their canonical name at config-load time. This list MUST + # equal penguin.arch_registry.all_names() — enforced by a unit test + # (structure.py can't import penguin at schema-definition time). + Optional[Literal[ + "armel", "arm", "armle", + "aarch64", "arm64", + "mipsel", + "mipseb", "mipsbe", + "mips64el", + "mips64eb", "mips64be", + "powerpc", "ppc", + "powerpc64", "ppc64", + "powerpc64le", "ppc64le", "powerpc64el", "ppc64el", + "riscv64", "riscv", "rv64", + "loongarch64", "loongarch", "la64", + "x86_64", "intel64", "amd64", "x86-64", "x64", + ]], Field( None, title="Architecture of guest", - examples=["armel", "aarch64", "mipsel", "mipseb", "mips64el", "mips64eb", "intel64"], + description="Canonical name or an accepted alias (normalized at load, e.g. intel64 -> x86_64).", + examples=["x86_64", "armel", "aarch64", "mipsel", "mipseb", "mips64el", "powerpc64le"], ), ] kernel: Annotated[ diff --git a/src/penguin/penguin_config/templating.py b/src/penguin/penguin_config/templating.py index 5a9c7e786..417a46ca5 100644 --- a/src/penguin/penguin_config/templating.py +++ b/src/penguin/penguin_config/templating.py @@ -112,7 +112,14 @@ def build_context(raw_config, extra=None, env=None, where="vars"): ctx["core"] = dict(core) arch = core.get("arch") if arch: + # Expose the canonical arch name so {{ arch }} is stable regardless of + # which accepted alias the user wrote (e.g. intel64 -> x86_64). ctx["arch"] = arch + try: + from penguin.arch_registry import normalize_arch + ctx["arch"] = normalize_arch(arch) + except Exception: + pass ctx.update(_arch_derived_vars(arch)) user_vars = raw_config.get("vars") if isinstance(raw_config, dict) else None if isinstance(user_vars, dict): diff --git a/src/penguin/penguin_prep.py b/src/penguin/penguin_prep.py index 0fc16702f..0d06c506d 100644 --- a/src/penguin/penguin_prep.py +++ b/src/penguin/penguin_prep.py @@ -3,6 +3,8 @@ import subprocess import re +from penguin.arch_registry import normalize_arch + def _summarize_compiler_stderr(stderr): text = stderr.decode(errors="replace") if isinstance(stderr, bytes) else str(stderr) @@ -173,7 +175,7 @@ def _summarize_compiler_stderr(stderr): ), ), ), - intel64=dict( + x86_64=dict( target_triple="x86_64-unknown-linux-musl", libnvram_arch_name="x86_64", default_abi="default", @@ -208,7 +210,7 @@ def _summarize_compiler_stderr(stderr): def add_lib_inject_for_abi(config, abi): """Compile lib_inject for the ABI and put it in /igloo""" - arch = config["core"]["arch"] + arch = normalize_arch(config["core"]["arch"]) lib_inject = config.get("lib_inject", dict()) arch_info = ARCH_ABI_INFO[arch] @@ -274,7 +276,7 @@ def add_lib_inject_for_abi(config, abi): def add_lib_inject_all_abis(conf): """Add lib_inject for all supported ABIs to /igloo""" - arch = conf["core"]["arch"] + arch = normalize_arch(conf["core"]["arch"]) arch_info = ARCH_ABI_INFO[arch] for abi in arch_info["abis"].keys(): add_lib_inject_for_abi(conf, abi) diff --git a/src/penguin/penguin_run.py b/src/penguin/penguin_run.py index b1cb1f160..43f54da90 100755 --- a/src/penguin/penguin_run.py +++ b/src/penguin/penguin_run.py @@ -16,6 +16,7 @@ from .plugin_manager import ArgsBox from .utils import hash_image_inputs, get_penguin_kernel_version from .q_config import load_q_config, ROOTFS +from . import arch_registry def _env_int(name: str, default: int, min_value: int = 0) -> int: @@ -299,10 +300,9 @@ def run_config( if vpn_enabled: append += f" CID={vpn_args['CID']} " - if archend in ["armel", "aarch64"]: - append = append.replace("console=ttyS0", "console=ttyAMA0") - elif archend in ["powerpc", "powerpc64", "powerpc64el"]: - append = append.replace("console=ttyS0", "console=hvc0 console=ttyS0") + _console = arch_registry.spec(archend).console_replacement + if _console is not None: + append = append.replace(_console[0], _console[1]) telnet_port = find_free_port() if telnet_port is None: diff --git a/src/penguin/q_config.py b/src/penguin/q_config.py index 44339f0b4..58dae89f3 100644 --- a/src/penguin/q_config.py +++ b/src/penguin/q_config.py @@ -8,67 +8,11 @@ a helper function to load the configuration for a given architecture. """ -# Note armel is just panda-system-arm and mipseb is just panda-system-mips +# Note armel is just panda-system-arm and mipseb is just panda-system-mips. +# The per-arch QEMU facts now live in the single arch registry +# (penguin.arch_registry); this module just projects an ArchSpec into the +# legacy q_config dict shape that the rest of the runner expects. ROOTFS: str = "/dev/vda" # Common to all -qemu_configs: dict[str, dict[str, str]] = { - "armel": { - "qemu_machine": "virt", - "arch": "arm", - "kernel_whole": "zImage.armel", - }, - "aarch64": { - "qemu_machine": "virt", - "kconf_group": "arm64", - "cpu": "cortex-a57", - "kernel_whole": "zImage.arm64", - }, - "loongarch64": { - "qemu_machine": "virt", - "cpu": "la464", - "kernel_fmt": "vmlinuz.efi" - }, - "mipsel": { - "qemu_machine": "malta", - }, - "mipseb": { - "qemu_machine": "malta", - "arch": "mips", - "kernel_whole": "vmlinux.mipseb" - }, - "mips64el": { - "qemu_machine": "malta", - "cpu": "MIPS64R2-generic", - "kernel_whole": "vmlinux.mips64el", - }, - "mips64eb": { - "qemu_machine": "malta", - "arch": "mips64", - "kernel_whole": "vmlinux.mips64eb", - "cpu": "MIPS64R2-generic", - }, - "powerpc64el": { - "qemu_machine": "pseries", - "arch": "ppc64", - "cpu": "power9", - }, - "powerpc64": { - "qemu_machine": "pseries", - "arch": "ppc64", - "cpu": "power9", - "kconf_group": "powerpc64", - "kernel_whole": "vmlinux.powerpc64" - }, - "riscv64": { - "qemu_machine": "virt", - "kernel_fmt": "Image", - }, - "intel64": { - "qemu_machine": "pc", - "arch": "x86_64", - "kconf_group": "x86_64", - "kernel_fmt": "bzImage", - }, -} def load_q_config(conf: dict) -> dict[str, str]: @@ -78,16 +22,29 @@ def load_q_config(conf: dict) -> dict[str, str]: :param conf: Configuration dictionary containing 'core' and 'arch' keys. :type conf: dict - :return: QEMU configuration dictionary for the specified architecture. + :return: A fresh QEMU configuration dictionary for the specified architecture. :rtype: dict[str, str] - :raises ValueError: If the architecture is unknown. + :raises ValueError: If the architecture is unknown or has no QEMU machine. """ + from penguin import arch_registry + archend = conf["core"]["arch"] try: - q_config = qemu_configs[archend] - q_config["kconf_group"] = q_config.get("kconf_group", archend) - q_config["arch"] = q_config.get("arch", archend) + s = arch_registry.spec(archend) except KeyError: raise ValueError(f"Unknown architecture: {archend}") + if s.qemu_machine is None: + raise ValueError(f"Unknown architecture: {archend}") + + # Return a fresh dict each call (the old code mutated a shared module dict). + q_config = { + "qemu_machine": s.qemu_machine, + "arch": s.panda_arch, + "kconf_group": s.kconf_group, + "kernel_fmt": s.kernel_fmt, + "kernel_whole": s.kernel_whole, + } + if s.cpu is not None: + q_config["cpu"] = s.cpu return q_config diff --git a/src/penguin/utils.py b/src/penguin/utils.py index 099feeceb..184343208 100644 --- a/src/penguin/utils.py +++ b/src/penguin/utils.py @@ -15,6 +15,7 @@ import os import subprocess import penguin +from penguin import arch_registry from threading import Lock from typing import List, Tuple, Any, Optional, Dict @@ -245,13 +246,7 @@ def get_arch_subdir(config: Dict[str, Any]) -> str: :return: Architecture subdirectory string. :rtype: str """ - arch = config["core"]["arch"] - if arch == "intel64": - return "x86_64" - elif arch in ["powerpc64el", "powerpc64le"]: - return "powerpc64" - else: - return arch + return arch_registry.arch_subdir(config["core"]["arch"]) def get_arch_dir(config: Dict[str, Any]) -> str: @@ -276,12 +271,33 @@ def get_driver_kmod_path(config: Dict[str, Any]) -> str: :rtype: str """ kernel_path = os.path.dirname(config["core"]["kernel"]) - arch_dir = get_arch_subdir(config) - if arch_dir == "aarch64": - arch_dir = "arm64" + arch_dir = arch_registry.kmod_subdir(config["core"]["arch"]) return f"{kernel_path}/igloo.ko.{arch_dir}" +def resolve_arch_asset(arch: str, base_dir: str, prefix: str = "", suffix: str = "") -> str: + """ + Resolve the on-disk name of an asset that is named by the *config* arch + (e.g. ``guesthopper.``, ``sysroots/``), preferring the canonical + name and falling back to the first alias whose file/dir actually exists. + + This lets the canonical name flip to ``x86_64`` while still working against + assets/build artifacts that are still named with an alias (``intel64``) + until the producing build/sibling repo catches up. + + :param arch: A config arch name or alias. + :param base_dir: Directory the asset lives in. + :param prefix: Filename prefix before the arch token (e.g. "guesthopper."). + :param suffix: Filename suffix after the arch token. + :return: The arch token (canonical or alias) to use; canonical if none exist. + """ + s = arch_registry.spec(arch) + for candidate in (s.canonical, *s.aliases): + if os.path.exists(os.path.join(base_dir, f"{prefix}{candidate}{suffix}")): + return candidate + return s.canonical + + def hash_image_inputs(proj_dir: str, conf: Dict[str, Any]) -> str: """ Create a hash of all the inputs of the image creation process. diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index ce69a9f32..c6e8f04a0 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -422,8 +422,9 @@ def test_legacy_at_placeholders_untouched(): ("loongarch64", "loongarch64", "loongarch"), ]) def test_arch_derived_context_vars(arch, arch_dir, dylib_dir): + from penguin.arch_registry import normalize_arch ctx = templating.build_context({"core": {"arch": arch}}) - assert ctx["arch"] == arch + assert ctx["arch"] == normalize_arch(arch) # {{ arch }} is canonicalized assert ctx["arch_dir"] == arch_dir assert ctx["dylib_dir"] == dylib_dir @@ -558,5 +559,142 @@ def test_explicit_core_timeout_wins_over_legacy(): assert cfg["core"]["timeout"] == 5 +# --------------------------------------------------------------------------- # +# arch registry: aliases, normalization, consolidation, canonical flip +# --------------------------------------------------------------------------- # +from penguin import arch_registry + + +@pytest.mark.parametrize("alias,canonical", [ + ("x86_64", "x86_64"), ("intel64", "x86_64"), ("amd64", "x86_64"), + ("x86-64", "x86_64"), ("x64", "x86_64"), + ("aarch64", "aarch64"), ("arm64", "aarch64"), + ("armel", "armel"), ("arm", "armel"), + ("powerpc64le", "powerpc64le"), ("ppc64le", "powerpc64le"), + ("powerpc64el", "powerpc64le"), ("ppc64el", "powerpc64le"), + ("powerpc64", "powerpc64"), ("ppc64", "powerpc64"), + ("powerpc", "powerpc"), ("ppc", "powerpc"), + ("mipseb", "mipseb"), ("mipsel", "mipsel"), + ("riscv64", "riscv64"), ("rv64", "riscv64"), + ("loongarch64", "loongarch64"), ("loongarch", "loongarch64"), +]) +def test_normalize_arch(alias, canonical): + assert arch_registry.normalize_arch(alias) == canonical + assert arch_registry.normalize_arch(alias.upper()) == canonical # case-insensitive + + +def test_normalize_arch_unknown_raises(): + with pytest.raises(KeyError): + arch_registry.normalize_arch("sparc") + assert arch_registry.is_known("intel64") and not arch_registry.is_known("sparc") + + +def test_schema_literal_matches_registry(): + import typing + ann = structure.Core.model_fields["arch"].annotation + lit = next(a for a in typing.get_args(ann) if typing.get_origin(a) is typing.Literal) + assert set(typing.get_args(lit)) == set(arch_registry.all_names()) + + +# Parity vs the OLD hardcoded mappings — guards against regressions in the flip. +def _old_arch_subdir(a): + if a == "intel64": + return "x86_64" + if a in ("powerpc64el", "powerpc64le"): + return "powerpc64" + return a + + +def _old_dylib(a): + if a == "aarch64": + return "arm64" + if a == "intel64": + return "x86_64" + if a == "loongarch64": + return "loongarch" + if a == "powerpc64le": + return "ppc64el" + if "powerpc" in a: + return a.replace("powerpc", "ppc") + return _old_arch_subdir(a) + + +@pytest.mark.parametrize("arch", [ + "armel", "aarch64", "mipsel", "mipseb", "mips64el", "mips64eb", + "powerpc", "powerpc64", "powerpc64le", "riscv64", "loongarch64", "intel64", +]) +def test_subdir_dylib_parity_with_old(arch): + # intel64 (legacy config name) and x86_64 (new canonical) must agree. + assert arch_registry.arch_subdir(arch) == _old_arch_subdir(arch) + assert arch_registry.dylib_subdir(arch) == _old_dylib(arch) + assert arch_registry.arch_subdir(arch) == arch_registry.arch_subdir(arch_registry.normalize_arch(arch)) + + +def test_x86_subdirs(): + assert arch_registry.arch_subdir("intel64") == "x86_64" == arch_registry.arch_subdir("x86_64") + assert arch_registry.dylib_subdir("intel64") == "x86_64" == arch_registry.dylib_subdir("x86_64") + assert arch_registry.kmod_subdir("aarch64") == "arm64" + + +def test_q_config_powerpc64le_and_fresh_dict(): + from penguin.q_config import load_q_config + q = load_q_config({"core": {"arch": "powerpc64le"}}) + assert q["arch"] == "ppc64" and q["qemu_machine"] == "pseries" + assert load_q_config({"core": {"arch": "ppc64el"}})["arch"] == "ppc64" # alias + # returns a fresh dict each call (old code mutated a shared module dict) + a = load_q_config({"core": {"arch": "mipsel"}}); a["arch"] = "ZZ" + assert load_q_config({"core": {"arch": "mipsel"}})["arch"] == "mipsel" + + +def test_abi_info_rekeyed_and_normalizes(): + from penguin.abi_info import arch_abi_info, ARCH_ABI_INFO + assert "x86_64" in ARCH_ABI_INFO and "intel64" not in ARCH_ABI_INFO + assert arch_abi_info("intel64") is arch_abi_info("x86_64") + + +def test_dropin_dylib_dir_consolidated(): + from penguin.dropin_compile import _dylib_dir + assert _dylib_dir("intel64") == "x86_64" == _dylib_dir("x86_64") + assert _dylib_dir("aarch64") == "arm64" + + +def test_load_config_arch_alias_normalized(): + """arch: intel64 and arch: x86_64 produce identical realized configs.""" + def realize(arch): + with tempfile.TemporaryDirectory() as tmp: + proj = Path(tmp, "proj") + (proj / "base").mkdir(parents=True) + with tarfile.open(proj / "base" / "fs.tar.gz", "w"): + pass + (proj / "config.yaml").write_text(textwrap.dedent(f""" + core: + arch: {arch} + version: 2 + env: {{}} + pseudofiles: {{}} + nvram: {{}} + lib_inject: {{}} + static_files: {{}} + plugins: {{}} + """)) + return pc.load_config( + str(proj), str(proj / "config.yaml"), validate=True, + resolved_kernel="/igloo_static/kernels/6.13/zImage.x86_64", + ) + a = realize("x86_64") + b = realize("intel64") + assert a["core"]["arch"] == "x86_64" and b["core"]["arch"] == "x86_64" + assert a == b + + +def test_templating_arch_alias_canonical_and_derived(): + raw = {"core": {"arch": "intel64"}, "x": "{{ arch }} {{ arch_dir }} {{ dylib_dir }}"} + out, _ = templating.render_config(raw) + assert out["x"] == "x86_64 x86_64 x86_64" + raw2 = {"core": {"arch": "arm64"}, "x": "{{ arch }} {{ dylib_dir }}"} + out2, _ = templating.render_config(raw2) + assert out2["x"] == "aarch64 arm64" + + if __name__ == "__main__": raise SystemExit(pytest.main([__file__, "-v"])) From 022af4bf3f1cc3667b2ff86ec54ffa23a62704da Mon Sep 17 00:00:00 2001 From: Luke Craig Date: Sun, 7 Jun 2026 10:41:32 -0400 Subject: [PATCH 2/2] Fix flake8: move test import to top, split semicolon statement --- tests/unit_tests/test_config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index c6e8f04a0..52c205490 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -22,6 +22,7 @@ import pytest import penguin.penguin_config as pc +from penguin import arch_registry from penguin.penguin_config import structure from penguin.penguin_config import gen_docs, templating from penguin.penguin_config.errors import format_validation_error @@ -562,7 +563,6 @@ def test_explicit_core_timeout_wins_over_legacy(): # --------------------------------------------------------------------------- # # arch registry: aliases, normalization, consolidation, canonical flip # --------------------------------------------------------------------------- # -from penguin import arch_registry @pytest.mark.parametrize("alias,canonical", [ @@ -642,7 +642,8 @@ def test_q_config_powerpc64le_and_fresh_dict(): assert q["arch"] == "ppc64" and q["qemu_machine"] == "pseries" assert load_q_config({"core": {"arch": "ppc64el"}})["arch"] == "ppc64" # alias # returns a fresh dict each call (old code mutated a shared module dict) - a = load_q_config({"core": {"arch": "mipsel"}}); a["arch"] = "ZZ" + a = load_q_config({"core": {"arch": "mipsel"}}) + a["arch"] = "ZZ" assert load_q_config({"core": {"arch": "mipsel"}})["arch"] == "mipsel"