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
10 changes: 9 additions & 1 deletion .github/workflows/compat-harness.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,8 +42,16 @@ jobs:
steps:
- uses: actions/checkout@v6
- id: load
# Filter fixtures by PHP versions present in bundles.lock so a fixture
# referencing a not-yet-built PHP version doesn't fail fast against an
# unresolvable lockfile entry. Plan-and-build's harness call (after
# update-lock) reads the freshly-committed lockfile and picks up the
# full set; the standalone harness invocation here only runs fixtures
# whose PHP version is already buildable on the current ref.
run: |
matrix=$(yq -o=json '.fixtures' test/compat/fixtures.yaml | jq -c '{include: .}')
versions_json=$(jq -c '[.bundles | keys[] | select(startswith("php:")) | split(":")[1]] | unique' bundles.lock)
matrix=$(yq -o=json '.fixtures' test/compat/fixtures.yaml \
| jq --argjson v "$versions_json" -c '{include: [.[] | select(.["php-version"] as $pv | $v | index($pv))]}')
{
echo "matrix<<EOF"
echo "$matrix"
Expand Down
34 changes: 25 additions & 9 deletions bundles.lock
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,42 @@
{
"schema_version": 2,
"generated_at": "2026-04-21T09:19:23.251904138Z",
"generated_at": "2026-04-21T12:47:58.865851119Z",
"bundles": {
"ext:apcu:5.1.28:8.4:linux:x86_64:nts": {
"digest": "sha256:b59849d63e6069cc0fc3d7e662419de9542a816dd379f666761bfc71c78211ac",
"spec_hash": "sha256:953544a10ba7aa615bf06f36f21617420aba5300dd2c3853b425fdbe14a66c44"
"digest": "sha256:886dec98c6e464e1c8e1a483793053fc43fc4a8341ca5be4f52792458f327ae0",
"spec_hash": "sha256:d2a77d17e7d3f12185be1023373634e8bb4c79aee3f673cbebe4a2ee284c9e6e"
},
"ext:apcu:5.1.28:8.5:linux:x86_64:nts": {
"digest": "sha256:ed41df8e28cf07d32c03be29bf9b61bf82076a0968f8d407bb6d5bbc8e0dd22f",
"spec_hash": "sha256:dbfafcf364380e781417ef9b948308bfa78847a45c76352b92b52ad9d716bf3c"
},
"ext:pcov:1.0.12:8.4:linux:x86_64:nts": {
"digest": "sha256:6740f550dfee77f29b1d01fe234a6eaf45ef970c3fdfd26745c65c208d4fac63",
"spec_hash": "sha256:e34862339407c92e247301b55ccbd06011fac3ff17f4d9b04c7aed6a53a161d7"
"digest": "sha256:55cf1b90782d252ba32c94aedba6a869df45ab9c3c3091b066e92efe73e8a330",
"spec_hash": "sha256:e5ac7dc8c1ca08624b89217c585f1020cefd992fafef7296e96468c6b1f974f4"
},
"ext:pcov:1.0.12:8.5:linux:x86_64:nts": {
"digest": "sha256:cb672fe7d540927dca35208a77753a99f9ea66f6070b6b5461d5477a8e995644",
"spec_hash": "sha256:e2ca096604e859fca9c8565955d52e5405804612f3cfb142f84ddb5dfb0d8ffc"
},
"ext:redis:6.2.0:8.4:linux:x86_64:nts": {
"digest": "sha256:ac33ac604bc08ac7760e7f97eee8c14b6a9826d62a705aa6441e1d480f8b7e9a",
"spec_hash": "sha256:c8309cc6fe56fc125a80126459d48f3da11928661f7eb80bc90bae6eae49c9f4"
"digest": "sha256:7fc363f8e09589af6967eafdda0938554f5fac4483138dd4ae4ebc12b4693695",
"spec_hash": "sha256:b12d7950cd5000a28449c89d48dcc522772ac02e251a6251c7af38c76c9dc212"
},
"ext:xdebug:3.5.1:8.4:linux:x86_64:nts": {
"digest": "sha256:e2199d5b9e1c2435f18a926e35ad8660780ede77b56f38f8cb7707a2aef7e1d3",
"spec_hash": "sha256:976d4afe384f4a83b172aada160332c008a09e7a933e20daecb6e313337eafa7"
"digest": "sha256:2ef5e2f7cfd19504ea7242f1cc997d57487c796b25e4edbff6b63dbdcf7ebee5",
"spec_hash": "sha256:37a2896dffad31bd2780f5736e836396c843be868d3d8bb33177da76fd008fcc"
},
"ext:xdebug:3.5.1:8.5:linux:x86_64:nts": {
"digest": "sha256:9f83c173d863c1b15310ed9ba13159c1d6be5cd038b65fcb0d1de92f45739ba8",
"spec_hash": "sha256:c71f538daddb457e8608f7c2da934f00e60ba585d4673ca2bac52a136a29cea4"
},
"php:8.4:linux:x86_64:nts": {
"digest": "sha256:9b1e47ff354c8a03e71a9223d0c5005d9bc83bd4ba17c2055e7c1ce54ecf108a",
"spec_hash": "sha256:f8fc1e3cb7ff8248b4c88c8eb6a767e845d6ac6166f51033c528a7f9aa272e3a"
},
"php:8.5:linux:x86_64:nts": {
"digest": "sha256:597ee0728e7966de8e97a60a7ead68d15e5b2118995b99f3db6353ff1f3e844a",
"spec_hash": "sha256:e797fcb0759061f587b9c3582ee394bfa366dead66f2479a473abd9a822fa291"
}
}
}
2 changes: 1 addition & 1 deletion catalog/extensions/apcu.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ source:
versions:
- "5.1.28"
abi_matrix:
php: ["8.4"]
php: ["8.4", "8.5"]
os: ["linux"]
arch: ["x86_64"]
ts: ["nts"]
Expand Down
2 changes: 1 addition & 1 deletion catalog/extensions/pcov.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ source:
versions:
- "1.0.12"
abi_matrix:
php: ["8.4"]
php: ["8.4", "8.5"]
os: ["linux"]
arch: ["x86_64"]
ts: ["nts"]
Expand Down
8 changes: 7 additions & 1 deletion catalog/extensions/redis.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,10 +5,16 @@ source:
versions:
- "6.2.0"
abi_matrix:
php: ["8.4"]
php: ["8.4", "8.5"]
os: ["linux"]
arch: ["x86_64"]
ts: ["nts"]
exclude:
# redis 6.2.0 references ext/standard/php_smart_string.h which was removed
# in PHP 8.5; compilation fails. Upstream redis 6.3.0 ships the fix, but a
# version bump is its own single-purpose slice per the version-expansion
# spec non-goals. Until that slice lands, redis is unavailable on 8.5.
- { php: "8.5" }
runtime_deps:
linux: []
ini:
Expand Down
2 changes: 1 addition & 1 deletion catalog/extensions/xdebug.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ source:
versions:
- "3.5.1"
abi_matrix:
php: ["8.4"]
php: ["8.4", "8.5"]
os: ["linux"]
arch: ["x86_64"]
ts: ["nts"]
Expand Down
41 changes: 41 additions & 0 deletions catalog/php.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -235,6 +235,47 @@ versions:
- uri
- opcache
- zlib
sources:
url: "https://www.php.net/distributions/php-{version}.tar.xz"
sig: "https://www.php.net/distributions/php-{version}.tar.xz.asc"
abi_matrix:
os: ["linux"]
arch: ["x86_64"]
ts: ["nts"]
configure_flags:
common: >-
--enable-mbstring
--with-curl
--with-zlib
--with-openssl
--enable-bcmath
--enable-calendar
--enable-exif
--enable-ftp
--enable-intl
--with-zip
--enable-soap
--enable-sockets
--with-pdo-mysql
--with-pdo-sqlite
--with-sqlite3
--with-readline
--with-sodium
--enable-gd
--with-freetype
--with-jpeg
--with-webp
--with-ffi
--with-gettext
--enable-pcntl
--enable-posix
--enable-shmop
--enable-sysvmsg
--enable-sysvsem
--enable-sysvshm
linux: >-
--with-pdo-pgsql
--with-pgsql
smoke:
- "php -v"
- "php -m"
Expand Down
9 changes: 8 additions & 1 deletion cmd/compat-diff/diff.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"sort"
"strings"
)

