Repository navigation
Reduce the size of the Turbo binaries in the phpstan/phpstan package #15423
Description
Feature request
Since 2.3.0, phpstan/phpstan ships 32 Turbo binaries, and every install carries all of them. A project needs one. This issue collects measurements for the options that came up, so that we can pick a direction. It follows #15386 and #15410.
The decision to make is where the size goes away:
- in the download and in anything compressed, such as a CI artifact or a Docker layer,
- only on disk after install,
- in the phpstan/phpstan git history.
The options below differ on exactly that.
| Option | Composer download | CI artifact (compressed) | Disk | Git history |
|---|---|---|---|---|
| Today | 108.6 MB | 108.6 MB | 256 MB | grows on every build |
| Zip the binaries inside the package | no change | no change | ~140 MB | no change |
| Put the binaries inside the phar | no change | no change | ~140 MB | no change |
| User deletes the other platforms after install | no change | 21.5 MB | 65 MB | no change |
| Download the matching binary on first run | ~8 MB | ~8 MB | ~38 MB | stops growing |
| Commit binaries only for a release | no change | no change | no change | grows per release only |
The ~140, ~8 and ~38 MB values are estimates from the measured parts below. The other values are measured.
My lean is the first-run download, with the bundled binaries or no Turbo as the offline fallback. It is the only option that makes the download smaller, and it also shrinks the CI artifact. Zipping or phar-packing only helps the disk, because the Composer download and the CI artifact are already compressed.
Open questions for the first-run download:
bin/phpstanselects the binary and restarts the process before the Composer autoloader and before the config are loaded. That code may use only PHP 7.4 functions, so it has no Guzzle, no configuredtmpDirand no proxy settings.- The phar must carry the expected hash of each binary, and the code must check it before it loads the file.
vendor/can be read-only, so the binary needs another location. A shared temporary directory is not safe for a file that PHP loads as an extension.- Several processes can start at the same time on a fresh install.
- An offline CI job must still work.
A workaround that works today: Turbo checks is_file() and runs without the extension when the file is missing. I tested this on 2.3.0 with only macos-arm64/ left (Turbo enabled) and with no binaries (Turbo off, analysis still works). A Composer post-install-cmd and post-update-cmd can run:
find vendor/phpstan/phpstan/turbo-ext -mindepth 1 -maxdepth 1 -type d ! -name linux-gnu-x86_64 -exec rm -rf {} +
Replace linux-gnu-x86_64 with the platform directory that the project uses.
Measurements
Package, from the GitHub zipballs. Disk sizes are from du -sh:
| 2.2.17 | 2.3.0 | |
|---|---|---|
| Composer download | 13.8 MB | 108.6 MB |
| Disk after install | 52 MB | 256 MB |
turbo-ext/ on disk |
23 MB | 226 MB |
| One binary | 0.35 to 1.1 MB | 6.6 to 8.4 MB |
The binaries are already stripped. In the Linux x86_64 binary, .text is 6.3 MB of 7.3 MB. The turbo-ext Makefile records that -Os takes a binary from 7.5 to 5.0 MB. It also makes the analysis 10.4% slower, or 6.7% slower with PGO.
Compression of turbo-ext/ in 2.3.0:
| Size | |
|---|---|
| Inside today's Composer zip | 103.8 MB |
zip -r |
103.9 MB |
zip -9 -r |
103.5 MB |
| tar.gz | 103.5 MB |
tar.zst -19 --long=27 |
39.4 MB |
tar.xz -9e |
35.6 MB |
| One Linux x86_64 binary, gzip / xz | 3.3 / 2.6 MB |
The binaries of one platform are almost the same across PHP versions. Only an archive that compresses all files as one stream can use that, for example xz or zstd with a long window. PHP has no built-in xz or zstd decompression.
phpstan.phar stores its 7,183 files uncompressed (30.6 MB). gzip shrinks the whole file to 4.7 MB.
A .so can't be loaded from a phar:// path, so binaries in the phar must be extracted to disk first. The Phar extension reads zip files with its own code and only needs zlib, so extraction does not need ext-zip.
CI artifacts: GitLab's default limit is 100 MB per compressed artifact archive. phpstan/phpstan 2.3.0 alone is 108.6 MB compressed, which is consistent with the failure in #15410. With only linux-gnu-x86_64/ left, the package zips to 21.5 MB.
Git history: since 2026-07-14, 86 commits on 2.3.x changed turbo-ext/. They added 2,312 distinct binaries with 10.4 GB uncompressed: 7.6 GB in September and 2.3 GB in October up to 2026-10-06. GitHub reports the repository at 9.6 GB. I did not measure how well git compresses these files.
Activity
rvanlaak commentedon Oct 7, 2026
Installing the turbo extension would be a great task for a Composer post-install script - when you don't prefer letting
piehandle it.Maybe a
pie.jsoncan be added so PIE can resolve that one of the composer packages expects an extension will be installed?cc @asgrim
SanderMuller commentedon Oct 7, 2026
Another use case comes from the 2.3.0 release discussion: PHPStan as a non-dev dependency in a Docker image. See #15385 (comment). The delete command in the issue can run there as a
RUNstep aftercomposer install.It also changes my lean. Containers are often ephemeral, read-only or offline, so a download on the first run would repeat in every fresh container, or fail. The binary should come from an explicit step during
composer installor the image build, as suggested above. The first run is then only a fallback.ondrejmirtes commentedon Oct 8, 2026
These suggestions are missing an out-of-the-box idea I came up with - share common version-agnostic code in one dynamically loaded file, have much smaller version-specific extension files. This will bring down the size of the package tremendously. I'm currently working on that, stay tuned.
talkinnl commentedon Oct 8, 2026
Shared code approach sounds sexy, I’m curious how much it’ll save! :)
Sounds like the best option. Before we have that, the “User deletes the other platforms after install” trick is a fair workaround, it’s a low-code pragmatic tweak which already fixes the CI and Docker size regressions.
ondrejmirtes commentedon Oct 8, 2026
Work in progress phpstan/phpstan-src#6703
rvanlaak commentedon Oct 8, 2026
Great progress! We run PHPStan with Turbo in FrankenPHP (
dunglas/frankenphp:php8.5, Debian, thread-safe). With nolinux-gnuZTS binary before 8.6, we build Turbo ourselves through PIE and have Renovate keep its version in step withphpstan/phpstan. Every FrankenPHP andphp:*-ztsuser has to work this out on their own.The
turbo-compilecomment says 8.3–8.5 ZTS was left out because those hosts are "a few percent". After this PR, though, each extra leg is a ~70 KB thin extension against the existingphpstan_turbo_core.so, which Linux already shares between ZTS and NTS. So it costs a lot less now.Could you add gnu-only ZTS legs for 8.3/8.4/8.5? I expect the main cost is the
turbo-build:gnu-php8.x-ztsimages. I'm happy to help with those if that's useful.ondrejmirtes commentedon Oct 8, 2026
Now that adding more builds to the matrix means the package will only grow by a few dozens kilobytes, we can add back ZTS pretty cheaply. I'll add it and release PHPStan 2.3.1 with these improvements 😊
ondrejmirtes commentedon Oct 8, 2026
PHPStan 2.3.0: 104 MB download, 268 MB unzipped
2.3.x: 27.4 MB download, 86 MB unzipped