STF-1307: Add bundled libmaxminddb build mode - #265
Conversation
|
@coderabbitai review Generated by Claude Code |
Undoes the two TEMPORARY commits, so the gitlink is back at 2194f58 (v1.13.1) and this branch is mergeable again. .gitmodules was never touched and still tracks main. Their purpose is served. The bundled lanes had never executed anywhere -- the submodule pointed at a tag with no ext/libmaxminddb, so setup resolved binaries=false, Linux and macOS skipped, and Windows took the fetched-library fallback -- which meant the bundled path's first real run would have been a live release, since dev-bin/release.sh moves this pointer as part of cutting one. Moving it by hand instead ran that path on a pull request, and it found a real defect on Windows: ext/maxminddb.c and libmaxminddb/src/maxminddb.c both resolved to $(BUILD_DIR)\maxminddb.obj under phpize, so all four legs linked a DLL with no get_module() and PHP refused to load it. Fixed in maxmind/MaxMind-DB-Reader-php#265, not here. With that fix in place every lane was green: Linux x86_64 and arm64 (nts and zts) at a measured glibc floor of 2.33 against a 2.36 ceiling, macOS arm64 at minos 11.0, Windows x64 and x86 (nts and ts) building the vendored sources with no library fetched, source-build building from the tarball's own libmaxminddb, and verify-assets finding the expected 11 assets. Recorded in the pull request body, since the commit that proved it is not part of this branch. Once #265 lands and a tag carries ext/libmaxminddb, this pointer moves to that tag through release.sh and the same lanes run for real. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Dq18Ws7Bpj9H1eq4sicaz
|
Claude here, on behalf of Greg. Pushed 03ebcc4, which moves the inline self-containment checks in The two copies had already drifted, in the direction of the weaker one. This workflow treated an unmeasurable glibc floor as an error; the extension repo's copy wrapped the whole measuring pipeline in
Follow-up needed in maxmind/MaxMind-DB-Reader-php-ext#2: it should delete its own |
|
Claude here, on behalf of Greg. Pushed three fixups against 03ebcc4 addressing C1, C2, I5 and I6 from review. C1 is a fair hit on code I wrote — worth stating plainly, because the earlier review of the extension repo flagged this exact pattern and I then fixed only one of the four checks while the commit message claimed the gate was sound. C1 + C2 — The NEEDED The One thing worth recording: my first attempt at the RUNPATH pattern used I5 — I6 — Not addressed, and all outside the gate: C3 ( |
|
Claude here, on behalf of Greg. Worked through the rest of the review — 11 more commits, covering C3 and I1–I8 plus the suggestions I judged worth taking. C3 I3 + I4 I7 I2 I8 I5/I6 gate coverage Suggestions taken: Deliberately not done. Dropping I could not build the extension here, so config.m4 is verified by review and by CI, not locally. The shell and the version lint were both run. If |
434bb7c to
87cfd09
Compare
8dc6b64 to
27ba586
Compare
Both paths in 003-open-basedir.phpt are POSIX absolute paths and the test has no --SKIPIF--, so on Windows it fails for reasons unrelated to what it tests. That is what aborted the Windows run in maxmind/MaxMind-DB-Reader-php-ext#2 before the self-containment gate step was reached, leaving that repository's gate unexercised on the platform it was added for. The condition is deliberately the platform alone. 001 and 002 both skip when the extension is not loaded, which makes this the one test that *fails* rather than skips when a build produces an extension that cannot load -- the property that made run-tests.php notice the Windows object-collision bug. An extension_loaded() guard here would remove it, so the comment says so. PHP_OS_FAMILY is available from PHP 7.2, this extension's floor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fe126e5 to
16d43ab
Compare
Refuses to ship an extension object that is not self-contained. Every check exists because the failure it catches would otherwise reach users as a binary that loads on the build machine and nowhere else. On Linux: no libmaxminddb in DT_NEEDED, nothing outside the C runtime and the loader in DT_NEEDED at all, no RUNPATH or RPATH, no undefined MMDB_ symbols, no *exported* MMDB_ symbols, get_module still exported, no GLIBC_PRIVATE, and a measured glibc floor within a documented maximum. On macOS the same shape against otool and Mach-O, with the deployment target standing in for the glibc floor and every slice of a universal object measured rather than just the first. Two rules shape the implementation. Every tool runs in its own assignment and the greps read captured output. `set -e` is suspended inside an `if` condition, so `if some-tool "$so" | grep ...; then fail; fi` cannot distinguish "the tool found nothing" from "the tool is not installed" -- both are a non-zero pipeline and both skip the fail. A gate whose checks pass when their tools are missing is worse than no gate, because it reports success. An unmeasurable result is a failure, not a pass. An empty DT_NEEDED list, an unreadable glibc floor, an unreadable LC_BUILD_VERSION minos and an empty operand to at_most are all fatal, because each means the measurement did not happen rather than that it came back clean. The limit comes from the caller. MAX_GLIBC and MACOSX_DEPLOYMENT_TARGET are read from the environment and `set -u` turns a caller that forgets them into a failure rather than a skip. maxmind/MaxMind-DB-Reader-php-ext runs this same script over the objects it publishes, holding itself to a lower glibc floor because it builds in a digest-pinned bookworm container; a single implementation is the only way that bar cannot drift between the two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
16d43ab to
0938185
Compare
`--with-maxminddb-bundled` vendors libmaxminddb as a submodule at ext/libmaxminddb and compiles it into the extension, so the resulting object needs nothing but libc. That is a prerequisite for distributing precompiled builds: an object linking a system libmaxminddb is not much use as a prebuilt, because the user still has to install the library. The default is unchanged -- without the flag the extension links a system libmaxminddb exactly as before. libmaxminddb's maxminddb.h includes "maxminddb_config.h" unconditionally, a header its own build system generates and which we never run. ext/bundled-include supplies it as a tracked file and both build systems pass the values it would have defined on the command line, which keeps those authoritative even where a generated header is also present. It is tracked rather than written at configure time because generating it meant writing inside the submodule, which left it dirty after every build. config.w32 reads the libmaxminddb version out of the submodule's configure.ac rather than repeating it: nothing else checks that file, since there is no Windows workflow here. config.m4 keeps a literal, which test-bundled.yml asserts against configure.ac at runtime on every build. Three macro choices are load-bearing and documented where they are made. HAVE_CONFIG_H=0 works only because libmaxminddb tests its value with `#if` while our own maxminddb.c tests definedness with `#ifdef`; -UHAVE_CONFIG_H has to precede it because PHP's CPPFLAGS already define it. MMDB_UINT128_IS_BYTE_ARRAY=1 is set identically on both platforms because it decides the layout of the MMDB_entry_data_s union that maxminddb.c and the vendored sources pass between each other. MMDB_LITTLE_ENDIAN is deliberately left undefined when AC_C_BIGENDIAN cannot determine the answer or the build is universal, so maxminddb.h derives it from __BYTE_ORDER__ per architecture; a wrong value here compiles, links, loads, and returns garbage for every float and double while strings and integers stay correct. -fvisibility=hidden keeps the vendored MMDB_* API out of the object's export table. Vendoring turns those from someone else's exports into ours, and PHP dlopens extensions with RTLD_GLOBAL on common builds, so a process that also loads something linked against a system libmaxminddb could bind across the two. Windows needs no equivalent, MSVC exporting nothing unmarked. On Windows, ADD_SOURCES needs an explicit object directory: under MODE_PHPIZE confutils.js derives it from each entry's dirname, and our entries are bare filenames, so libmaxminddb/src/maxminddb.c and ext/maxminddb.c both resolved to maxminddb.obj. Whichever compiled last won, and when libmaxminddb's did the DLL linked without get_module and PHP rejected it as "Invalid library". test-bundled.yml builds the whole matrix, 9 PHP versions on two architectures, in containers with no system libmaxminddb and no PKG_CONFIG_PATH. It runs the phpt suite, the PHPUnit suite against the built object, the shared gate, and a standalone load of the object with no ini file. The PHPUnit suite matters most: MMDB_UINT128_IS_BYTE_ARRAY=1 is the path every bundled build takes and no other job in this repository compiles it, so ReaderTest's assertion of the exact string returned for 2^120 is the only thing standing between a swapped high/low word and a plausible-looking wrong answer. Its float and double cases are likewise the only exercise MMDB_LITTLE_ENDIAN gets. Every check asserts its own preconditions: a missing pkg-config, a `make test` that ran no tests, and an unreadable version are all failures rather than quiet passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The schedule and cooldown match the existing entries. ext/libmaxminddb is excluded. It is pinned to a release tag and compiled into the extension, and the gitsubmodule ecosystem advances a submodule to the tip of its tracked branch, which would take us off release tags. Bumping it is a deliberate action: the version has to be updated in ext/config.m4 and ext/config.w32 at the same time, which dev-bin/check-libmaxminddb-version.sh asserts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0938185 to
b10e47c
Compare
horgh
left a comment
There was a problem hiding this comment.
I thought it looked fine, but Claude had a word or two.
| dnl loads, so only a version assertion catches it. 7.4 and later are | ||
| dnl fine, which is what makes the breakage easy to miss. Do not move | ||
| dnl them while 7.2 and 7.3 are supported. | ||
| CFLAGS="$CFLAGS -fvisibility=hidden -UHAVE_CONFIG_H -DHAVE_CONFIG_H=0 -DMMDB_UINT128_USING_MODE=0 -DMMDB_UINT128_IS_BYTE_ARRAY=1 -DPACKAGE_VERSION='\"1.13.3\"'" |
There was a problem hiding this comment.
Critical: -DPACKAGE_VERSION on the global CFLAGS redefines PHP's own macro on 7.2/7.3, and --enable-maxminddb-debug turns that into a build failure.
PACKAGE_VERSION reaches ext/maxminddb.c too, which includes php.h → main/php_config.h. PHP only started stripping PACKAGE_* from its generated headers in 7.4 (PHP_PATCH_CONFIG_HEADERS), so 7.2 and 7.3 redefine it. This is not hypothetical — it is in this branch's own green CI (run 30946532655):
/usr/include/php/20170718/main/../main/php_config.h:2259: warning: "PACKAGE_VERSION" redefined
/usr/include/php/20180731/main/../main/php_config.h:2278: warning: "PACKAGE_VERSION" redefined
Today only a warning, but line 107 appends -Werror under --enable-maxminddb-debug, and bundled-debug pins 8.4 — so --with-maxminddb-bundled --enable-maxminddb-debug on 7.2/7.3 fails to compile and nothing in CI covers that combination.
Suggestion: put the version define in ext/bundled-include/maxminddb_config.h behind #ifndef PACKAGE_VERSION. That header is already included unconditionally by maxminddb.h, so it reaches the bundled sources (which include no PHP headers) while the guard makes it a no-op for our maxminddb.c — and it dissolves the PHP 7.2/7.3 extra-cflags problem that lines 70-77 exist to work around. Failing that, add a 7.2 or 7.3 leg to bundled-debug so the breakage is at least visible.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Fixed in 972858c, as suggested. The define moves to ext/bundled-include/maxminddb_config.h behind #ifndef PACKAGE_VERSION; ext/maxminddb.c includes php.h before maxminddb.h, so the guard skips for our TU, and that file reads MMDB_lib_version() rather than the macro anyway.
Before/after, redefinition warnings per job: 7.2 1 → 0, 7.3 1 → 0, 8.4 0 → 0, with MMDB_LIB_VERSION: 1.13.3 still reported on 7.2 so the header really is supplying it. Also checked directly: a TU defining PACKAGE_VERSION before including maxminddb.h fails under -Werror with the old -D and compiles clean now.
🤖 Claude (Claude Code) on behalf of Greg.
| run: composer install --no-progress --prefer-dist --optimize-autoloader | ||
|
|
||
| - name: Test with phpunit using the bundled extension | ||
| run: php -d extension=ext/modules/maxminddb.so vendor/bin/phpunit |
There was a problem hiding this comment.
Critical: this step passes green when the extension does not load.
Nothing asserts the object was loaded. A failed extension= load is a warning, not fatal, and PHP continues. phpunit.xml.dist bootstraps vendor/autoload.php, whose PSR-4 map resolves MaxMind\Db\Reader to the pure-PHP src/MaxMind/Db/Reader.php, and the suite is written to pass either way — tests/MaxMind/Db/Test/ReaderTest.php:162:
'uint128' => \extension_loaded('maxminddb') ? '0x0100...' : '1329227995784915872903807060280344576',So if dlopen fails for any reason (a missing symbol, a path-resolution change — note the sibling step at line 123 deliberately uses an absolute mktemp -d path while this one uses a relative one), PHP prints Unable to load dynamic library, the autoloader supplies the pure-PHP Reader, every test passes, step is green.
That matters more than usual here because the comment at lines 126-142 designates this step as the only place MMDB_UINT128_IS_BYTE_ARRAY=1 and MMDB_LITTLE_ENDIAN are ever exercised — "caught here and nowhere else". If the extension silently isn't loaded, it is caught nowhere.
Suggestion: php -d extension=ext/modules/maxminddb.so -r 'exit(extension_loaded("maxminddb") ? 0 : 1);' before the run, or an absolute path plus a hard-failing bootstrap.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Fixed in 68f2001. Added an extension_loaded("maxminddb") precondition and made the path absolute, matching the sibling step. Also added a gmp precondition — see your :53 comment; you were right that only half the uint128 coverage is gmp-gated, and the comment claimed otherwise.
🤖 Claude (Claude Code) on behalf of Greg.
| if grep -q "Cannot run tests without CLI sapi" "$log"; then | ||
| fail "make test ran no tests: no CLI sapi was found" | ||
| fi | ||
| if ! grep -qE "Tests +(passed|failed)" "$log"; then |
There was a problem hiding this comment.
Critical: this asserts the tests ran, never that they passed.
Tests failed : N is never read, and this grep is satisfied by either word. Combined with the >= 1 floor at line 39, a run where the extension loads, 001 and 002 pass, and 003-open-basedir.phpt fails gives passed=2, prints make test passed 2 tests. and exits 0.
That is reachable precisely on the versions this script exists for: the file header explains the phpize test target does not propagate run-tests.php's status (on 7.2/7.3's scripts/Makefile.frag the last statement in the then-branch is rm tmp-php.ini, so make test exits 0 regardless), and 7.2/7.3 are both in the matrix. And 003 is the file this PR just designated the canary.
Suggestion: also parse Tests failed / Tests warned and require 0, or require passed to equal the reported number of tests.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Fixed in f0690de. Now reads Tests failed and Tests warned and requires both to be zero, with an unreadable count treated as a parse failure. Verified against synthetic summaries: your exact scenario — 2 passed, 1 failed — returned 0 before and fails now.
🤖 Claude (Claude Code) on behalf of Greg.
| refute() { # <description> <command...> | ||
| local what="$1" | ||
| shift | ||
| if "$@" >/dev/null 2>&1; then |
There was a problem hiding this comment.
Critical: refute() cannot distinguish "the gate rejected this" from "the gate never ran".
Output is discarded and only the exit status is inspected, so any non-zero exit counts as a correct rejection — including 126/127. I confirmed this by execution: point $gate at a nonexistent path and all four refutes "pass", the script prints the gate rejected all four. and exits 0.
Concrete false passes: gate-extension.sh loses its +x bit or gets renamed; dirname "${BASH_SOURCE[0]}" resolves wrong; a bash syntax error anywhere in the gate. The premise of this file is that a gate broken into certifying anything would go unnoticed — but a gate broken into always failing sails through all four cases just as quietly.
Suggestion: add a positive control ("$gate" "$so" must succeed) and capture output, grepping for the specific ::error:: string each case is supposed to produce.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Reproduced and fixed in 5826192. With no gate present at all it printed "the gate rejected all four." and exited 0, exactly as you describe.
Three changes: a positive control requiring the gate to accept a known-good object; every case now matches the specific ::error:: string rather than any non-zero exit; and the gate's presence and +x are checked up front. Verified against four broken gates — missing, always-succeeds, always-fails, and rejects-with-the-wrong-message. The old version caught one of the four.
🤖 Claude (Claude Code) on behalf of Greg.
| dnl MMDB_lib_version() returns PACKAGE_VERSION, and the extension | ||
| dnl exposes that as MMDB_LIB_VERSION and through phpinfo(), so it must | ||
| dnl be kept in sync with the version of the libmaxminddb submodule. | ||
| dnl dev-bin/check-libmaxminddb-version.sh asserts that. |
There was a problem hiding this comment.
Important: dev-bin/check-libmaxminddb-version.sh does not exist.
It is not in the tree or anywhere in history; dev-bin/ holds only clang-format-all.sh, gate-extension.sh, release.sh, run-ext-tests.sh, test-gate-extension.sh, verify-extension.php. This comment is the only occurrence of that name in the repository.
The hardcoded 1.13.3 on line 78 is in fact checked — by test-bundled.yml's "Load the extension on its own" step (lines 110-124) scraping configure.ac, plus verify-extension.php — and config.w32:16-17 describes that mechanism correctly. So the two build files give contradictory accounts of the same invariant and only one is real. Point this at test-bundled.yml.
Worth noting the asymmetry while you are here: config.w32 reads the version out of the submodule precisely so it cannot drift, while config.m4 keeps a literal whose only guard is CI — so a local bump produces a silently wrong MMDB_LIB_VERSION and phpinfo() with no local signal.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Fixed in 972858c — the reference is gone along with the flag it described. You are right that I deleted that script and left the comment pointing at it.
On the asymmetry: config.m4 now carries no version literal at all. It lives in ext/bundled-include/maxminddb_config.h (one place, guarded), config.w32 still derives from configure.ac, and test-bundled.yml's load step compares MMDB_LIB_VERSION against configure.ac on every build.
🤖 Claude (Claude Code) on behalf of Greg.
| php -n -d extension="$dir/maxminddb.so" dev-bin/verify-extension.php \ | ||
| tests/data/test-data/GeoIP2-City-Test.mmdb "$version" | ||
|
|
||
| # ext/tests is three .phpt files that decode nothing. Every behavioural |
There was a problem hiding this comment.
Nit: "ext/tests is three .phpt files" (and run-ext-tests.sh:7-8's "two of the three phpt files skip") are accurate today and wrong the moment a fourth lands — and they have to be updated in lockstep, in two files. Phrasing that does not count them would not rot.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Fixed in 68f2001 / f0690de — both now say "the phpt files" without counting them.
🤖 Claude (Claude Code) on behalf of Greg.
| # bookworm container and holds itself to 2.36. The two numbers differ on | ||
| # purpose, which is why dev-bin/gate-extension.sh takes the limit from its | ||
| # caller rather than hard-coding one. | ||
| MAX_GLIBC: '2.38' |
There was a problem hiding this comment.
Nit: worth recording as an accepted flake source rather than a finding — lines 17-21 already concede that measuring the floor against an unpinned shivammathur/node:latest-<arch> makes this an assertion about a moving target. Pinning by digest is what would make the number mean something stable over time; until then a green run does not carry the guarantee the variable's name implies.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Agreed, and declining for this PR. It is real but pre-existing and shared with test.yml:14, so pinning is a two-file change that will show up in the diff of an unrelated PR unless it gets its own. Happy to do it separately.
🤖 Claude (Claude Code) on behalf of Greg.
| #ifndef MAXMINDDB_CONFIG_H | ||
| #define MAXMINDDB_CONFIG_H | ||
|
|
||
| /* Intentionally empty. |
There was a problem hiding this comment.
Suggestion: this rationale is stated three times — here, at ext/config.m4:128-131, and at ext/config.w32:24-29. All three say maxminddb.h includes the header unconditionally and that the submodule root is deliberately off the include path; two of the three also explain the dirty-submodule history. Keep the canonical version here (it is the right home) and have the two build files point at it, so a change does not have to be mirrored in three places.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Fixed in 972858c — the header is now the canonical account and both build files point at it rather than restating it.
🤖 Claude (Claude Code) on behalf of Greg.
| @@ -0,0 +1,17 @@ | |||
| #ifndef MAXMINDDB_CONFIG_H | |||
There was a problem hiding this comment.
Nit: package.xml lists config.m4 and config.w32 but not this file or the submodule. That is consistent with "PECL keeps using the system library" and so is probably deliberate — flagging it only so the omission is a decision on the record rather than an oversight.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Deliberate — PECL keeps using the system library, so the bundled sources and this header are not part of the PECL package. Recording it here as a decision rather than an oversight, which I take to be what you were after.
🤖 Claude (Claude Code) on behalf of Greg.
| Jean-Baptiste Nahan. GitHub #231. | ||
| * Replaced `XtOffsetOf()` with `offsetof()`. The `XtOffsetOf()` alias has | ||
| been removed in PHP 8.6. Pull request by Remi Collet. GitHub #252. | ||
| * The extension can now be built from a bundled copy of libmaxminddb, on |
There was a problem hiding this comment.
Suggestion: README.md:150-163 ("Installing Extension from Source") still documents a bare ./configure with no mention of --with-maxminddb-bundled or the required git submodule update --init.
The user-facing documentation for this feature is currently this one CHANGELOG entry, against ~320 lines of internal commentary in the diff. A few lines in the README would be the highest-leverage documentation in the PR.
🤖 Comment by Claude (Claude Code) on behalf of Will.
There was a problem hiding this comment.
Taken, in f96d52a — and you were right that it was the highest-leverage item here. --with-maxminddb-bundled and the git submodule update --init are documented, and the existing recipe gained the --with-maxminddb it was missing.
🤖 Claude (Claude Code) on behalf of Greg.
|
Claude, please do not make any changes or reply to any comments unless asked. |
1 similar comment
|
Claude, please do not make any changes or reply to any comments unless asked. |
`-DPACKAGE_VERSION` on the global CFLAGS also reaches ext/maxminddb.c, which includes php.h. PHP only began stripping PACKAGE_* from its generated headers in 7.4, so on 7.2 and 7.3 this redefined PHP's own macro. It is a warning today -- visible in this branch's own green CI, php_config.h:2259 on 7.2 and :2278 on 7.3, absent on 8.4 -- but --enable-maxminddb-debug adds -Werror, and the bundled-debug job pins 8.4, so `--with-maxminddb-bundled --enable-maxminddb-debug` on 7.2 or 7.3 fails to compile with nothing in CI covering it. The define moves into ext/bundled-include/maxminddb_config.h behind `#ifndef PACKAGE_VERSION`. That header is what maxminddb.h includes unconditionally, so it reaches libmaxminddb's sources, which include no PHP headers and are where MMDB_lib_version() is compiled. ext/maxminddb.c includes php.h before maxminddb.h, so the guard finds the macro already defined and skips -- and that file never reads PACKAGE_VERSION anyway, it calls MMDB_lib_version(). config.w32 keeps passing its own /D, read out of the submodule's configure.ac, which still wins over the header default via the command line. Verified by compiling a translation unit that defines PACKAGE_VERSION before including maxminddb.h, as php.h does: with the old -D and -Werror it fails with "'PACKAGE_VERSION' redefined"; with the header it compiles clean. A standalone MMDB_lib_version() still returns 1.13.3 with no -D present. This also removes the reason config.m4 could not use PHP_NEW_EXTENSION's extra-cflags argument, and the stale reference to a version-check script that no longer exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The summary grep was satisfied by either "Tests passed" or "Tests failed", and the only numeric assertion was passed >= 1. A run where 001 and 002 pass and 003-open-basedir.phpt fails therefore reported "make test passed 2 tests." and exited 0. That is reachable on exactly the versions this script exists for: the header already explains that phpize's `test` target does not propagate run-tests.php's status on 7.2 and 7.3, and both are in the matrix. 003 is also the file this branch just designated the canary for an extension that builds but cannot load. Now reads the failed and warned counts too and requires both to be zero, with an unreadable count treated as a parse failure rather than a zero. Verified against synthetic summaries: 3/0/0 passes; 2 passed with 1 failed now fails; 0 passed with 3 skipped still fails; 1 warned fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`refute()` inspected only the exit status and discarded the output, so any non-zero exit counted as a correct rejection -- including 126 and 127. With no gate-extension.sh present at all the suite printed "the gate rejected all four." and exited 0. A gate broken into refusing everything passed just as quietly as one working correctly, which is the opposite of the failure the file was written to catch and exactly as invisible. Three changes. A positive control runs first and requires the gate to accept a known-good object, which is what a suite of rejection cases cannot establish on its own. Every refutation now matches the specific `::error::` message the gate should print, so "the gate never ran" and "the gate refused for the wrong reason" are both distinguishable from "the gate refused". And the gate's presence and executability are checked up front rather than inferred. The case list also grows from four to eight. Three of the original four died at argument or file checks before reaching a single grep, so nothing observed the checks that matter: exported MMDB_ symbols -- the assertion that -fvisibility=hidden worked, which is the whole RTLD_GLOBAL argument -- a missing get_module, a RUNPATH, or an unexpected runtime dependency. Those now have throwaway gcc fixtures, including a locally built shared library to depend on so the test needs no development packages. Verified against four broken gates: missing, always-succeeds, always-fails, and rejects-with-the-wrong-message. The old version caught only the second. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The PHPUnit step passed green when the extension did not load. A failed
`extension=` is a warning rather than a fatal, PHP continues,
phpunit.xml.dist bootstraps vendor/autoload.php, and the PSR-4 map resolves
MaxMind\Db\Reader to the pure-PHP implementation -- which the suite is written
to accept, ReaderTest.php:162 selecting its expectation on
extension_loaded('maxminddb'). So a dlopen failure left every assertion
testing src/ instead of the object this job just built, in the one job whose
comment claims MMDB_UINT128_IS_BYTE_ARRAY and MMDB_LITTLE_ENDIAN are "caught
here and nowhere else".
gmp is the same shape one level down: ReaderTest's decimal uint128 cases call
markTestIncomplete when it is missing, which does not fail a run, so the value
assertions vanish silently. Both are now explicit preconditions.
The extension path also becomes absolute, matching the standalone load step
above it; a relative path resolves against the cwd rather than extension_dir
and was one plausible way for the load to start failing unnoticed.
Two comment corrections while here: a five-line block was duplicated verbatim,
and "which this job did not run at all" was contradicted by the steps thirteen
lines below it. Also drops the "system-libmaxminddb" phrasing, since test.yml
builds libmaxminddb from git rather than installing a distro package.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…le slices Two holes in check 4, both of the kind the file's own header says it exists to prevent. The `|| true` was attached to the whole pipeline while the comment claimed it covered only the grep. With pipefail set, a sed, sort or tail failure was swallowed too. The `[ -z "$floor" ]` guard catches a total failure, but a partial one yields a floor lower than the object really requires, which then passes the ceiling check. Demonstrated with a missing sed: the old shape reaches the comparison with an empty floor and exits 0, the new one aborts. Scoping it to the grep keeps the no-match case it was added for. The macOS deployment target had the mirror-image problem. The awk only sees LC_BUILD_VERSION, and the empty-result guard fires only when every slice lacks one -- so a universal object with one slice targeting 10.13 or lower, which emits LC_VERSION_MIN_MACOSX instead, leaves that slice unmeasured while the modern slices keep all_minos non-empty. That is precisely the case the error text below it was written to describe. Now rejected outright. The nine-case gate battery is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…oaded Two vacuous paths. $argc was never checked and $expected never validated, so an empty argument compared against an empty MMDB_LIB_VERSION succeeded and the script exited 0 having proved nothing -- and "" is exactly the value a PACKAGE_VERSION that never reached the compiler produces, which is the failure this assertion exists to catch. The workflow caller guards against an empty version, but the script is shared with maxmind/MaxMind-DB-Reader-php-ext, where the caller is out of sight. It also never checked that the class came from the extension. Safe under the documented `php -n -d extension=...` invocation, since without an autoloader a missing class is fatal, but a caller who drops -n or runs it where composer's autoloader is registered would validate the pure-PHP Reader and pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
config.w32 tested FolderExists on libmaxminddb\src where config.m4 tests for libmaxminddb/src/maxminddb.c. An empty or half-populated leftover src directory -- an interrupted submodule checkout, say -- passes the folder test and then fails opaquely at compile time, which is the failure mode the matching check in config.m4 was added to avoid. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test.yml and clang-addresssanitizer.yml still called bare `NO_INTERACTION=1 make test`, so every hazard dev-bin/run-ext-tests.sh documents applied to them unaltered: the target exiting 0 having run nothing without a CLI sapi, run-tests.php counting SKIPPED as a pass, and the failure counts going unread. Those two jobs are the large majority of the matrix. The point of extracting the script was that one implementation cannot drift. Leaving the inline copies in place was the drift. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ext/config.w32 has never had automated coverage. There is no Windows workflow in this repository, yet the CHANGELOG advertises the bundled build "on both Unix-like systems and Windows", and the parts that are unverified include two with documented failure modes that produce a DLL which builds fine and then cannot load: the ADD_SOURCES object-directory fix and the deliberate omission of HAVE_CONFIG_H. One job: build with php/php-windows-builder in local mode -- no extension-url, so it builds this checkout rather than cloning, which is the point -- then load the DLL and run dev-bin/verify-extension.php against a real database. That covers the load path and, through MMDB_LIB_VERSION, the version config.w32 scrapes out of the submodule. The explicit gate is not redundant with the phpt run, it is the only signal. 001 and 002 skip when the extension is not loaded, and 003 now skips on Windows outright, so an unloadable DLL makes every test skip and run-tests.php exits 0. The sibling repository shipped a DLL with no get_module export in exactly that shape. 003's comment is corrected to say so rather than claiming a property it no longer has on Windows. One job rather than a matrix: config.w32 does not vary by PHP version, arch or thread safety in any way this exercises, and PHP <= 7.4 needs a toolchain windows-latest no longer carries. php/setup-php-sdk was the other candidate and is archived; its toolset mapping also misclassifies the VS2026 that windows-latest now runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The feature's entire user-facing documentation was one CHANGELOG line, against several hundred lines of internal commentary in the build files. README's install-from-source section still showed a bare `./configure` with no mention of --with-maxminddb-bundled and no submodule checkout, which is the step someone following it would fail on first. Also adds the previously-missing --with-maxminddb to the existing recipe and says what that path links against, so the two are distinguishable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate-rejection step in test-bundled.yml restated test-gate-extension.sh's own header and had already fallen out of sync with it in this branch -- the copy said two cases where the script refutes eight. It now points at the script. gate-extension.sh claimed the Linux and Darwin symbol checks "have already drifted once", which is not citable from anything in this branch's history. The structural reason for sharing them stands on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The action's local mode cannot be used for an extension in a subdirectory. It selects itself by finding a config.w32 anywhere in the tree, but Get-ExtensionName then reads "config.w32" relative to the repository root, so ext/config.w32 is detected and the next step fails with "Cannot find path ...\MaxMind-DB-Reader-php\config.w32". Cloning instead also gets the action's recursive submodule init, which local mode skips, and matches the configuration maxmind/MaxMind-DB-Reader-php-ext already builds this config.w32 with. Also drops the leading backslash from extension_loaded(): php-cs-fixer wants native constants qualified and native functions not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude here, on behalf of Greg. Thanks — this was a good catch list, and I have replied to each comment inline. Overview: All 4 Critical and all 11 Important are fixed, in thirteen new commits on top of the reviewed four (no fixups, since the branch has been reviewed). Each Critical was reproduced before being touched. Of the Suggestions and Nits I took 21 and declined 8, each with reasons on the comment itself. The declines are: the Darwin branch (dead here, live in the ext repo's macOS lane, which runs the same file); the unpinned container digest and the Two results worth pulling out of the thread:
The gate self-test now fails against all four broken gates — missing, always-succeeds, always-fails, rejects-with-the-wrong-message. It caught one of the four before. Two things I found while doing the work, both worth knowing: The Windows point was sharper than the review states, and it was self-inflicted. The
All jobs green, including the new Windows one. |
Nine small things, all cases where a comment or a list says something the code does not support. "Every shared object needs at least libc" is false -- a trivial gcc -shared object has no DT_NEEDED at all. The check still fails safe; the premise would have misled whoever debugged an unexpected "the gate proved nothing". The musl entries in the dependency allowlist were unreachable: check 5 requires a measurable GLIBC_x.y floor and treats its absence as fatal, so a musl-linked object can never get that far. Allowlisting one only looked like support. Removed, with a note saying why rather than leaving the reader to work it out. An unset MAX_GLIBC or MACOSX_DEPLOYMENT_TARGET aborted with bash's "unbound variable" instead of this script's own annotation -- fail-closed but invisible in the Actions UI, and inconsistent with the care taken over the argument check. The numbered checks also ran 1, 2, 4a, 4; the symbol checks are now named rather than sub-numbered across the function boundary they moved into, and the rest renumber in order. In config.m4: only AC_C_BIGENDIAN's third argument defaults to an abort, so the claim about "the last two" was over-broad; -UHAVE_CONFIG_H does not come first in the flag string, it precedes -DHAVE_CONFIG_H=0, which is the actual requirement; and the submodule-check comment narrated this PR's own development rather than the failure mode. --with-maxminddb-bundled without --with-maxminddb is also no longer a silent no-op. In config.w32: the version match is captured rather than read back out of RegExp.$1, which is engine-global state shared with every other config.w32 in the same scope; the "these match config.m4 except" clause now enumerates all three divergences instead of one; and the verbatim confutils.js quote, pinned to no version, becomes prose. The nine-case gate battery and the eight-case self-test are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maxmind/MaxMind-DB-Reader-php#265 merged, so main now carries ext/libmaxminddb and the --with-maxminddb-bundled configure flag the Linux and macOS lanes need. Until now the submodule pointed at v1.13.1, which predates that work, so setup's `git ls-tree HEAD ext/libmaxminddb` came back empty, binaries was false, and both lanes skipped on every run. They build here for the first time -- along with gate-extension.sh, stage-asset.sh, the clean-container load test, and collect-assets.sh's BINARIES=true arithmetic, none of which has ever executed. Windows also switches over: it gates on the same output and was building against the fetched system library, and now builds bundled like the others. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maxmind/MaxMind-DB-Reader-php#265 consolidated gate-extension.sh and verify-extension.php there, with headers saying they are shared with this repository and reached through the submodule checkout. This repository was still running its own copies, which had drifted well behind. The local gate was 95 lines against the submodule's 234, and kept both fail-open bugs the consolidation fixed: `objdump` inside a `|| true` that covers the whole pipeline, with an empty floor skipping the ceiling assertion entirely, and an unreadable macOS minos warning rather than failing. It also lacked the exported-MMDB_ and get_module checks, the DT_NEEDED allowlist, the GLIBC_PRIVATE and GLIBC_ABI_DT_RELR handling, argument and file guards, at_most's component padding, and the capture-then-grep discipline that keeps a missing tool from reading as a passing check. On a universal object it measured only the first slice's minos. The shared verify-extension.php takes a second argument, the expected MMDB_LIB_VERSION, so setup now scrapes it from the submodule's configure.ac and publishes it as an output. Every load check compares against it, which catches a PACKAGE_VERSION that never reached the compiler as well as a stale submodule bump -- on Windows too, via a new parameter on gate-extension.ps1. gate-extension.ps1 stays local: it has no counterpart upstream, and its dumpbin /exports get_module check is the direct test for the object-collision failure that once shipped a DLL PHP would not load. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maxmind/MaxMind-DB-Reader-php#265 added ext/libmaxminddb and the --with-maxminddb-bundled configure flag. The extension objects this repository publishes are built with that flag, so that they depend on nothing but the C runtime and a user installing a prebuilt binary needs no libmaxminddb of their own. It also brings the shared dev-bin scripts the release workflow runs: gate-extension.sh and verify-extension.php, whose headers name this repository as the second caller. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
configure
gains--with-maxminddb-bundled, which compiles the newext/libmaxminddbsubmodule (pinned to 1.13.3) into the extension instead of linking a system library. Bothconfig.m4andconfig.w32implement it. The default build is unchanged on either platform: on Unix it still resolves libmaxminddb through pkg-config, so distro packaging keeps working, and on Windows it still takeslibmaxminddb.libormaxminddb.lib. Verified that the default Unix build's compile and link command lines are identical tomain's, and thatconfig.w32`'s non-bundled path is byte-for-byte what it was.PIE's Unix installer copies only
{extension-name}.so, so a prebuilt cannotdepend on a system libmaxminddb. This mirrors what the Python reader does, which
compiles the same two sources on Windows as well and needs no import library
there at all.
A new
test-bundled.ymlbuilds the bundled mode across the same PHP versionmatrix and runners as
test.yml, with no libmaxminddb installed, plus onebuild with
--enable-maxminddb-debugto show the vendored sources survive-Werror. It then gates on the properties a prebuilt needs: no libmaxminddb inldd, noRUNPATH/RPATH, no undefinedMMDB_symbols, and a glibc floor ator below
GLIBC_2.38(what the current sources measure, from__isoc23_strtol). Finally it copies the lone.soto an empty directory anddoes a real lookup there.
Windows
Windows links libmaxminddb 1.5.0, published January 2021 and the only version
PHP publishes; that staleness is what forced the two-name
CHECK_LIBin #263.Bundling puts Windows on the same pinned 1.13.3 as every other platform.
config.w32defines the same macros asconfig.m4, which matters becauseMMDB_UINT128_IS_BYTE_ARRAYdecides the layout of theMMDB_entry_data_sunionthat
maxminddb.cand the vendored sources pass between each other.HAVE_CONFIG_His the one deliberate exception:config.m4defines it only tostop PHP's Unix
CPPFLAGSfrom sending libmaxminddb after aconfig.hof itsown, and the Windows build never defines it, so defining it there would instead
make our own
maxminddb.cinclude aconfig.hthat is not present.config.m4writes the stubmaxminddb_config.hfromconfigure, which neverruns on Windows, so
config.w32writes the same stub itself rather than the filebeing committed. Committing it would mean the Unix path overwriting a tracked
file on every build, and two mechanisms for one file. Both now write identical
text, which is why the
config.m4line is touched.ws2_32.libis asked for explicitly. php-src already has it in$(LIBS), butMMDB_open()callsWSAStartup()and the dependency is ours now, which is alsowhy
sapi/cliandsapi/cginame it themselves.MSVC cannot be run here, so this was desk-checked instead: every
confutils.jsfunction called was read in php-src and confirmed to have thesignature used; the
ADD_SOURCEScall follows the idiom php-src'sext/uriandext/pcreand PECL's xdebug and php-memcached use for sources in asubdirectory;
ADD_SOURCES's object-path arithmetic was replayed to confirm theobjects land in
$(BUILD_DIR)\libmaxminddb\srcwith nomaxminddb.objcollision, in both phpize and in-tree modes, and that
ADD_SOURCEScreates thatdirectory itself (there is no
PHP_ADD_BUILD_DIRto forget); and the flag stringwas replayed through
ADD_FLAG,generate_makefileand MSVC'sargvrules toconfirm
PACKAGE_VERSIONarrives as the string literal"1.13.3"and that theMakefile line ends in no stray backslash. maxmind/MaxMind-DB-Reader-php-ext#2
switches that repository's Windows lane to the flag so the four Windows legs
exercise it, on the same submodule-tree condition its Linux and macOS lanes
already use.
Two things to know:
PACKAGE_VERSIONis passed on the command line in bundled mode, andMMDB_lib_version()returns it, so it has to be bumped whenever thesubmodule is. CI asserts it matches the submodule's
configure.ac, and bothconfig.m4andconfig.w32now carry the literal.config.w32, so the Windows path isexercised only by STF-1287: Build and publish PIE precompiled binaries on tag MaxMind-DB-Reader-php-ext#2, and only once that
repository's submodule points at a commit carrying
ext/libmaxminddb.package.xmlis untouched, as PECL keeps using the system library..github/dependabot.ymlgains agitsubmodulestanza that ignoresext/libmaxminddb. Dependabot moves submodules to the tracked branch's tipwhen no newer tag exists, which would take the vendored copy off release tags.
#264 adds the same stanza without the ignore, so that
tests/datagetsupdates; the two stanzas are otherwise identical, so whichever lands second
just needs the ignore list kept.
Generated by Claude Code