From 77158104f1fbfaa362b9cf5f4d9eff373e8db4a6 Mon Sep 17 00:00:00 2001 From: Carl Smedstad Date: Sun, 9 Nov 2025 19:16:01 +0100 Subject: [PATCH] Support system Zig in addition to the one bundled in a Python package (#3398) * Support system Zig in addition to the one bundled in a Python package Add support for locating the Zig executable with the following precedence: 1. ziglang module - if installed, use bundled Zig. 2. zig in PATH - fallback to system installation. On Arch Linux we don't package the ziglang Python package. This change makes it possible for pwndbg to use the Zig executable from our zig0.14 package [0]. [0]: https://archlinux.org/packages/extra/x86_64/zig0.14/ Disclaimer: Authored with assistance from Claude Code. * Fail if found Zig has unsupported version Only version 0.14.1 works, 0.15+ doesn't * Address PR comments - Increase version check timeout from 1s to 15s (necessary on MacOS). - Cache get_zig_executable() result. - Only check version of system Zig. Python packaged one is locked. --- pwndbg/lib/zig.py | 57 +++++++++++++++++++++++------ tests/library/qemu_user/conftest.py | 5 ++- tests/tests.py | 5 ++- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/pwndbg/lib/zig.py b/pwndbg/lib/zig.py index bbcfbcdf9..2ea3478fd 100644 --- a/pwndbg/lib/zig.py +++ b/pwndbg/lib/zig.py @@ -1,7 +1,9 @@ from __future__ import annotations +import os import os.path import pathlib +import shutil import subprocess import tempfile from typing import Dict @@ -9,6 +11,7 @@ from typing import List from typing import Literal from typing import Tuple +import pwndbg.lib.cache from pwndbg.lib.arch import PWNDBG_SUPPORTED_ARCHITECTURES_TYPE from pwndbg.lib.arch import ArchDefinition from pwndbg.lib.arch import Platform @@ -71,6 +74,44 @@ _asm_header: Dict[str, str] = { } +ZIG_SUPPORTED_VERSION = "0.14.1" + + +@pwndbg.lib.cache.cache_until("forever") +def get_zig_executable() -> str: + """ + Get the path to the zig executable. + Precedence: ziglang module, zig in PATH. + """ + try: + import ziglang # type: ignore[import-untyped] + return os.path.join(os.path.dirname(ziglang.__file__), "zig") + except ImportError: + pass + + zig_path = shutil.which("zig") + if zig_path is None: + raise ValueError("Python module ziglang not available and zig not found in PATH") + + try: + result = subprocess.run( + [zig_path, "version"], + capture_output=True, + text=True, + timeout=15, + ) + version = result.stdout.strip() + if version != ZIG_SUPPORTED_VERSION: + raise ValueError( + f"Unsupported Zig version: {version}. " + f"Only version {ZIG_SUPPORTED_VERSION} is supported." + ) + except Exception as e: + raise ValueError(f"Failed to check Zig version at {zig_path}: {e}") + + return zig_path + + def _get_zig_target(arch: ArchDefinition) -> str | None: if arch.platform == Platform.LINUX: # "gnu", "gnuabin32", "gnuabi64", "gnueabi", "gnueabihf", @@ -90,10 +131,7 @@ def _get_zig_target(arch: ArchDefinition) -> str | None: def flags(arch: ArchDefinition) -> List[str]: - try: - import ziglang # type: ignore[import-untyped] - except ImportError: - raise ValueError("Can't import ziglang") + zig_executable = get_zig_executable() zig_target = _get_zig_target(arch) if zig_target is None: @@ -102,7 +140,7 @@ def flags(arch: ArchDefinition) -> List[str]: ) return [ - os.path.join(os.path.dirname(ziglang.__file__), "zig"), + zig_executable, "cc", "-target", zig_target, @@ -120,10 +158,7 @@ def asm(arch: ArchDefinition, data: str, includes: List[pathlib.Path] | None = N def _asm(arch_mapping: str, data: str, includes: List[pathlib.Path] | None = None) -> bytes: - try: - import ziglang - except ImportError: - raise ValueError("Can't import ziglang") + zig_executable = get_zig_executable() header = _asm_header.get(arch_mapping, None) if header is None: @@ -148,7 +183,7 @@ def _asm(arch_mapping: str, data: str, includes: List[pathlib.Path] | None = Non # Build the binary with Zig compile_process = subprocess.run( [ - os.path.join(os.path.dirname(ziglang.__file__), "zig"), + zig_executable, "cc", "-target", target, @@ -167,7 +202,7 @@ def _asm(arch_mapping: str, data: str, includes: List[pathlib.Path] | None = Non # Extract bytecode objcopy_process = subprocess.run( [ - os.path.join(os.path.dirname(ziglang.__file__), "zig"), + zig_executable, "objcopy", "-O", "binary", diff --git a/tests/library/qemu_user/conftest.py b/tests/library/qemu_user/conftest.py index e34276b71..2bf2e4a0f 100644 --- a/tests/library/qemu_user/conftest.py +++ b/tests/library/qemu_user/conftest.py @@ -13,9 +13,9 @@ from typing import Tuple import gdb import pytest -import ziglang from pwndbg.lib import tempfile +from pwndbg.lib.zig import get_zig_executable _start_binary_called = False @@ -148,9 +148,10 @@ def qemu_assembly_run(): compiled_file = os.path.join(tmpdir, "out.elf") # Build the binary with Zig + zig_executable = get_zig_executable() compile_process = subprocess.run( [ - os.path.join(os.path.dirname(ziglang.__file__), "zig"), + zig_executable, "cc", *extra_cli_args, f"--target={zig_target}", diff --git a/tests/tests.py b/tests/tests.py index eda4daf03..6a1dff9bc 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -14,7 +14,7 @@ import time from enum import Enum from pathlib import Path -import ziglang +from pwndbg.lib.zig import get_zig_executable from .host import TestHost from .host import TestResult @@ -337,11 +337,12 @@ def make_all(path: Path, jobs: int = multiprocessing.cpu_count()): print(f"[+] make -C {path} -j{jobs} all") try: + zig_executable = get_zig_executable() subprocess.check_call( [ "make", f"-j{jobs}", - "ZIGCC=" + os.path.join(os.path.dirname(ziglang.__file__), "zig") + " cc", + f"ZIGCC={zig_executable} cc", "all", ], cwd=str(path),