type probe struct {
Expand DownExpand Up@@ -62,12 +63,18 @@ func joinSorted(xs []string) string {
return string(b)
}

// fixtureMatches returns true if fixtures contains "*" or the exact fixture name.
// fixtureMatches returns true if fixtures contains "*", the exact fixture
// name, or a trailing-wildcard pattern (e.g. "multi-ext*" matches both
// "multi-ext" and "multi-ext-85"). Trailing wildcard is the only glob
// supported; anything else is an exact match.
func fixtureMatches(fixtures []string, fixture string) bool {
for _, f := range fixtures {
if f == "*" || f == fixture {
return true
}
if strings.HasSuffix(f, "*") && strings.HasPrefix(fixture, strings.TrimSuffix(f, "*")) {
return true
}
}
return false
}
Expand Down
23 changes: 23 additions & 0 deletions cmd/compat-diff/diff_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -86,6 +86,29 @@ func TestDiffFixtureGlobFilter(t *testing.T) {
}
}

func TestDiffFixtureTrailingWildcard(t *testing.T) {
a := loadProbe(t, "probe-bare.json")
b := loadProbe(t, "probe-bare-ini-shift.json")
// "bare*" should match both "bare" and "bare-85" — the version-expansion
// allowlist relies on this to avoid listing every version suffix.
al := allowlist{Deviations: []deviation{
{Path: "ini.memory_limit", Kind: "ignore", Reason: "x", Fixtures: []string{"bare*"}},
}}
for _, fixture := range []string{"bare", "bare-85", "bare-81"} {
t.Run(fixture, func(t *testing.T) {
res := diffProbes(a, b, al, fixture)
if len(res) != 0 {
t.Fatalf("expected wildcard to match fixture %q, got %d diffs", fixture, len(res))
}
})
}
// A non-matching fixture must NOT be swallowed by the wildcard.
res := diffProbes(a, b, al, "exclusion")
if len(res) != 1 {
t.Fatalf("expected wildcard to NOT match non-prefixed fixture, got %d diffs", len(res))
}
}

