Manticore builds multi-file PHP projects from a single cargo-style manifest,
manticore.json. One manifest defines every artifact — your application
binaries and any libraries they link — and the same definition drives both a
normal build and the compiler's own self-rebuild.
This is an end-user guide. For the internal design rationale see
docs/design/build-and-packaging.md.
manticore.json at your project root declares two kinds of target:
{
"libraries": [{
"name": "util",
"src": "src/Util",
"output": "lib/util.o",
"exclude": []
}],
"applications": [{
"name": "app",
"src": "src",
"output": "bin/app",
"entry": "src/main.php",
"exclude": ["src/Util"],
"libraries": ["util"]
}]
}Build everything:
manticore build # uses ./manticore.json
manticore build path/to.json # explicit manifest
manticore build --libs-only # build only the library targets, skip appsbuild compiles every libraries[] entry first (each → a standalone .o
plus an .o.sig interface), then every applications[] entry (→ an
executable that imports its dependencies' .sig and links their .o).
| Field | Meaning |
|---|---|
name |
label, shown in build output |
src |
directory scanned recursively for *.php |
output |
path of the linked executable |
entry |
optional — the file whose top-level code becomes main(). Every other file contributes only its declarations. Omit it and files load in `find … |
exclude |
optional — path prefixes to skip in the src scan (e.g. a subtree compiled separately as a library) |
libraries |
optional — which user library targets to import + link. Omit ⇒ all, [] ⇒ none, a named subset ⇒ just those. Independent of the stdlib (see below). |
stdlib |
optional — set false to opt OUT of the always-on stdlib runtime. Only the self-contained compiler (which embeds src/Runtime) needs this. |
composer |
optional — true also compiles the Composer view of the project (see below); the object form { "vendor": false } narrows it |
extensions |
optional — names of native extensions to bind and link. Every name must exist in the manifest's top-level extensions map, or the build fails |
name, src, output, exclude — same as above. A library is compiled with
--emit-library: a standalone .o with no main(), plus <output>.sig.
A library marked "runtime": true is the bundled stdlib — built, but treated as
the always-on runtime rather than a normal dependency (see below).
They are not the same switch, and confusing them is the usual manifest bug.
"stdlib": falseon an application opts out of the always-on runtime. A"runtime": truelibrary is injected and linked into every app regardless of thelibrariesselection, so"libraries": ["mylib"]can never accidentally drop the stdlib. You opt out only when your app already embeds that source."libraries"selects among the other (non-runtime) libraries: omit the key ⇒ all of them,[]⇒ none, a list of names ⇒ just those. An application that embeds a copy of a library's source must exclude it here, or the same symbols are defined twice.
The compiler's own manifest is the worked example — it bundles src/Runtime
directly, so it opts out of the runtime, and it has no other libraries to select:
{
"libraries": [{ "name": "stdlib", "src": "src/Runtime",
"output": "lib/manticore_stdlib.o", "exclude": [],
"runtime": true }],
"applications": [{ "name": "compiler", "src": "src", "output": "bin/manticore",
"entry": "src/zzz_entry.php", "stdlib": false }]
}The stdlib library still builds — that is what user programs link.
When a library builds, Manticore writes <output>.sig next to the .o — a
JSON table of the library's exported symbols (global functions, including
namespaced and FFI bindings). A dependent target imports the .sig to type
and resolve cross-unit calls without re-parsing the dependency's source.
This is what lets a distributed compiler ship only bin/ + lib/ (the
.o + .sig, no PHP sources) and still type-check and link user programs
against the bundled stdlib. You can inspect a .sig with:
manticore dump-sig src/Util/*.phpA .sig carries the library's classes, interfaces, enums and constants
alongside its functions:
{"schema":2,"abi":7,
"functions":[…],
"classes":[{"name":"Acme\\Point","kind":"class","id":651526131271716,
"props":[…],"sprops":[…],"methods":[…],"consts":[…],
"layout":{"size":32,"hdr":16,"bag":-1,"offs":[["x",16],["y",24]],
"sum":4634270821217}}],
"constants":[{"n":"ACME_VERSION","v":{"k":"str","v":"1.0"}}],
"libs":[…],"weak":[…]}The dependent hydrates each entry into a declaration with no method bodies and
compiles it through the ordinary class pipeline, so its slot order, its bag
inheritance and its constant table are computed by the same code that built the
library's. The method symbols and the static-property cells stay in the
library's .o; the application declares them.
layout is a check, not information: the importer recomputes the offsets and
refuses on any disagreement. Every other way the two sides can drift produces a
wrong answer; drifting here writes past the end of an instance.
Class constants are const-folded at export (const B = self::A . '!' ships as
the finished string), so a dependent never has to evaluate anything. An
initializer that does not reduce to a literal, array or enum case is refused
when the library is built — where its author can fix it.
⚠ Still not exportable: traits (compile-time copy-paste — there is no
compiled form to link against, so a library's internal trait works but useing
one across the boundary does not), generic (@template) classes and
#[TypeDef]s. A generic or TypeDef is recorded in the .sig as
"unsupported" so the diagnostic can name the reason.
⚠ The bundled stdlib (runtime: true) exports functions only, by design: its
classes are internal or compiler-owned, and its .o is linked into every
program rather than selected as a dependency. The class-shaped stdlib surface
stays in prelude/.
Set "composer": true on an application and Manticore builds the project the way
Composer sees it: the project's own composer.json autoload roots (psr-4,
psr-0 and classmap directories) and every installed package listed in
composer.lock, rooted at vendor/<name>/.
{ "applications": [{ "name": "app", "src": "src", "output": "bin/app",
"entry": "src/main.php", "composer": true }] }The object form narrows it: { "vendor": false } takes only the project's own
autoload — useful while a dependency uses PHP the compiler does not support yet.
The key point: vendor/ is source to compile, not an autoload map evaluated at
runtime. Those directories are unioned into the same whole-program source set
as src, and a directory already covered by src is skipped, so an
"App\\": "src/" mapping does not double-scan. The build prints each directory
it adds (build: + composer autoload '…').
The stdlib is always on. Whether you compile a single file or build a
manifest, every program transparently has the bundled stdlib available —
str_pad, floor, array_reverse, json_decode, ctype_*,
file_get_contents, … — with zero manifest ceremony. You never list it.
manticore compile app.php -o app # str_pad() just works
manticore build manticore.json # …and so does every app targetHow it works: the compiler locates the bundled stdlib interface
lib/manticore_stdlib.o.sig (relative to the manticore binary, or via
MANTICORE_STDLIB_SIG / MANTICORE_STDLIB_O), injects externs for the stdlib
functions your code references, and links lib/manticore_stdlib.o at the final
step only when you actually call one — a program that touches no stdlib
function links nothing extra. The binary stays fully static (stdlib.o + libc).
The stdlib is independent of the libraries selection: an app that depends
on specific user libraries ("libraries": ["mylib"]) still gets the stdlib. The
only way to opt out is an explicit "stdlib": false — which only the
self-contained compiler uses, because it already embeds src/Runtime and would
otherwise double-define it.
Note there are two tiers of standard functions. A core set (strlen, substr,
count, floor, sqrt, printf, …) are codegen builtins — emitted inline
as IR / LLVM intrinsics, always present, no link. The rest are PHP stdlib
(src/Runtime/Stdlib/*.php), compiled once into manticore_stdlib.o and reached
as above. Both are transparent to you — call the function, it works.
The compiler is written in PHP, so the first binary must be seeded by a stock PHP interpreter; after that it builds itself from the manifest.
bin/compile # COLD seed: PHP (Zend) builds a throwaway seed binary,
# which then runs `build manticore.json` → native bin/manticore
# + lib/manticore_stdlib.{o,sig}
bin/build # SELF-HOST: the existing bin/manticore builds the manifest
# to a temp path, smoke-tests it, then atomically swaps in
bin/build --seed # force the cold seed even if a binary exists
bin/build --verify# rebuild, then run the fixpoint + suite gatebin/compile and bin/build both end by running build manticore.json —
the manifest is the single source of truth. Only the first binary needs the
Zend interpreter (the manifest build itself can't run under Zend: its file IO
fills mutable libc buffers that Zend's immutable strings can't provide). The
emitted binaries make no PHP-runtime calls — they link against libc only.
A self-rebuild is byte-identical: gen2 and gen3 emit the same IR.
An extension binds a native C library (zlib, libcurl, …) into your program:
thin PHP glue — an FFI binding plus a wrapper — compiled into the application,
and the native library linked by cc. The native lib never touches Manticore's
arena/rc heap, so it adds no runtime-corruption surface.
Declare extensions once under the manifest's top-level extensions, then opt
each application in by name:
{
"extensions": {
"zlib": { "src": "ext/zlib", "link": ["z"], "static": false }
},
"applications": [{
"name": "app", "src": "src/app", "output": "bin/app",
"entry": "src/app/main.php", "extensions": ["zlib"]
}]
}| Extension field | Meaning |
|---|---|
src |
directory of the extension's PHP glue (*.php), compiled into every app that opts in |
link |
native libraries to link — each becomes -l<name> (so "z" → -lz) |
static |
reserved for static-archive linking (dynamic -l today) |
Opt-in is per application ("extensions": [...]), so a binary links libcurl
only if it actually uses the curl extension — small programs stay small.
The glue is ordinary PHP with FFI attributes. Bind the C symbol, then wrap it:
<?php
use Ffi\Library;
use Ffi\Symbol;
/** uLong crc32(uLong crc, const Bytef *buf, uInt len) */
#[Library('z'), Symbol('crc32')]
function __ffi_crc32(int $crc, string $buf, int $len): int { return 0; }
function ext_zlib_crc32(string $s): int {
return __ffi_crc32(0, $s, strlen($s));
}#[Symbol('crc32')] makes __ffi_crc32 a thin extern forwarder — its PHP body
is ignored; the compiler emits a direct call to the C symbol. Param/return
types map to the C ABI (string → ptr, int → i64, …). The full example
plus a build gate lives in ext/zlib* and tools/ext_smoke.sh. For the FFI
binding mechanism in full — type mapping, \Ffi\Ptr handles, the
body-is-ignored model, memory across the boundary — see docs/ffi.md.
A built compiler is self-contained in two directories:
bin/manticore the native compiler
lib/manticore_stdlib.o prebuilt stdlib object
lib/manticore_stdlib.o.sig its interface
Ship those — no PHP sources required. Users run
manticore compile their.php -o their and link the bundled stdlib by .sig
automatically.
myproj/
manticore.json
src/
main.php // <?php echo greet("world"), "\n";
Util/
greet.php // <?php function greet(string $n): string { return "hi $n"; }
{
"libraries": [{ "name": "util", "src": "src/Util", "output": "lib/util.o", "exclude": [] }],
"applications": [{ "name": "app", "src": "src", "output": "bin/app",
"entry": "src/main.php", "exclude": ["src/Util"], "libraries": ["util"] }]
}manticore build # → lib/util.o (+ .sig) and bin/app
./bin/app # hi worldutil builds to a standalone object; app excludes src/Util from its own
scan, imports lib/util.o.sig to resolve greet, and links lib/util.o.