diff --git a/src/Building/PcreCompilerFlags.php b/src/Building/PcreCompilerFlags.php
new file mode 100644
index 00000000..12d6fffc
--- /dev/null
+++ b/src/Building/PcreCompilerFlags.php
@@ -0,0 +1,33 @@
+phpBinaryPath->usesExternalPcre()) {
+ return null;
+ }
+
+ try {
+ $flags = Process::run(['pkg-config', '--cflags', 'libpcre2-8']);
+ } catch (ProcessFailedException) {
+ return null;
+ }
+
+ return $flags !== '' ? $flags : null;
+ }
+}
diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php
index fd2a483e..1d19371f 100644
--- a/src/Building/UnixBuild.php
+++ b/src/Building/UnixBuild.php
@@ -5,6 +5,7 @@
namespace Php\Pie\Building;
use Composer\IO\IOInterface;
+use Composer\Util\Platform;
use LogicException;
use Php\Pie\ComposerIntegration\BundledPhpExtensionsRepository;
use Php\Pie\Downloading\DownloadedPackage;
@@ -24,6 +25,7 @@
use function Safe\rename;
use function sprintf;
use function str_starts_with;
+use function trim;
use const DIRECTORY_SEPARATOR;
@@ -104,7 +106,7 @@ private function buildFromSource(
$configureOptions[] = '--with-php-config=' . $phpConfigPath;
}
- $this->configure($downloadedPackage, $configureOptions, $io, $outputCallback);
+ $this->configure($downloadedPackage, $targetPlatform, $configureOptions, $io, $outputCallback);
$optionsOutput = count($configureOptions) ? ' with options: ' . implode(' ', $configureOptions) : '.';
$io->write('Configure complete' . $optionsOutput);
@@ -208,11 +210,23 @@ private function phpize(
*/
private function configure(
DownloadedPackage $downloadedPackage,
+ TargetPlatform $targetPlatform,
array $configureOptions,
IOInterface $io,
callable|null $outputCallback,
): void {
$configureCommand = ['./configure', ...$configureOptions];
+ $configureEnv = null;
+
+ $pcreCompilerFlags = PcreCompilerFlags::forTargetPlatform($targetPlatform);
+ if ($pcreCompilerFlags !== null) {
+ $configureEnv = ['CPPFLAGS' => trim(Platform::getEnv('CPPFLAGS') . ' ' . $pcreCompilerFlags)];
+
+ $io->write(
+ 'PHP uses an external PCRE library, adding to CPPFLAGS: ' . $pcreCompilerFlags . '',
+ verbosity: IOInterface::VERBOSE,
+ );
+ }
$io->write(
'Running configure step with: ' . implode(' ', $configureCommand) . '',
@@ -223,6 +237,7 @@ private function configure(
$configureCommand,
$downloadedPackage->extractedSourcePath,
outputCallback: $outputCallback,
+ env: $configureEnv,
);
}
diff --git a/src/File/BinaryFile.php b/src/File/BinaryFile.php
index 733d3d36..5baa4a4e 100644
--- a/src/File/BinaryFile.php
+++ b/src/File/BinaryFile.php
@@ -5,11 +5,13 @@
namespace Php\Pie\File;
use Php\Pie\Util;
+use Safe\Exceptions\HashException;
+use function error_clear_last;
use function file_exists;
use function hash;
use function hash_equals;
-use function Safe\hash_file;
+use function hash_file;
/**
* @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks
@@ -33,10 +35,23 @@ public function __construct(
/** @param non-empty-string $filePath */
public static function fromFileWithSha256Checksum(string $filePath): self
{
- return new self(
- $filePath,
- hash_file(self::HASH_TYPE_SHA256, $filePath),
- );
+ error_clear_last();
+
+ /**
+ * `\Safe\hash_file` always passes 4 params, which breaks in PHP <8.1.20 / <8.2.7
+ * We can use `\Safe\has_file` if it either gets fixed upstream, or we drop PHP 8.2
+ *
+ * @link https://github.com/php/php-src/issues/11180
+ *
+ * @phpstan-ignore theCodingMachineSafe.function
+ */
+ $checksum = hash_file(self::HASH_TYPE_SHA256, $filePath);
+
+ if ($checksum === false) {
+ throw HashException::createFromPhpError();
+ }
+
+ return new self($filePath, $checksum);
}
public function verify(): void
diff --git a/src/Installing/Ini/OndrejPhpenmod.php b/src/Installing/Ini/OndrejPhpenmod.php
index 9048ea79..4f55bf0d 100644
--- a/src/Installing/Ini/OndrejPhpenmod.php
+++ b/src/Installing/Ini/OndrejPhpenmod.php
@@ -16,12 +16,14 @@
use Symfony\Component\Process\Exception\ProcessFailedException;
use function array_unshift;
+use function dirname;
use function file_exists;
use function is_dir;
use function is_writable;
use function rtrim;
use function Safe\preg_match;
use function sprintf;
+use function str_starts_with;
use const DIRECTORY_SEPARATOR;
@@ -40,7 +42,22 @@ public function __construct(
public function canBeUsed(TargetPlatform $targetPlatform): bool
{
- return $this->phpenmodPath() !== null;
+ return $this->phpenmodPath() !== null
+ && $this->targetPhpScansDirectoryManagedByPhpenmod($targetPlatform);
+ }
+
+ private function targetPhpScansDirectoryManagedByPhpenmod(TargetPlatform $targetPlatform): bool
+ {
+ $additionalPhpIniPath = $targetPlatform->phpBinaryPath->additionalIniDirectory();
+
+ if ($additionalPhpIniPath === null) {
+ return false;
+ }
+
+ return str_starts_with(
+ $additionalPhpIniPath,
+ dirname(sprintf($this->modsAvailablePath, $targetPlatform->phpBinaryPath->majorMinorVersion())) . DIRECTORY_SEPARATOR,
+ );
}
public function setup(
diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php
index ee3046c2..644ade68 100644
--- a/src/Platform/TargetPhp/PhpBinaryPath.php
+++ b/src/Platform/TargetPhp/PhpBinaryPath.php
@@ -294,6 +294,11 @@ public function buildProvider(): string|null
return null;
}
+ public function usesExternalPcre(): bool
+ {
+ return preg_match('/^Configure Command\s*=>.*--with-external-pcre\b/m', $this->phpinfo()) === 1;
+ }
+
/**
* Returns a map where the key is the name of the extension and the value is the version ('0' if not defined)
*
diff --git a/test/assets/fake-pkg-config-pcre/pkg-config b/test/assets/fake-pkg-config-pcre/pkg-config
new file mode 100755
index 00000000..78199ec8
--- /dev/null
+++ b/test/assets/fake-pkg-config-pcre/pkg-config
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+
+if [ "$*" = "--cflags libpcre2-8" ]; then
+ echo "$PIE_TEST_PCRE_CFLAGS"
+ exit 0
+fi
+
+echo "unknown fake pkg-config command: $*" >&2
+exit 1
diff --git a/test/end-to-end/Dockerfile b/test/end-to-end/Dockerfile
index 9742bfc0..ad8c1b0e 100644
--- a/test/end-to-end/Dockerfile
+++ b/test/end-to-end/Dockerfile
@@ -2,6 +2,7 @@ FROM boxproject/box:4.7.0 AS box_base
FROM alpine:3.24 AS alpine_base
FROM fedora:44 AS fedora_base
FROM ubuntu:26.04 AS ubuntu_base
+FROM ubuntu:22.04 AS ubuntu_2204_base
FROM homebrew/brew:4.6.20 AS homebrew_base
FROM redhat/ubi9-minimal:9.8 AS ubi9_base
@@ -41,16 +42,22 @@ COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie
RUN pie install --auto-install-build-tools -v asgrim/example-pie-extension:2.0.5
RUN pie show
-FROM homebrew_base AS test_pie_installs_build_tools_with_brew
+FROM homebrew_base AS homebrew_base_with_pie
RUN brew update && brew install php
COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie
USER root
RUN rm -f /etc/apt/sources.list.d/github-cli.list && apt-get update && apt-get install -y unzip
RUN apt-get remove --allow-remove-essential -y apt
USER linuxbrew
+
+FROM homebrew_base_with_pie AS test_pie_installs_build_tools_with_brew
RUN pie install --auto-install-build-tools -v asgrim/example-pie-extension:2.0.5
RUN pie show
+FROM homebrew_base_with_pie AS test_pie_installs_extension_using_pcre_with_brew
+RUN pie install --auto-install-build-tools -v apcu/apcu
+RUN php -m | grep -i apcu
+
FROM ubuntu_base AS test_pie_installs_system_deps_on_ubuntu
RUN apt-get update && apt install -y unzip curl wget gcc make autoconf libtool bison re2c pkg-config libzip-dev libssl-dev libonig-dev
RUN mkdir -p /opt/php \
@@ -78,6 +85,13 @@ RUN dnf install -y php php-pecl-zip unzip gcc make autoconf bison re2c libtool p
COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie
RUN pie install -v --auto-install-system-dependencies --force php/sodium
+FROM ubuntu_2204_base AS test_pie_installs_on_php_with_three_argument_hash_file
+RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y php8.1-cli php8.1-dev unzip ca-certificates gcc make autoconf libtool
+RUN php -r 'exit(version_compare(PHP_VERSION, "8.1.20", "<") ? 0 : 1);'
+COPY --from=build_pie_phar /app/pie.phar /usr/local/bin/pie
+RUN pie install -v apcu/apcu
+RUN pie show
+
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 \
diff --git a/test/integration/Building/PcreCompilerFlagsTest.php b/test/integration/Building/PcreCompilerFlagsTest.php
new file mode 100644
index 00000000..5f95cab5
--- /dev/null
+++ b/test/integration/Building/PcreCompilerFlagsTest.php
@@ -0,0 +1,96 @@
+ */
+ private array $oldEnv;
+
+ public function setUp(): void
+ {
+ if (Platform::isWindows()) {
+ self::markTestSkipped('Bash script does not run on Windows.');
+ }
+
+ $this->oldEnv = $_ENV;
+ }
+
+ protected function tearDown(): void
+ {
+ $_ENV = $this->oldEnv;
+ }
+
+ public function testFlagsFromPkgConfigAreReturnedWhenPhpUsesExternalPcre(): void
+ {
+ $_ENV['PATH'] = realpath(self::FAKE_PKG_CONFIG_PATH) . PATH_SEPARATOR . getenv('PATH');
+ $_ENV['PIE_TEST_PCRE_CFLAGS'] = '-I/opt/homebrew/Cellar/pcre2/10.49/include';
+
+ self::assertSame(
+ '-I/opt/homebrew/Cellar/pcre2/10.49/include',
+ PcreCompilerFlags::forTargetPlatform($this->targetPlatformWherePhpUsesExternalPcre(true)),
+ );
+ }
+
+ public function testNoFlagsWhenPhpUsesBundledPcre(): void
+ {
+ $_ENV['PATH'] = realpath(self::FAKE_PKG_CONFIG_PATH) . PATH_SEPARATOR . getenv('PATH');
+ $_ENV['PIE_TEST_PCRE_CFLAGS'] = '-I/opt/homebrew/Cellar/pcre2/10.49/include';
+
+ self::assertNull(PcreCompilerFlags::forTargetPlatform($this->targetPlatformWherePhpUsesExternalPcre(false)));
+ }
+
+ public function testNoFlagsWhenPkgConfigReportsNone(): void
+ {
+ $_ENV['PATH'] = realpath(self::FAKE_PKG_CONFIG_PATH) . PATH_SEPARATOR . getenv('PATH');
+ $_ENV['PIE_TEST_PCRE_CFLAGS'] = '';
+
+ self::assertNull(PcreCompilerFlags::forTargetPlatform($this->targetPlatformWherePhpUsesExternalPcre(true)));
+ }
+
+ public function testNoFlagsWhenPkgConfigIsNotAvailable(): void
+ {
+ $_ENV['PATH'] = realpath(self::PATH_WITHOUT_PKG_CONFIG);
+
+ self::assertNull(PcreCompilerFlags::forTargetPlatform($this->targetPlatformWherePhpUsesExternalPcre(true)));
+ }
+
+ private function targetPlatformWherePhpUsesExternalPcre(bool $usesExternalPcre): TargetPlatform
+ {
+ $phpBinaryPath = $this->createMock(PhpBinaryPath::class);
+ $phpBinaryPath->method('usesExternalPcre')->willReturn($usesExternalPcre);
+
+ return new TargetPlatform(
+ OperatingSystem::NonWindows,
+ OperatingSystemFamily::Linux,
+ $phpBinaryPath,
+ Architecture::x86_64,
+ ThreadSafetyMode::NonThreadSafe,
+ 1,
+ null,
+ null,
+ );
+ }
+}
diff --git a/test/unit/Installing/Ini/OndrejPhpenmodTest.php b/test/unit/Installing/Ini/OndrejPhpenmodTest.php
index 2a6fe1f9..3016568d 100644
--- a/test/unit/Installing/Ini/OndrejPhpenmodTest.php
+++ b/test/unit/Installing/Ini/OndrejPhpenmodTest.php
@@ -115,11 +115,53 @@ public function testCanBeUsedReturnsFalseWhenPhpenmodNotInPath(): void
#[RequiresOperatingSystemFamily('Linux')]
public function testCanBeUsedReturnsTrueWhenPhpenmodInPath(): void
{
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/etc/php/8.4/cli/conf.d');
+ $this->mockPhpBinary
+ ->method('majorMinorVersion')
+ ->willReturn('8.4');
+
self::assertTrue(
(new OndrejPhpenmod(
$this->checkAndAddExtensionToIniIfNeeded,
self::GOOD_PHPENMOD,
- self::NON_EXISTENT_MODS_AVAILABLE_PATH,
+ ))->canBeUsed($this->targetPlatform),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testCanBeUsedReturnsFalseWhenTargetPhpScansTheOndrejConfDirectoryOfAnotherVersion(): void
+ {
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/etc/php/8.3/cli/conf.d');
+ $this->mockPhpBinary
+ ->method('majorMinorVersion')
+ ->willReturn('8.4');
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
+ ))->canBeUsed($this->targetPlatform),
+ );
+ }
+
+ #[RequiresOperatingSystemFamily('Linux')]
+ public function testCanBeUsedReturnsFalseWhenTargetPhpDoesNotScanTheOndrejConfDirectory(): void
+ {
+ $this->mockPhpBinary
+ ->method('additionalIniDirectory')
+ ->willReturn('/home/someone/php-src-build/lib/php.ini.d');
+ $this->mockPhpBinary
+ ->method('majorMinorVersion')
+ ->willReturn('8.4');
+
+ self::assertFalse(
+ (new OndrejPhpenmod(
+ $this->checkAndAddExtensionToIniIfNeeded,
+ self::GOOD_PHPENMOD,
))->canBeUsed($this->targetPlatform),
);
}
diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
index ac8b7ed2..8b7fcbc2 100644
--- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
+++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php
@@ -583,6 +583,30 @@ public function testBuildProviderNullWhenNotConfigured(): void
self::assertNull($phpBinary->buildProvider());
}
+ /** @return array */
+ public static function externalPcreProvider(): array
+ {
+ return [
+ 'configured with external PCRE' => ["Configure Command => './configure' '--prefix=/opt/homebrew/Cellar/php/8.5.11' '--with-external-pcre' '--with-zlib'", true],
+ 'configured with bundled PCRE' => ["Configure Command => './configure' '--prefix=/usr/local' '--with-openssl'", false],
+ 'configured without external PCRE' => ["Configure Command => './configure' '--without-external-pcre'", false],
+ 'no configure command reported' => ["PHP Version => 8.4.1\nBuild Provider => Debian", false],
+ 'option mentioned outside the configure command' => ["Configure Command => './configure' '--with-openssl'\nSomething Else => --with-external-pcre", false],
+ ];
+ }
+
+ #[DataProvider('externalPcreProvider')]
+ public function testUsesExternalPcre(string $phpinfo, bool $expectedUsesExternalPcre): void
+ {
+ $phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);
+
+ $phpBinary->expects(self::once())
+ ->method('phpinfo')
+ ->willReturn($phpinfo);
+
+ self::assertSame($expectedUsesExternalPcre, $phpBinary->usesExternalPcre());
+ }
+
public function testDebugBuildModeReturnsDebugWhenYes(): void
{
$phpBinary = $this->createPartialMock(PhpBinaryPath::class, ['phpinfo']);