Metadata-Version: 2.3
Name: zig-maturin
Version: 1.0.1
Summary: Build and publish Zig-powered Python extensions with native cross-compilation
Author: Ricardo Robles Fernández
Author-email: Ricardo Robles Fernández <ricardo.r.f@hotmail.com>
License: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Topic :: Software Development :: Build Tools
Requires-Dist: click>=8.0
Requires-Dist: pytest>=8 ; extra == 'dev'
Requires-Dist: ruff>=0.6 ; extra == 'dev'
Requires-Python: >=3.12
Project-URL: Homepage, https://github.com/rroblf01/zig-maturin
Project-URL: Repository, https://github.com/rroblf01/zig-maturin
Project-URL: Documentation, https://github.com/rroblf01/zig-maturin
Provides-Extra: dev
Description-Content-Type: text/markdown

# zig-maturin

**Build and publish Zig-powered Python extensions — `pip install`, no compiler on the user's side.**

`zig-maturin` is [Maturin](https://github.com/PyO3/maturin) + [PyO3](https://github.com/PyO3/pyo3) for Zig: a high-level Zig library (`pyo3zig`) for writing Python extensions, plus a CLI that compiles them into ready-to-install wheels.

```python
import my_extension
my_extension.add(2, 3)          # -> 5
my_extension.greet("world")     # -> "Hello, world!"
```

The extension author needs Zig. The end user just `pip install`s a wheel — no Zig, no compiler, nothing to build.

## Why Zig

| | Rust + Maturin | Zig + zig-maturin |
|---|---|---|
| Cross-compilation | needs Docker / extra toolchains | **built-in**: `--target aarch64-macos` works out of the box |
| Toolchain size | gigabytes | a few megabytes |
| Honest manylinux | via `auditwheel` | **glibc pinned at build** (`gnu.2.28`) → the tag is true |
| C-API access | `unsafe extern` | native C interop |

## Performance

Summing `0..1_000_000` in a tight loop, Zig (releasing the GIL) vs pure Python
(`examples/bench.py`, CPython 3.13, x86_64):

| Implementation | Time per call | Speedup |
|---|---|---|
| Zig (`pz.allowThreads`) | **3.3 ms** | — |
| Pure Python `sum(range(n))` | 15.6 ms | 4.8× |
| Pure Python `for` loop | 45.9 ms | 14× |

Numbers vary by machine; run `python examples/bench.py` after building the demo.

## Install

```bash
pip install zig-maturin     # the build tool (pure Python)
```

No system toolchain is required: if `zig` is not on PATH, the build pulls in the
[`ziglang`](https://pypi.org/project/ziglang/) wheel (a pinned Zig binary)
automatically and compiles through it. If you already have
[Zig 0.16](https://ziglang.org/download/) on PATH, that is used instead (no
download).

## Quick start

```bash
zig-maturin scaffold my_extension
cd my_extension
zig-maturin develop                 # build + install into the current venv
python -c "import my_extension; print(my_extension.hello())"
zig-maturin build                   # produce a wheel in dist/
```

Scaffolded projects use the **PEP 517 backend**
(`build-backend = "zig_maturin.buildapi"`), so the standard tools work too —
`pip install .`, `pip wheel .`, `python -m build`. If no `zig` is on PATH the
backend adds `ziglang` as a build dependency automatically, so this works with
no system toolchain:

```bash
pip install .                       # builds + installs the extension
pip install -e .                    # editable (PEP 660); re-run to recompile
python -m build                     # wheel + sdist in dist/
```

### What scaffold generates

`zig-maturin scaffold my_extension` writes a complete, buildable project and
wires up the Zig dependency for you (it runs `zig fetch --save` if `zig` is on
PATH):

```
my_extension/
├── pyproject.toml      # [tool.zig-maturin] config + PEP 517 backend
├── build.zig           # the Zig build script (links pyo3zig + the C shim)
├── build.zig.zon       # dependency manifest (zig-maturin pinned with a hash)
└── src/main.zig        # your extension — edit this
```

You normally only touch `src/main.zig`. The generated `build.zig` already wires
in the two modules and the C shim — you rarely need to change it:

```zig
const zm_dep = b.dependency("zig-maturin", .{ .target = target, .optimize = optimize });

const mod = b.createModule(.{
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
    .imports = &.{
        .{ .name = "zig-maturin", .module = zm_dep.module("zig-maturin") }, // low-level C-API
        .{ .name = "pyo3zig", .module = zm_dep.module("pyo3zig") },         // high-level layer
    },
});

const lib = b.addLibrary(.{ .name = "my_extension", .linkage = .dynamic, .root_module = mod });
lib.root_module.link_libc = true;
lib.root_module.addCSourceFile(.{ .file = zm_dep.path("pyo3zig_capi.c"), .flags = &.{} });
// CPython symbols resolve against the interpreter at import time:
lib.linker_allow_shlib_undefined = true;
b.installArtifact(lib);
```

**Adding pyo3zig to an existing Zig project** (instead of scaffolding): run
`zig fetch --save=zig-maturin git+https://github.com/rroblf01/zig-maturin`, then
add the two `.imports` and the `addCSourceFile`/`link_libc`/
`linker_allow_shlib_undefined` lines above to your `build.zig`.

For a feature-by-feature tour of a real, compiled extension, see the
[examples walkthrough](examples/README.md) (the source is
[`pyo3zig_example.zig`](pyo3zig_example.zig), built by `zig build` and exercised
by the test suite).

### Mixed Python/Zig packages

Ship pure-Python code alongside the native module: put a package at
`<python-source>/<module-name>/` (e.g. `src/my_extension/__init__.py`). The wheel
then bundles those `.py` files and nests the compiled extension inside the
package as `my_extension/my_extension.<so>`, which `__init__.py` re-exports:

```python
# src/my_extension/__init__.py
from .my_extension import *      # the Zig extension
from .helpers import wrapper     # pure-Python companion code
```

Without such a package, the extension is installed as a top-level `module.so`.

### One wheel for all CPython versions (abi3)

Set `abi3` in `[tool.zig-maturin]` to build against the stable ABI and ship a
single `cp312-abi3-<platform>` wheel that works on that CPython and every later
version:

```toml
[tool.zig-maturin]
abi3 = "3.12"
```

Or, without touching `pyproject.toml`, pass it on the command line:

```bash
zig-maturin build --abi3 3.12
```

The framework already uses out-of-line refcounting and accessor functions (not
inline macros), so abi3 adds ~no runtime overhead here. Managed `__dict__` and
weakref need the GC pre-header and are unavailable under abi3 (cyclic GC of
`?*pz.PyObject` fields still works).

### Free-threading (no-GIL, PEP 703) — not yet

Free-threaded interpreters (`python3.13t` / `python3.14t`) are **not supported
yet**. The C shim already opts in (`Py_MOD_GIL_NOT_USED`) and is refcount-clean,
but a free-threaded build has a wider `PyObject` header, so the Zig-side module
and instance layouts need a free-threaded-specific definition before the
extension can load there. Regular (with-GIL) CPython is fully supported.

### Sub-interpreters

Modules use multi-phase init (PEP 489) and declare support for sub-interpreters,
so the extension can be imported into more than one interpreter (which
single-phase modules cannot). Type objects and the cached `datetime` / awaitable
/ exception objects are keyed per interpreter — each interpreter gets its own, so
nothing is shared across the interpreter boundary. Classes, operators,
inheritance, container conversions, `datetime` and custom exceptions all work in
a sub-interpreter (see `tests/test_subinterp.py`).

Shared-GIL ("legacy") sub-interpreters are fully supported. Per-interpreter-GIL
(truly parallel interpreters) is not yet declared — the per-interpreter caches
rely on the shared GIL for serialization.

## Stability

`1.0.0` is the first stable release. The public Zig API (`pyo3zig`, imported as
`pz`) and the `zig_maturin` build tooling follow semantic versioning: breaking
changes wait for a major bump.

## Writing an extension

A module is declared with `pyModule` and exported with `exportModule`:

```zig
const std = @import("std");
const pz = @import("pyo3zig");

// Turn a Zig panic into a Python exception instead of crashing the interpreter.
pub const panic = pz.panic;

fn add(a: i64, b: i64) i64 {
    return a + b;
}

fn greet(name: []const u8) !pz.PyString {
    var buf: [256]u8 = undefined;
    return pz.PyString.init(try std.fmt.bufPrint(&buf, "Hello, {s}!", .{name}));
}

const Mod = pz.pyModule("my_extension", .{
    .doc = "An extension written in Zig.",
    .functions = &.{
        pz.pyFnNamed("add", add),
        pz.pyFnNamed("greet", greet),
    },
});

comptime {
    pz.exportModule(Mod);
}
```

Plain Zig functions are wrapped automatically: argument count, type conversion,
and error handling are all derived from the signature. Returning `!T` makes a
Zig error surface as a Python exception.

### Type conversions

| Zig | Python (argument) | Python (return) |
|---|---|---|
| `i8`..`i64`, `u8`..`u64` | `int` | `int` |
| `f32`, `f64` | `float` | `float` |
| `std.math.Complex(f64/f32)` | `complex` (int/float coerced) | `complex` |
| `bool` | `bool` | `bool` |
| `enum` | `int` (validated; bad value → `ValueError`) | `int` |
| `pz.DateTime` | `datetime.datetime` | `datetime.datetime` |
| `[]const u8` | `str` / `bytes` / `bytearray` / `os.PathLike` (borrowed) | `str` |
| `?T` | `T` or `None` | `T` or `None` |
| `[]T`, `[N]T` | `list` / `tuple` | `list` |
| `std.StringHashMap(V)`, `std.AutoHashMap(K,V)` | `dict` | `dict` |
| `pz.PySet`, `pz.PyFrozenSet` | — | `set` / `frozenset` |
| tuple struct | `list` / `tuple` | `tuple` |
| plain struct | `dict` (by field name; field defaults honored) | `dict` (by field name) |
| `*MyClass` | an instance of `MyClass` (borrowed) | — |
| `?*pz.PyObject` | any object | any object (passthrough) |

Conversion is bidirectional: a `list`/`tuple` becomes a `[]T` argument, a `dict`
becomes a struct argument, and an instance of one of your classes can be passed
to a function as a `*MyClass` pointer. A type mismatch raises a precise
`TypeError` (`expected int, got str`).

### Keyword arguments and defaults

Zig reflection doesn't expose parameter names, so declare them explicitly:

```zig
fn power(base: i64, exp: i64) i64 { ... }

pz.pyFnKw("power", power, .{
    .args = &.{ "base", "exp" },
    .defaults = .{ .exp = @as(i64, 2) },   // optional, by name
});
```

```python
power(3)               # 9   (exp defaults to 2)
power(2, 10)           # 1024
power(base=5, exp=3)   # 125
```

### Classes

A Zig `extern struct` becomes a Python class. Fields are exposed as attributes;
declare optional dunder methods directly on the struct:

```zig
const Greeter = extern struct {
    val: i64,

    pub fn init(v: i64) Greeter {
        return .{ .val = v };
    }
    pub fn __str__(self: *Greeter) !pz.PyString { ... }
    pub fn __hash__(self: *Greeter) i64 { return self.val; }
    pub fn __eq__(self: *Greeter, other: *Greeter) bool { return self.val == other.val; }
    pub fn __deinit__(self: *Greeter) void { ... }   // called on GC
};

fn greet_method(self: *Greeter) !pz.PyString { ... }

const GreeterClass = pz.PyClass(Greeter, .{
    .methods = &.{ pz.wrapMethodNamed(Greeter, "greet", greet_method) },
    .readonly = &.{"val"},   // expose `val` read-only
});
```

Register the class in the module's `.classes` field.

**Hooks** (declared on the struct):

- Lifecycle / repr: `init`, `__deinit__` (called on GC), `__str__`, `__repr__`,
  `__hash__`, `__format__` (`format()` / f-string specs), `__call__` (callable
  instances), `__await__` (awaitable; `await obj` resolves to its return value)
  or `__await_delegate__(self) ?*PyObject` (delegate to a real awaitable and
  suspend to the event loop), `__aiter__`/`__anext__` (`async for x in obj`;
  `__anext__(self) -> ?T`),
  `__enter__`/`__exit__` (context manager, `with obj:`), `__reduce__`
  (pickle), `__getstate__`/`__setstate__`, `__bytes__` (`bytes(obj)`),
  `__floor__`/`__ceil__`/`__trunc__` (`math.*`), `__round__`,
  `__copy__`/`__deepcopy__` (`copy.copy`/`copy.deepcopy`, returning a fresh
  instance), `__fspath__` (`os.PathLike`), `__length_hint__`.
- Comparisons: `__eq__` (and `__ne__` derived from it), plus `__lt__`, `__le__`,
  `__gt__`, `__ge__` — define any subset (just `__lt__` enables `sorted()`).
  Defining `__eq__` without `__hash__` makes instances unhashable, as in Python.
- Container / iterator protocols: `__len__`, `__getitem__`, `__setitem__`,
  `__delitem__`, `__contains__`, `__iter__`, `__next__`, `__reversed__` (a type
  with `__next__` is its own iterator; `__getitem__` normalizes negative indices
  when `__len__` is present).
- Operators: `__add__`, `__sub__`, `__mul__`, `__truediv__`, `__floordiv__`,
  `__mod__`, `__pow__`, `__matmul__`, `__divmod__`, bitwise `__and__`/`__or__`/`__xor__`/
  `__lshift__`/`__rshift__`, unary `__neg__`/`__pos__`/`__abs__`/`__invert__`/
  `__bool__`, the matching in-place forms (`__iadd__`, `__imul__`, `__iand__`,
  `__ilshift__`, … — every operator has one), and conversions
  `__int__`/`__float__`/`__index__`. A binary op's second parameter can be the
  same type (`*Self`) or a scalar (e.g. `i64` for `vec * 2`); define
  `__radd__`/`__rsub__`/`__rmul__` for the reflected form (`2 * vec`). Operands
  that don't match yield `NotImplemented`. A result of type `Self` is wrapped
  into a new instance.
- Attributes: `__getattr__(self, name)` (consulted only when normal lookup
  fails) and `__setattr__(self, name, value)` (intercepts every assignment).
- Descriptors: `__get__(self, obj, objtype)`, `__set__(self, obj, value)`,
  `__delete__(self, obj)` — use an instance as a managed attribute on a class.
- Subclass hook: `__init_subclass__(cls)` — fires when a Python subclass is
  created (`cls` is the new subclass).
- Buffer: `__buffer__(self) []const u8` (read-only) or `__buffer_mut__(self)
  []u8` (writable) exposes a zero-copy view to `memoryview`/`bytes`/numpy.

Every class is also subscriptable for type hints — `MyClass[int]` yields a
`types.GenericAlias` (auto `__class_getitem__`).

**Cyclic GC:** a class storing a `?*pz.PyObject` field automatically gets
`Py_TPFLAGS_HAVE_GC` with `tp_traverse`/`tp_clear`, so reference cycles are
collectable. The framework owns one reference per field (incref on set, decref
on clear/dealloc); don't manually decref those fields in `__deinit__`. These GC
classes also support `weakref.ref(obj)` and a managed `__dict__` (arbitrary
Python attributes, GC-traced) — both cleared on dealloc. Value classes (no
PyObject field) have neither, keeping instances minimal.

**Subclassing from Python:** every class is `Py_TPFLAGS_BASETYPE`, so Python code
can `class Sub(MyClass): ...` and inherit fields, methods, and operators —
including classes with `__deinit__` or cyclic GC. Teardown runs in `tp_finalize`,
so CPython's `subtype_dealloc` correctly tears down a subclass's `__dict__`,
weakrefs, and GC; `__deinit__` fires for subclass instances and cycles through
them stay collectable. Wider integers `i65..i128`/`u65..u128` are supported as
arguments and return values (round-tripped through CPython's bigint).

**`PyClass` config:**

```zig
const Vec2Class = pz.PyClass(Vec2, .{
    .doc = "A 2D vector.",   // class docstring (help(Vec2))
    // keyword __init__ with defaults
    .init_args     = &.{ "x", "y" },
    .init_defaults = .{ .y = @as(i64, 0) },
    // keyword __call__ with defaults (when the class defines __call__)
    .call_args     = &.{ "k" },
    .call_defaults = .{ .k = @as(i64, 1) },
    // computed (read-only) properties
    .properties = &.{ .{ .name = "length_sq", .get = vec2_length_sq } },
    .methods = &.{
        pz.wrapMethodNamed(Vec2, "dot", vec2_dot),         // positional method
        pz.wrapMethodKw(Vec2, "scale", scale, .{ .args = &.{"k"} }),  // kwargs method
        pz.staticMethod("dims", vec2_dims),                // @staticmethod
        pz.classMethod(Vec2, "from_pair", vec2_from_pair), // @classmethod / alt constructor
    },
});
```

A `classMethod` whose Zig function returns `T` (or `!T`) is treated as an
**alternative constructor**: the returned struct is wrapped into a fresh
instance, so `Vec2.from_pair(...)` returns a `Vec2`.

**Enums:** expose a Zig `enum` as a real Python `enum.IntEnum` with
`pz.enumClass(MyEnum, "Name")`, then list it in `.classes` alongside your
`PyClass` types — each variant becomes a member (`Color.RED == 0`). (A plain
`enum` used as a function argument or return value converts as an `int`.)

### Releasing the GIL

Wrap a long pure-Zig computation in `pz.allowThreads` to release the GIL while
it runs, so other Python threads make progress:

```zig
fn heavy_sum(n: i64) i64 {
    return pz.allowThreads(compute_sum, .{n});
}
```

### Typed exceptions

Raise a specific built-in (or custom) Python exception, then return any Zig
error — the framework preserves the one you set instead of remapping it:

```zig
fn parse_positive(x: i64) !i64 {
    if (x <= 0) {
        pz.setError(pz.PyExc_ValueError(), "value must be positive");
        return error.NotPositive;
    }
    return x;
}
```

For a reusable custom exception, declare a `pz.exceptionClass`, register it in
`.classes`, and raise it from Zig:

```zig
const MyError = pz.exceptionClass("mymod.MyError", pz.PyExc_ValueError);
// .classes = &.{ ..., MyError }

fn check(n: i64) !i64 {
    if (n < 0) { MyError.raise("must be non-negative"); return error.Bad; }
    return n;
}
```

`MyError` becomes `mymod.MyError` (a `ValueError` subclass) on the Python side.
Pass `null` as the base for a plain `Exception`. (`pz.newException(...)` remains
for one-off types not registered as a class.)

### Inheritance

A Zig class can inherit from another Zig class with `.base`. The derived struct
must embed the base's struct as its **first field**; the base's methods and
field accessors are then inherited and `isinstance`/`issubclass` work:

```zig
const Animal = extern struct {
    legs: i64,
    pub fn init(legs: i64) Animal { return .{ .legs = legs }; }
};
const AnimalClass = pz.PyClass(Animal, .{ .init_args = &.{"legs"}, ... });

const Dog = extern struct {
    base: Animal,            // the base, embedded first
    good: bool,
    pub fn init(legs: i64, good: bool) Dog {
        return .{ .base = .{ .legs = legs }, .good = good };
    }
};
const DogClass = pz.PyClass(Dog, .{ .base = AnimalClass, .init_args = &.{ "legs", "good" }, ... });
```

```python
d = Dog(4, True)
isinstance(d, Animal)   # True; d.legs and inherited methods work
class Puppy(Dog): ...   # Python can subclass the derived class too
```

Register the base in `.classes` before the derived class.

### Computed properties

Expose getter/setter attributes (not backed by a struct field) with
`.properties`:

```zig
const Vec2Class = pz.PyClass(Vec2, .{
    .properties = &.{
        .{ .name = "length_sq", .get = vec2_length_sq },        // read-only
        .{ .name = "x", .get = vec2_get_x, .set = vec2_set_x },  // read-write
    },
});
```

### Variadic functions (`*args` / `**kwargs`)

`pz.pyFnRaw` registers a function that receives the raw argument tuple and
keyword dict:

```zig
fn sum_all(args: ?*pz.PyObject, kwargs: ?*pz.PyObject) i64 {
    var total: i64 = 0;
    var i: isize = 0;
    while (i < pz.PyTuple_Size(args)) : (i += 1)
        total += pz.PyLong_AsLongLong(pz.PyTuple_GetItem(args, i));
    return total;
}
// pz.pyFnRaw("sum_all", sum_all)
```

### Module constants

```zig
const Mod = pz.pyModule("my_extension", .{
    .constants = .{ .VERSION = "1.0", .MAX_ITEMS = @as(i64, 100) },
    .functions = &.{ ... },
    .classes   = &.{ GreeterClass },
});
```

### Submodules

Nest modules with `.submodules`. Each child is set as an attribute of the parent
and registered in `sys.modules` under its dotted name, so both attribute access
and `import parent.child` work:

```zig
const MathxMod = pz.pyModule("mathx", .{
    .constants = .{ .E = @as(f64, 2.71828) },
    .functions = &.{ pz.pyFnNamed("triple", triple) },
});

const Mod = pz.pyModule("my_extension", .{
    .submodules = .{ MathxMod },
    .functions  = &.{ ... },
});
```

```python
import my_extension
my_extension.mathx.triple(4)        # attribute access
from my_extension.mathx import triple   # dotted import
```

### Error handling and panics

- A Zig **error** (`!T`) becomes a Python exception (mapped by kind: `error.Overflow`
  → `OverflowError`, etc.).
- A Zig **panic** (out-of-bounds, `@panic`, integer overflow in safe builds) would
  normally abort the whole interpreter. Opt into the safety net with
  `pub const panic = pz.panic;` and it becomes a `RuntimeError` instead — the
  interpreter stays alive. (Caveat: the recovery skips `defer`s between the panic
  site and the call boundary, so that window leaks.)

### Type stubs (`.pyi`)

Type hints are generated **at compile time** from your Zig signatures:

```zig
const STUB = pz.moduleStub(.{
    .{ .name = "add", .func = add, .args = &.{ "a", "b" } },
    .{ .name = "greet", .func = greet, .args = &.{"name"} },
}) ++ "\n" ++ pz.classStub(.{
    .name = "Greeter", .type = Greeter, .init = &.{"v"},
    .methods = .{ .{ .name = "greet", .func = greet_method } },
});
fn __pyi__() []const u8 { return STUB; }
// register pz.pyFnNamed("__pyi__", __pyi__)
```

`classStub` emits a `class` block (struct fields as attributes, `__init__`,
methods, and `.properties` as `@property` accessors). For variadic functions set
`.raw = true` on the `moduleStub` entry (rendered as `*args, **kwargs`), and for
custom exceptions use `pz.exceptionStub("MyError", "ValueError")`. `complex`,
`datetime`, optionals and containers map to their Python spellings automatically.

`zig-maturin build` calls `__pyi__()` on native builds and ships the resulting
`my_extension.pyi` inside the wheel, so type checkers see your signatures.

## CLI

| Command | Description |
|---|---|
| `zig-maturin scaffold <name>` | Create a new project (pyproject, build.zig, src/main.zig). |
| `zig-maturin develop` | Build and install into the current environment. |
| `zig-maturin build` | Build a wheel in `dist/`. |
| `zig-maturin sdist` | Build a source distribution. |
| `zig-maturin generate-ci` | Write a GitHub Actions workflow that builds + publishes wheels. |

`build` / `develop` options: `--target <triple>` (repeatable), `--release`,
`--out <dir>`, `--abi3 <X.Y>` (build a stable-ABI wheel), and for
cross-compilation `--python-include` / `--python-libdir` / `--python-lib`.

## Cross-compilation

Zig cross-compiles out of the box. Linux glibc targets are pinned so the
manylinux tag is honest:

```bash
zig-maturin build --target x86_64-linux-gnu --target aarch64-macos
# -> manylinux_2_28_x86_64, macosx_11_0_arm64
```

| Zig target | Wheel tag |
|---|---|
| `x86_64-linux-gnu` (→ `gnu.2.28`) | `manylinux_2_28_x86_64` |
| `aarch64-linux-gnu` | `manylinux_2_28_aarch64` |
| `x86_64-linux-musl` | `musllinux_1_2_x86_64` |
| `aarch64-macos` / `x86_64-macos` | `macosx_11_0_arm64` / `_x86_64` |
| `x86_64-windows` / `aarch64-windows` | `win_amd64` / `win_arm64` |

Cross-compiling needs the **target** Python's headers (and, on Windows, its
`pythonXY.lib`); supply them via `--python-include` / `--python-libdir` /
`--python-lib` or `[tool.zig-maturin]`. Native builds detect them via
`sysconfig` automatically.

## Configuration

```toml
[tool.zig-maturin]
module-name = "my_extension"     # default: project name
zig-source  = "src/main.zig"
# cross-compilation overrides (optional):
# python-include = "..."
# python-libdir  = "..."
# python-lib     = "python312"
```

## Troubleshooting

| Symptom | Cause / fix |
|---|---|
| `error: expected ... found` on `zig build`, or unknown builtins | Wrong Zig version. zig-maturin targets **Zig 0.16**; check `zig version`, or just don't install Zig and let the build pull in the pinned `ziglang` wheel. |
| `zig fetch failed` during scaffold | No `zig` on PATH at scaffold time. Run the printed command later: `zig fetch --save=zig-maturin git+https://github.com/rroblf01/zig-maturin`. The build itself still works toolchain-free. |
| `python3-config not found` / `Python.h` missing | Install the Python dev headers (`python3-dev` / `python3-devel`), or use the official `python.org` build. For **cross-compilation** the host can't detect the target's headers — pass `--python-include` (and on Windows `--python-libdir`/`--python-lib`). |
| `ImportError: undefined symbol: Py...` at import | Almost always an ABI mismatch: the wheel was built for a different Python. Rebuild against the interpreter you're importing into, or build an `abi3 = "3.12"` wheel for forward compatibility. |
| `ModuleNotFoundError` after `zig build` | `zig build` drops the `.so` in `zig-out/lib/lib<name>.so`; Python needs it as `<name>.so` on `sys.path`. Use `zig-maturin develop` (installs it correctly) instead of importing from `zig-out`. |
| Interpreter aborts on a Zig panic | Add `pub const panic = pz.panic;` to turn panics into `RuntimeError` (see [Error handling and panics](#error-handling-and-panics)). |

## Requirements

- Python 3.12+ (free-threaded `3.13t` / `3.14t` not supported yet)
- Zig 0.16 — on PATH, or pulled in automatically as the `ziglang` wheel
- Linux, macOS, or Windows

## License

MIT — [Ricardo Robles Fernández](https://github.com/rroblf01)

## Related

- [Maturin](https://github.com/PyO3/maturin), [PyO3](https://github.com/PyO3/pyo3) — the Rust originals
- [zig-python](https://github.com/dzfranklin/zig-python) — Python C-API bindings for Zig
