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.
pull/3399/head
Carl Smedstad 4 weeks ago committed by GitHub
parent 755ff4ccc2
commit 77158104f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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",

@ -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}",

@ -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),

Loading…
Cancel
Save