From a2b81f4921e1f2948aa0ef3f15c715537b5084a7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Oct 2026 21:20:11 +0100 Subject: [PATCH 1/3] 757: use native hash_file to workaround PHP bug php/php-src#11180 --- src/File/BinaryFile.php | 25 ++++++++++++++++++++----- test/end-to-end/Dockerfile | 8 ++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) 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/test/end-to-end/Dockerfile b/test/end-to-end/Dockerfile index 1ea8e95c..bd94ad4a 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 @@ -77,3 +78,10 @@ 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 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 18c52eb5ef2a85ba1f1a8f5c9911e13864f1e348 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 7 Oct 2026 21:40:07 +0100 Subject: [PATCH 2/3] 761: only use OndrejPhpenmod if we're actually using the Ondrej PHP install --- src/Installing/Ini/OndrejPhpenmod.php | 19 +++++++- .../Installing/Ini/OndrejPhpenmodTest.php | 44 ++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) 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/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), ); } From 3534082ced2e7ccb5b9c686b00e6128a14534e5c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 8 Oct 2026 19:30:24 +0100 Subject: [PATCH 3/3] 760: pass PCRE compile flags down if using external PCRE2 --- src/Building/PcreCompilerFlags.php | 33 +++++++ src/Building/UnixBuild.php | 17 +++- src/Platform/TargetPhp/PhpBinaryPath.php | 5 + test/assets/fake-pkg-config-pcre/pkg-config | 9 ++ test/end-to-end/Dockerfile | 8 +- .../Building/PcreCompilerFlagsTest.php | 96 +++++++++++++++++++ .../Platform/TargetPhp/PhpBinaryPathTest.php | 24 +++++ 7 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 src/Building/PcreCompilerFlags.php create mode 100755 test/assets/fake-pkg-config-pcre/pkg-config create mode 100644 test/integration/Building/PcreCompilerFlagsTest.php 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 f287b1dc..1ae1f400 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; @@ -23,6 +24,7 @@ use function implode; use function Safe\rename; use function sprintf; +use function trim; use const DIRECTORY_SEPARATOR; @@ -101,7 +103,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); @@ -174,11 +176,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) . '', @@ -189,6 +203,7 @@ private function configure( $configureCommand, $downloadedPackage->extractedSourcePath, outputCallback: $outputCallback, + env: $configureEnv, ); } diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index cd2f78d0..f6e0b1a8 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -237,6 +237,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 1ea8e95c..eee33cd4 100644 --- a/test/end-to-end/Dockerfile +++ b/test/end-to-end/Dockerfile @@ -41,16 +41,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 \ 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/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 096918b0..78e478a3 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -523,6 +523,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']);