Menu

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.0

Re-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-world

Each 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.fish

The 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-install

Composer 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/*.php

What the host needs, and why#

DependencyNeeded forWhere it is used
clang, LLVM ≥ 15assembling emitted LLVM IR → objectMain.php — clang -c, the compile and library paths
cclinking objects → executableMain.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 wgeta first build fetches the pinned bootstrap releasetools/fetch_bootstrap.sh
libpcre2 (8-bit) + pcre2-configpreg_*Main.php::pcre2_link_flags(), src/Runtime/Pcre.php
OpenSSL 3 (libssl + libcrypto) + pkg-configTLS streams, hash/hmacMain.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/curlMain.php::generic_link_flags(), prelude/curl.php
libsqlite3 + pkg-config sqlite3 — only to compile a program that mentions PDOpdo_sqliteMain.php::generic_link_flags(), prelude/pdo_sqlite.php
libxml2 (dev package) — only to compile a program that uses DOM* / SimpleXMLext/dom, SimpleXMLMain.php::generic_link_flags(), prelude/xml.php
bash, find, sort, xargs, sed, awk, grep, mktempbuild scriptsbin/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"#


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/curl

Homebrew'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_sqlite

Alpine 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#

PlatformStatus
macOS arm64supported — the primary development and gate platform
macOS x86_64supported
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:

TargetWhat it is
baseclang + the -dev libraries the compiler links against. No php.
toolchainbase + PHP 8.5 — the difftest oracle
buildtoolchain + the compiler: bin/build from the source tree, starting from the pinned release
runtimebase + 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).

Edit this page on GitHub