Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Static Php Cli Laravel Package

crazywhalecc/static-php-cli

Static PHP CLI builds standalone, statically linked PHP binaries with selected extensions for Linux, macOS, and Windows, making it easy to package and distribute PHP applications without runtime dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

static-php-cli (command name spc) is a build orchestrator: it downloads PHP + extension sources, compiles them statically, and emits a self-contained php binary (CLI, FPM, embed, or a micro.sfx runtime for PHARs). You use it whenever "the target machine has no PHP and no package manager" is a requirement.

Minimal setup inside the project that needs the artifact:

composer require --dev crazywhalecc/static-php-cli
./vendor/bin/spc doctor      # verify compiler, autoconf, bison, re2c, pkg-config, etc.
./vendor/bin/spc list        # what extensions/features this spc version knows about

Then the three-phase build (downloads are cached and reusable, builds are not):

./vendor/bin/spc download --all
./vendor/bin/spc build "bcmath,curl,openssl,mbstring,phar" --build-cli
# artifact: buildroot/bin/php

Verify immediately with the artifact itself, not with your system PHP:

./buildroot/bin/php -m
./buildroot/bin/php -r 'echo PHP_VERSION, PHP_EOL;'

First real use case: ship a CLI script/phar to a hardened host or scratch container. Build --build-micro instead of --build-cli, then combine:

./vendor/bin/spc build "phar,mbstring,openssl,curl" --build-micro
./vendor/bin/spc micro:combine app.phar -M buildroot/bin/micro.sfx -O app
./app --help

Where to look first when something misbehaves: spc <command> --help, then buildroot/ (binaries, logs, per-extension build dirs) and downloads/ (tarballs). The config/ directory in the package defines each extension's source, dependencies, and configure flags — that is the authoritative answer to "can this extension be built statically?". The registry grows with every minor/patch release (2.8.5 added deepclone and decimal), so spc list is only meaningful for the version you have locked.

Implementation Patterns

Project-local build script. Treat the build as a repeatable task, not a one-off. Wire it into composer.json so everyone builds the same extension set:

"scripts": {
  "build:cli": "spc download --all && spc build \"phar,mbstring,openssl,curl,pcntl\" --build-cli",
  "build:app": "spc build \"phar,mbstring,openssl\" --build-micro && spc micro:combine build/app.phar -M buildroot/bin/micro.sfx -O build/app"
}

Keep the extension list in one place (a shell script or a Makefile target) — extension order and set drift is the number-one source of "works on my machine" build failures.

