Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ updates:
codeql:
patterns:
- github/codeql-action*
- package-ecosystem: gitsubmodule
directory: /
schedule:
interval: weekly
day: monday
time: '14:00'
cooldown:
default-days: 7
ignore:
# libmaxminddb is pinned to release tags and compiled into the
# extension. Dependabot moves submodules to the latest commit on the
# tracked branch when no newer tag exists, which would take us off
# release tags.
- dependency-name: ext/libmaxminddb
2 changes: 1 addition & 1 deletion .github/workflows/clang-addresssanitizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
./configure --with-maxminddb --enable-maxminddb-debug
make clean
make -j2
NO_INTERACTION=1 make test
../dev-bin/run-ext-tests.sh
env:
# -isystem as otherwise Clang generates warnings for includes there.
# I don't think it should, but I haven't discovered a better way to
Expand Down
277 changes: 277 additions & 0 deletions .github/workflows/test-bundled.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
name: Bundled libmaxminddb

on:
push:
pull_request:
schedule:
- cron: '15 4 * * SUN'

permissions: {}

env:
# The highest glibc symbol version a bundled build here is allowed to
# reference. This repository publishes no binaries, so nobody inherits this
# number directly -- it is a tripwire on the sources, and the floor that
# actually reaches users is the extension repository's.
#
# The floor measured here is the *build container's*, not the runner's: the
# jobs below run in shivammathur/node:latest-<arch>. That tag is unpinned, so
# this is an assertion about a moving target -- an upstream image rebuild
# onto a newer glibc will either fail this job or shift what the number
# means. Pinning the image by digest is the way to make it stable.
#
# The extension repository builds what it publishes inside a digest-pinned
# 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.


jobs:
bundled:
runs-on: ${{ matrix.runner }}
container: shivammathur/node:latest-${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
arch: ["amd64", "arm64v8"]
php-version: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64v8
runner: ubuntu-24.04-arm

name: "PHP ${{ matrix.php-version }} bundled build on ${{ matrix.runner }}"
steps:
- name: Install PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ matrix.php-version }}
# gmp so ReaderTest's uint128 boundary cases run rather than skip;
# they are gated on it, and uint128 is the branch this job is the
# only one to compile.
extensions: "mbstring, intl, gmp"

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: gmp being absent deletes the uint128 assertions silently rather than failing.

tests/MaxMind/Db/Test/ReaderTest.php:72, :104 and :125 use markTestIncomplete, which does not fail the run. So if gmp is not loaded under php -d extension=… for any reason, PHPUnit exits 0 having asserted nothing about the branch this comment calls "the branch this job is the only one to compile".

Note the two assertions are not both gmp-gated: ReaderTest.php:162 (the 0x0100… hex form) is gated on extension_loaded('maxminddb') and runs without gmp, while :76 (the decimal form) is the gmp-gated one. The comment reads as though gmp is what unlocks the 2^120 check; only half of it is.

An explicit extension_loaded('gmp') precondition step, or --fail-on-incomplete, closes it.

🤖 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('gmp') precondition, and corrected the comment. Thank you for the :162 vs :76 distinction; the comment did read as though gmp unlocked the whole 2^120 check, and it is only the decimal half.

🤖 Claude (Claude Code) on behalf of Greg.

tools: "composer, phpize"

- name: Checkout
# We use v1 due to https://github.com/actions/checkout/issues/334
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: true
persist-credentials: false

# We deliberately do not install libmaxminddb, and we do not set
# PKG_CONFIG_PATH, so that the build cannot silently fall back to a
# system library.
- name: Check that no system libmaxminddb is available
run: |
# Without this, an image with no pkg-config makes the check below
# exit 127, the `if` false, and the step green having proved
# nothing.
if ! command -v pkg-config >/dev/null; then
echo "::error::pkg-config is not installed, so this precondition cannot be verified"
exit 1
fi
if pkg-config --exists libmaxminddb; then
echo "::error::a system libmaxminddb is installed, so this job would not test the bundled sources"
exit 1
fi
# pkg-config only sees libraries that ship a .pc file on
# PKG_CONFIG_PATH, so a `make install` into /usr/local would slip
# past it. What actually proves the bundled code is inside the
# object is gate-extension.sh's NEEDED and undefined-MMDB_ checks.

- name: Build extension
run: |
cd ext
phpize
./configure --with-maxminddb --with-maxminddb-bundled
make clean
make
../dev-bin/run-ext-tests.sh

# Shared with maxmind/MaxMind-DB-Reader-php-ext, which runs this same
# script over the objects it publishes. Keeping one implementation means
# a check added for the released binaries is a check this job performs
# too, and neither can quietly weaken relative to the other.
- name: Check that the extension is self-contained
run: dev-bin/gate-extension.sh ext/modules/maxminddb.so

