Skip to content

Commit

Permalink
Implement code review feedbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
tigitz committed Jan 31, 2024
1 parent 1a0798e commit b48fdc1
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Add `wait_for_docker_container()` function to wait for a docker container to be ready
* Add `AsSymfonyTask` attribute to map Symfony Command
* Add a `debug` command
* Add a `compile` command that puts together a customizable PHP binary with a repacked castor app into one executable file
* Add `Context->name` property (automatically set by the application)
* Edited the duration of update check from `60 days` to `24 hours`
* Revise the usage of the terms `command` and `task` for consistency through code and docs.
Expand Down
49 changes: 49 additions & 0 deletions doc/going-further/compile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Compiling your application into a standalone binary

[Putting your Castor application into a PHAR archive](repack.md) can be a good way to easily share and use it in various environments.

However, you need to ensure that PHP is installed and configured correctly in all the environments where you want to use your Castor app.
This can be a hassle, especially if you don't have control over the environments.

To simplify things, Castor's `compile` command goes further and puts together a customizable PHP binary with your PHAR archive to make one executable file that can be used in any setting.

## Pre-requisites

Follow the `repack` command [pre-requistes](repack.md#pre-requisites) instructions

> [!WARNING]
> Compiling is not supported yet on Windows.
## Running the Compile Command

To compile your Castor application, navigate to your project directory and run:

```bash
vendor/bin/castor compile
```

### Options

Make sure to show the command description to see all the available options:
```bash
vendor/bin/castor compile -h
``````
### Behavior

The `compile` command performs several steps:

1. Executes the `repack` command to build your PHAR archive.
2. Downloads or uses an existing Static PHP CLI tool to compile PHP and the PHAR archive into a binary.
3. If required, it automatically installs dependencies and compiles PHP with the specified extensions.
4. Combines the compiled PHP and your PHAR file into a single executable.

## Post-Compilation

Once the compilation is finished, your Castor application is transformed into a standalone binary called `my-app` by default (you can use the `--app-name=` option to change it).
This binary is now ready to be distributed and run in environments that do not have PHP installed.

You can simply run it like any other executable:

```bash
./my-app
```
4 changes: 4 additions & 0 deletions doc/going-further/repack.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
You have created a Castor application, with many tasks, and you want to
distribute it as a single phar file? Castor can help you with that.

## Pre-requisites

In your project, install Castor as a dependency:

```bash
Expand All @@ -18,6 +20,8 @@ configuration. See the [PHP
documentation](https://www.php.net/manual/en/phar.configuration.php#ini.phar.readonly) to disabled
`phar.readonly`.

## Running the Repack Command

Then, run the repack command to create the new phar:

```
Expand Down
174 changes: 110 additions & 64 deletions src/Console/Command/CompileCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Castor\Console\Command;

use Castor\PathHelper;
use Castor\PlatformUtil;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\ArrayInput;
Expand All @@ -20,15 +21,13 @@
*/
class CompileCommand extends Command
{
private HttpClientInterface $httpClient;
private Filesystem $fs;

public function __construct()
{
public function __construct(
private HttpClientInterface $httpClient,
readonly private Filesystem $fs = new Filesystem()
) {
parent::__construct();

$this->httpClient = HttpClient::create();
$this->fs = new Filesystem();
}

protected function configure(): void
Expand All @@ -49,12 +48,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
{
$os = $input->getOption('os');
if ('windows' === $os) {
throw new \RuntimeException('Standalone binary compilation is not yet supported for Windows.');
throw new \InvalidArgumentException('Standalone binary compilation is not yet supported for Windows.');
}

$arch = $input->getOption('arch');
if (!\in_array($arch, ['x86_64', 'aarch64'])) {
throw new \RuntimeException('Target architecture must be one of x86_64 or aarch64');
throw new \InvalidArgumentException('Target architecture must be one of "x86_64" or "aarch64"');
}

$io = new SymfonyStyle($input, $output);
Expand All @@ -64,83 +63,56 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$io->section('Compiling PHP and your Castor app PHAR archive into a standalone binary');

$spcBinaryPath = sys_get_temp_dir() . '/castor-php-static-compiler/spc';
$spcBinaryPath = PlatformUtil::getCacheDirectory() . '/castor-php-static-compiler/spc';
$spcBinaryDir = \dirname($spcBinaryPath);

if (!$this->fs->exists($spcBinaryDir)) {
$io->text(sprintf('Creating directory "%s" as it does not exist.', $spcBinaryDir));
$this->fs->mkdir($spcBinaryDir, 0o755);
}

if (!$this->fs->exists($spcBinaryPath)) {
$spcSourceUrl = sprintf('https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-%s-%s', $os, $arch);
$io->text(sprintf('Downloading the static-php-cli (spc) tool from "%s" to "%s"', $spcSourceUrl, $spcBinaryPath));
$this->downloadSPC($spcSourceUrl, $spcBinaryPath, $output);
$io->newLine(2);
} else {
$io->text(sprintf('Using the static-php-cli (spc) tool from "%s"', $spcBinaryPath));
}
$this->setupSPC(
$spcBinaryDir,
$spcBinaryPath,
$io,
$os,
$arch,
$output
);

$forcePHPRebuild = $input->getOption('php-rebuild');

if (!$this->fs->exists($spcBinaryDir . '/buildroot/bin/micro.sfx') || $forcePHPRebuild) {
$installSPCDepsProcess = new Process(
command: [
$spcBinaryPath, 'doctor',
'--auto-fix',
],
cwd: $spcBinaryDir,
timeout: 5 * 60
);
$io->text('Running command: ' . $installSPCDepsProcess->getCommandLine());
$installSPCDepsProcess->mustRun(fn ($type, $buffer) => print ($buffer));
$this->installPHPBuildTools($spcBinaryPath, $spcBinaryDir, $io);

$phpVersion = $input->getOption('php-version');
$phpExtensions = $input->getOption('php-extensions');

$downloadProcess = new Process(
command: [
$spcBinaryPath, 'download',
'--for-extensions=' . $phpExtensions,
'--with-php=' . $phpVersion,
],
cwd: $spcBinaryDir,
timeout: 5 * 60
$this->downloadPHPSourceDeps(
$spcBinaryPath,
$phpExtensions,
$phpVersion,
$spcBinaryDir,
$io
);
$io->text('Running command: ' . $downloadProcess->getCommandLine());
$downloadProcess->mustRun(fn ($type, $buffer) => print ($buffer));

$buildProcess = new Process(
command: [
$spcBinaryPath, 'build', $phpExtensions,
'--build-micro',
'--arch=' . $arch,
'-r',
],
cwd: $spcBinaryDir,
timeout: 60 * 60

$this->buildPHP(
$spcBinaryPath,
$phpExtensions,
$arch,
$spcBinaryDir,
$io
);
$io->text('Running command: ' . $buildProcess->getCommandLine());
$buildProcess->mustRun(fn ($type, $buffer) => print ($buffer));
}

// __DIR__ will be castor-app/vendor/jolicode/castor/src/Console/Command when running castor-app/vendor/bin/castor
$castorAppSourceDir = PathHelper::realpath(__DIR__ . '/../../../../../../');
$appName = $input->getOption('app-name');
$pharFilePath = sprintf('%s/%s.%s.phar', $castorAppSourceDir, $appName, $os);

$buildProcess = new Process(
[
$spcBinaryPath,
'micro:combine', $pharFilePath,
'--output=' . $castorAppSourceDir . '/' . $appName,
],
cwd: $spcBinaryDir
$this->mergePHPandPHARIntoSingleExecutable(
$spcBinaryPath,
$pharFilePath,
$castorAppSourceDir . '/' . $appName,
$spcBinaryDir,
$io
);

$io->text('Running command: ' . $buildProcess->getCommandLine());
$buildProcess->mustRun(fn ($type, $buffer) => print ($buffer));

return Command::SUCCESS;
}

Expand Down Expand Up @@ -186,4 +158,78 @@ private function downloadSPC(string $spcSourceUrl, string $spcBinaryDestination,

$progressBar->finish();
}

private function installPHPBuildTools(string $spcBinaryPath, string $spcBinaryDir, SymfonyStyle $io): void
{
$installSPCDepsProcess = new Process(
command: [
$spcBinaryPath, 'doctor',
'--auto-fix',
],
cwd: $spcBinaryDir,
timeout: 5 * 60
);
$io->text('Running command: ' . $installSPCDepsProcess->getCommandLine());
$installSPCDepsProcess->mustRun(fn ($type, $buffer) => print ($buffer));
}

private function downloadPHPSourceDeps(string $spcBinaryPath, mixed $phpExtensions, mixed $phpVersion, string $spcBinaryDir, SymfonyStyle $io): void
{
$downloadProcess = new Process(
command: [
$spcBinaryPath, 'download',
'--for-extensions=' . $phpExtensions,
'--with-php=' . $phpVersion,
],
cwd: $spcBinaryDir,
timeout: 5 * 60
);
$io->text('Running command: ' . $downloadProcess->getCommandLine());
$downloadProcess->mustRun(fn ($type, $buffer) => print ($buffer));
}

private function buildPHP(string $spcBinaryPath, mixed $phpExtensions, mixed $arch, string $spcBinaryDir, SymfonyStyle $io): void
{
$buildProcess = new Process(
command: [
$spcBinaryPath, 'build', $phpExtensions,
'--build-micro',
'--arch=' . $arch,
'-r',
],
cwd: $spcBinaryDir,
timeout: 60 * 60
);
$io->text('Running command: ' . $buildProcess->getCommandLine());
$buildProcess->mustRun(fn ($type, $buffer) => print ($buffer));
}

private function mergePHPandPHARIntoSingleExecutable(string $spcBinaryPath, string $pharFilePath, string $appBinaryFilePath, string $spcBinaryDir, SymfonyStyle $io): void
{
$mergePHPandPHARProcess = new Process(
[
$spcBinaryPath,
'micro:combine', $pharFilePath,
'--output=' . $appBinaryFilePath,
],
cwd: $spcBinaryDir
);

$io->text('Running command: ' . $mergePHPandPHARProcess->getCommandLine());
$mergePHPandPHARProcess->mustRun(fn ($type, $buffer) => print ($buffer));
}

private function setupSPC(string $spcBinaryDir, string $spcBinaryPath, SymfonyStyle $io, mixed $os, mixed $arch, OutputInterface $output): void
{
$this->fs->mkdir($spcBinaryDir, 0o755);

if ($this->fs->exists($spcBinaryPath)) {
$io->text(sprintf('Using the static-php-cli (spc) tool from "%s"', $spcBinaryPath));
} else {
$spcSourceUrl = sprintf('https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-%s-%s', $os, $arch);
$io->text(sprintf('Downloading the static-php-cli (spc) tool from "%s" to "%s"', $spcSourceUrl, $spcBinaryPath));
$this->downloadSPC($spcSourceUrl, $spcBinaryPath, $output);
$io->newLine(2);
}
}
}
2 changes: 1 addition & 1 deletion src/Console/Command/RepackCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$os = $input->getOption('os');
if (!\in_array($os, ['linux', 'macos', 'windows'])) {
throw new \RuntimeException('The os option must be one of linux, macos or windows.');
throw new \InvalidArgumentException('The os option must be one of linux, macos or windows.');
}

$finder = new ExecutableFinder();
Expand Down

0 comments on commit b48fdc1

Please sign in to comment.