| 1: | <?php declare(strict_types = 1); |
| 2: | |
| 3: | namespace PHPStan\Turbo; |
| 4: | |
| 5: | use Phar; |
| 6: | use function dirname; |
| 7: | use function file_get_contents; |
| 8: | use function get_cfg_var; |
| 9: | use function is_dir; |
| 10: | use function is_file; |
| 11: | use function is_string; |
| 12: | use function php_uname; |
| 13: | use function sprintf; |
| 14: | use function strpos; |
| 15: | use const PHP_DEBUG; |
| 16: | use const PHP_MAJOR_VERSION; |
| 17: | use const PHP_MINOR_VERSION; |
| 18: | use const PHP_OS_FAMILY; |
| 19: | use const PHP_VERSION_ID; |
| 20: | use const PHP_ZTS; |
| 21: | |
| 22: | /** |
| 23: | * Locates the distributed turbo extension binary matching the current runtime |
| 24: | * so it can be loaded via `-d extension=` — into spawned worker processes |
| 25: | * (ProcessHelper), or into the restarted main process (the turbo row of |
| 26: | * PHPStan\Process\Restart\ExtensionRegistry) whose pcntl_fork()ed workers then |
| 27: | * inherit it. |
| 28: | * |
| 29: | * The binaries are committed to the phpstan/phpstan repository next to |
| 30: | * phpstan.phar (turbo-ext/<platform>/phpstan_turbo-<minor>.so, .dll on |
| 31: | * Windows) by the phar.yml commit job, so they only exist for phar-based |
| 32: | * installations. Each is a thin per-PHP-version library loading the |
| 33: | * platform's shared core from its own directory (phpstan_turbo_core.so; |
| 34: | * phpstan_turbo_core.dll and phpstan_turbo_core-zts.dll on Windows, whose |
| 35: | * thread-safe PHP is a different DLL to link against), so both must be |
| 36: | * there — |
| 37: | * a source checkout loads its locally built extension through php.ini |
| 38: | * instead. Only non-debug builds for PHP >= MINIMUM_PHP_VERSION_ID are |
| 39: | * shipped; a ZTS variant (-zts filename |
| 40: | * suffix) exists for Windows, where XAMPP, WampServer and Scoop default |
| 41: | * to thread-safe PHP, and for Linux, where the official Docker images are |
| 42: | * thread-safe from 8.6 on (and the php:*-zts ones before) — a thread-safe |
| 43: | * PHP on macOS finds no binary and runs without the extension. Workers run the regular |
| 44: | * entrypoint, so TurboExtensionEnabler still gates activation on the |
| 45: | * expected extension version. |
| 46: | * |
| 47: | * bin/phpstan loads this class before the Composer autoloader (for the |
| 48: | * process restart), so it must only call functions native to PHP 7.4 - the |
| 49: | * symfony polyfills are not registered yet. PreAutoloadFilesTest guards this. |
| 50: | */ |
| 51: | final class TurboExtensionSelector |
| 52: | { |
| 53: | |
| 54: | /** |
| 55: | * The oldest PHP the extension is built for — the phar.yml turbo-compile |
| 56: | * matrix. Older runtimes have no binary to find, so they skip the lookup |
| 57: | * (and with it the process restart) entirely. |
| 58: | */ |
| 59: | public const MINIMUM_PHP_VERSION_ID = 80200; |
| 60: | |
| 61: | /** @api */ |
| 62: | public const EXTENSION_PATH_INI = 'phpstan.turboExtensionPath'; |
| 63: | |
| 64: | /** |
| 65: | * The extension path this process was given through -d — by the restart, |
| 66: | * or by ProcessHelper when spawned as a worker. Null when the extension |
| 67: | * came from the php.ini or is not loaded at all. |
| 68: | */ |
| 69: | public static function getRestartExtensionPath(): ?string |
| 70: | { |
| 71: | $path = get_cfg_var(self::EXTENSION_PATH_INI); |
| 72: | if (!is_string($path) || $path === '') { |
| 73: | return null; |
| 74: | } |
| 75: | |
| 76: | return $path; |
| 77: | } |
| 78: | |
| 79: | /** |
| 80: | * The extension path a spawned worker needs as `-d extension=`, along with |
| 81: | * `-d` EXTENSION_PATH_INI with the same path. Null when the worker gets the |
| 82: | * extension from the php.ini it inherits, or when there is no extension. |
| 83: | * |
| 84: | * @api |
| 85: | */ |
| 86: | public static function findExtensionForWorkers(?string $pharPath = null): ?string |
| 87: | { |
| 88: | if (TurboExtensionEnabler::isLoaded()) { |
| 89: | $restartPath = self::getRestartExtensionPath(); |
| 90: | if ($restartPath !== null) { |
| 91: | // loaded through a -d flag — the restart's own, or the one |
| 92: | // ProcessHelper gave this process when spawning it as a worker: |
| 93: | // spawned workers do not inherit command-line -d flags, so they |
| 94: | // need it passed explicitly |
| 95: | return $restartPath; |
| 96: | } |
| 97: | |
| 98: | // loaded through php.ini — workers inherit the ini file |
| 99: | return null; |
| 100: | } |
| 101: | |
| 102: | return self::findExtension($pharPath); |
| 103: | } |
| 104: | |
| 105: | /** |
| 106: | * Whether PHPStan runs from a bare copy of phpstan.phar, without the |
| 107: | * turbo-ext/ directory the phpstan/phpstan Composer package ships next to |
| 108: | * it, on a runtime the extension is built for, and no php.ini loads the |
| 109: | * extension either. |
| 110: | */ |
| 111: | public static function isMissingNextToPhar(): bool |
| 112: | { |
| 113: | $platformDirectory = self::getPlatformDirectoryNextToPhar(); |
| 114: | if ($platformDirectory === null) { |
| 115: | return false; |
| 116: | } |
| 117: | if (TurboExtensionEnabler::isLoaded()) { |
| 118: | return false; |
| 119: | } |
| 120: | |
| 121: | return !is_dir(dirname($platformDirectory)); |
| 122: | } |
| 123: | |
| 124: | /** |
| 125: | * Locates the distributed extension binary for the current platform — |
| 126: | * present only next to a phar-based installation. |
| 127: | * |
| 128: | * Another program that loads PHPStan's phar, Rector for example, passes |
| 129: | * the path to that phar: Phar::running() only knows the phar that runs. |
| 130: | */ |
| 131: | public static function findExtension(?string $pharPath = null): ?string |
| 132: | { |
| 133: | $platformDirectory = self::getPlatformDirectoryNextToPhar($pharPath); |
| 134: | if ($platformDirectory === null) { |
| 135: | return null; |
| 136: | } |
| 137: | |
| 138: | $file = sprintf( |
| 139: | '%s/phpstan_turbo-%d.%d%s.%s', |
| 140: | $platformDirectory, |
| 141: | PHP_MAJOR_VERSION, |
| 142: | PHP_MINOR_VERSION, |
| 143: | (bool) PHP_ZTS ? '-zts' : '', |
| 144: | PHP_OS_FAMILY === 'Windows' ? 'dll' : 'so', |
| 145: | ); |
| 146: | if (!is_file($file)) { |
| 147: | return null; |
| 148: | } |
| 149: | |
| 150: | // without its core next to it, PHP would warn at startup that it |
| 151: | // cannot load the extension |
| 152: | if (!is_file(dirname($file) . '/' . self::resolveCoreFileName(PHP_OS_FAMILY, (bool) PHP_ZTS))) { |
| 153: | return null; |
| 154: | } |
| 155: | |
| 156: | return $file; |
| 157: | } |
| 158: | |
| 159: | /** |
| 160: | * The directory of turbo-ext/ next to the running phar that holds the |
| 161: | * binaries for this platform, whether it exists or not. Null when no |
| 162: | * binary is built for this runtime: PHP older than the minimum, a debug |
| 163: | * build, a run from source, or a platform without binaries. |
| 164: | */ |
| 165: | private static function getPlatformDirectoryNextToPhar(?string $pharPath = null): ?string |
| 166: | { |
| 167: | if (PHP_VERSION_ID < self::MINIMUM_PHP_VERSION_ID) { |
| 168: | return null; |
| 169: | } |
| 170: | if ((bool) PHP_DEBUG) { |
| 171: | return null; |
| 172: | } |
| 173: | |
| 174: | $pharPath ??= Phar::running(false); |
| 175: | if ($pharPath === '') { |
| 176: | return null; |
| 177: | } |
| 178: | |
| 179: | $platform = self::resolvePlatformDirectory(PHP_OS_FAMILY, php_uname('m'), self::isMusl()); |
| 180: | if ($platform === null) { |
| 181: | return null; |
| 182: | } |
| 183: | |
| 184: | return sprintf('%s/turbo-ext/%s', dirname($pharPath), $platform); |
| 185: | } |
| 186: | |
| 187: | /** |
| 188: | * The shared core the extension binary loads, in the same directory. |
| 189: | */ |
| 190: | public static function resolveCoreFileName(string $osFamily, bool $zts): string |
| 191: | { |
| 192: | if ($osFamily === 'Windows') { |
| 193: | return $zts ? 'phpstan_turbo_core-zts.dll' : 'phpstan_turbo_core.dll'; |
| 194: | } |
| 195: | |
| 196: | // one core serves the thread-safe and the non-thread-safe extension: |
| 197: | // it reaches the engine's globals through the pointers the extension |
| 198: | // hands it, never through TSRM itself |
| 199: | return 'phpstan_turbo_core.so'; |
| 200: | } |
| 201: | |
| 202: | public static function resolvePlatformDirectory(string $osFamily, string $machine, bool $isMusl): ?string |
| 203: | { |
| 204: | if ($osFamily === 'Darwin') { |
| 205: | // arm64 (Apple Silicon) only - there is no Intel build. An x86_64 |
| 206: | // PHP under Rosetta reports x86_64 here as well, and cannot load |
| 207: | // the arm64 binary either. |
| 208: | return $machine === 'arm64' ? 'macos-arm64' : null; |
| 209: | } |
| 210: | if ($osFamily === 'Windows') { |
| 211: | return $machine === 'AMD64' || $machine === 'x86_64' ? 'windows-x86_64' : null; |
| 212: | } |
| 213: | if ($osFamily !== 'Linux') { |
| 214: | return null; |
| 215: | } |
| 216: | |
| 217: | $architecture = $machine === 'aarch64' ? 'arm64' : $machine; |
| 218: | if ($architecture !== 'x86_64' && $architecture !== 'arm64') { |
| 219: | return null; |
| 220: | } |
| 221: | |
| 222: | return sprintf('linux-%s-%s', $isMusl ? 'musl' : 'gnu', $architecture); |
| 223: | } |
| 224: | |
| 225: | /** |
| 226: | * libc has no PHP constant; this checks what loader is actually |
| 227: | * mapped into the running process rather than whether a musl loader |
| 228: | * merely exists somewhere on disk — a glibc host with musl-tools |
| 229: | * installed has one too, and would otherwise be misdetected as musl. |
| 230: | */ |
| 231: | public static function isMusl(): bool |
| 232: | { |
| 233: | return self::resolveIsMusl(@file_get_contents('/proc/self/maps'), is_file('/etc/alpine-release')); |
| 234: | } |
| 235: | |
| 236: | public static function resolveIsMusl(string|false $selfMaps, bool $hasAlpineRelease): bool |
| 237: | { |
| 238: | if ($selfMaps !== false) { |
| 239: | // strpos() rather than str_contains(): this runs before the Composer |
| 240: | // autoloader, so the symfony polyfill is not loaded yet and |
| 241: | // str_contains() does not exist on PHP < 8.0 |
| 242: | return strpos($selfMaps, '/ld-musl-') !== false; |
| 243: | } |
| 244: | |
| 245: | // no procfs to inspect (non-Linux, or a sandboxed container without |
| 246: | // /proc) — Alpine's own musl PHP packages are still worth detecting |
| 247: | return $hasAlpineRelease; |
| 248: | } |
| 249: | |
| 250: | } |
| 251: |