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
33 changes: 33 additions & 0 deletions src/Building/PcreCompilerFlags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Php\Pie\Building;

use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Util\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */
final class PcreCompilerFlags
{
private function __construct()
{
}

/** @return non-empty-string|null */
public static function forTargetPlatform(TargetPlatform $targetPlatform): string|null
{
if (! $targetPlatform->phpBinaryPath->usesExternalPcre()) {
return null;
}

try {
$flags = Process::run(['pkg-config', '--cflags', 'libpcre2-8']);
} catch (ProcessFailedException) {
return null;
}

return $flags !== '' ? $flags : null;
}
}
17 changes: 16 additions & 1 deletion src/Building/UnixBuild.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +25,7 @@
use function Safe\rename;
use function sprintf;
use function str_starts_with;
use function trim;

use const DIRECTORY_SEPARATOR;

Expand Down Expand Up @@ -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('<info>Configure complete</info>' . $optionsOutput);
Expand Down Expand Up @@ -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(
'<comment>PHP uses an external PCRE library, adding to CPPFLAGS: ' . $pcreCompilerFlags . '</comment>',
verbosity: IOInterface::VERBOSE,
);
}

$io->write(
'<comment>Running configure step with: ' . implode(' ', $configureCommand) . '</comment>',
Expand All @@ -223,6 +237,7 @@ private function configure(
$configureCommand,
$downloadedPackage->extractedSourcePath,
outputCallback: $outputCallback,
env: $configureEnv,
);
}

Expand Down
25 changes: 20 additions & 5 deletions src/File/BinaryFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 18 additions & 1 deletion src/Installing/Ini/OndrejPhpenmod.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions src/Platform/TargetPhp/PhpBinaryPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*
Expand Down
9 changes: 9 additions & 0 deletions test/assets/fake-pkg-config-pcre/pkg-config
Original file line number Diff line number Diff line change
@@ -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
16 changes: 15 additions & 1 deletion test/end-to-end/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
96 changes: 96 additions & 0 deletions test/integration/Building/PcreCompilerFlagsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace Php\PieIntegrationTest\Building;

use Composer\Util\Platform;
use Php\Pie\Building\PcreCompilerFlags;
use Php\Pie\Platform\Architecture;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Platform\ThreadSafetyMode;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

use function getenv;
use function Safe\realpath;

use const PATH_SEPARATOR;

#[CoversClass(PcreCompilerFlags::class)]
final class PcreCompilerFlagsTest extends TestCase
{
private const FAKE_PKG_CONFIG_PATH = __DIR__ . '/../../assets/fake-pkg-config-pcre';
private const PATH_WITHOUT_PKG_CONFIG = __DIR__;

/** @var array<array-key, mixed> */
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,
);
}
}
44 changes: 43 additions & 1 deletion test/unit/Installing/Ini/OndrejPhpenmodTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
}
Expand Down
Loading
Loading