Menu
Guides
- Install
- CLI reference
- The superset
- Differences from PHP
- Async
- Modules
- FFI
- Attributes
- Generics
- Memory model
- Typed arrays (Ds)
- HTTP server
- WebSocket
- PDO
- curl
Reference
Internals
Installing Manticore — host dependencies and platform support
Manticore compiles PHP to a native binary with no PHP runtime in it — no
interpreter, no php.ini, no extension loader. It is not statically linked,
though: an output binary links libc, PCRE2 and OpenSSL dynamically, plus whatever
a program's FFI bindings name (libcurl, libsqlite3, …), so the machine that RUNS
it needs those libraries too. The compiler additionally shells out to a real
toolchain, so the host needs clang and cc present at build time.
This is an end-user guide. Quick version lives in the README's Requirements.
Quick install#
The installer takes a published build when one exists for your platform (linux
and macOS, arm64 and amd64), verified against the release's SHA256SUMS, and
otherwise builds from source — the compiler compiles itself. Either way it
needs the host toolchain present (it checks and
tells you what is missing), then puts everything under $MANTICORE_HOME
(default ~/.manticore).
curl -fsSL https://raw.githubusercontent.com/manticorephp/compiler/main/install.sh | bash
# then, as the script prints:
export PATH="$HOME/.manticore/bin:$PATH"
manticore version # -> manticore 0.17.0Re-running the installer upgrades in place. When it builds from source, a
working manticore rebuilds the new version with itself (self-host, fast) and
a first build starts from the pinned bootstrap release (BOOTSTRAP_VERSION). Knobs: MANTICORE_HOME,
MANTICORE_VERSION (a specific release), MANTICORE_FROM_SOURCE=1 (skip the
download), MANTICORE_REF (branch/tag), MANTICORE_REPO, MANTICORE_SRC
(build a local checkout instead of cloning).
Upgrading#
manticore upgrade # install the latest release
manticore upgrade --check # installed vs latest; exit 0 current, 1 update available, 2 error
manticore upgrade --version 0.13.0 # a specific release (a downgrade is allowed)upgrade runs the share/install.sh that ships in every release tarball, in strict
mode. The new build is staged and smoke-tested, then swapped in with rollback; the
previous binary stays as bin/.manticore.prev. The release checksum is mandatory: a
mismatch or missing SHA256SUMS aborts and leaves the install untouched, and strict
mode never falls back to a source build. A compiler not installed by install.sh
(package manager, container image, a layout without share/install.sh) is refused with
a message naming the likely owner — update it with that installer.
Strict-mode exit codes of install.sh (set MANTICORE_STRICT=1): 2 release
unreachable, 3 no published build for this platform, 4 checksum failure,
5 staging or smoke-test failure. Knobs: MANTICORE_STRICT, MANTICORE_HOME,
MANTICORE_VERSION.
Releases carry share/install.sh inside the tarball (next to bin/ and lib/); that
is the script upgrade runs, so it always matches the installed layout.
Published Linux builds are made on Debian 12 (glibc 2.36), which is also what development and CI use: a release has to run on the distribution someone already has, and glibc is backwards compatible, not forwards.
Checking your install#
manticore doctor # clang, cc, bundled runtime files, host libraries, a hello-world smoke test, update check
manticore doctor --offline # skip the update check
manticore doctor --no-smoke # skip compiling and running the hello-worldEach line is [ OK ], [WARN] or [FAIL], with a hint for the last two. A FAIL means the
compiler cannot work here (clang missing or older than 15, no cc, a missing runtime file,
a failed smoke test); doctor then exits 1. A WARN is advisory (a host library not found, an
install not managed by install.sh, an update available or unreachable) and exits 0.
manticore version --check mirrors upgrade --check: installed vs latest, exit 0 current,
1 update available, 2 error.
Shell completions#
manticore completions bash > ~/.local/share/bash-completion/completions/manticore
manticore completions zsh > ~/.zfunc/_manticore # ~/.zfunc must be in fpath before compinit
manticore completions fish > ~/.config/fish/completions/manticore.fishThe scripts are generated from the command and option declarations, so they match the installed compiler; regenerate them after an upgrade.
Via Composer#
composer create-project manticorephp/compiler manticore # builds into ~/.manticore
# or, if it is already a dependency:
vendor/bin/manticore-installComposer here is a delivery + build trigger, not a runtime: the package ships
the PHP source and runs install.sh. (Composer only auto-runs scripts for the
root project, so a plain composer require of Manticore as a dependency needs
one manual vendor/bin/manticore-install.)
The installed layout is self-contained and needs no environment variables — the binary finds its runtime relative to itself:
$MANTICORE_HOME/bin/manticore
$MANTICORE_HOME/lib/manticore_stdlib.o(.sig)
$MANTICORE_HOME/lib/prelude/*.phpWhat the host needs, and why#
| Dependency | Needed for | Where it is used |
|---|---|---|
clang, LLVM ≥ 15 | assembling emitted LLVM IR → object | Main.php — clang -c, the compile and library paths |
cc | linking objects → executable | Main.php — the system("cc …") link step |
| PHP 8.5 (Zend) | optional — only the difftest oracle (and a few Zend-hosted dev tools) | tools/difftest.sh |
curl or wget | a first build fetches the pinned bootstrap release | tools/fetch_bootstrap.sh |
libpcre2 (8-bit) + pcre2-config | preg_* | Main.php::pcre2_link_flags(), src/Runtime/Pcre.php |
OpenSSL 3 (libssl + libcrypto) + pkg-config | TLS streams, hash/hmac | Main.php::openssl_link_flags(), src/Runtime/Openssl.php, src/Runtime/Crypto.php |
libcurl ≥ 7.68 + curl-config — only to compile a program that calls curl_* | ext/curl | Main.php::generic_link_flags(), prelude/curl.php |
libsqlite3 + pkg-config sqlite3 — only to compile a program that mentions PDO | pdo_sqlite | Main.php::generic_link_flags(), prelude/pdo_sqlite.php |
libxml2 (dev package) — only to compile a program that uses DOM* / SimpleXML | ext/dom, SimpleXML | Main.php::generic_link_flags(), prelude/xml.php |
bash, find, sort, xargs, sed, awk, grep, mktemp | build scripts | bin/build, tools/*.sh |
pcre2-config --libs8 and pkg-config --libs openssl are how the link flags
are discovered; if either tool is missing, Manticore falls back to literal
-lpcre2-8 and -lssl -lcrypto, which works only if the libraries sit on the
default search path.
libcurl is different from the other two: it is demand-gated. A program that
never calls a curl_* function emits no libcurl wrapper, so -lcurl never
reaches its link line and the library need not be installed at all. When it is
needed, discovery goes pkg-config --libs curl → curl-config --libs →
-lcurl; the first always fails (the pkg-config module is called libcurl), so
in practice curl-config is the one that answers — and it ships in the
development package, not with the curl command-line tool.
libsqlite3 is demand-gated the same way, and easier: a program that never
mentions PDO never links it, and pkg-config --libs sqlite3 answers on the
first probe. It too lives in the development package, not in the sqlite3
command-line tool.
PHP is not needed to build. With no compiler yet, bin/build fetches the release
named in BOOTSTRAP_VERSION (verified against its SHA256SUMS), builds the tree
with it, and lets the result rebuild itself; from then on the compiler rebuilds
itself. There is no Zend seed. Nothing the compiler emits ever calls into a PHP
runtime.
Hard floors — these are not "prefer newer"#
- LLVM ≥ 15. Manticore emits opaque-pointer IR. clang 14 and older reject
it outright:
ptr type is only supported in -opaque-pointers mode. Debian bookworm's stock clang is 14, so it cannot build Manticore. - PHP 8.5 for the oracle (
tools/difftest.sh). 8.5 is Manticore's target language version; an older Zend disagrees with the source it runs. glibc ≥ 2.34, measured rather than assumed:
readelf -Von a released binary reportsGLIBC_2.34as the highest symbol version it references. The floor starts withstat/lstat/fstat, which manticore binds by name and glibc only began exporting in 2.33 (before that it was__xstat/__lxstat/__fxstat), and 2.34 is where the rest of what it uses settles. Ubuntu 20.04 (2.31) and Debian 11 (2.31) therefore cannot run it; RHEL 9 (2.34) is exactly at the floor; Ubuntu 22.04 (2.35) and Debian 12 (2.36) have room.⚠ The floor is the symbol versions a binary REFERENCES, not the glibc of the machine that built it. A common assumption is wrong in the other direction: a compiler built on trixie (glibc 2.41) was expected to be unusable on bookworm (2.36), and it ran there without complaint — it asks for nothing newer than 2.34. Building on the older base is still the right default, because it makes the floor a property of the build rather than of whichever symbols a release happened not to touch.
Per-OS setup#
macOS (arm64 / x86_64)#
xcode-select --install # clang + cc + ld
brew install php pcre2 openssl@3 pkg-config curl # curl: only for ext/curlHomebrew's php tracks the current release; check php -v reports 8.5.
Debian / Ubuntu#
The DEFAULT clang is too old on both suites (14 on bookworm), and the stock
php is 8.2 — but a versioned clang-22 is in Debian's own archive, so only
php needs a third-party source:
sudo apt-get install -y \
gcc libc6-dev binutils make \
libpcre2-dev libssl-dev pkg-config \
libcurl4-openssl-dev libsqlite3-dev libxml2-dev \
netbase
# clang / LLVM: the newest versioned package the suite carries
sudo apt-get install -y clang-22 lld-22
sudo ln -sf /usr/bin/clang-22 /usr/local/bin/clang
sudo ln -sf /usr/bin/clang-22 /usr/local/bin/cc
# PHP 8.5 (sury.org)
sudo apt-get install -y php8.5-cli php8.5-mbstring
sudo update-alternatives --set php /usr/bin/php8.5⚠ netbase is not optional. It installs /etc/services and /etc/protocols,
which a bare debian:12 ships without — omit it and the network stdlib silently
degrades (getservbyname("http") returns false) instead of failing loudly.
The root Dockerfile covers the same ground without pinning a version: it takes
the highest clang-NN the suite offers and refuses below 15. Pin clang-22
above only if you want a specific toolchain.
⚠ libxml2-dev, not just libxml2. prelude/xml.php binds libxml2 by name,
so the link needs the libxml2.so symlink the development package carries —
the runtime libxml2.so.2 that clang drags in is not enough. This one went
undeclared for a long time and the XML cases passed anyway, because the LLVM
installer used to pull llvm-NN-dev, which depends on libxml2-dev. Removing
that installer took the symlink with it and turned eight dom_*/simplexml_*
cases red on both arches — a dependency held up by an accident.
Alpine (musl)#
apk add clang lld gcc musl-dev binutils pcre2-dev openssl-dev curl-dev sqlite-dev \
pkgconf bash file make tzdata libxml2-dev \
php85 php85-ctype php85-mbstring php85-tokenizer php85-openssl \
php85-phar php85-session php85-posix php85-iconv php85-fileinfo \
php85-curl php85-pdo_sqliteAlpine splits php far finer than Debian does, and the split is not cosmetic: php85
alone has no ctype, and Zend dies on the first line of a Zend-hosted tool with
Call to undefined function ctype_digit(). Everything after php85-mbstring above is
what Debian's php8.5-cli bundles and Alpine does not.
tzdata because Alpine ships no zone database and the date functions read
/usr/share/zoneinfo directly; libxml2-dev for the libxml2.so link symlink, as on
Debian. The prebuilt route is shorter: docker pull ghcr.io/manticorephp/compiler:alpine,
or install.sh, which notices musl and fetches the linux-musl tarball.
Dockerfile.alpine is this list, as the same four stages as the Debian image, and
bash tools/docker/run_tests.sh --alpine runs the usual gate against it (its own image
tag and its own compiler-cache volume — a glibc binary does not run in a musl
container). CI runs it on every push, on both arches, next to the glibc rows, and the
release publishes it.
Where the C library itself answers differently, the answer is the host's php, not
glibc's: musl's iconv has no //TRANSLIT or //IGNORE, php on Alpine returns false
for both, and so does a Manticore binary there. A case whose output depends on it keeps
a tests/aot/expected/<name>.musl.out.
musl was also the first allocator to refuse three memory bugs glibc had been quietly absorbing (a nested write through a referenced element, a foreach value over arrays of arrays, a scalar read off a freed temp) — worth remembering the next time a case is red only here.
musl exports plain stat/lstat/fstat and has glob/globfree, but lacks
GLOB_BRACE, GLOB_ONLYDIR and the LFS64 aliases (stat64) — a few filesystem
functions degrade accordingly.
Platform support#
| Platform | Status |
|---|---|
| macOS arm64 | supported — the primary development and gate platform |
| macOS x86_64 | supported |
| Linux glibc ≥ 2.34 (arm64 / x86_64) | supported — full build + self-host fixpoint pass |
| Linux glibc < 2.34 (e.g. Ubuntu 20.04, Debian 11) | unsupported — cannot link stat |
| Linux musl / Alpine (arm64 / x86_64) | supported — full build + suite per push; :alpine image and linux-musl tarballs; minus some glob constants |
Both macOS and Linux build the compiler from the pinned bootstrap release, self-host
(bin/build rebuilds the compiler byte-for-byte), and pass the full AOT suite
and the self-host fixpoint. To reproduce the Linux build + suite in a container,
see tools/docker/README.md.
Docker#
The published image carries the compiler and the toolchain it shells out to, which is the one form of delivery that needs nothing installed on the far side:
docker run --rm -v "$PWD":/work -u "$(id -u):$(id -g)" \
ghcr.io/manticorephp/compiler manticore compile app.php -o app-u "$(id -u):$(id -g)" is not decoration: the image runs as uid 1000, and
without it the binary it writes into your bind mount comes back owned by someone
else.
The root Dockerfile builds that image, and three stages lead to it:
| Target | What it is |
|---|---|
base | clang + the -dev libraries the compiler links against. No php. |
toolchain | base + PHP 8.5 — the difftest oracle |
build | toolchain + the compiler: bin/build from the source tree, starting from the pinned release |
runtime | base + that compiler — what gets published |
# a ready host environment; mount a checkout and work in it
docker build --target toolchain -t manticore-toolchain .
docker run --rm -it -v "$PWD":/build/manticore -w /build/manticore \
manticore-toolchain bash
# the published shape, built locally
docker build --target runtime -t manticore .php lives in toolchain and not in base on purpose: the shipped compiler is a
native binary and never asks for an interpreter, so runtime branches off
base and carries none.
The base is ARG DEBIAN_TAG=12 (bookworm, glibc 2.36) for development, CI and
releases alike. One base is not tidiness: glibc is backwards compatible and not
forwards, so a compiler published from a newer base cannot bootstrap a build on an
older one — split bases split the bootstrap chain. Dockerfile.alpine is the musl
counterpart, with the same four stages.
To run the libc probes and the AOT suite in a container, see
tools/docker/README.md.
Troubleshooting#
ptr type is only supported in -opaque-pointers mode during the
assemble step — clang is older than 15. Install a newer LLVM and make sure bare
clang on PATH resolves to it (apt.llvm.org installs clang-21, not clang).
undefined reference to 'pcre2_compile_8' (and six siblings) at
link — a macOS-only symbol-scraper failure. If you see it on a
current checkout, tools/link_stubs.sh failed to recognise your linker's
diagnostic format; it prints the raw linker output when that happens, so file
that output as a bug.
undefined reference to '__xstat' / missing stat at link — glibc older
than 2.34. See the hard floors above.
pcre2-config: command not found or link errors mentioning -lpcre2-8 —
install the PCRE2 development package (libpcre2-dev, pcre2-dev, or
brew install pcre2), not just the runtime library. Same shape for OpenSSL.
Build runs out of memory — bin/build compiles the whole compiler as one
LLVM module; the self-build peak is about 0.5 GB, so any machine or Docker VM
that runs clang comfortably has room. If it is OOM-killed, check the container's
hard memory limit, then re-run bin/build to a clean finish (a failed build
poisons bin/manticore and lib/*.o).