From 9a4089f58d99f4e5f81861df196599b267d75539 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 9 Oct 2026 13:42:24 +0200 Subject: [PATCH 1/3] Tell the user when a phar runs without the Turbo extension Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Command/AnalyseCommand.php | 11 +++++++++++ src/Turbo/TurboExtensionSelector.php | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 398a78e4ab4..ad7f4a54414 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -30,6 +30,7 @@ use PHPStan\Internal\DirectoryCreatorException; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\TurboExtensionEnabler; +use PHPStan\Turbo\TurboExtensionSelector; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -56,6 +57,7 @@ use function is_string; use function microtime; use function pathinfo; +use function preg_match; use function rewind; use function sprintf; use function str_contains; @@ -340,6 +342,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int TurboExtensionEnabler::EXPECTED_EXTENSION_VERSION, $phpstanVersion === ComposerHelper::UNKNOWN_VERSION ? 'your PHPStan version' : sprintf('PHPStan %s', $phpstanVersion), )); + } elseif (TurboExtensionSelector::isMissingNextToPhar()) { + $phpstanVersion = ComposerHelper::getPhpStanVersion(); + $errorOutput->getStyle()->note(sprintf( + 'PHPStan runs without its Turbo extension.' . "\n" + . 'With the extension, PHPStan is up to 60%% faster.' . "\n" + . 'Install PHPStan with Composer: composer require --dev phpstan/phpstan' . "\n" + . 'Or install the extension with PIE: pie install %s', + preg_match('/^\d+\.\d+\.\d+$/', $phpstanVersion) === 1 ? sprintf('phpstan/turbo:%s', $phpstanVersion) : 'phpstan/turbo', + )); } if ($pro) { diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index b7e39640259..a829c2fc78d 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -5,6 +5,7 @@ use Phar; use function dirname; use function file_get_contents; +use function is_dir; use function is_file; use function php_uname; use function sprintf; @@ -73,6 +74,32 @@ public static function findExtensionForWorkers(): ?string return self::findExtension(); } + /** + * Whether PHPStan runs from a bare copy of phpstan.phar, without the + * turbo-ext/ directory the phpstan/phpstan Composer package ships next to + * it, on a runtime the extension is built for, and no php.ini loads the + * extension either. + */ + public static function isMissingNextToPhar(): bool + { + if (PHP_VERSION_ID < self::MINIMUM_PHP_VERSION_ID || (bool) PHP_DEBUG) { + return false; + } + if (TurboExtensionEnabler::isLoaded()) { + return false; + } + + $pharPath = Phar::running(false); + if ($pharPath === '') { + return false; + } + if (self::resolvePlatformDirectory(PHP_OS_FAMILY, php_uname('m'), self::isMusl()) === null) { + return false; + } + + return !is_dir(dirname($pharPath) . '/turbo-ext'); + } + /** * Locates the distributed extension binary for the current platform — * present only next to a phar-based installation. From 827a912ff1194a2cfc7f9f2978e63b356215c94c Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 9 Oct 2026 14:26:42 +0200 Subject: [PATCH 2/3] Call it PHPStan Turbo, and test the note on the built phar Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/phar.yml | 40 ++++++++++++++++++++++++++++ src/Command/AnalyseCommand.php | 6 ++--- src/Turbo/TurboExtensionSelector.php | 2 ++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index b99ffd2822a..a8517b75e93 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -166,6 +166,46 @@ jobs: - '.github/workflows/phar.yml' - '.github/scripts/**' + turbo-missing-note: + name: "Note about a phar without PHPStan Turbo" + needs: compiler-tests + runs-on: "ubuntu-latest" + timeout-minutes: 10 + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + # the compiler-tests job runs the phar on PHP 8.2, where no binary exists + # and the note is not printed + - name: "Install PHP" + uses: "shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962" # 2.40.0 + with: + coverage: "none" + php-version: "8.4" + + - name: "Download PHAR" + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: phar-file + path: bare + + # a copy of phpstan.phar alone prints the note, the same phar with a + # turbo-ext/ directory next to it (as in the Composer package) does not + - name: "Check the note" + run: | + printf ' test.php + php bare/phpstan.phar analyse -l 0 --no-progress test.php 2> bare.err + cat bare.err + grep -q 'PHPStan runs without PHPStan Turbo' bare.err + mkdir -p with-turbo-ext/turbo-ext + cp bare/phpstan.phar with-turbo-ext/ + php with-turbo-ext/phpstan.phar analyse -l 0 --no-progress test.php 2> with-turbo-ext.err + cat with-turbo-ext.err + ! grep -q 'PHPStan Turbo' with-turbo-ext.err + turbo-version: name: "Turbo Extension Version Check" runs-on: "ubuntu-latest" diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index ad7f4a54414..88e07f64676 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -345,10 +345,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif (TurboExtensionSelector::isMissingNextToPhar()) { $phpstanVersion = ComposerHelper::getPhpStanVersion(); $errorOutput->getStyle()->note(sprintf( - 'PHPStan runs without its Turbo extension.' . "\n" - . 'With the extension, PHPStan is up to 60%% faster.' . "\n" + 'PHPStan runs without PHPStan Turbo.' . "\n" + . 'With PHPStan Turbo, analysis is up to 60%% faster.' . "\n" . 'Install PHPStan with Composer: composer require --dev phpstan/phpstan' . "\n" - . 'Or install the extension with PIE: pie install %s', + . 'Or install PHPStan Turbo with PIE: pie install %s', preg_match('/^\d+\.\d+\.\d+$/', $phpstanVersion) === 1 ? sprintf('phpstan/turbo:%s', $phpstanVersion) : 'phpstan/turbo', )); } diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index a829c2fc78d..2eee4b60ad1 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -82,6 +82,8 @@ public static function findExtensionForWorkers(): ?string */ public static function isMissingNextToPhar(): bool { + // no binary is built for these runtimes, so installing PHPStan with + // Composer would not bring the extension either (see findExtension()) if (PHP_VERSION_ID < self::MINIMUM_PHP_VERSION_ID || (bool) PHP_DEBUG) { return false; } From 5a24d4891d805b6b29e0be5d01a787a3ed39d4fa Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 9 Oct 2026 14:30:09 +0200 Subject: [PATCH 3/3] Share the runtime checks of findExtension() and isMissingNextToPhar() Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Turbo/TurboExtensionSelector.php | 64 +++++++++++++++------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/Turbo/TurboExtensionSelector.php b/src/Turbo/TurboExtensionSelector.php index 2eee4b60ad1..11b7449fe20 100644 --- a/src/Turbo/TurboExtensionSelector.php +++ b/src/Turbo/TurboExtensionSelector.php @@ -82,24 +82,15 @@ public static function findExtensionForWorkers(): ?string */ public static function isMissingNextToPhar(): bool { - // no binary is built for these runtimes, so installing PHPStan with - // Composer would not bring the extension either (see findExtension()) - if (PHP_VERSION_ID < self::MINIMUM_PHP_VERSION_ID || (bool) PHP_DEBUG) { + $platformDirectory = self::getPlatformDirectoryNextToPhar(); + if ($platformDirectory === null) { return false; } if (TurboExtensionEnabler::isLoaded()) { return false; } - $pharPath = Phar::running(false); - if ($pharPath === '') { - return false; - } - if (self::resolvePlatformDirectory(PHP_OS_FAMILY, php_uname('m'), self::isMusl()) === null) { - return false; - } - - return !is_dir(dirname($pharPath) . '/turbo-ext'); + return !is_dir(dirname($platformDirectory)); } /** @@ -108,27 +99,14 @@ public static function isMissingNextToPhar(): bool */ public static function findExtension(): ?string { - if (PHP_VERSION_ID < self::MINIMUM_PHP_VERSION_ID) { - return null; - } - if ((bool) PHP_DEBUG) { - return null; - } - - $pharPath = Phar::running(false); - if ($pharPath === '') { - return null; - } - - $platform = self::resolvePlatformDirectory(PHP_OS_FAMILY, php_uname('m'), self::isMusl()); - if ($platform === null) { + $platformDirectory = self::getPlatformDirectoryNextToPhar(); + if ($platformDirectory === null) { return null; } $file = sprintf( - '%s/turbo-ext/%s/phpstan_turbo-%d.%d%s.%s', - dirname($pharPath), - $platform, + '%s/phpstan_turbo-%d.%d%s.%s', + $platformDirectory, PHP_MAJOR_VERSION, PHP_MINOR_VERSION, (bool) PHP_ZTS ? '-zts' : '', @@ -147,6 +125,34 @@ public static function findExtension(): ?string return $file; } + /** + * The directory of turbo-ext/ next to the running phar that holds the + * binaries for this platform, whether it exists or not. Null when no + * binary is built for this runtime: PHP older than the minimum, a debug + * build, a run from source, or a platform without binaries. + */ + private static function getPlatformDirectoryNextToPhar(): ?string + { + if (PHP_VERSION_ID < self::MINIMUM_PHP_VERSION_ID) { + return null; + } + if ((bool) PHP_DEBUG) { + return null; + } + + $pharPath = Phar::running(false); + if ($pharPath === '') { + return null; + } + + $platform = self::resolvePlatformDirectory(PHP_OS_FAMILY, php_uname('m'), self::isMusl()); + if ($platform === null) { + return null; + } + + return sprintf('%s/turbo-ext/%s', dirname($pharPath), $platform); + } + /** * The shared core the extension binary loads, in the same directory. */