PHAR → single binary pipeline. The common day-to-day workflow for CLI tools (Laravel Zero, Symfony Console, custom scripts):

  1. composer install --no-dev --classmap-authoritative
  2. build the phar (box, php -d phar.readonly=0, or your tool's own app:build)
  3. spc build ... --build-micro
  4. spc micro:combine app.phar -M buildroot/bin/micro.sfx -O dist/app

Ship dist/app; the recipient needs nothing installed. This is the highest-leverage pattern — one artifact per platform, no runtime dependency management.

CI with aggressive caching. Downloads dominate cold builds. Cache downloads/ (and, if your CI tolerates it, buildroot/) keyed on the spc version + PHP version + extension list:

- uses: actions/cache@v4
  with:
    path: downloads
    key: spc-${{ hashFiles('composer.lock') }}-${{ hashFiles('build/extensions.txt') }}
- run: ./vendor/bin/spc download --all
- run: ./vendor/bin/spc build "$(cat build/extensions.txt)" --build-cli

Build a matrix (PHP 8.2/8.3/8.4, x86_64/aarch64) and upload each binary as a release artifact. The spc version is a build input: pin it in composer.lock and rebuild when you bump it.

Multi-stage Docker. Two viable shapes:

  • glibc-compatible runtime: build in debian:bookworm (with the toolchain), copy buildroot/bin/php into debian:bookworm-slim or ubuntu:.... This works because the runtime has a compatible glibc. Linux Mint hosts are now recognised as Ubuntu/Debian variants by spc's OS detection, so doctor gives you the right package hints there too.
  • scratch/distroless: you must compile against musl (build inside Alpine with the musl toolchain) or the binary will not run. Never assume "static" means "runs anywhere" — check ldd output and the target libc before committing to FROM scratch.

FPM for lean containers. --build-fpm gives you a static php-fpm you can drop into a minimal image without apt install php*. Pair with the extension set your framework actually needs (for Laravel: pdo_mysql/pdo_pgsql, mbstring, openssl, ctype, fileinfo, tokenizer, xml, curl, redis/sqlite3 as applicable) and verify with a real request, not just php -m.

Custom/private extensions. Use spc craft to scaffold a config for an extension not in the registry, then reference it like any built-in. This is the extension point for in-house C extensions or forks that PECL won't cover statically.

Gotchas and Tips

  • The build host is a dependency. spc doctor is not optional; missing bison, re2c, autoconf, pkg-config, or dev headers produce confusing per-extension failures deep into the build. Run it first, every time. On rolling distros, keep spc current: 2.8.5 carries fixes for ICU on Windows, Swoole, krb5, PostgreSQL header/library copying on Windows, and xlswriter against modern Clang with C23 defaults.
  • Budget disk and time. A full download --all plus build is multi-GB and tens of minutes. Cache downloads/ in CI; never rebuild from scratch on every commit.
  • Extension order and transitive deps matter. curl needs openssl (or another TLS backend) built first; intl needs ICU; pdo_mysql needs mysqlnd. If a build fails, read the extension's entry in config/ for its declared depends, and try --with-suggested-libs to let spc link system libraries for extensions that want them.
  • No dynamic loading. Anything not compiled in is unavailable — dl() is a no-op, extension_dir is meaningless, and composer packages that require a missing extension fail at runtime, not build time. Diff php -m against your composer.json ext-* requirements in CI.
  • libc is the classic trap. A binary built on Ubuntu/glibc will not run on Alpine/musl and vice versa. Decide the target libc before writing the Dockerfile, and test the artifact in the actual runtime image. If you build shared extensions with the zig toolchain, note that 2.8.5 fixes an aarch64 segfault seen with zig 0.16.0 and drops the pre-0.16 workaround — the zig version is part of your build contract.
  • micro.sfx is a different SAPI. php_sapi_name() returns micro; a system php.ini is not read, so set required directives in your bootstrap (ini_set, date.timezone, memory limits) or embed them at combine time. $_SERVER, argv, and getcwd() behave like CLI but are not identical — smoke-test argument parsing and config discovery in the combined binary, not just under buildroot/bin/php.
  • PHAR path semantics. Inside a PHAR, __DIR__/__FILE__ resolve to phar://... paths. Use Phar::running() or your framework's base-path helper for filesystem access, and remember that writing next to the binary requires resolving the real executable path, not a phar path.
  • Don't strip until you've debugged. --no-strip keeps symbols so gdb/valgrind are usable on a crashing extension. Re-enable stripping for release artifacts (size matters when shipping a single binary).
  • Debugging builds: re-run the failing step with --debug for verbose compiler output; the per-extension build directory under buildroot/ retains the generated configure invocation and logs. Reproducing that command by hand is usually faster than re-running the whole pipeline.
  • Pin everything you can. spc version, PHP version, and extension set are all build inputs; unpinned builds silently change behavior between releases — and even patch releases shift which extensions exist and how they are patched. Pin spc in composer.lock and record the PHP version in your build script or CI matrix; rebuild deliberately when you bump it.
  • Test the artifact, not the build. A green build only proves compilation. Run your actual test suite against buildroot/bin/php (or the combined binary) before publishing — this catches missing extensions, ini differences, and SAPI quirks that php -m will not.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle