Skip to content

STF-1307: Add bundled libmaxminddb build mode - #265

Merged
horgh merged 17 commits into
mainfrom
stf-1287-bundled-libmaxminddb
Aug 5, 2026
Merged

STF-1307: Add bundled libmaxminddb build mode#265
horgh merged 17 commits into
mainfrom
stf-1287-bundled-libmaxminddb

Conversation

@claude

@claude claude Bot commented Jul 31, 2026

Copy link
Copy Markdown

configuregains--with-maxminddb-bundled, which compiles the new ext/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 to main's, and that config.w32`'s non-bundled path is byte-for-byte what it was.

PIE's Unix installer copies only {extension-name}.so, so a prebuilt cannot
depend 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.yml builds the bundled mode across the same PHP version
matrix and runners as test.yml, with no libmaxminddb installed, plus one
build with --enable-maxminddb-debug to show the vendored sources survive
-Werror. It then gates on the properties a prebuilt needs: no libmaxminddb in
ldd, no RUNPATH/RPATH, no undefined MMDB_ symbols, and a glibc floor at
or below GLIBC_2.38 (what the current sources measure, from
__isoc23_strtol). Finally it copies the lone .so to an empty directory and
does 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_LIB in #263.
Bundling puts Windows on the same pinned 1.13.3 as every other platform.

config.w32 defines the same macros as config.m4, which matters because
MMDB_UINT128_IS_BYTE_ARRAY decides the layout of the MMDB_entry_data_s union
that maxminddb.c and the vendored sources pass between each other.
HAVE_CONFIG_H is the one deliberate exception: config.m4 defines it only to
stop PHP's Unix CPPFLAGS from sending libmaxminddb after a config.h of its
own, and the Windows build never defines it, so defining it there would instead
make our own maxminddb.c include a config.h that is not present.

config.m4 writes the stub maxminddb_config.h from configure, which never
runs on Windows, so config.w32 writes the same stub itself rather than the file
being 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.m4 line is touched.

ws2_32.lib is asked for explicitly. php-src already has it in $(LIBS), but
MMDB_open() calls WSAStartup() and the dependency is ours now, which is also
why sapi/cli and sapi/cgi name it themselves.

MSVC cannot be run here, so this was desk-checked instead: every
confutils.js function called was read in php-src and confirmed to have the
signature used; the ADD_SOURCES call follows the idiom php-src's ext/uri and
ext/pcre and PECL's xdebug and php-memcached use for sources in a
subdirectory; ADD_SOURCES's object-path arithmetic was replayed to confirm the
objects land in $(BUILD_DIR)\libmaxminddb\src with no maxminddb.obj
collision, in both phpize and in-tree modes, and that ADD_SOURCES creates that
directory itself (there is no PHP_ADD_BUILD_DIR to forget); and the flag string
was replayed through ADD_FLAG, generate_makefile and MSVC's argv rules to
confirm PACKAGE_VERSION arrives as the string literal "1.13.3" and that the
Makefile 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_VERSION is passed on the command line in bundled mode, and
    MMDB_lib_version() returns it, so it has to be bumped whenever the
    submodule is. CI asserts it matches the submodule's configure.ac, and both
    config.m4 and config.w32 now carry the literal.
  • Nothing in this repository's CI builds config.w32, so the Windows path is
    exercised 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.xml is untouched, as PECL keeps using the system library.

.github/dependabot.yml gains a gitsubmodule stanza that ignores
ext/libmaxminddb. Dependabot moves submodules to the tracked branch's tip
when no newer tag exists, which would take the vendored copy off release tags.
#264 adds the same stanza without the ignore, so that tests/data gets
updates; the two stanzas are otherwise identical, so whichever lands second
just needs the ignore list kept.


Generated by Claude Code

@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Author

@coderabbitai review


Generated by Claude Code

claude Bot pushed a commit to maxmind/MaxMind-DB-Reader-php-ext that referenced this pull request Aug 3, 2026
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
@oschwald

oschwald commented Aug 3, 2026

Copy link
Copy Markdown
Member

Claude here, on behalf of Greg.

Pushed 03ebcc4, which moves the inline self-containment checks in test-bundled.yml into dev-bin/gate-extension.sh so that this repo and maxmind/MaxMind-DB-Reader-php-ext#2 run one implementation instead of two.

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 || true and then skipped the assertion when the result came back empty, so a missing objdump would have certified the binary rather than failing it. Verified locally against a real shared object: with objdump off $PATH the old shape exits 0, the shared script exits non-zero.

MAX_GLIBC_VERSION is renamed to MAX_GLIBC and the limit stays at the call site — this job builds on the runner and measures 2.38, the extension repo builds in a pinned bookworm container and holds itself to 2.36. Both are right for how they build, so the script takes the number rather than hard-coding one.

file(1) is now non-fatal. Nothing asserts on its output, and it is a separate package from binutils, so it should not be able to fail the gate in a build image that lacks it.

Follow-up needed in maxmind/MaxMind-DB-Reader-php-ext#2: it should delete its own dev-bin/gate-extension.sh and call MaxMind-DB-Reader-php/dev-bin/gate-extension.sh through the submodule instead. That can only land once the submodule points at a commit carrying this script, so it is gated on this PR merging. Its gate-extension.ps1 has no second caller today and I have left it where it is.

@oschwald

oschwald commented Aug 3, 2026

Copy link
Copy Markdown
Member

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 — 8199bd7. set -e is suspended inside an if condition, so if some-tool "$so" | grep …; then fail; fi could not tell "found nothing" from "not installed". Only check 4 ran its tool in an assignment, which is why only check 4 failed closed. Every tool now runs in its own assignment and the greps read captured output. Reproduced before and after, against a real .so with readelf and nm each removed from $PATH: before 0, after 127.

The NEEDED sed also required GNU's parenthesised (NEEDED); llvm-readelf prints it bare, so the extraction yielded nothing and the check passed on an object that did link libmaxminddb. Parens are now optional and an empty result is fatal. Verified with a shimmed readelf emitting llvm-style output naming libmaxminddb.so.0: exit 1, correct error.

The ldd check is dropped rather than converted — it duplicated the NEEDED check less reliably, and as the review notes it comes from libc-bin, not binutils. The "readelf, nm and objdump are genuinely required" comment was wrong and is corrected. Argument and file-existence checks added.

One thing worth recording: my first attempt at the RUNPATH pattern used \\{0,1\\} in a grep -E, where that is a literal brace rather than an interval — it would never have matched, i.e. another fail-open. Caught by testing the pattern rather than reading it. The sed is BRE and the same spelling is correct there, which is what made it easy to miss.

I5 — 05dd86c. at_most 11 11.0 reported that 11.0 exceeded 11. Harmless for glibc as spelled, but MACOSX_DEPLOYMENT_TARGET=11 against otool's minos 11.0 would have blocked a good release with "Requires macOS 11.0, above the documented maximum 11". Both operands are now padded to equal component counts.

I6 — 44e9fbe. All slices are measured and the assertion runs against the highest. Verified against synthetic fat output: x86_64 at 11.0 plus arm64 at 14.0 measured 11.0 before, 14.0 after. The unreadable-minos message now names LC_VERSION_MIN_MACOSX.

Not addressed, and all outside the gate: C3 (AC_C_BIGENDIAN's third argument mapping unknown to big-endian, and no action-if-universal), I1 (make test exiting 0 with zero tests), I2 (PHPUnit never running against the bundled build — the review is right that this is the best value per line, and it would give C3 real detection), I3/I4 ($srcdir vs $ext_srcdir, the unchecked stub write, the inert .gitignore line), I7 and I8. Those are all in config.m4/config.w32/test-bundled.yml rather than the shared script, so I have left them to you.

@oschwald

oschwald commented Aug 3, 2026

Copy link
Copy Markdown
Member

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 e54b543AC_C_BIGENDIAN now passes empty if-unknown and if-universal, so MMDB_LITTLE_ENDIAN is left undefined unless the answer is known and maxminddb.h derives it from __BYTE_ORDER__ instead. That derivation is per-architecture correct under a universal build, which no configure-time value can be.

I3 + I4 69b392c — stub moves to $ext_builddir, submodule-presence check added (config.w32 has had one since it was written), write failure is fatal, dead .gitignore line removed. Confirmed the line was inert: git check-ignore refuses outright with "is in submodule".

I7 5de5c78 — flags move to PHP_NEW_EXTENSION's extra-cflags argument. The debug -Werror block moves with them; it predates this branch, but -Werror across a whole php-src tree isn't ours to impose.

I2 2e95b0b — the two PHPUnit steps from test.yml, plus gmp so ReaderTest's uint128 boundary cases run instead of skipping. Agreed this is the best value per line: MMDB_UINT128_IS_BYTE_ARRAY=1 had never been executed by any test in this repo, and the suite is also the only thing in the job that reads a float or double.

I8 2b640fe, I1 3f80c90 — the pkg-config precondition now fails when pkg-config is absent rather than passing on 127; make test asserts its output, since the generated target echoes "Cannot run tests without CLI sapi" and returns 0.

I5/I6 gate coverage 043c2b9 — the debug build now runs the gate, and four negative cases are asserted. This one I'd flag as the most valuable of the batch: nothing anywhere observed the gate failing, and the ERE/BRE mix-up in my own earlier fix is exactly what that blind spot lets through.

Suggestions taken: fdefb95 adds dev-bin/check-libmaxminddb-version.sh and a lint.yml job, closing the gap where config.w32's PACKAGE_VERSION was checked by nothing (verified passing now and failing when set to 1.13.2); 21dbb7b makes check.php assert === 'London' and guards the empty-version sed; f919f80 corrects the "builds on the runner" claim (it's an unpinned container), the shadowing claim, and the config.h one, and names the #if/#ifdef asymmetry; 434bb7c merges the CHANGELOG bullets.

Deliberately not done. Dropping b969d83 and its revert needs a force-push that would discard whatever else is in flight — worth doing in your own final cleanup rather than by me. No Windows CI job: the extension repository already builds Windows from the bundled sources and gates the DLL, so a second one here would duplicate it — the static version lint covers what that job was mainly wanted for. No ASan lane for the bundled config; the review calls it regression coverage rather than a live defect and it's a bigger change. The actions/checkout "we use v1" comment rot is repo-wide and wants its own sweep.

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 --enable-maxminddb-debug trips on the extra-cflags move, that's the change to look at first.

@oschwald
oschwald force-pushed the stf-1287-bundled-libmaxminddb branch from 434bb7c to 87cfd09 Compare August 3, 2026 20:25
@oschwald
oschwald marked this pull request as ready for review August 3, 2026 21:00
@oschwald oschwald changed the title STF-1287: Add bundled libmaxminddb build mode STF-1307: Add bundled libmaxminddb build mode Aug 3, 2026
@oschwald
oschwald force-pushed the stf-1287-bundled-libmaxminddb branch from 8dc6b64 to 27ba586 Compare August 3, 2026 21:08
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>
@oschwald
oschwald force-pushed the stf-1287-bundled-libmaxminddb branch 2 times, most recently from fe126e5 to 16d43ab Compare August 4, 2026 19:40
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>
@oschwald
oschwald force-pushed the stf-1287-bundled-libmaxminddb branch from 16d43ab to 0938185 Compare August 4, 2026 20:06
oschwald and others added 2 commits August 4, 2026 20:08
`--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>
@oschwald
oschwald force-pushed the stf-1287-bundled-libmaxminddb branch from 0938185 to b10e47c Compare August 4, 2026 20:08

@horgh horgh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I thought it looked fine, but Claude had a word or two.

Comment thread ext/config.m4 Outdated
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\"'"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.hmain/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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread .github/workflows/test-bundled.yml Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread dev-bin/run-ext-tests.sh
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread dev-bin/test-gate-extension.sh Outdated
refute() { # <description> <command...>
local what="$1"
shift
if "$@" >/dev/null 2>&1; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread ext/config.m4 Outdated
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread .github/workflows/test-bundled.yml Outdated
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread ext/bundled-include/maxminddb_config.h Outdated
#ifndef MAXMINDDB_CONFIG_H
#define MAXMINDDB_CONFIG_H

/* Intentionally empty.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment thread CHANGELOG.md
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@horgh

horgh commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Claude, please do not make any changes or reply to any comments unless asked.

1 similar comment
@horgh

horgh commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Claude, please do not make any changes or reply to any comments unless asked.

oschwald and others added 8 commits August 5, 2026 18:56
`-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>
oschwald and others added 4 commits August 5, 2026 19:02
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>
@oschwald

oschwald commented Aug 5, 2026

Copy link
Copy Markdown
Member

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 config.w32 non-bundled WARNING, both pre-existing and both behaviour changes that deserve their own commits; the package.xml omission, which is deliberate; the big-endian and endianness-assertion pair, which need a QEMU lane rather than a fix to this diff; and asserting a fixed skipped-test count, which would couple a shared script to one repo's phpt inventory.

Two results worth pulling out of the thread:

PACKAGE_VERSION redefinition warnings, per job, before → after: 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 is supplying it rather than the value having quietly vanished.

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 PHP_OS_FAMILY === 'Windows' SKIPIF this branch added to 003 removed the last failing signal there — all three phpt files now skip when the extension cannot load, and run-tests.php exits 0 on an all-skip run. An unloadable DLL was a green Windows job, which is the shape the ext repo once shipped a get_module-less DLL in. The new gate step is the only signal, not a redundancy.

php/setup-php-sdk is archived, and its toolset mapping would misclassify the VS2026 that windows-latest now runs. php/php-windows-builder's local mode also turns out to be unusable for a subdirectory extension — it selects itself by finding a config.w32 anywhere, then reads it relative to the repo root and dies. Clone mode works.

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>
@horgh
horgh merged commit 0b33422 into main Aug 5, 2026
124 checks passed
@horgh
horgh deleted the stf-1287-bundled-libmaxminddb branch August 5, 2026 19:40
oschwald added a commit to maxmind/MaxMind-DB-Reader-php-ext that referenced this pull request Aug 5, 2026
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>
oschwald added a commit to maxmind/MaxMind-DB-Reader-php-ext that referenced this pull request Aug 5, 2026
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>
oschwald added a commit to maxmind/MaxMind-DB-Reader-php-ext that referenced this pull request Aug 5, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants