box
box copied to clipboard
Compile your app into a single binary
Feature Request
PHARs are a format that allows one to "compile" your application into a single file. Box also supports Docker to allow one to depend only on Docker and no longer on the PHP version installed on the machine.
An alternative to Docker could be to compile PHP statically with the necessary extensions. This way you would have two files:
- The PHAR
- The static PHP binary
Both could then be shipped in a self-extracting file. This also opens up the path to allow to package the application as a debian, ubuntu or OSX package without too much trouble.
The only known limitation would be if the application depends on a PHP extension that depends on a system library or extensions that cannot be compiled statically. Examples:
-
Xdebug
is a PHP extension that cannot be compiled statically -
ext-intl
which depends on icu4c. If the link to theicu4c
binary is hard-coded in the statically compiled extension, then the extension will work properly only on one machine
Resources:
- self extracting file
- PHP compilation:
- https://github.com/phpbrew/phpbrew/blob/master/src/PhpBrew/Command/InstallCommand.php
- https://github.com/phpbrew/phpbrew/blob/master/src/PhpBrew/Build.php
- https://github.com/phpbrew/phpbrew/blob/master/src/PhpBrew/CommandBuilder.php
There is also http://virtphp.org/ which can pack a PHP version with extension etc.
@MacFJA I'm having trouble to understand the difference with phpbrew tbh, but in any case good reference for another example on how to do it.
The big difference between virtPHP and PhpBrew (as far as I know) is the fact that virtPHP duplicate the existing (local) PHP version and add PECL, PEAR and composer to it, where PhpBrew build/compile a PHP from zero.
I did some test with virtPHP, and I don't this it can be used: some path in PECL and PEAR configuration are absolute and so can't work outside the installation path.
I also try PhpBrew, but it need a lots of system dev dependencies, I will try to run it inside of a Docker image
Did you manage to get anywhere @MacFJA?
I made some tests with PhpBrew, I have the result: there are some absolute path inside the build PHP, so it won't work outsite the installation path.
Maybe I miss some configuration, but right now neither virtPHP nor PhpBrew can be used to make "fat binary"
I will take a look on AppImage, FlatPak, Snap and ZeroInstall to see if they can be used for this purpose
there are some absolute path inside the build PHP
Do you have some examples? I thought it would be the case only for extensions having dependencies on your system applications
I'm all for adding more shipping targets as well. Right now there is Docker but we can go further for sure
Here's some thoughts after some discussions and hacking:
- clone https://github.com/php/php-src, it's easier and faster to switch between various branches than downloading the tar on the fly, at least to experiment
- call
./buildconf
- the next step is
./configure
.
There is several flags to add (which are documented); for information you can also check php -i | grep "Configure Command"
to see what your php binary has been built with.
The --static
flag here is key and to bundle extensions with it, their appropriate --enable-extname=static
flag should be called as well.
- call
make smth
(smth
is one of the command available, need to check which one - definitely notinstall
!)
To include third-party extensions, i.e. the ones not provided by php-src, their source code need to be moved to under the php-src ext
directory (like the extensions already present there). This step needs to be done before calling ./buildconf
.
The output of this should be the PHP static binary as well as .a
files. Now the PHP static binary may not ship some requirements from the system (to see this, use otool -L my-bin
. There is two ways to go from there:
- Explicitly document those and expect the user to install those requirements beforehand
- Go one step further (more on this bellow).
To avoid having the user to install those requirements which can be tricky, it should be possible to get those system libraries in .a
files as well. Once done, it's possible to assemble all the .a
into the final PHP binary again, which will time will include the system libraries.
It is also worth mentioning that there is likely the following points to be aware of:
- When compiling the PHP binary, the "default" flags shouldn't be taken: you don't want the binary to include CGI, FPM & sessions for example since it's for a CLI application
- Some extensions cannot be compiled statically (e.g.
xdebug
although this peculiar case should not be an issue) - Most applications don't document correctly their extension dependencies. It's often the case for exoteric dependencies but much less for the ones traditionally included in the core. For example most applications don't declare their dependency on
ext-json
despite usingjson_(de)encode()
. Recommending a tool and encouraging its usage will be necessary here
Once all of this is done, there is still the question of how to package the app itself for which there is likely multiple alternatives depending of the platform or other. Maybe one way would be to create a C application which ships both the PHAR and the static binary and manages to do the call, to effectively distribute a single, dependency free, binary file.
Ah, there is also the question of licenses for both extensions and system libraries.
@theofidry are you and @mpociot collaborating on this? 👀
https://twitter.com/marcelpociot/status/1498244220620099588
I would love to see some work done here, but no I did not get more details regarding the experiment neither can I see an attempt to try to add it to laravel zero. I don't know if the experiment was just not conclusive or if @mpociot switched to something else.
Just tried this with a very basic CLI tool of mine and it seems to work. I used Box without any configuration, then followed https://github.com/crazywhalecc/static-php-cli/blob/master/README-en.md#packing-php-code-into-a-static-binary and the binary it produced executes my app (it's a single-command Symfony console app) fine.
Reproducing repo at: https://github.com/dreadnip/static.
Yet another resource: https://pronskiy.com/blog/php-script-as-binary/
One thing Box should do here is
- produce a list of extensions required by the PHAR (sourced from the Box req checker?)
- hash these into a config
- pre-configure the build process and output the binary into
./build/<hash>
(so it can be rebuilt if the reqs change)
Otherwise the procedure is pretty straighforward, I've tested it and it works fine.
In my case I needed to figure out what extensions I needed, but once I had them, it just worked and produced a fully functional Linux x64 binary from my Box built PHAR.
Do you have an example to preconfigure the build process?
Getting the information about the extensions needed (at least if documented correctly) should indeed be easy (even for an existing PHAR given the requirement checker was enabled)
I'll try to find it, but basically it was a list of PHP extensions I've extracted from the PHAR, that was the most annoying part because IIRC there were some extensions which were required implicitly and not declared correctly so I was adding an extension at a time, compiling, seeing it fails and doing it over and over until it worked.
This means you need to be able to add extensions on top of the detected ones because the detection will not be 100% correct. This might mean adding an extension, but also removing one because you're sure it's not used and want to make your binary smaller. For example, Symfony FrameworkBundle will always require ext-xml
, but you might not use any of that, omitting that extension saves you ~4MB which is huge since the final binary is say 13MB.
Once I knew the list of extensions I require, the process of producing the final executable was trivial, I just slapped together the Box-produced PHAR and the compiled shim and it worked first try.
Ah, one more thing to consider: when this process is going on, the req checker should probably not be built into the PHAR which gets appended to the executable even if requested in config. This allows building both a PHAR (with req checker) and executables (without req checker) from the same config file.
Alternative would be having a "debug build" which would then do the right thing in either case:
- PHAR debug build: has req checker
- binary debug build: has req checker, but lenient (to allow missing extensions which you know you don't need)
- PHAR prod build: has req checker, but lenient (to allow missing extensions which you know you don't need), req checker also doesn't allow verbose flags, it only presents itself when there's a hard fail
- binary prod build: doesn't have a req checker
This means you'd be able to produce "debug" and "prod" builds instead of having or not having a req checker, the debug build is a higher level of abstraction where the req checker is a side effect. This would probably also fix #922 by default.
Oh boi I feel like I'll need some refactoring for the Compile command...