func TestDiffAllowKindBothEmptyEqual(t *testing.T) {
a := loadProbe(t, "probe-bare-ext-empty.json")
b := loadProbe(t, "probe-bare-ext-empty.json")
Expand Down
1 change: 1 addition & 0 deletions cmd/phpup/main.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -75,6 +75,7 @@ func main() {
"redis": {Name: "redis", Kind: catalog.ExtensionKindPECL, Versions: []string{"6.2.0"}},
"xdebug": {Name: "xdebug", Kind: catalog.ExtensionKindPECL, Versions: []string{"3.5.1"}, Ini: []string{"zend_extension=xdebug"}},
"pcov": {Name: "pcov", Kind: catalog.ExtensionKindPECL, Versions: []string{"1.0.12"}, Ini: []string{"extension=pcov"}},
"apcu": {Name: "apcu", Kind: catalog.ExtensionKindPECL, Versions: []string{"5.1.28"}},
},
}

Expand Down
8 changes: 4 additions & 4 deletions docs/compat-matrix.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -516,20 +516,20 @@ deviations:
- path: ini.xdebug.mode
kind: ignore
reason: 'v2 apt xdebug install unreliable on ubuntu-24.04; theirs often unloaded'
fixtures: ['multi-ext']
fixtures: ['multi-ext*']
- path: ini.xdebug.start_with_request
kind: ignore
reason: 'xdebug version-default drift (v2 apt unreliable; ours = xdebug 3.5.1 default)'
fixtures: ['multi-ext']
fixtures: ['multi-ext*']

