box-project / box

📦🚀 Fast, zero config application bundler with PHARs.
https://box-project.github.io/box
MIT License
1.12k stars 101 forks source link

Compile your app into a single binary #331

Open theofidry opened 5 years ago

theofidry commented 5 years ago

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:

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:

Resources:

MacFJA commented 5 years ago

There is also http://virtphp.org/ which can pack a PHP version with extension etc.

theofidry commented 5 years ago

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

MacFJA commented 5 years ago

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

theofidry commented 5 years ago

Did you manage to get anywhere @MacFJA?

MacFJA commented 5 years ago

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

theofidry commented 5 years ago

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

theofidry commented 5 years ago

I'm all for adding more shipping targets as well. Right now there is Docker but we can go further for sure

theofidry commented 5 years ago

Here's some thoughts after some discussions and hacking:

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.

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:

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:

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.

wesmontgomery commented 2 years ago

@theofidry are you and @mpociot collaborating on this? 👀

https://twitter.com/marcelpociot/status/1498244220620099588

theofidry commented 2 years ago

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.

sanderdlm commented 1 year ago

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.

theofidry commented 10 months ago

Yet another resource: https://pronskiy.com/blog/php-script-as-binary/

dkarlovi commented 10 months ago

One thing Box should do here is

  1. produce a list of extensions required by the PHAR (sourced from the Box req checker?)
  2. hash these into a config
  3. 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.

theofidry commented 10 months ago

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)

dkarlovi commented 10 months ago

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.

dkarlovi commented 10 months ago

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:

  1. PHAR debug build: has req checker
  2. binary debug build: has req checker, but lenient (to allow missing extensions which you know you don't need)
  3. 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
  4. 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.

theofidry commented 10 months ago

Oh boi I feel like I'll need some refactoring for the Compile command...

crazywhalecc commented 3 months ago

I recently tried to integrate box into static-php-cli, and I found that box depends on the composer executable. It seems not feasible to package the whole packager into a binary file. I have to download composer.phar as a dependency check. But I haven't found any other good way to check the extension list that doesn't rely on composer.

dkarlovi commented 3 months ago

@crazywhalecc when I was building my prototype, I was using trial and error until it worked.

One thing I've done is, I've skipped an extension (XML) because I know it's not used even though it's requested as a dependency.

This leads me to believe any list you detect in an automated way will in the end need an override (to add or remove extensions) by the user, in box's case it would be in the config file, I imagine.