Skip to content

Honor a cache-restored build when a versioned extension mismatches - #1108

Merged
shivammathur merged 2 commits into
shivammathur:developfrom
PaystackHQ:fix-versioned-extension-cache
Aug 19, 2026
Merged

shivammathur merged 2 commits into
shivammathur:developfrom
PaystackHQ:fix-versioned-extension-cache

Conversation

@ibrahimlawal-paystack

Copy link
Copy Markdown
Contributor

Problem

When a PHP package install bundles its own default build of a versioned extension (e.g. redis ships with a recent version pre-enabled), add_pecl_extension correctly detects the version mismatch against the pinned version and falls back to pecl_install - but it does this even when a matching build has already been restored from a CI cache into a separate directory, since shared_extension only checks that some file exists at ext_dir, not which version it is.

In practice this means shivammathur/cache-extensions never actually helps for a pinned-version PECL extension: the cache-restored .so gets silently overwritten by the PHP package's own default build before add_pecl_extension ever runs, so the version check always mismatches and always falls through to a full pecl_install, regardless of caching.

This reproduces the exact symptom reported in #21 and #37 (both currently open) - a cached build of a versioned extension is ignored and recompiled on every run.

Fix

Confirmed the mechanism by instrumenting enable_extension/add_pecl_extension directly and running in CI:

  • enable_extension(redis,...) finds check_extension=yes immediately (a default version, e.g. 6.0.2, is already loaded before this function ever runs)
  • the version comparison correctly detects the mismatch against the pinned version (e.g. 5.3.2)
  • disabling and re-enabling alone doesn't help, since the .so file backing ext_dir/redis.so is still the package's default build, not the cache-restored one
  • copying a cache-restored build into ext_dir before the re-check resolves it: phpversion('redis') then correctly reports the pinned version and the run logs Enabled instead of Installed and enabled

This adds an opt-in CACHED_EXTENSIONS_DIR. When set and a version mismatch is detected, add_pecl_extension looks for $extension.so there and copies it into ext_dir before re-checking, ahead of falling back to pecl_install. When CACHED_EXTENSIONS_DIR is unset, behavior is unchanged - existing workflows aren't affected.

The intended pairing is: restore a cache to a directory outside ext_dir (so the package install can't overwrite it before this check runs), pass that path via CACHED_EXTENSIONS_DIR, and after Setup PHP completes, refresh that directory from the freshly-built (or now-confirmed-cached) .so for the next run's cache save.

Verified end-to-end in our own CI against a real ~4500-test PHP 7.3 suite: Setup PHP dropped from ~50-70s (recompiling redis, rdkafka, apcu from source every run) to ~7s on a cache hit, with all three extensions logging Enabled.

Happy to add a test case or adjust the approach if you'd prefer a different shape for this (e.g. checking the cache directory unconditionally rather than only on mismatch, or wiring cache-extensions itself to populate CACHED_EXTENSIONS_DIR directly).

When a PHP package install bundles its own default build of a versioned
extension (e.g. redis ships with its own recent version pre-enabled),
add_pecl_extension correctly detects the version mismatch against the
pinned version and falls back to pecl_install - but it does this even
when a matching build has already been restored from a CI cache into a
separate directory, since shared_extension only checks that *some* file
exists at ext_dir, not which version it is.

This adds an opt-in CACHED_EXTENSIONS_DIR: when set and a mismatch is
detected, look for extension.so there, and if found, copy it into
ext_dir and re-check before falling back to a full pecl_install. When
CACHED_EXTENSIONS_DIR is unset, behavior is unchanged.

Related to shivammathur/cache-extensions#21 and shivammathur#37, where a cached
build of a versioned PECL extension is silently ignored and
recompiled on every run regardless of caching.
@ibrahimlawal-paystack

Copy link
Copy Markdown
Contributor Author

I don't have write access here so I can't formally request reviewers - tagging the people who've actually touched this file/directory before, in case any of you have a moment: @shivammathur (you wrote add_pecl_extension originally), @alcaeus (you were looking at this exact caching gap in #21), @tillkruss, @GrahamCampbell.

@shivammathur shivammathur left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ibrahimlawal-paystack

Thanks for working on this. Could you please update the PR to avoid introducing CACHED_EXTENSIONS_DIR env?

For an exact version request such as redis-5.3.7, setup-php could do the following:

  • Keep the active binary at its normal path: redis.so on Unix or php_redis.dll on Windows.
  • Preserve a second copy directly in extension_dir as redis-5.3.7, without a .so or .dll suffix.
  • On subsequent runs, when redis-5.3.7 is requested and the preserved file exists, copy it over the active binary, enable the extension, and verify that the loaded version is 5.3.7.
  • If the preserved file is missing or invalid, follow the current flow: check the installed version and install the requested version on mismatch.
  • After installing a versioned extension, preserve the resulting binary for the next run.

cache-extensions already caches extension_dir, so no additional directory or configuration is required. The preserved copy should not have a shared-library suffix, otherwise setup-php may treat it as an active extension while scanning.

… dir

Per @shivammathur's review on the PR: drop the opt-in
CACHED_EXTENSIONS_DIR env var. Instead, after installing a versioned
extension, keep a second copy of the built .so in ext_dir itself under
a name with no .so suffix (e.g. redis-5.3.7), so it can't be mistaken
for the active extension and rides along in the same ext_dir that
cache-extensions already caches wholesale - no separate cache
directory, key, or workflow step required. On a version mismatch,
restore that preserved build over the active binary and re-check
before falling back to a full pecl_install.
@ibrahimlawal-paystack

Copy link
Copy Markdown
Contributor Author

Updated - dropped CACHED_EXTENSIONS_DIR entirely. add_pecl_extension now preserves the built .so under ext_dir/$extension-$version (no suffix, so it isn't picked up as active), and restores that over the active binary on a version mismatch before falling back to pecl_install. Since it lives inside ext_dir, cache-extensions caches it automatically - no extra directory, key, or workflow step needed on the caller's side. Matches the flow you outlined; let me know if I've missed anything.

@shivammathur
shivammathur changed the base branch from main to develop August 19, 2026 07:50
@shivammathur
shivammathur merged commit 33139db into shivammathur:develop Aug 19, 2026
kevinAlbs added a commit to kevinAlbs/mongo-php-library that referenced this pull request Oct 21, 2026
The 2.37.2 pin predates shivammathur/setup-php#1108 ("Honor a
cache-restored build when a versioned extension mismatches"), so the
bundled default mongodb build (reporting 2.5.3) overwrites the
cache-restored build and every job falls through to pecl, whose
fail-open fallback silently enables the wrong version.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants