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
5 changes: 4 additions & 1 deletion docs/extension-maintainers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
34 changes: 34 additions & 0 deletions src/Building/UnixBuild.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use function implode;
use function Safe\rename;
use function sprintf;
use function str_starts_with;

use const DIRECTORY_SEPARATOR;

Expand Down Expand Up @@ -96,6 +97,8 @@ private function buildFromSource(

$io->write('<info>phpize complete</info>.');

$configureOptions = $this->withDetectedLibdirOption($configureOptions, $targetPlatform);

$phpConfigPath = $targetPlatform->phpBinaryPath->phpConfigPath();
if ($phpConfigPath !== null) {
$configureOptions[] = '--with-php-config=' . $phpConfigPath;
Expand Down Expand Up @@ -126,6 +129,37 @@ private function buildFromSource(
return BinaryFile::fromFileWithSha256Checksum($expectedSoFile);
}

/**
* @param list<non-empty-string> $configureOptions
*
* @return list<non-empty-string>
*/
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<non-empty-string> $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';
Expand Down
20 changes: 19 additions & 1 deletion src/Command/CommandHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down
22 changes: 22 additions & 0 deletions src/Platform/TargetPhp/PhpBinaryPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -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())))) {
Expand Down
7 changes: 7 additions & 0 deletions test/assets/fake-php-config-configure-options-proxy.sh
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions test/end-to-end/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
80 changes: 76 additions & 4 deletions test/integration/Building/UnixBuildTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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<non-empty-string> $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);
}
}
33 changes: 33 additions & 0 deletions test/unit/Command/CommandHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,39 @@ public function testBindConfigureOptionsFromPackageThrowsWhenTwoPackagesDeclareS
CommandHelper::bindConfigureOptionsFromPackage($command, [$packageA, $packageB], $input);
}

/** @param list<array{name: string, needs-value?: bool}> $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
{
Expand Down
31 changes: 31 additions & 0 deletions test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,37 @@ public function testWarningsAndDeprecationsAreFiltered(): void
self::assertSame(self::VALID_PHP_WITH_WARNINGS, $phpBinary->phpBinaryPath);
}

/** @return array<string, array{string, string|null}> */
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();
Expand Down
Loading