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.)
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/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/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/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/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
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);
+ }
}
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
{
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();