From 109ccdcc75c91383d78d3571f46409f942f44c3a Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 9 Apr 2026 12:42:05 +0100 Subject: [PATCH 1/2] 541: replace some common placeholders before build --- composer.json | 1 + composer.lock | 145 +++++++++++++++++- src/Building/PlaceholderReplacer.php | 110 +++++++++++++ .../InstallAndBuildProcess.php | 8 + .../unit/Building/PlaceholderReplacerTest.php | 95 ++++++++++++ .../InstallAndBuildProcessTest.php | 2 + 6 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 src/Building/PlaceholderReplacer.php create mode 100644 test/unit/Building/PlaceholderReplacerTest.php diff --git a/composer.json b/composer.json index 34d485be..cf7de69b 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "symfony/console": "^6.4.34", "symfony/event-dispatcher": "^6.4.32", "symfony/process": "^6.4.33", + "thecodingmachine/safe": "^3.4", "thephpf/attestation": "^0.0.5", "webmozart/assert": "^1.12.1" }, diff --git a/composer.lock b/composer.lock index 10f7885a..651acc5b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "48a2962a573d719c0aed82d08736db8f", + "content-hash": "0d6aad05d5b57cff34ea80b579bcbd96", "packages": [ { "name": "composer/ca-bundle", @@ -2508,6 +2508,149 @@ ], "time": "2026-02-08T20:44:54+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, { "name": "thephpf/attestation", "version": "0.0.5", diff --git a/src/Building/PlaceholderReplacer.php b/src/Building/PlaceholderReplacer.php new file mode 100644 index 00000000..3383aa61 --- /dev/null +++ b/src/Building/PlaceholderReplacer.php @@ -0,0 +1,110 @@ + + * + * This would perform a replacement on the given file. I [searched through GitHub](https://github.com/search?q=%22tasks%3Areplace%22+language%3AXML&type=code&p=1&l=XML) + * to find some common replacements to make, and defined them in a mostly hard-coded way, or at least the ones that + * make sense or low-hanging fruit (all underscores can also use hyphens): + * + * - @name@ or @package_name@ - replaces with the extension name (NOT the Composer package name) + * - @version@ or @package_version@ - replaces with the Composer "pretty" version + * - @release_date@ - release date according to Composer package, formatted 'Y-m-d\TH:i:sP' + * - @php_bin@ - path to the PHP binary + * + * Some omitted replacements are `@php_dir@` (seems to actually point to PEAR directory, which is redundant anyway), + * `@bin_dir@` (could get this with PHP_BINDIR constant from the $targetPlatform, but not sure if is worth the time), + * and a few other variations (@phd_ide_version@ for example, which could be replaced with @package_version@). + * + * If this feature needs more flexibility in future, we can look into it, but this implementation seems to be a fairly + * easy win for some basic replacements anyway... + */ +class PlaceholderReplacer +{ + private const FILE_EXTENSIONS = ['c', 'h']; + + public function replacePlaceholdersWithPlaceholderReplacements(IOInterface $io, TargetPlatform $targetPlatform, DownloadedPackage $downloadedPackage): void + { + $replacements = [ + 'ext-name' => $downloadedPackage->package->extensionName()->name(), + 'version' => $downloadedPackage->package->composerPackage()->getPrettyVersion(), + 'release-date' => (string) $downloadedPackage->package->composerPackage()->getReleaseDate()?->format(DateTimeInterface::ATOM), + 'php-bin' => $targetPlatform->phpBinaryPath->phpBinaryPath, + ]; + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($downloadedPackage->extractedSourcePath)) as $file) { + assert($file instanceof SplFileInfo); + + if (! $file->isFile() || ! in_array($file->getExtension(), self::FILE_EXTENSIONS)) { + continue; + } + + $io->write('Replacing placeholders in: ' . $file->getPathname(), verbosity: IOInterface::VERY_VERBOSE); + try { + $this->replaceReplacementsInFile($replacements, $file->getPathname()); + } catch (FilesystemException $e) { + $io->write('Placeholder replacement failed in : ' . $file->getPathname(), verbosity: IOInterface::VERBOSE); + $io->write((string) $e, verbosity: IOInterface::VERBOSE); + } + } + } + + /** + * @param array{ext-name: string, version: string, release-date: string, php-bin: string} $replacements + * + * @throws FilesystemException + */ + private function replaceReplacementsInFile(array $replacements, string $filename): void + { + file_put_contents( + $filename, + str_ireplace( + [ + '@name@', + '@package_name@', + '@package-name@', + '@package_version@', + '@package-version@', + '@release_date@', + '@release-date@', + '@php_bin@', + '@php-bin@', + ], + [ + $replacements['ext-name'], + $replacements['ext-name'], + $replacements['ext-name'], + $replacements['version'], + $replacements['version'], + $replacements['release-date'], + $replacements['release-date'], + $replacements['php-bin'], + $replacements['php-bin'], + ], + file_get_contents($filename), + ), + ); + } +} diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php index 2d11ab84..69985a78 100644 --- a/src/ComposerIntegration/InstallAndBuildProcess.php +++ b/src/ComposerIntegration/InstallAndBuildProcess.php @@ -7,6 +7,7 @@ use Composer\Package\CompletePackageInterface; use Composer\PartialComposer; use Php\Pie\Building\Build; +use Php\Pie\Building\PlaceholderReplacer; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\Installing\Install; @@ -20,6 +21,7 @@ public function __construct( private readonly Build $pieBuild, private readonly Install $pieInstall, private readonly InstalledJsonMetadata $installedJsonMetadata, + private readonly PlaceholderReplacer $placeholderReplacer, ) { } @@ -42,6 +44,12 @@ public function __invoke( $downloadedPackage->extractedSourcePath, )); + $this->placeholderReplacer->replacePlaceholdersWithPlaceholderReplacements( + $io, + $composerRequest->targetPlatform, + $downloadedPackage, + ); + $this->installedJsonMetadata->addDownloadMetadata( $composer, $composerRequest, diff --git a/test/unit/Building/PlaceholderReplacerTest.php b/test/unit/Building/PlaceholderReplacerTest.php new file mode 100644 index 00000000..7be851e1 --- /dev/null +++ b/test/unit/Building/PlaceholderReplacerTest.php @@ -0,0 +1,95 @@ +setReleaseDate($releaseDate); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( + Package::fromComposerCompletePackage($composerPackage), + $testPath, + ); + + $mockPhpBinary = $this->createMock(PhpBinaryPath::class); + /** @phpstan-ignore property.notFound */ + (fn () => $this->phpBinaryPath = '/path/to/php') + ->bindTo($mockPhpBinary, PhpBinaryPath::class)(); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $mockPhpBinary, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + null, + ); + + $replacer = new PlaceholderReplacer(); + $replacer->replacePlaceholdersWithPlaceholderReplacements( + $this->createMock(IOInterface::class), + $targetPlatform, + $downloadedPackage, + ); + + self::assertSame(self::ORIGINAL_CONTENT, file_get_contents($testPath . DIRECTORY_SEPARATOR . 'hello.rs')); + self::assertSame(self::EXPECTED_CONTENT, file_get_contents($testPath . DIRECTORY_SEPARATOR . 'hello.c')); + self::assertSame(self::EXPECTED_CONTENT, file_get_contents($testPath . DIRECTORY_SEPARATOR . 'hello.h')); + + (new Filesystem())->remove($testPath); + } +} diff --git a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php index ec5c1b6c..6318ba6e 100644 --- a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php +++ b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php @@ -8,6 +8,7 @@ use Composer\Package\CompletePackage; use Composer\PartialComposer; use Php\Pie\Building\Build; +use Php\Pie\Building\PlaceholderReplacer; use Php\Pie\ComposerIntegration\InstallAndBuildProcess; use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\ComposerIntegration\PieComposerRequest; @@ -46,6 +47,7 @@ public function setUp(): void $this->pieBuild, $this->pieInstall, $this->installedJsonMetadata, + $this->createMock(PlaceholderReplacer::class), ); } From c37d0ac0d20e0a84b939af6d52fc496447b327a0 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 14 Apr 2026 07:37:45 +0100 Subject: [PATCH 2/2] 541: add docs for placeholder replacements --- docs/extension-maintainers.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 40e8b4c3..e9f4e94f 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -579,3 +579,18 @@ jobs: ``` Source: [https://github.com/php/php-windows-builder?tab=readme-ov-file#examples](https://github.com/php/php-windows-builder?tab=readme-ov-file#examples) + +## Other features + +### Placeholder Replacement + +To help backwards compatibility with PECL extensions, PIE supports some automatic placeholder replacements within +all `.c` and .`h` files found within the downloaded source directory. These placeholders are replaced after the +download step, and before the build step. PIE will automatically replace the following placeholders: + +| Placeholder | Description | Example | +|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| `@name@`, `@package_name@`, `@package-name@` | The short, internal name of the PHP extension (e.g., `xdebug`). _Note: this is not the Packagist package name (e.g. `xdebug/xdebug`)_. | `xdebug` | +| `@version@`, `@package_version@`, `@package-version@` | The "pretty" version of the package defined in `composer.json`. | `3.3.2` | +| `@release_date@`, `@release-date@` | The formatted release date according to Composer package metadata. | `2024-01-15T10:00:00+00:00` | +| `@php_bin@`, `@php-bin@` | The full path to the PHP binary executable used during the build. | `/usr/bin/php8.4` |