From c1662257fd2a1d15d1923779cdd817dc9f6c2b4c Mon Sep 17 00:00:00 2001 From: Andriy Kravchenko Date: Wed, 21 Oct 2026 12:49:17 -0700 Subject: [PATCH] Enable bundled GD with JPEG and FreeType configure options --- .github/workflows/continuous-integration.yml | 6 ++ .../BundledPhpExtensionsRepository.php | 22 ++++++- test/assets/check-bundled-gd.php | 63 +++++++++++++++++++ test/install-bundled-gd.php | 41 ++++++++++++ .../BundledPhpExtensionsRepositoryTest.php | 35 +++++++++++ 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 test/assets/check-bundled-gd.php create mode 100644 test/install-bundled-gd.php diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b68467b9..b60d77e8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -141,10 +141,12 @@ jobs: libexif-dev \ libgd-dev \ libpng-dev \ + libjpeg-dev \ libtiff-dev \ libfreetype-dev \ libfreetype6 \ libfontconfig1-dev \ + fonts-dejavu-core \ libgmp-dev \ libssl-dev \ libsodium-dev \ @@ -199,6 +201,10 @@ jobs: run: sudo php test/install-bundled-php-exts.php php-config env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Test bundled GD image formats and FreeType rendering + run: sudo php test/install-bundled-gd.php php-config /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} end-to-end-tests: runs-on: ubuntu-latest diff --git a/src/ComposerIntegration/BundledPhpExtensionsRepository.php b/src/ComposerIntegration/BundledPhpExtensionsRepository.php index f285b90a..72eb88a6 100644 --- a/src/ComposerIntegration/BundledPhpExtensionsRepository.php +++ b/src/ComposerIntegration/BundledPhpExtensionsRepository.php @@ -38,6 +38,7 @@ class BundledPhpExtensionsRepository extends ArrayRepository * os-families?: non-empty-list, * type?: ExtensionType, * priority?: int, + * configure-options?: list, * }> */ private static array $bundledPhpExtensions = [ @@ -69,7 +70,22 @@ class BundledPhpExtensionsRepository extends ArrayRepository 'lib-ffi' => '*', ], ], - // ['name' => 'gd'], // build failure - ext/gd/gd.c:79:11: fatal error: ft2build.h: No such file or directory + [ + 'name' => 'gd', + 'require' => ['php' => '>= 7.4.0'], + 'configure-options' => [ + [ + 'name' => 'with-jpeg', + 'description' => 'Enable JPEG support in bundled GD', + 'needs-value' => false, + ], + [ + 'name' => 'with-freetype', + 'description' => 'Enable FreeType support in bundled GD', + 'needs-value' => false, + ], + ], + ], ['name' => 'gettext'], ['name' => 'gmp'], ['name' => 'iconv'], @@ -259,6 +275,10 @@ static function (string $target, string $constraint) use ($extension, $versionPa $phpExt['priority'] = $extension['priority']; } + if (array_key_exists('configure-options', $extension)) { + $phpExt['configure-options'] = $extension['configure-options']; + } + $package->setPhpExt($phpExt); return $package; diff --git a/test/assets/check-bundled-gd.php b/test/assets/check-bundled-gd.php new file mode 100644 index 00000000..dac1f4fc --- /dev/null +++ b/test/assets/check-bundled-gd.php @@ -0,0 +1,63 @@ + true, 'JPEG Support' => $configured, 'FreeType Support' => $configured] as $feature => $expected) { + if ($info[$feature] !== $expected) { + throw new RuntimeException('Unexpected GD capability: ' . $feature); + } +} + +$image = imagecreatetruecolor(80, 40); +if ($image === false) { + throw new RuntimeException('Could not create an image'); +} + +$background = imagecolorallocate($image, 20, 40, 60); +imagefill($image, 0, 0, $background); + +foreach ($configured ? ['png', 'jpeg'] : ['png'] as $format) { + ob_start(); + $written = $format === 'png' ? imagepng($image) : imagejpeg($image); + $encoded = ob_get_clean(); + if (! $written || $encoded === false) { + throw new RuntimeException('Could not encode ' . $format); + } + + $decoded = imagecreatefromstring($encoded); + if ($decoded === false || imagesx($decoded) !== 80 || imagesy($decoded) !== 40) { + throw new RuntimeException('Could not round-trip ' . $format); + } + + if ($format === 'png' && imagecolorat($decoded, 0, 0) !== $background) { + throw new RuntimeException('PNG round-trip changed the pixel colour'); + } +} + +if ($configured) { + $font = $argv[2] ?? ''; + if (! is_file($font)) { + throw new RuntimeException('Missing TrueType test font: ' . $font); + } + + $foreground = imagecolorallocate($image, 255, 255, 255); + if (imagettftext($image, 16, 0, 5, 25, $foreground, $font, 'GD') === false) { + throw new RuntimeException('FreeType could not render text'); + } + + $rendered = false; + for ($x = 0; $x < 80; $x++) { + for ($y = 0; $y < 40; $y++) { + $rendered = $rendered || imagecolorat($image, $x, $y) !== $background; + } + } + + if (! $rendered) { + throw new RuntimeException('FreeType did not draw any pixels'); + } +} + +echo 'GD ' . ($configured ? 'JPEG/FreeType' : 'default') . ' checks passed.' . PHP_EOL; diff --git a/test/install-bundled-gd.php b/test/install-bundled-gd.php new file mode 100644 index 00000000..bda635ac --- /dev/null +++ b/test/install-bundled-gd.php @@ -0,0 +1,41 @@ + ' . PHP_EOL; + exit(1); +} + +$phpBinaryPath = PhpBinaryPath::fromPhpConfigExecutable($phpConfigPath); + +foreach (['default' => [], 'configured' => ['--with-jpeg', '--with-freetype']] as $variant => $options) { + echo Process::run([ + $phpBinaryPath->phpBinaryPath, + __DIR__ . '/../bin/pie', + 'install', + 'php/gd', + '--force', + '--no-interaction', + '--with-php-config=' . $phpBinaryPath->phpConfigPath(), + '-v', + '-j2', + ...$options, + ], timeout: null) . PHP_EOL; + + // Start a new PHP process so it loads the extension just installed by PIE. + echo Process::run([ + $phpBinaryPath->phpBinaryPath, + __DIR__ . '/assets/check-bundled-gd.php', + $variant, + $fontPath, + ]) . PHP_EOL; +} diff --git a/test/unit/ComposerIntegration/BundledPhpExtensionsRepositoryTest.php b/test/unit/ComposerIntegration/BundledPhpExtensionsRepositoryTest.php index e66b926f..6d62fef8 100644 --- a/test/unit/ComposerIntegration/BundledPhpExtensionsRepositoryTest.php +++ b/test/unit/ComposerIntegration/BundledPhpExtensionsRepositoryTest.php @@ -6,6 +6,7 @@ use Composer\Package\CompletePackageInterface; use Php\Pie\ComposerIntegration\BundledPhpExtensionsRepository; +use Php\Pie\ConfigureOption; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; @@ -22,6 +23,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Process\Exception\ProcessFailedException; +use function array_map; use function Safe\mkdir; use function Safe\realpath; use function sys_get_temp_dir; @@ -46,6 +48,7 @@ public static function bundledRepositoryPackageNames(): array ['php/enchant'], ['php/exif'], ['php/ffi'], + ['php/gd'], ['php/gettext'], ['php/gmp'], ['php/iconv'], @@ -111,6 +114,38 @@ public function testBundledRepository(string $packageName): void self::assertSame('8.1.0', $package->getPrettyVersion()); } + public function testGdExposesOptionalJpegAndFreeTypeFlags(): void + { + $phpBinary = $this->createMock(PhpBinaryPath::class); + $phpBinary->method('version')->willReturn('8.1.0'); + + $repository = BundledPhpExtensionsRepository::forTargetPlatform( + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinary, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + null, + ), + ); + + $composerPackage = $repository->findPackage('php/gd', '8.1.0'); + self::assertInstanceOf(CompletePackageInterface::class, $composerPackage); + + $package = Package::fromComposerCompletePackage($composerPackage); + + self::assertSame( + [['with-jpeg', false], ['with-freetype', false]], + array_map( + static fn (ConfigureOption $option): array => [$option->name, $option->needsValue], + $package->configureOptions(), + ), + ); + } + public function testMakeCommandForXmlReader(): void { $phpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_test_bundled_', true);