diff --git a/.github/actions/cache/action.yaml b/.github/actions/cache/action.yaml index 4280e0f1..906e75b8 100644 --- a/.github/actions/cache/action.yaml +++ b/.github/actions/cache/action.yaml @@ -16,13 +16,13 @@ runs: set -e # Should be the same command as the one in tools/static/castor.php - cache_dirname_linux_amd64=$(tests/bin/compile-get-cache-key phar-location-is-not-used-in-cache-key --os=linux --arch=x86_64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl) + cache_dirname_linux_amd64=$(tests/bin/compile-get-cache-key phar-location-is-not-used-in-cache-key --os=linux --arch=x86_64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl,curl) cache_key_linux_amd64=$(basename $cache_dirname_linux_amd64) echo cache_dirname_linux_amd64=$cache_dirname_linux_amd64 >> $GITHUB_ENV echo cache_key_linux_amd64=$cache_key_linux_amd64 >> $GITHUB_ENV # Should be the same command as the one in tools/static/castor.php - cache_dirname_darwin_amd64=$(tests/bin/compile-get-cache-key phar-location-is-not-used-in-cache-key --os=macos --arch=x86_64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl) + cache_dirname_darwin_amd64=$(tests/bin/compile-get-cache-key phar-location-is-not-used-in-cache-key --os=macos --arch=x86_64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl,curl) cache_key_darwin_amd64=$(basename $cache_dirname_darwin_amd64) echo cache_dirname_darwin_amd64=$cache_dirname_darwin_amd64 >> $GITHUB_ENV echo cache_key_darwin_amd64=$cache_key_darwin_amd64 >> $GITHUB_ENV diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 550187f5..6ef7bde6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,8 +77,10 @@ jobs: if: matrix.castor.method == 'phar' || matrix.castor.method == 'static' - name: Compile Custom Built PHP along Castor phar for Linux - # Should be the same command as the one in tools/static/castor.php - run: bin/castor compile tools/phar/build/castor.linux-amd64.phar --php-extensions=mbstring,phar,posix,tokenizer,pcntl --binary-path=${{ github.workspace }}/${{ matrix.castor.bin }} + run: | + set -e + bin/castor castor:static:linux + mv castor.linux-amd64 ${{ github.workspace }}/${{ matrix.castor.bin }} if: matrix.castor.method == 'static' # We use box in a test, so we need to make it available everywhere diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d21d8a..afeeceb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Not released yet +## 0.13.1 (2024-02-27) + +* Fix instruction for downloading new castor version as a phar + ## 0.13.0 (2024-02-23) * Add a `compile` command that puts together a customizable PHP binary with a diff --git a/phpunit.xml b/phpunit.xml index 41fc5ce3..6ef8f346 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -28,5 +28,6 @@ + diff --git a/src/Console/Application.php b/src/Console/Application.php index 6fcf0958..483f3b4b 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -17,13 +17,11 @@ use Castor\ListenerDescriptor; use Castor\PlatformUtil; use Castor\SectionOutput; -use Castor\Stub\StubsGenerator; use Castor\SymfonyTaskDescriptor; use Castor\TaskDescriptor; use Castor\TaskDescriptorCollection; use Castor\VerbosityLevel; use Castor\WaitForHelper; -use JoliCode\PhpOsHelper\OsHelper; use Monolog\Logger; use Psr\Cache\CacheItemPoolInterface; use Psr\Log\LogLevel; @@ -37,17 +35,15 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\Process\Process; use Symfony\Component\VarDumper\Cloner\AbstractCloner; use Symfony\Contracts\Cache\CacheInterface; -use Symfony\Contracts\HttpClient\Exception\ExceptionInterface as HttpExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; /** @internal */ class Application extends SymfonyApplication { public const NAME = 'castor'; - public const VERSION = 'v0.13.0'; + public const VERSION = 'v0.13.1'; // "Current" objects availables at some point of the lifecycle private InputInterface $input; @@ -61,7 +57,6 @@ public function __construct( public readonly ContextRegistry $contextRegistry, public readonly EventDispatcher $eventDispatcher, public readonly ExpressionLanguage $expressionLanguage, - public readonly StubsGenerator $stubsGenerator, public readonly Logger $logger, public readonly Filesystem $fs, public HttpClientInterface $httpClient, @@ -170,11 +165,6 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI { $this->command = $command; - if ('_complete' !== $command->getName() && !class_exists(\RepackedApplication::class)) { - $this->stubsGenerator->generateStubsIfNeeded($this->rootDir . '/.castor.stub.php'); - $this->displayUpdateWarningIfNeeded(new SymfonyStyle($input, $output)); - } - return parent::doRunCommand($command, $input, $output); } @@ -255,78 +245,4 @@ private function configureContext(InputInterface $input, OutputInterface $output $this->contextRegistry->setCurrentContext($context->withName($input->getOption('context'))); } - - private function displayUpdateWarningIfNeeded(SymfonyStyle $symfonyStyle): void - { - $item = $this->cache->getItem('last-update-warning'); - if ($item->isHit()) { - return; - } - - // We save it right now, even if there are some failures later. We don't - // want to waste bandwidth or CPU usage, nor log too much information. - $item->expiresAfter(60 * 60 * 24); - $item->set(true); - $this->cache->save($item); - - $response = $this->httpClient->request('GET', 'https://api.github.com/repos/jolicode/castor/releases/latest', [ - 'timeout' => 1, - ]); - - try { - $latestVersion = $response->toArray(); - } catch (HttpExceptionInterface) { - $this->logger->info('Failed to fetch latest Castor version from GitHub.'); - - return; - } - - if (version_compare($latestVersion['tag_name'], self::VERSION, '<=')) { - return; - } - - $symfonyStyle->block(sprintf('A new Castor version is available (%s, currently running %s).', $latestVersion['tag_name'], self::VERSION), escape: false); - - if ($pharPath = \Phar::running(false)) { - $assets = match (true) { - OsHelper::isWindows() || OsHelper::isWindowsSubsystemForLinux() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'windows')), - OsHelper::isMacOS() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'darwin')), - OsHelper::isUnix() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'linux')), - default => [], - }; - - if (!$assets) { - $this->logger->info('Failed to detect the correct release url adapted to your system.'); - - return; - } - - $latestReleaseUrl = reset($assets)['browser_download_url'] ?? null; - - if (!$latestReleaseUrl) { - $this->logger->info('Failed to fetch latest phar url.'); - - return; - } - - if (OsHelper::isUnix()) { - $symfonyStyle->block('Run the following command to update Castor:'); - $symfonyStyle->block(sprintf('curl "%s" -Lfso castor && chmod u+x castor && mv castor %s', $latestReleaseUrl, $pharPath), escape: false); - } else { - $symfonyStyle->block(sprintf('Download the latest version at %s', $latestReleaseUrl), escape: false); - } - - $symfonyStyle->newLine(); - - return; - } - - $process = new Process(['composer', 'global', 'config', 'home', '--quiet']); - $process->run(); - $globalComposerPath = trim($process->getOutput()); - - if ($globalComposerPath && str_contains(__FILE__, $globalComposerPath)) { - $symfonyStyle->block('Run the following command to update Castor: composer global update jolicode/castor', escape: false); - } - } } diff --git a/src/Console/ApplicationFactory.php b/src/Console/ApplicationFactory.php index e59122d8..98303351 100644 --- a/src/Console/ApplicationFactory.php +++ b/src/Console/ApplicationFactory.php @@ -10,6 +10,8 @@ use Castor\ExpressionLanguage; use Castor\Fingerprint\FingerprintHelper; use Castor\FunctionFinder; +use Castor\Listener\GenerateStubsListener; +use Castor\Listener\UpdateCastorListener; use Castor\Monolog\Processor\ProcessProcessor; use Castor\PathHelper; use Castor\PlatformUtil; @@ -48,6 +50,15 @@ public static function create(): SymfonyApplication $logger = new Logger('castor', [], [new ProcessProcessor()]); $fs = new Filesystem(); $eventDispatcher = new EventDispatcher(logger: $logger); + $eventDispatcher->addSubscriber(new UpdateCastorListener( + $cache, + $httpClient, + $logger, + )); + $eventDispatcher->addSubscriber(new GenerateStubsListener( + new StubsGenerator($logger), + $rootDir, + )); /** @var SymfonyApplication */ // @phpstan-ignore-next-line @@ -57,7 +68,6 @@ public static function create(): SymfonyApplication $contextRegistry, $eventDispatcher, new ExpressionLanguage($contextRegistry), - new StubsGenerator($logger), $logger, $fs, $httpClient, diff --git a/src/Console/Command/CompileCommand.php b/src/Console/Command/CompileCommand.php index b583106a..56ec06ff 100644 --- a/src/Console/Command/CompileCommand.php +++ b/src/Console/Command/CompileCommand.php @@ -20,6 +20,10 @@ */ class CompileCommand extends Command { + // When something **important** related to the compilation changed, increase + // this version to invalide the cache + private const CACHE_VERSION = '1'; + public function __construct( private readonly HttpClientInterface $httpClient, private readonly Filesystem $fs @@ -37,7 +41,7 @@ protected function configure(): void ->addOption('os', null, InputOption::VALUE_REQUIRED, 'Target OS for PHP compilation', 'linux', ['linux', 'macos']) ->addOption('arch', null, InputOption::VALUE_REQUIRED, 'Target architecture for PHP compilation', 'x86_64', ['x86_64', 'aarch64']) ->addOption('php-version', null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format', '8.3') - ->addOption('php-extensions', null, InputOption::VALUE_REQUIRED, 'PHP extensions required, in a comma-separated format. Defaults are the minimum required to run a basic "Hello World" task in Castor.', 'mbstring,phar,posix,tokenizer') + ->addOption('php-extensions', null, InputOption::VALUE_REQUIRED, 'PHP extensions required, in a comma-separated format. Defaults are the minimum required to run a basic "Hello World" task in Castor.', 'mbstring,phar,posix,tokenizer,curl') ->addOption('php-rebuild', null, InputOption::VALUE_NONE, 'Ignore cache and force PHP build compilation.') ->setHidden(true) ; @@ -253,7 +257,7 @@ private function generatePHPBuildCacheKey(InputInterface $input): string sort($phpExtensions); hash_update($c, implode(',', $phpExtensions)); - hash_update_file($c, __FILE__); + hash_update($c, self::CACHE_VERSION); return hash_final($c); } diff --git a/src/Listener/GenerateStubsListener.php b/src/Listener/GenerateStubsListener.php new file mode 100644 index 00000000..4ee1f27b --- /dev/null +++ b/src/Listener/GenerateStubsListener.php @@ -0,0 +1,43 @@ +getCommand(); + if (!$command) { + return; + } + if ('_complete' === $command->getName()) { + return; + } + + $this->stubsGenerator->generateStubsIfNeeded($this->rootDir . '/.castor.stub.php'); + } + + public static function getSubscribedEvents(): array + { + return [ + // Must be before the command is executed, because we have to check + // for many command options + ConsoleEvents::COMMAND => 'generateStubs', + ]; + } +} diff --git a/src/Listener/UpdateCastorListener.php b/src/Listener/UpdateCastorListener.php new file mode 100644 index 00000000..ea41a7ad --- /dev/null +++ b/src/Listener/UpdateCastorListener.php @@ -0,0 +1,157 @@ +getCommand(); + if (!$command) { + return; + } + if ('_complete' === $command->getName()) { + return; + } + $input = $event->getInput(); + if ($input->hasOption('format') && 'json' === $input->getOption('format')) { + return; + } + + $this->displayUpdateWarningIfNeeded($input, $event->getOutput()); + } + + public static function getSubscribedEvents(): array + { + return [ + // Must be before the command is executed, because we have to check + // for many command options + ConsoleEvents::COMMAND => 'checkUpdate', + ]; + } + + private function displayUpdateWarningIfNeeded(InputInterface $input, OutputInterface $output): void + { + $item = $this->cache->getItem('castor-releases'); + + if ($item->isHit()) { + $latestVersion = $item->get(); + } else { + $latestVersion = null; + $item->expiresAfter(60 * 60 * 24); + + try { + $latestVersion = $this + ->httpClient + ->request('GET', 'https://api.github.com/repos/jolicode/castor/releases/latest', [ + 'timeout' => 1, + ]) + ->toArray() + ; + } catch (ExceptionInterface) { + $this->logger->info('Failed to fetch latest Castor version from GitHub.'); + + $item->expiresAfter(60 * 10); + + return; + } + + $this->cache->save($item->set($latestVersion)); + } + + if (!$latestVersion) { + return; + } + + if (version_compare($latestVersion['tag_name'], Application::VERSION, '<=')) { + return; + } + + $symfonyStyle = new SymfonyStyle($input, $output); + + $symfonyStyle->block(sprintf('A new Castor version is available (%s, currently running %s).', $latestVersion['tag_name'], Application::VERSION), escape: false); + + // Installed via phar + if ($pharPath = \Phar::running(false)) { + $assets = match (true) { + OsHelper::isWindows() || OsHelper::isWindowsSubsystemForLinux() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'windows')), + OsHelper::isMacOS() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'darwin')), + OsHelper::isUnix() => array_filter($latestVersion['assets'], fn (array $asset) => str_contains($asset['name'], 'linux')), + default => [], + }; + + if (!$assets) { + $this->logger->info('Failed to detect the correct release url adapted to your system.'); + + return; + } + + $latestReleaseUrl = reset($assets)['browser_download_url'] ?? null; + // Fow now, we force the phar since it has more capabilities than + // the static binary, and it's more tested + if (!str_ends_with($latestReleaseUrl, '.phar')) { + $latestReleaseUrl .= '.phar'; + } + + if (!$latestReleaseUrl) { + $this->logger->info('Failed to fetch latest phar url.'); + + return; + } + + if (OsHelper::isUnix()) { + $symfonyStyle->block('Run the following command to update Castor:'); + $symfonyStyle->block(sprintf('curl "%s" -Lfso castor && chmod u+x castor && mv castor %s', $latestReleaseUrl, $pharPath), escape: false); + } else { + $symfonyStyle->block(sprintf('Download the latest version at %s', $latestReleaseUrl), escape: false); + } + + $symfonyStyle->newLine(); + + return; + } + + $globalComposerPath = $this->cache->get('castor-composer-global-path', function (): string { + $process = new Process(['composer', 'global', 'config', 'home', '--quiet']); + $process->run(); + + return trim($process->getOutput()); + }); + + // Installed via composer global + if ($globalComposerPath && str_contains(__FILE__, $globalComposerPath)) { + $symfonyStyle->block('Run the following command to update Castor: composer global update jolicode/castor', escape: false); + } + } +} diff --git a/tools/static/castor.php b/tools/static/castor.php index beb8632b..6e0fbf85 100644 --- a/tools/static/castor.php +++ b/tools/static/castor.php @@ -6,20 +6,22 @@ use function Castor\run; +// Extensions should be in sync with .github/actions/cache/action.yaml + #[AsTask(description: 'Build static binary for Linux system')] function linux() { - run('bin/castor compile tools/phar/build/castor.linux-amd64.phar --os=linux --arch=x86_64 --binary-path=castor.linux-amd64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl', timeout: 0); + run('bin/castor compile tools/phar/build/castor.linux-amd64.phar --os=linux --arch=x86_64 --binary-path=castor.linux-amd64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl,curl', timeout: 0); } #[AsTask(description: 'Build static binary for MacOS (amd64) system')] function darwinAmd64() { - run('bin/castor compile tools/phar/build/castor.darwin-amd64.phar --os=macos --arch=x86_64 --binary-path=castor.darwin-amd64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl', timeout: 0); + run('bin/castor compile tools/phar/build/castor.darwin-amd64.phar --os=macos --arch=x86_64 --binary-path=castor.darwin-amd64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl,curl', timeout: 0); } #[AsTask(description: 'Build static binary for MacOS (arm64) system')] function darwinArm64() { - run('bin/castor compile tools/phar/build/castor.darwin-arm64.phar --os=macos --arch=aarch64 --binary-path=castor.darwin-arm64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl', timeout: 0); + run('bin/castor compile tools/phar/build/castor.darwin-arm64.phar --os=macos --arch=aarch64 --binary-path=castor.darwin-arm64 --php-extensions=mbstring,phar,posix,tokenizer,pcntl,curl', timeout: 0); }