# A prebuilt extension is installed as a lone maxminddb.so, so check that
# the object loads with no ini file and no build tree beside it. Note
# that this is not a self-containment proof: every library present at
# build time is still installed on this machine. gate-extension.sh above
# is what establishes that.
- name: Load the extension on its own
run: |
# autoupdate and autoconf >= 2.70 routinely rewrite AC_INIT's
# spacing, at which point this sed yields nothing and the
# comparison below would fail with an empty "expected".
version="$(sed -n 's/^AC_INIT(\[libmaxminddb\], \[\([^]]*\)\].*/\1/p' \
ext/libmaxminddb/configure.ac)"
if [ -z "$version" ]; then
echo "::error::could not read the libmaxminddb version from ext/libmaxminddb/configure.ac"
exit 1
fi
dir="$(mktemp -d)"
cp ext/modules/maxminddb.so "$dir/"
php -n -d extension="$dir/maxminddb.so" dev-bin/verify-extension.php \
tests/data/test-data/GeoIP2-City-Test.mmdb "$version"

# The phpt files in ext/tests decode nothing. Every behavioural assertion
# against the C code -- all twelve MMDB data types, the corrupt-database
# paths, the closed-reader paths -- lives in the PHPUnit suite, which
# test.yml runs against a libmaxminddb built from git, and which this job
# did not previously run at all. Hence the steps below.
#
# It matters most for uint128. MMDB_UINT128_IS_BYTE_ARRAY=1 is the path
# every bundled build takes, and no other job in this repository
# exercises it: they all link an external libmaxminddb, whose own
# configure finds unsigned __int128 and compiles the other branch.
# ReaderTest asserts the exact string the extension returns for 2^120, so
# a swapped high/low word or a wrong shift is caught here and nowhere
# else -- it would otherwise produce a plausible-looking hex string and
# crash nothing.
#
# The suite also reads floats and doubles, which are the only consumers
# of MMDB_LITTLE_ENDIAN.
- name: Install dependencies
run: composer install --no-progress --prefer-dist --optimize-autoloader

# Both preconditions are load-bearing, and neither failure is loud on its
# own.
#
# A failed `extension=` load is a warning, not a fatal, and PHP carries
# on. phpunit.xml.dist bootstraps vendor/autoload.php, whose PSR-4 map
# resolves MaxMind\Db\Reader to the pure-PHP implementation, and the
# suite is written to pass either way -- so an extension that does not
# load leaves every assertion above testing src/ rather than the object
# this job built.
#
# gmp is what turns ReaderTest's decimal uint128 case from
# markTestIncomplete into a real assertion, and markTestIncomplete does
# not fail a run. (The hex form at ReaderTest.php:162 is gated on
# extension_loaded('maxminddb'), not on gmp, so only half the uint128
# coverage depends on this -- but it is the half that checks the value
# rather than the format.)
- name: Check the extension and gmp are actually loaded
run: |
php -d extension="$PWD/ext/modules/maxminddb.so" -r \
'exit(extension_loaded("maxminddb") ? 0 : 1);' ||
{ echo "::error::the bundled extension did not load; PHPUnit would silently test the pure-PHP reader"; exit 1; }
php -r 'exit(extension_loaded("gmp") ? 0 : 1);' ||
{ echo "::error::gmp is not loaded; ReaderTest's uint128 value assertions would be skipped, not run"; exit 1; }

- name: Test with phpunit using the bundled extension
run: php -d extension="$PWD/ext/modules/maxminddb.so" vendor/bin/phpunit

bundled-debug:
runs-on: ubuntu-latest
container: shivammathur/node:latest-amd64

name: "Bundled build with debug flags"
steps:
- name: Install PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.4'
extensions: "mbstring, intl"
tools: "composer, phpize"

- name: Checkout
# We use v1 due to https://github.com/actions/checkout/issues/334
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: true
persist-credentials: false

# --enable-maxminddb-debug adds -Werror, so this shows that the bundled
# libmaxminddb sources build warning free too.
- name: Build extension
run: |
cd ext
phpize
./configure --with-maxminddb --with-maxminddb-bundled --enable-maxminddb-debug
make clean
make
../dev-bin/run-ext-tests.sh

# The debug build differs in flags and codegen, so it is the object most
# likely to pick up something the release build does not -- a RUNPATH
# from a different link line, say. It was the one bundled object the gate
# never saw.
- name: Check that the extension is self-contained
run: dev-bin/gate-extension.sh ext/modules/maxminddb.so

# Nothing else exercises the gate's failure paths: every other caller
# runs it over an object it expects to pass. See the script's header for
# what it refutes and why.
- name: Check that the gate rejects what it should
run: dev-bin/test-gate-extension.sh ext/modules/maxminddb.so

windows-bundled:
runs-on: windows-latest
name: "Windows bundled build (x64, nts, PHP 8.4)"
steps:
# The action clones and initialises its own copy to build from; this
# checkout supplies dev-bin/verify-extension.php and the tests/data
# database the gate below queries.
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: recursive
persist-credentials: false

# extension-url rather than the action's local mode, which cannot be used
# here: local mode selects itself by finding a config.w32 anywhere in the
# tree, but Get-ExtensionName then reads "config.w32" relative to the
# repository root, so an extension living in ext/ is detected and then
# immediately fails. Cloning also gets the action's own recursive
# submodule init, which local mode does not do. This is the shape
# maxmind/MaxMind-DB-Reader-php-ext already builds this config.w32 with.
#
# One job is enough. 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.
- name: Build the extension
uses: php/php-windows-builder/extension@29352c0ef9e8ce65264ea9e287881a6f7758a953 # 1.9.0
with:
extension-url: https://github.com/${{ github.repository }}
extension-ref: ${{ github.event.pull_request.head.sha || github.sha }}
build-directory: winbuild
php-version: '8.4'
arch: x64
ts: nts
args: --with-maxminddb --with-maxminddb-bundled

# Load-bearing, not belt-and-braces. 001 and 002 skip when the extension
# is not loaded and 003 skips on Windows outright, so every phpt skips
# and run-tests.php exits 0 -- a DLL that builds but cannot load is
# otherwise a green job. That is not hypothetical: a DLL with no
# get_module export shipped from the sibling repository exactly this way.
#
# Uses the php.exe the build downloaded, not the runner's: version, arch,
# thread safety and toolset all have to match. Neither is on PATH in a
# later step, so both are located by search.
- name: Gate the built DLL
shell: pwsh
run: |
$dll = @(Get-ChildItem winbuild -Recurse -File -Filter php_maxminddb.dll)
$php = @(Get-ChildItem winbuild -Recurse -File -Filter php.exe |
Where-Object { $_.Directory.Name -eq 'php-bin' })
if ($dll.Count -ne 1) { throw "expected one php_maxminddb.dll, found $($dll.Count)" }
if ($php.Count -ne 1) { throw "expected one php-bin\php.exe, found $($php.Count)" }

$ac = Get-Content ext/libmaxminddb/configure.ac -Raw
if ($ac -notmatch 'AC_INIT\(\[libmaxminddb\], \[([^\]]+)\]') {
throw "could not read the libmaxminddb version from ext/libmaxminddb/configure.ac"
}
$version = $Matches[1]

# verify-extension.php asserts the extension is loaded, queries a real
# database and compares MMDB_LIB_VERSION -- which also covers the
# version config.w32 scrapes out of the submodule.
& $php[0].FullName -n -d "extension=$($dll[0].FullName)" `
dev-bin/verify-extension.php `
tests/data/test-data/GeoIP2-City-Test.mmdb $version
if ($LASTEXITCODE -ne 0) { throw "the DLL did not load and query cleanly" }
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
./configure --with-maxminddb --enable-maxminddb-debug
make clean
make
NO_INTERACTION=1 make test
../dev-bin/run-ext-tests.sh
cd ..

- name: Install dependencies
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[submodule "tests/data"]
path = tests/data
url = https://github.com/maxmind/MaxMind-DB
[submodule "ext/libmaxminddb"]
path = ext/libmaxminddb
url = https://github.com/maxmind/libmaxminddb
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ CHANGELOG
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.

both Unix-like systems and Windows, by passing `--with-maxminddb-bundled`
to `configure` (or to `configure.bat` on Windows). This produces an
extension that does not depend on a system libmaxminddb, which is a
prerequisite for distributing precompiled builds; on Windows it also
replaces the 1.5.0 import library that PHP publishes for Windows builds.
The default is unchanged: without the flag, the extension links against a
system libmaxminddb as before. GitHub #265.

1.13.1 (2025-11-21)
-------------------
Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,18 +149,39 @@ pecl install maxminddb

### Installing Extension from Source ###

Alternatively, you may install it from the source. To do so, run the following
commands from the top-level directory of this distribution:
Alternatively, you may install it from the source. This links against the
libmaxminddb installed on your system, so install that first as described
above. Run the following commands from the top-level directory of this
distribution:

```
cd ext
phpize
./configure
./configure --with-maxminddb
make
make test
sudo make install
```

#### Building without a system libmaxminddb ####

Passing `--with-maxminddb-bundled` compiles a bundled copy of libmaxminddb
into the extension instead, so the result depends on nothing but the C
runtime. This is what the precompiled builds use. The bundled sources are a
git submodule, so they have to be checked out first:

```
git submodule update --init
cd ext
phpize
./configure --with-maxminddb --with-maxminddb-bundled
make
make test
sudo make install
```

The same flag works on Windows, passed to `configure.bat`.

You then must load your extension. The recommended method is to add the
following to your `php.ini` file:

Expand Down
Loading
Loading