From 0807ca0c1dc11323a674555b803e3368b45d03c8 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 23 Sep 2025 21:41:11 +0100 Subject: [PATCH 1/6] Use pkg-config to detect libraries on platform --- .../PhpBinaryPathBasedPlatformRepository.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php index 1a4085d4..a604fac6 100644 --- a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php +++ b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php @@ -12,8 +12,11 @@ use Php\Pie\ExtensionName; use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Util\Process; +use Symfony\Component\Process\Exception\ProcessFailedException; use UnexpectedValueException; +use function explode; use function in_array; use function str_replace; use function str_starts_with; @@ -77,6 +80,8 @@ public function __construct(PhpBinaryPath $phpBinaryPath, Composer $composer, In $this->addPackage($this->packageForExtension($extension, $extensionVersion)); } + $this->addLibrariesUsingPkgConfig(); + parent::__construct(); } @@ -107,4 +112,31 @@ private function packageForExtension(string $name, string $prettyVersion): Compl return $package; } + + private function detectLibraryWithPkgConfig(string $alias, string $library): void + { + try { + $pkgConfigResult = Process::run(['pkg-config', '--print-provides', '--print-errors', $library]); + } catch (ProcessFailedException) { + return; + } + + [$library, $prettyVersion] = explode('=', $pkgConfigResult); + if (! $library || ! $prettyVersion) { + return; + } + + $version = $this->versionParser->normalize($prettyVersion); + + $lib = new CompletePackage('lib-' . $alias, $version, $prettyVersion); + $lib->setDescription('The ' . $alias . ' library, ' . $library); + $this->addPackage($lib); + } + + private function addLibrariesUsingPkgConfig(): void + { + $this->detectLibraryWithPkgConfig('bz2', 'bzip2'); + $this->detectLibraryWithPkgConfig('curl', 'libcurl'); + $this->detectLibraryWithPkgConfig('sodium', 'libsodium'); + } } From 1f41cff12975833c7d4b34134f3d96739bc0d61f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 30 Sep 2025 19:48:18 +0100 Subject: [PATCH 2/6] Define some lib dependencies for sodium, curl, bz2 --- .github/workflows/continuous-integration.yml | 1 + .../BundledPhpExtensionsRepository.php | 11 +++++++++-- .../PhpBinaryPathBasedPlatformRepository.php | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 239a0d3e..512273e4 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -95,6 +95,7 @@ jobs: libsasl2-dev \ libpq-dev \ libsqlite3-dev \ + libbz2-dev \ libzip-dev - name: "Set php-src download URL" run: echo "php_src_download_url=https://www.php.net/distributions/php-${{ matrix.php-versions }}.tar.gz" >> $GITHUB_ENV diff --git a/src/ComposerIntegration/BundledPhpExtensionsRepository.php b/src/ComposerIntegration/BundledPhpExtensionsRepository.php index b2f54563..ba69c006 100644 --- a/src/ComposerIntegration/BundledPhpExtensionsRepository.php +++ b/src/ComposerIntegration/BundledPhpExtensionsRepository.php @@ -41,9 +41,13 @@ class BundledPhpExtensionsRepository extends ArrayRepository private static array $bundledPhpExtensions = [ ['name' => 'bcmath'], ['name' => 'bz2'], +// 'require' => ['lib-bz2' => '*'], libbz2-dev does not provide a bzip2.pc for pkg-config ... ['name' => 'calendar'], ['name' => 'ctype'], - ['name' => 'curl'], + [ + 'name' => 'curl', + 'require' => ['lib-curl' => '*'], + ], ['name' => 'dba'], [ 'name' => 'dom', @@ -148,7 +152,10 @@ class BundledPhpExtensionsRepository extends ArrayRepository ['name' => 'sockets'], [ 'name' => 'sodium', - 'require' => ['php' => '>= 7.2.0'], + 'require' => [ + 'php' => '>= 7.2.0', + 'lib-sodium' => '*', + ], ], [ 'name' => 'sqlite3', diff --git a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php index a604fac6..0311b0ed 100644 --- a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php +++ b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php @@ -135,7 +135,7 @@ private function detectLibraryWithPkgConfig(string $alias, string $library): voi private function addLibrariesUsingPkgConfig(): void { - $this->detectLibraryWithPkgConfig('bz2', 'bzip2'); + $this->detectLibraryWithPkgConfig('bz2', 'bzip2'); // @todo bzip2 doesn't have pkg-config .pc file $this->detectLibraryWithPkgConfig('curl', 'libcurl'); $this->detectLibraryWithPkgConfig('sodium', 'libsodium'); } From ae1f53b5c797a808f38849f2ca93f4c1d439e5aa Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 2 Oct 2025 19:27:15 +0100 Subject: [PATCH 3/6] Define more dependencies for bundled extensions with simple pkg-config deps --- .../BundledPhpExtensionsRepository.php | 12 +++++++++--- .../PhpBinaryPathBasedPlatformRepository.php | 4 +++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ComposerIntegration/BundledPhpExtensionsRepository.php b/src/ComposerIntegration/BundledPhpExtensionsRepository.php index ba69c006..bfeae8b1 100644 --- a/src/ComposerIntegration/BundledPhpExtensionsRepository.php +++ b/src/ComposerIntegration/BundledPhpExtensionsRepository.php @@ -41,7 +41,6 @@ class BundledPhpExtensionsRepository extends ArrayRepository private static array $bundledPhpExtensions = [ ['name' => 'bcmath'], ['name' => 'bz2'], -// 'require' => ['lib-bz2' => '*'], libbz2-dev does not provide a bzip2.pc for pkg-config ... ['name' => 'calendar'], ['name' => 'ctype'], [ @@ -63,7 +62,10 @@ class BundledPhpExtensionsRepository extends ArrayRepository ['name' => 'exif'], [ 'name' => 'ffi', - 'require' => ['php' => '>= 7.4.0'], + 'require' => [ + 'php' => '>= 7.4.0', + 'lib-ffi' => '*', + ], ], // ['name' => 'gd'], // build failure - ext/gd/gd.c:79:11: fatal error: ft2build.h: No such file or directory ['name' => 'gettext'], @@ -192,11 +194,15 @@ class BundledPhpExtensionsRepository extends ArrayRepository 'require' => [ 'php' => '>= 5.2.0', 'ext-libxml' => '*', + 'lib-xslt' => '*', ], ], [ 'name' => 'zip', - 'require' => ['php' => '>= 5.2.0'], + 'require' => [ + 'php' => '>= 5.2.0', + 'lib-zip' => '*', + ], ], ['name' => 'zlib'], ]; diff --git a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php index 0311b0ed..0653e481 100644 --- a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php +++ b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php @@ -135,8 +135,10 @@ private function detectLibraryWithPkgConfig(string $alias, string $library): voi private function addLibrariesUsingPkgConfig(): void { - $this->detectLibraryWithPkgConfig('bz2', 'bzip2'); // @todo bzip2 doesn't have pkg-config .pc file $this->detectLibraryWithPkgConfig('curl', 'libcurl'); $this->detectLibraryWithPkgConfig('sodium', 'libsodium'); + $this->detectLibraryWithPkgConfig('ffi', 'libffi'); + $this->detectLibraryWithPkgConfig('xslt', 'libxslt'); + $this->detectLibraryWithPkgConfig('zip', 'libzip'); } } From 3db1d1b0a7cde7ffdcb69463c40e95695637dbc4 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 2 Oct 2025 19:59:52 +0100 Subject: [PATCH 4/6] Define a bunch more libraries but not as dependencies for various reasons --- .../PhpBinaryPathBasedPlatformRepository.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php index 0653e481..fec096ef 100644 --- a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php +++ b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php @@ -113,6 +113,14 @@ private function packageForExtension(string $name, string $prettyVersion): Compl return $package; } + /** + * The `$alias` parameter is the name of the dependency in `composer.json`, + * but without the `lib-` prefix; e.g. `curl` would be `lib-curl` in the + * `composer.json`. + * + * The `$library` parameter should be the name of the library to look up + * using `pkg-config`. + */ private function detectLibraryWithPkgConfig(string $alias, string $library): void { try { @@ -136,9 +144,29 @@ private function detectLibraryWithPkgConfig(string $alias, string $library): voi private function addLibrariesUsingPkgConfig(): void { $this->detectLibraryWithPkgConfig('curl', 'libcurl'); + $this->detectLibraryWithPkgConfig('enchant', 'enchant'); + $this->detectLibraryWithPkgConfig('enchant-2', 'enchant-2'); $this->detectLibraryWithPkgConfig('sodium', 'libsodium'); $this->detectLibraryWithPkgConfig('ffi', 'libffi'); $this->detectLibraryWithPkgConfig('xslt', 'libxslt'); $this->detectLibraryWithPkgConfig('zip', 'libzip'); + $this->detectLibraryWithPkgConfig('png', 'libpng'); + $this->detectLibraryWithPkgConfig('avif', 'libavif'); + $this->detectLibraryWithPkgConfig('webp', 'libwebp'); + $this->detectLibraryWithPkgConfig('jpeg', 'libjpeg'); + $this->detectLibraryWithPkgConfig('xpm', 'xpm'); + $this->detectLibraryWithPkgConfig('freetype2', 'freetype2'); + $this->detectLibraryWithPkgConfig('gdlib', 'gdlib'); + $this->detectLibraryWithPkgConfig('gmp', 'gmp'); + $this->detectLibraryWithPkgConfig('sasl', 'libsasl2'); + $this->detectLibraryWithPkgConfig('onig', 'oniguruma'); + $this->detectLibraryWithPkgConfig('odbc', 'libiodbc'); + $this->detectLibraryWithPkgConfig('capstone', 'capstone'); + $this->detectLibraryWithPkgConfig('pcre', 'libpcre2-8'); + $this->detectLibraryWithPkgConfig('edit', 'libedit'); + $this->detectLibraryWithPkgConfig('snmp', 'netsnmp'); + $this->detectLibraryWithPkgConfig('argon2', 'libargon2'); + $this->detectLibraryWithPkgConfig('uriparser', 'liburiparser'); + $this->detectLibraryWithPkgConfig('exslt', 'libexslt'); } } From 64a57be2c5ba88fa80baf1440ce10f3bc561d7e7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 2 Oct 2025 20:43:26 +0100 Subject: [PATCH 5/6] handle non semver versions of platform libs --- .../PhpBinaryPathBasedPlatformRepository.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php index fec096ef..15d1de73 100644 --- a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php +++ b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php @@ -134,7 +134,11 @@ private function detectLibraryWithPkgConfig(string $alias, string $library): voi return; } - $version = $this->versionParser->normalize($prettyVersion); + try { + $version = $this->versionParser->normalize($prettyVersion); + } catch (UnexpectedValueException) { + $version = '*'; // @todo check this is the best way to handle unparsed versions? + } $lib = new CompletePackage('lib-' . $alias, $version, $prettyVersion); $lib->setDescription('The ' . $alias . ' library, ' . $library); From 60f3a0154995cece3c7f788f1a1ce3923fd4c6f9 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 6 Oct 2025 20:59:33 +0100 Subject: [PATCH 6/6] Added testing for libraries present on test harness --- .../PhpBinaryPathBasedPlatformRepository.php | 2 +- ...pBinaryPathBasedPlatformRepositoryTest.php | 103 +++++++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php index 15d1de73..a73bfaec 100644 --- a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php +++ b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php @@ -124,7 +124,7 @@ private function packageForExtension(string $name, string $prettyVersion): Compl private function detectLibraryWithPkgConfig(string $alias, string $library): void { try { - $pkgConfigResult = Process::run(['pkg-config', '--print-provides', '--print-errors', $library]); + $pkgConfigResult = Process::run(['pkg-config', '--print-provides', '--print-errors', $library], timeout: 10); } catch (ProcessFailedException) { return; } diff --git a/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php b/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php index 1bc77d9d..9d1f593f 100644 --- a/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php +++ b/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php @@ -9,15 +9,23 @@ use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Semver\Constraint\Constraint; +use Composer\Util\Platform; use Php\Pie\ComposerIntegration\PhpBinaryPathBasedPlatformRepository; use Php\Pie\DependencyResolver\Package; use Php\Pie\ExtensionName; use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Util\Process; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Exception\ProcessFailedException; +use function array_combine; +use function array_filter; use function array_map; +use function in_array; +use function str_starts_with; #[CoversClass(PhpBinaryPathBasedPlatformRepository::class)] final class PhpBinaryPathBasedPlatformRepositoryTest extends TestCase @@ -54,7 +62,10 @@ public function testPlatformRepositoryContainsExpectedPacakges(): void ], array_map( static fn (PackageInterface $package): string => $package->getName() . ':' . $package->getPrettyVersion(), - $platformRepository->getPackages(), + array_filter( + $platformRepository->getPackages(), + static fn (PackageInterface $package): bool => ! str_starts_with($package->getName(), 'lib-'), + ), ), ); } @@ -88,7 +99,10 @@ public function testPlatformRepositoryExcludesExtensionBeingInstalled(): void ], array_map( static fn (PackageInterface $package): string => $package->getName() . ':' . $package->getPrettyVersion(), - $platformRepository->getPackages(), + array_filter( + $platformRepository->getPackages(), + static fn (PackageInterface $package): bool => ! str_starts_with($package->getName(), 'lib-'), + ), ), ); } @@ -128,8 +142,91 @@ public function testPlatformRepositoryExcludesReplacedExtensions(): void ], array_map( static fn (PackageInterface $package): string => $package->getName() . ':' . $package->getPrettyVersion(), - $platformRepository->getPackages(), + array_filter( + $platformRepository->getPackages(), + static fn (PackageInterface $package): bool => ! str_starts_with($package->getName(), 'lib-'), + ), ), ); } + + /** @return array */ + public static function installedLibraries(): array + { + // data providers cannot return empty, even if the test is skipped + if (Platform::isWindows()) { + return ['skip' => ['skip']]; + } + + $installedLibs = array_filter( + [ + ['curl', 'libcurl'], + ['enchant', 'enchant'], + ['enchant-2', 'enchant-2'], + ['sodium', 'libsodium'], + ['ffi', 'libffi'], + ['xslt', 'libxslt'], + ['zip', 'libzip'], + ['png', 'libpng'], + ['avif', 'libavif'], + ['webp', 'libwebp'], + ['jpeg', 'libjpeg'], + ['xpm', 'xpm'], + ['freetype2', 'freetype2'], + ['gdlib', 'gdlib'], + ['gmp', 'gmp'], + ['sasl', 'libsasl2'], + ['onig', 'oniguruma'], + ['odbc', 'libiodbc'], + ['capstone', 'capstone'], + ['pcre', 'libpcre2-8'], + ['edit', 'libedit'], + ['snmp', 'netsnmp'], + ['argon2', 'libargon2'], + ['uriparser', 'liburiparser'], + ['exslt', 'libexslt'], + ], + static function (array $pkg): bool { + try { + Process::run(['pkg-config', '--print-provides', '--print-errors', $pkg[1]], timeout: 10); + + return true; + } catch (ProcessFailedException) { + return false; + } + }, + ); + + return array_combine( + array_map( + static fn (array $pkg): string => $pkg[0], + $installedLibs, + ), + array_map( + static fn (array $pkg): array => [$pkg[0]], + $installedLibs, + ), + ); + } + + #[DataProvider('installedLibraries')] + public function testLibrariesAreIncluded(string $packageName): void + { + if (Platform::isWindows()) { + self::markTestSkipped('pkg-config not available on Windows'); + } + + self::assertTrue(in_array( + 'lib-' . $packageName, + array_map( + static fn (PackageInterface $package): string => $package->getName(), + (new PhpBinaryPathBasedPlatformRepository( + PhpBinaryPath::fromCurrentProcess(), + $this->createMock(Composer::class), + $this->createMock(InstalledPiePackages::class), + ExtensionName::normaliseFromString('extension_being_installed'), + ))->getPackages(), + ), + )); + } }