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 398a78e4ab4..88e07f64676 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 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 PHPStan Turbo 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..11b7449fe20 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; @@ -74,32 +75,38 @@ public static function findExtensionForWorkers(): ?string } /** - * Locates the distributed extension binary for the current platform — - * present only next to a phar-based installation. + * 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 findExtension(): ?string + public static function isMissingNextToPhar(): bool { - if (PHP_VERSION_ID < self::MINIMUM_PHP_VERSION_ID) { - return null; + $platformDirectory = self::getPlatformDirectoryNextToPhar(); + if ($platformDirectory === null) { + return false; } - if ((bool) PHP_DEBUG) { - return null; + if (TurboExtensionEnabler::isLoaded()) { + return false; } - $pharPath = Phar::running(false); - if ($pharPath === '') { - return null; - } + return !is_dir(dirname($platformDirectory)); + } - $platform = self::resolvePlatformDirectory(PHP_OS_FAMILY, php_uname('m'), self::isMusl()); - if ($platform === null) { + /** + * Locates the distributed extension binary for the current platform — + * present only next to a phar-based installation. + */ + public static function findExtension(): ?string + { + $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' : '', @@ -118,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. */