From 14f7f7b301ac64270ea3a689b2bc2ae7c7e13e78 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 21 Sep 2026 22:06:56 +0100 Subject: [PATCH 1/5] Add reproducer case for lib64 detection on RHEL distro (specifically, Rocky 8) --- test/end-to-end/Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/end-to-end/Dockerfile b/test/end-to-end/Dockerfile index 1ea8e95c..9742bfc0 100644 --- a/test/end-to-end/Dockerfile +++ b/test/end-to-end/Dockerfile @@ -77,3 +77,12 @@ FROM fedora_base AS test_pie_installs_system_deps_on_fedora RUN dnf install -y php php-pecl-zip unzip gcc make autoconf bison re2c libtool php-devel COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie RUN pie install -v --auto-install-system-dependencies --force php/sodium + +FROM quay.io/rockylinux/rockylinux:8 AS test_lib64_detection_on_rocky8 +RUN dnf module enable -y php:8.2 && dnf install -y php-cli php-devel unzip gcc gcc-c++ make autoconf bison flex libtool git cmake openssl-devel cyrus-sasl-devel +RUN git clone --depth 1 --branch 1.1.4 https://github.com/awesomized/libmemcached /tmp/libmemcached \ + && cmake -S /tmp/libmemcached -B /tmp/libmemcached/build -DBUILD_TESTING=OFF -DBUILD_DOCSONLY=OFF -DENABLE_SASL=ON \ + && cmake --build /tmp/libmemcached/build -j$(nproc) \ + && cmake --install /tmp/libmemcached/build +COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie +RUN pie install -v php-memcached/php-memcached From 0a6d7a1ed22e8a36a35dfdbb15d8dbb3c7832c74 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 23 Sep 2026 20:47:17 +0100 Subject: [PATCH 2/5] 741: add \Php\Pie\Platform\TargetPhp\PhpBinaryPath::phpConfigLibdir --- src/Platform/TargetPhp/PhpBinaryPath.php | 22 +++++++++++++ .../Platform/TargetPhp/PhpBinaryPathTest.php | 31 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index cd2f78d0..bf596c72 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -166,6 +166,28 @@ public function phpConfigExtensionPath(): string|null return $extensionDir !== '' ? $extensionDir : null; } + /** @return non-empty-string|null */ + public function phpConfigLibdir(): string|null + { + if ($this->phpConfigPath === null) { + return null; + } + + $configureOptions = self::cleanWarningAndDeprecationsFromOutput(Process::run([$this->phpConfigPath, '--configure-options'])); + + return self::libdirFromConfigureOptions($configureOptions); + } + + /** @return non-empty-string|null */ + private static function libdirFromConfigureOptions(string $configureOptions): string|null + { + if (! preg_match('/--with-libdir=([^\s\'"]+)/', $configureOptions, $matches)) { + return null; + } + + return $matches[1]; + } + public function assertExtensionIsLoadedInRuntime(ExtensionName $extension, IOInterface|null $io = null): void { if (! in_array(strtolower($extension->name()), array_map('strtolower', array_keys($this->extensions())))) { diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 096918b0..d24b2b50 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -116,6 +116,37 @@ public function testWarningsAndDeprecationsAreFiltered(): void self::assertSame(self::VALID_PHP_WITH_WARNINGS, $phpBinary->phpBinaryPath); } + /** @return array */ + public static function configureOptionsProvider(): array + { + return [ + 'unquoted' => ['--prefix=/usr --libdir=/usr/lib64 --with-libdir=lib64 --enable-cli', 'lib64'], + 'quoted' => ["'--prefix=/usr' '--with-libdir=lib64' '--enable-cli'", 'lib64'], + 'multiarch value' => ['--with-libdir=lib/x86_64-linux-gnu --enable-cli', 'lib/x86_64-linux-gnu'], + 'full path' => ['--with-libdir=/opt/php/lib64 --enable-cli', '/opt/php/lib64'], + 'only --libdir' => ['--prefix=/usr --libdir=/usr/lib/x86_64-linux-gnu --enable-cli', null], + 'empty' => ['', null], + ]; + } + + #[DataProvider('configureOptionsProvider')] + public function testPhpConfigLibdirFromConfigureOptions(string $configureOptions, string|null $expectedLibdir): void + { + if (Platform::isWindows()) { + self::markTestSkipped('Bash script does not run on Windows.'); + } + + $tmpSh = tempnam(sys_get_temp_dir(), uniqid('pie_php_config_libdir_test')); + file_put_contents($tmpSh, "#!/usr/bin/env bash\necho \"" . $configureOptions . "\";\n"); + chmod($tmpSh, 0777); + + $phpBinary = $this->createPartialMock(PhpBinaryPath::class, []); + (new ReflectionMethod($phpBinary, '__construct'))->invoke($phpBinary, $tmpSh, $tmpSh); + + self::assertSame($expectedLibdir, $phpBinary->phpConfigLibdir()); + unlink($tmpSh); + } + public function testVersionFromCurrentProcess(): void { $phpBinary = PhpBinaryPath::fromCurrentProcess(); From 3b91379d75c3a7ac5f91d8d485180381885e665f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 29 Sep 2026 22:06:46 +0100 Subject: [PATCH 3/5] 741: inject any detected --with-libdir= option if possible --- src/Building/UnixBuild.php | 34 ++++++++ ...fake-php-config-configure-options-proxy.sh | 7 ++ test/integration/Building/UnixBuildTest.php | 80 ++++++++++++++++++- 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100755 test/assets/fake-php-config-configure-options-proxy.sh diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php index f287b1dc..fd2a483e 100644 --- a/src/Building/UnixBuild.php +++ b/src/Building/UnixBuild.php @@ -23,6 +23,7 @@ use function implode; use function Safe\rename; use function sprintf; +use function str_starts_with; use const DIRECTORY_SEPARATOR; @@ -96,6 +97,8 @@ private function buildFromSource( $io->write('phpize complete.'); + $configureOptions = $this->withDetectedLibdirOption($configureOptions, $targetPlatform); + $phpConfigPath = $targetPlatform->phpBinaryPath->phpConfigPath(); if ($phpConfigPath !== null) { $configureOptions[] = '--with-php-config=' . $phpConfigPath; @@ -126,6 +129,37 @@ private function buildFromSource( return BinaryFile::fromFileWithSha256Checksum($expectedSoFile); } + /** + * @param list $configureOptions + * + * @return list + */ + private function withDetectedLibdirOption(array $configureOptions, TargetPlatform $targetPlatform): array + { + if ($this->hasLibdirOption($configureOptions)) { + return $configureOptions; + } + + $phpConfigLibdir = $targetPlatform->phpBinaryPath->phpConfigLibdir(); + if ($phpConfigLibdir !== null) { + $configureOptions[] = '--with-libdir=' . $phpConfigLibdir; + } + + return $configureOptions; + } + + /** @param list $configureOptions */ + private function hasLibdirOption(array $configureOptions): bool + { + foreach ($configureOptions as $configureOption) { + if (str_starts_with($configureOption, '--with-libdir')) { + return true; + } + } + + return false; + } + private function renamesToConfigM4(DownloadedPackage $downloadedPackage, IOInterface $io): void { $configM4 = $downloadedPackage->extractedSourcePath . DIRECTORY_SEPARATOR . 'config.m4'; diff --git a/test/assets/fake-php-config-configure-options-proxy.sh b/test/assets/fake-php-config-configure-options-proxy.sh new file mode 100755 index 00000000..ece13be6 --- /dev/null +++ b/test/assets/fake-php-config-configure-options-proxy.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +if [ "$1" = '--configure-options' ]; then + echo "$PIE_TEST_FAKE_CONFIGURE_OPTIONS" +else + exec "$PIE_TEST_REAL_PHP_CONFIG" "$@" +fi diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index 5ef05fad..3fb6deca 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -26,10 +26,11 @@ #[CoversClass(UnixBuild::class)] final class UnixBuildTest extends TestCase { - private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; - private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; - private const TEST_PREBUILT_PATH_VALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/valid'; - private const TEST_PREBUILT_PATH_INVALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/invalid'; + private const COMPOSER_PACKAGE_EXTRA_KEY = 'download-url-method'; + private const TEST_EXTENSION_PATH = __DIR__ . '/../../assets/pie_test_ext'; + private const FAKE_PHP_CONFIG_CONFIGURE_OPTIONS_PROXY = __DIR__ . '/../../assets/fake-php-config-configure-options-proxy.sh'; + private const TEST_PREBUILT_PATH_VALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/valid'; + private const TEST_PREBUILT_PATH_INVALID = __DIR__ . '/../../assets/pre-packaged-binary-examples/invalid'; public function testUnixSourceBuildCanBuildExtension(): void { @@ -325,4 +326,75 @@ public function testVerboseOutputShowsCleanupMessages(): void self::assertStringContainsString('Running phpize --clean step', $outputString); self::assertStringContainsString('Build files cleaned up', $outputString); } + + private function useFakeConfigureOptions(string $configureOptions): void + { + $realPhpConfig = PhpBinaryPath::fromCurrentProcess()->phpConfigPath(); + if ($realPhpConfig === null) { + self::markTestSkipped('No system php-config found to build a wrapper from.'); + } + + $_ENV['PIE_TEST_FAKE_CONFIGURE_OPTIONS'] = $configureOptions; + $_ENV['PIE_TEST_REAL_PHP_CONFIG'] = $realPhpConfig; + } + + /** @param list $configureOptions */ + private function buildPieTestExtAndCaptureOutput(array $configureOptions): string + { + $output = new BufferIO(); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([self::COMPOSER_PACKAGE_EXTRA_KEY => DownloadUrlMethod::ComposerDefaultDownload->value]); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('pie_test_ext'), + 'pie_test_ext', + '0.1.0', + null, + ), + self::TEST_EXTENSION_PATH, + ); + + (new UnixBuild())->__invoke( + $downloadedPackage, + TargetPlatform::fromPhpBinaryPath(PhpBinaryPath::fromPhpConfigExecutable(self::FAKE_PHP_CONFIG_CONFIGURE_OPTIONS_PROXY), null, null), + $configureOptions, + $output, + ); + + (new Process(['make', 'clean'], $downloadedPackage->extractedSourcePath))->mustRun(); + (new Process(['phpize', '--clean'], $downloadedPackage->extractedSourcePath))->mustRun(); + + return $output->getOutput(); + } + + public function testWithLibdirIsAutoDetectedFromPhpConfig(): void + { + if (Platform::isWindows()) { + self::markTestSkipped('Unix build test cannot be run on Windows'); + } + + $this->useFakeConfigureOptions('--prefix=/usr --with-libdir=lib64'); + $outputString = $this->buildPieTestExtAndCaptureOutput(['--enable-pie_test_ext']); + + self::assertStringContainsString('Configure complete with options: --enable-pie_test_ext --with-libdir=lib64', $outputString); + } + + public function testWithLibdirIsNotOverriddenWhenAlreadyProvided(): void + { + if (Platform::isWindows()) { + self::markTestSkipped('Unix build test cannot be run on Windows'); + } + + $this->useFakeConfigureOptions('--prefix=/usr --with-libdir=lib64'); + $outputString = $this->buildPieTestExtAndCaptureOutput(['--enable-pie_test_ext', '--with-libdir=custom']); + + self::assertStringContainsString('Configure complete with options: --enable-pie_test_ext --with-libdir=custom', $outputString); + self::assertStringNotContainsString('lib64', $outputString); + } } From 25bb8634e5c62724891b76e0f1fbe0393195aaf8 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 29 Sep 2026 22:33:21 +0100 Subject: [PATCH 4/5] 741: add --with-libdir global option to allow end users to override it --- src/Command/CommandHelper.php | 20 ++++++++++++++- test/unit/Command/CommandHelperTest.php | 33 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index ecd6c39f..3af6de7a 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -83,6 +83,7 @@ final class CommandHelper private const OPTION_FORCE = 'force'; private const OPTION_SUPPRESS_DOWNLOAD_URL_METHOD = 'suppress-download-url-method'; private const OPTION_NO_CACHE = 'no-cache'; + private const OPTION_WITH_LIBDIR = 'with-libdir'; private const OPTION_AUTO_INSTALL_BUILD_TOOLS = 'auto-install-build-tools'; private const OPTION_SUPPRESS_BUILD_TOOLS_CHECK = 'no-build-tools-check'; private const OPTION_AUTO_INSTALL_SYSTEM_DEPENDENCIES = 'auto-install-system-dependencies'; @@ -499,6 +500,10 @@ public static function bindConfigureOptionsFromPackage(Command $command, array $ foreach ($packages as $package) { foreach ($package->configureOptions() as $configureOption) { + if ($configureOption->name === self::OPTION_WITH_LIBDIR) { + continue; + } + if (array_key_exists($configureOption->name, $optionOwners)) { throw ConfigureOptionCollision::forOptionName( $configureOption->name, @@ -518,6 +523,13 @@ public static function bindConfigureOptionsFromPackage(Command $command, array $ } } + $command->addOption( + self::OPTION_WITH_LIBDIR, + null, + InputOption::VALUE_REQUIRED, + 'Look for libraries in .../NAME rather than .../lib, e.g. --' . self::OPTION_WITH_LIBDIR . '=lib64. If omitted, PIE uses the value the target PHP was built with.', + ); + self::validateInput($input, $command); } @@ -528,11 +540,13 @@ public static function bindConfigureOptionsFromPackage(Command $command, array $ */ public static function processConfigureOptionsFromInput(array $packages, InputInterface $input): array { + $withLibdir = $input->hasOption(self::OPTION_WITH_LIBDIR) ? $input->getOption(self::OPTION_WITH_LIBDIR) : null; + $configureOptionsValues = []; foreach ($packages as $package) { $optionsForPackage = []; foreach ($package->configureOptions() as $configureOption) { - if (! $input->hasOption($configureOption->name)) { + if ($configureOption->name === self::OPTION_WITH_LIBDIR || ! $input->hasOption($configureOption->name)) { continue; } @@ -554,6 +568,10 @@ public static function processConfigureOptionsFromInput(array $packages, InputIn $optionsForPackage[] = '--' . $configureOption->name; } + if (is_string($withLibdir) && $withLibdir !== '') { + $optionsForPackage[] = '--' . self::OPTION_WITH_LIBDIR . '=' . $withLibdir; + } + $configureOptionsValues[$package->name()] = $optionsForPackage; } diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index f0a87101..b2a40b05 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -308,6 +308,39 @@ public function testBindConfigureOptionsFromPackageThrowsWhenTwoPackagesDeclareS CommandHelper::bindConfigureOptionsFromPackage($command, [$packageA, $packageB], $input); } + /** @param list $configureOptions */ + private function packageWithConfigureOptions(string $name, array $configureOptions): Package + { + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn($name); + $composerPackage->method('getPrettyVersion')->willReturn('1.0.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['configure-options' => $configureOptions]); + + return Package::fromComposerCompletePackage($composerPackage); + } + + public function testWithLibdirIsPassedToEveryPackage(): void + { + $packages = [ + $this->packageWithConfigureOptions('foo/bar', [['name' => 'with-libdir', 'needs-value' => true]]), + $this->packageWithConfigureOptions('baz/qux', []), + ]; + + $command = new Command(); + $input = new ArrayInput(['--with-libdir' => 'lib64']); + + CommandHelper::bindConfigureOptionsFromPackage($command, $packages, $input); + + self::assertSame( + [ + 'foo/bar' => ['--with-libdir=lib64'], + 'baz/qux' => ['--with-libdir=lib64'], + ], + CommandHelper::processConfigureOptionsFromInput($packages, $input), + ); + } + #[RequiresOperatingSystemFamily('Windows')] public function testWindowsMachinesCannotUseWithPhpConfigOption(): void { From 43e6796c2d4402b890321be8e224f564c53d736e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 29 Sep 2026 22:38:13 +0100 Subject: [PATCH 5/5] 741: add docs for --with-libdir option --- docs/extension-maintainers.md | 5 ++++- docs/usage.md | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 1ca8bd71..89575e47 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -184,7 +184,10 @@ Note that it is not possible for end users of PIE to specify configuration options that have not been defined in your extension's `configure-options` definition. Using the same example above `composer.json`, invoking PIE with an invalid option, such as `pie install myvendor/myext --something-else` will -result in an error `The "--something-else" option does not exist.`. +result in an error `The "--something-else" option does not exist.`. The +exception is `--with-libdir`, which PIE supports for every extension, and +passes automatically when the target PHP was built with it; you do not need to +declare it in `configure-options`. If an end user does not specify a flag defined in the `configure-options` definition, it will not be passed to `./configure`. There is no way to specify diff --git a/docs/usage.md b/docs/usage.md index d32020fe..1d0fa803 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -337,6 +337,18 @@ pie install example/some-extension --with-some-library-name=/path/to/the/lib --e > `pre-packaged-binary` download method, PIE will fall back to compiling the > extension using the configure options you have specified. +#### Library directory (`--with-libdir`) + +You may override the libraries directory using `--with-libdir`. When building +from source, PIE passes the same `--with-libdir` value that the target PHP was +built with (as reported by `php-config --configure-options`), if available. + +You may override this behaviour with: + +```shell +pie install example/some-extension --with-libdir=lib64 +``` + ### Build tools check PIE will attempt to check the presence of build tools (such as gcc, make, etc.)