A large step toward 1.0: a holistic schema redesign, a full layered refactor, new runtime/tooling features, and several codegen correctness fixes surfaced by a fresh Alpine Linux sweep.
⚠️ Breaking changes (no backward-compat — still beta)
The pnlx.json / pnl.json schema was redesigned so each key's meaning is clear, separating compile-time options from runtime options. JSON Schemas are frozen and date-versioned under schemas/<name>/2026-07-01/.
pnl.jsonfeatures(runtime):use_functions→global_functions,allow_cdata→cdata_arguments,use_php_scalars_in_params/return/const→scalar_params/scalar_returns/scalar_constants.pnl.jsonconfig:self_repository/packages_repository→release_repository/package_repository;load_paths→library_paths.pnlx.json:requires→native_libraries; newcompile_options{headers, definitions}(was top-levelheaders+require_definitions); newsetup{build_script, install}(wasself_build+installation);installation.<os>.install→setup.install.<os>.commands;checkIfExists→check_if_exists.- lockfile / pathmap:
requires→native_libraries. - CLI flags renamed to match:
--enable-global-functions,--enable-cdata-arguments,--enable-scalar-returns/--enable-scalar-constants.
Features
pnl doctor— diagnoses the toolchain: libclang (required), a C compiler, pkg-config, PHP + the FFI extension, and the workspace; exits non-zero on a required-check failure.ftpsnative-library fetching — viasuppaftpwithnative-tls(Security.framework on macOS, schannel on Windows, vendored OpenSSL on Linux, shared with git2).Pnlx\FFI\Allocator+AllocationScope— allocate inside the extension's own FFI scope and pin allocations against premature GC (the common use-after-free), with a use-after-release guard.- Constraint-aware dependency resolution — a version a dependency pins that conflicts with an already-installed version is now reported, not silently mis-resolved.
Codegen correctness fixes
All four were surfaced by a fresh Alpine Linux (musl) docker sweep and verified there:
- Case-colliding type files. A C struct tag and its typedef differing only in case (e.g.
Argon2_Context/argon2_context) generated two PHP files declaring the same (case-insensitive) class — fatal on a case-sensitive filesystem, while on macOS the field-less wrapper silently won. They are now deduplicated, keeping the field-carrying wrapper. - Quoted-include sibling headers. A library whose API lives in headers included by the entry header (z3's
z3_api.h, hdf5'sH5public.h) was generating a silently empty binding. Headers pulled in via#include "..."are now treated as package-owned. - Typedef names eaten by macro neutralization. An all-uppercase typedef (gdbm's
GDBM_FILE) matched the ABI-attribute-macro heuristic and was stubbed empty, truncating the pointer toint(a segfault at call time). Typedef names are now excluded from neutralization. - Struct-by-value arguments and returns. Structs passed or returned by value (quickjs's
JSValue, gdbm'sdatum) were marshalled as pointers; the generated marshaller now passes and receives them by value.
Refactor & internals
- Full layered module reorganization:
util ← model ← {native, sources} ← codegen ← app(plusembed), with the largest god-files split apart. - Lints codified in
Cargo.toml(-D warnings,clippy::all). - New conventions document at
docs/{en,ja}/conventions.md.
Verification
Alpine 3.21 docker sweep across all packages: 172/180 install, 171/172 installed-package examples pass. The remainder are packages genuinely absent from Alpine repos, plus one macOS-only exported symbol (p11_kit_check_version).