Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions src/Turbo/TurboExtensionSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
namespace PHPStan\Turbo;

use Phar;
use function count;
use function dirname;
use function file_get_contents;
use function getenv;
use function glob;
use function is_file;
use function php_uname;
use function sprintf;
use function str_contains;
use const PHP_DEBUG;
use const PHP_MAJOR_VERSION;
use const PHP_MINOR_VERSION;
Expand Down Expand Up @@ -115,18 +115,25 @@ public static function resolvePlatformDirectory(string $osFamily, string $machin
}

/**
* libc has no PHP constant; this is the same filesystem heuristic
* datadog-setup.php uses.
* libc has no PHP constant; this checks what loader is actually
* mapped into the running process rather than whether a musl loader
* merely exists somewhere on disk — a glibc host with musl-tools
* installed has one too, and would otherwise be misdetected as musl.
*/
public static function isMusl(): bool
{
if (is_file('/etc/alpine-release')) {
return true;
}
return self::resolveIsMusl(@file_get_contents('/proc/self/maps'), is_file('/etc/alpine-release'));
}

$muslLoaders = glob('/lib/ld-musl-*');
public static function resolveIsMusl(string|false $selfMaps, bool $hasAlpineRelease): bool
{
if ($selfMaps !== false) {
return str_contains($selfMaps, '/ld-musl-');
}

return $muslLoaders !== false && count($muslLoaders) > 0;
// no procfs to inspect (non-Linux, or a sandboxed container without
// /proc) — Alpine's own musl PHP packages are still worth detecting
return $hasAlpineRelease;
}

}
40 changes: 40 additions & 0 deletions tests/PHPStan/Turbo/TurboExtensionSelectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,44 @@ public function testResolvePlatformDirectory(string $osFamily, string $machine,
$this->assertSame($expected, TurboExtensionSelector::resolvePlatformDirectory($osFamily, $machine, $isMusl));
}

/**
* @return iterable<array{string|false, bool, bool}>
*/
public static function dataResolveIsMusl(): iterable
{
// captured from `php -r 'echo file_get_contents("/proc/self/maps");'`
// running inside the official php:8.4-cli (glibc/Debian) image
$glibcMaps = <<<'MAPS'
58d76a000000-58d76a159000 r--p 00000000 00:67 14567852 /usr/local/bin/php
58d76a200000-58d76a714000 r-xp 00200000 00:67 14567852 /usr/local/bin/php
7da75ca7a000-7da75ca85000 r--p 00029000 00:67 14556924 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7da75ca85000-7da75ca87000 r--p 00034000 00:67 14556924 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffe37e20000-7ffe37e41000 rw-p 00000000 00:00 0 [stack]
MAPS;

// same command, inside php:8.4-cli-alpine3.23 — /lib/ld-musl-x86_64.so.1
// is the musl dynamic loader mapped into the process
$muslMaps = <<<'MAPS'
58adcee00000-58adcef58000 r--p 00000000 00:67 14555462 /usr/local/bin/php
58adcf000000-58adcf525000 r-xp 00200000 00:67 14555462 /usr/local/bin/php
7934c7f6a000-7934c7f7e000 r--p 00000000 00:67 14554411 /lib/ld-musl-x86_64.so.1
7934c7f7e000-7934c7fd6000 r-xp 00014000 00:67 14554411 /lib/ld-musl-x86_64.so.1
7934c800c000-7934c8010000 rw-p 00000000 00:00 0
MAPS;

// glibc host with a musl loader present on disk (e.g. musl-tools
// installed) but not mapped into this process — the false positive
// the old glob-based heuristic produced
yield [$glibcMaps, false, false];
yield [$muslMaps, false, true];
yield [false, false, false];
yield [false, true, true];
}

#[DataProvider('dataResolveIsMusl')]
public function testResolveIsMusl(string|false $selfMaps, bool $hasAlpineRelease, bool $expected): void
{
$this->assertSame($expected, TurboExtensionSelector::resolveIsMusl($selfMaps, $hasAlpineRelease));
}

}
Loading