# disable_functions: Ondrej's php8.4-cli Debian package patches
# disable_functions: Ondrej's php8.*-cli Debian packages patch
# php.ini-development to blacklist all pcntl_* functions (CI hardening).
# Our source-built PHP uses the stock upstream php.ini-development which
# has disable_functions= empty. Replicating Ondrej's patch set is out of
# scope.
- path: ini.disable_functions
kind: ignore
reason: 'Ondrej Debian patch on php.ini-development adds pcntl_* blacklist; stock upstream does not'
fixtures: ['ini-file-development']
fixtures: ['ini-file-development*']
```
<!-- compat-harness:deviations:end -->
7 changes: 7 additions & 0 deletions internal/compose/compose.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,13 @@ func writeExtensionDirIni(layout *Layout) error {
}

func SymlinkExtension(soPath, extensionDir, name string) error {
// PHP 8.5 cores install opcache statically, so the extracted bundle has
// no extensions/ subdirectory. Earlier PHP cores happened to have one
// (shared opcache created it). Create on demand so the compose layer is
// independent of the core's shared-vs-static module choice.
if err := os.MkdirAll(extensionDir, 0o750); err != nil {
return fmt.Errorf("create extension dir: %w", err)
}
link := filepath.Join(extensionDir, name+".so")
if err := os.Remove(link); err != nil && !os.IsNotExist(err) {
return err
Expand Down
28 changes: 28 additions & 0 deletions internal/compose/compose_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,34 @@ func TestSymlinkExtension(t *testing.T) {
}
}

// Regression: PHP 8.5 cores install opcache statically (not shared), so the
// extracted bundle has no usr/local/lib/php/extensions/ directory. Composing
// a PECL extension against such a core must still succeed — SymlinkExtension
// creates the target directory on demand rather than failing on ENOENT.
func TestSymlinkExtensionCreatesMissingTargetDir(t *testing.T) {
dir := t.TempDir()
extDir := filepath.Join(dir, "usr", "local", "lib", "php", "extensions")
// deliberately do NOT mkdir extDir — simulates an 8.5 core bundle

soDir := filepath.Join(dir, "bundle")
os.MkdirAll(soDir, 0o755)
soPath := filepath.Join(soDir, "pcov.so")
os.WriteFile(soPath, []byte("fake so"), 0o644)

if err := SymlinkExtension(soPath, extDir, "pcov"); err != nil {
t.Fatalf("SymlinkExtension() error = %v", err)
}

link := filepath.Join(extDir, "pcov.so")
target, err := os.Readlink(link)
if err != nil {
t.Fatalf("Readlink() error = %v", err)
}
if target != soPath {
t.Errorf("symlink target = %q, want %q", target, soPath)
}
}

func TestWriteIniFragment(t *testing.T) {
dir := t.TempDir()
err := WriteIniFragment(dir, "redis", []string{"extension=redis"})
Expand Down
52 changes: 52 additions & 0 deletions test/compat/fixtures.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,3 +52,55 @@ fixtures:
extensions: 'redis'
ini-values: ''
coverage: 'none'

- name: bare-85
php-version: '8.5'
extensions: ''
ini-values: ''
coverage: 'none'

- name: coverage-pcov-85
php-version: '8.5'
extensions: ''
ini-values: ''
coverage: 'pcov'

- name: exclusion-85
php-version: '8.5'
extensions: ':opcache'
ini-values: ''
coverage: 'none'

- name: ini-and-coverage-85
php-version: '8.5'
extensions: ''
ini-values: 'memory_limit=256M,date.timezone=UTC'
coverage: 'xdebug'

- name: ini-file-development-85
php-version: '8.5'
extensions: ''
ini-values: ''
coverage: 'none'
ini-file: 'development'

# redis is excluded on 8.5 (see catalog/extensions/redis.yaml); these three
# fixtures substitute apcu to cover the same input-shape axes (multi-ext,
# none-reset, single-ext) against an extension that does build on 8.5.
- name: multi-ext-85
php-version: '8.5'
extensions: 'apcu, xdebug'
ini-values: ''
coverage: 'none'

- name: none-reset-85
php-version: '8.5'
extensions: 'none, apcu'
ini-values: ''
coverage: 'none'

- name: single-ext-85
php-version: '8.5'
extensions: 'apcu'
ini-values: ''
coverage: 'none'
Loading