Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/test-jvm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,43 @@ jobs:
mkdir -p test-install
cp dist/*.whl test-install/

# The tests below run on the runner (glibc 2.39). This step runs the wheel where its tag says it must
# work: a manylinux_2_28 container (glibc 2.28, AlmaLinux 8, the oldest system the Linux wheels are tagged for).
# pip refuses a wheel tagged above 2.28 there, so a JRE that raises the floor again fails here as well as in
# scripts/verify_wheel_platform_tag.py. The wheel's own JRE does the work: no Java in the container.
- name: Install and run the wheel in manylinux_2_28 (Linux only)
if: startsWith(matrix.platform, 'linux/')
shell: bash
env:
PLATFORM: ${{ matrix.platform }}
PYVER: ${{ matrix.python-version }}
run: |
case "$PLATFORM" in
linux/amd64) ARCH=x86_64 ;;
linux/arm64) ARCH=aarch64 ;;
*) echo "unexpected platform $PLATFORM" >&2; exit 1 ;;
esac
PYTAG="cp${PYVER//./}-cp${PYVER//./}"
ls -l test-install/*.whl
docker run --rm -i -e PYTAG="$PYTAG" -v "$PWD/test-install:/wheels:ro" \
"quay.io/pypa/manylinux_2_28_${ARCH}:latest" bash -es <<'CONTAINER'
ldd --version | head -n 1
test -x "/opt/python/$PYTAG/bin/python" || { echo "no $PYTAG in this image" >&2; ls /opt/python >&2; exit 1; }
"/opt/python/$PYTAG/bin/python" -m venv /tmp/smoke
# --only-binary: a source build of JPype must not hide a missing wheel
/tmp/smoke/bin/python -m pip install --quiet --only-binary=:all: /wheels/*.whl
cd /tmp
/tmp/smoke/bin/python - <<'PYSMOKE'
import tempfile
import arcadedb
with tempfile.TemporaryDirectory() as tmp:
with arcadedb.create_database(tmp + "/smoke") as db:
rows = db.query("sql", "SELECT 1 AS one").to_list()
assert rows and rows[0]["one"] == 1, rows
print("manylinux_2_28 smoke OK:", arcadedb.__version__, rows)
PYSMOKE
CONTAINER

- name: Set up Python for host testing
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
Expand Down
97 changes: 52 additions & 45 deletions scripts/Dockerfile.build
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@ FROM arcadedata/arcadedb:${ARCADEDB_TAG} AS java-builder

# Stage 2: Build minimal JRE with jlink
# Docker builds are Linux-only. macOS/Windows JREs must be created on matching native hosts.
FROM amazoncorretto:25 AS jre-builder
#
# The JDK is Eclipse Temurin, not amazoncorretto:25. The Corretto IMAGE is Amazon Linux 2023 (glibc 2.34) and its JDK is
# built against it: every binary needs GLIBC_2.34, which is what set the wheels' manylinux_2_34 floor. Temurin's Linux
# JDK builds need glibc 2.17 at most on x86_64 and aarch64 (scripts/verify_wheel_platform_tag.py --jre prints
# manylinux_2_17 for both), and the eclipse-temurin image carries that build unchanged. jlink copies the libraries of the
# JDK it runs, so the bundled JRE has the same floor. Both are GPLv2 with the Classpath Exception.
FROM eclipse-temurin:25-jdk AS jre-builder

# Install required tooling (findutils for jar exclusion, binutils for jlink)
RUN yum -y install findutils binutils && yum clean all
RUN apt-get update && apt-get install -y --no-install-recommends findutils binutils \
&& rm -rf /var/lib/apt/lists/*

ARG TARGET_PLATFORM
ARG USE_LOCAL_JARS
Expand Down Expand Up @@ -99,6 +106,11 @@ RUN echo "🔨 Building arcadedb-python-bridge.jar..." && \
# Automatically detect modules with jdeps, plus ensure jdk.zipfs (for JPype) and jdk.unsupported are included
# We exclude jboss/wildfly JARs because they have broken module descriptors that fail analysis.
# We also do NOT provide a classpath, forcing jdeps to ignore all missing dependencies (intra-jar or external).
#
# The eclipse-temurin JDK has no jmods directory (JEP 493: it is built with --enable-linkable-runtime), so jlink reads
# the modules from the JDK's own run-time image when --module-path is absent. There is no fallback that copies the
# whole JDK: the first Temurin build did that silently and produced a 130 MB wheel (251 MB JRE), over the 100 MB limit
# of scripts/verify_wheel_size.py. If jlink cannot build the JRE, the build stops.
RUN echo "🔍 Analyzing JARs with jdeps..." && \
JMODS_DIR="${JAVA_HOME}/jmods" && \
if [ ! -d "$JMODS_DIR" ]; then JMODS_DIR="${JAVA_HOME}/lib/jmods"; fi && \
Expand All @@ -123,41 +135,38 @@ RUN echo "🔍 Analyzing JARs with jdeps..." && \
echo "📦 Final modules list: ${REQUIRED_MODULES}" && \
echo "📦 Required modules:" && \
echo "$REQUIRED_MODULES" | tr ',' '\n' | sed 's/^/ - /' && \
echo "" && \
echo "🔨 Running jlink..." && \
if [ -d "$JMODS_DIR" ]; then \
echo "" ; \
echo "🔨 Running jlink..." ; \
if ! jlink \
--module-path "$JMODS_DIR" \
--add-modules "${REQUIRED_MODULES}" \
--ignore-signing-information \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress zip-9 \
--output /build/jre ; then \
echo "❌ jlink failed" ; \
exit 1 ; \
fi ; \
echo "" ; \
echo "✅ JRE build complete!" ; \
echo "📊 JRE size after jlink: $(du -sm /build/jre | cut -f1) MB" ; \
BAD_MODULES=$(/build/jre/bin/java --list-modules | sed 's/@.*//' | grep -xFf /build/jre_excluded_modules.list || true) ; \
if [ -n "$BAD_MODULES" ]; then \
echo "❌ the JRE contains excluded modules (another module requires them): $BAD_MODULES" ; \
exit 1 ; \
fi ; \
echo "✅ No excluded module in the JRE" ; \
find /build/jre -name '*.so' -type f -exec strip --strip-unneeded {} + ; \
echo "📊 JRE size after stripping native libraries: $(du -sm /build/jre | cut -f1) MB" ; \
JLINK_SOURCE="--module-path $JMODS_DIR" ; \
echo " modules from ${JMODS_DIR}" ; \
else \
echo "⚠️ jmods directory not found under ${JAVA_HOME}. Falling back to copying full JDK runtime." ; \
mkdir -p /build/jre ; \
cp -a ${JAVA_HOME}/bin /build/jre/bin ; \
cp -a ${JAVA_HOME}/lib /build/jre/lib ; \
cp -a ${JAVA_HOME}/conf /build/jre/conf || true ; \
cp -a ${JAVA_HOME}/release /build/jre/release || true ; \
echo "✅ Fallback JRE created from full JDK runtime." ; \
JLINK_SOURCE="" ; \
echo " no jmods directory: linking from the run-time image of ${JAVA_HOME} (JEP 493)" ; \
fi && \
if ! jlink \
${JLINK_SOURCE} \
--add-modules "${REQUIRED_MODULES}" \
--ignore-signing-information \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress zip-9 \
--output /build/jre ; then \
echo "❌ jlink failed" ; \
exit 1 ; \
fi && \
echo "" && \
echo "✅ JRE build complete!" && \
echo "📊 JRE size after jlink: $(du -sm /build/jre | cut -f1) MB" && \
BAD_MODULES=$(/build/jre/bin/java --list-modules | sed 's/@.*//' | grep -xFf /build/jre_excluded_modules.list || true) && \
if [ -n "$BAD_MODULES" ]; then \
echo "❌ the JRE contains excluded modules (another module requires them): $BAD_MODULES" ; \
exit 1 ; \
fi && \
echo "✅ No excluded module in the JRE" && \
find /build/jre -name '*.so' -type f -exec strip --strip-unneeded {} + && \
echo "📊 JRE size after stripping native libraries: $(du -sm /build/jre | cut -f1) MB" && \
echo "" && \
JRE_SIZE=$(du -sh /build/jre | cut -f1) && \
echo "📊 JRE size: $JRE_SIZE" && \
Expand Down Expand Up @@ -227,15 +236,13 @@ RUN echo "📌 Package name: ${PACKAGE_NAME}" && \
# Build the wheel
# Extract PEP 440 version from the engine jar or use BUILD_VERSION if provided.
#
# Wheel platform tag must match the highest glibc symbol version required by the
# bundled JRE. amazoncorretto:25 (Amazon Linux 2023, glibc 2.34) ships a JRE
# whose binaries link against GLIBC_2.34 at most; older glibc symbol versions
# cannot be back-ported. Issue #4037: tagging as manylinux_2_35 prevents
# installation on systems with glibc 2.34 (RHEL 9, Rocky 9, AlmaLinux 9, Amazon
# Linux 2023, ...) even though the bundled JRE runs there fine. Keep the tag
# aligned with the JRE's actual requirement; bump only when the JRE base image
# moves to a newer glibc. The verify_wheel_platform_tag.py step below enforces
# this invariant at build time.
# Wheel platform tag: manylinux_2_28, the same floor as the native wheels (glibc 2.28: RHEL, Rocky and
# AlmaLinux 8, Debian 10, Ubuntu 20.04 and newer). It is also the lowest the Python side can have: JPype's own Linux
# wheels are manylinux_2_28. The tag is the higher of that floor and the highest GLIBC_ symbol the bundled JRE
# references (2.17 with Temurin). Tagging higher than that locks out systems the wheel runs on (issue #4037: a
# manylinux_2_35 tag on a JRE that needed 2.34); tagging lower lets it install where the JRE cannot run. The
# verify_wheel_platform_tag.py step below enforces both at build time, and test-jvm.yml installs the built wheel in
# a manylinux_2_28 container.
ARG BUILD_VERSION=""
RUN if [ -n "${BUILD_VERSION}" ]; then \
export ARCADEDB_VERSION="${BUILD_VERSION}"; \
Expand All @@ -252,9 +259,9 @@ RUN if [ -n "${BUILD_VERSION}" ]; then \
sed -i 's|^description = .*|description = "'"${PACKAGE_DESCRIPTION}"'"|' pyproject.toml && \
if echo "${TARGET_PLATFORM}" | grep -q '^linux-'; then \
if [ "${TARGET_PLATFORM}" = "linux-x64" ]; then \
WHEEL_PLAT="manylinux_2_34_x86_64"; \
WHEEL_PLAT="manylinux_2_28_x86_64"; \
elif [ "${TARGET_PLATFORM}" = "linux-arm64" ]; then \
WHEEL_PLAT="manylinux_2_34_aarch64"; \
WHEEL_PLAT="manylinux_2_28_aarch64"; \
else \
WHEEL_PLAT=""; \
fi; \
Expand Down
32 changes: 21 additions & 11 deletions scripts/verify_wheel_platform_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@

Regression guard for issue #4037: the wheel was tagged ``manylinux_2_35`` while
the bundled JRE only required ``GLIBC_2.34``, blocking installation on systems
running glibc 2.34 (RHEL 9, Rocky 9, AlmaLinux 9, Amazon Linux 2023, ...). The
wheel platform tag must equal the highest ``GLIBC_x.y`` symbol version actually
referenced by the JRE binaries (``java``, ``libjvm.so``, ...). Tagging higher
locks out users unnecessarily; tagging lower lets the wheel install on systems
where the JRE cannot run.
running glibc 2.34 (RHEL 9, Rocky 9, AlmaLinux 9, Amazon Linux 2023, ...).

The expected tag is the higher of ``MANYLINUX_FLOOR`` (the oldest glibc the
package supports, manylinux_2_28, which is also the floor of JPype's own Linux
wheels) and the highest ``GLIBC_x.y`` symbol version referenced by the JRE
binaries (``java``, ``libjvm.so``, ...). Tagging higher locks users out
unnecessarily; tagging lower lets the wheel install on systems where the JRE
cannot run. With a JRE built for glibc 2.17 (Eclipse Temurin) the expected tag is
the floor; a JRE built on a newer system raises it.

Usage:
python verify_wheel_platform_tag.py <wheel_path> <jre_dir>
Verify a wheel's filename platform tag against the JRE in jre_dir.
python verify_wheel_platform_tag.py --jre <jre_dir>
Print the recommended manylinux tag for the JRE (no wheel check).
Print the manylinux tag for the JRE (no wheel check): the floor, or
the JRE's own requirement when that is higher.

Exits with status 0 on success, non-zero on mismatch.

Expand All @@ -29,6 +34,9 @@
from pathlib import Path

GLIBC_SYM_RE = re.compile(rb"GLIBC_(\d+)\.(\d+)(?:\.(\d+))?")
# The oldest glibc the package supports. The native wheels are built in manylinux_2_28, and JPype (the
# Python side) publishes manylinux_2_28 wheels only, so a lower tag could not install a working package.
MANYLINUX_FLOOR = (2, 28)
WHEEL_TAG_RE = re.compile(r"-(manylinux_(\d+)_(\d+)_(x86_64|aarch64))\.whl$")


Expand Down Expand Up @@ -96,7 +104,7 @@ def main() -> int:
args = parser.parse_args()

if args.jre_only is not None:
major, minor = max_glibc_in_dir(args.jre_only)
major, minor = max(max_glibc_in_dir(args.jre_only), MANYLINUX_FLOOR)
print(f"manylinux_{major}_{minor}")
return 0

Expand All @@ -106,21 +114,23 @@ def main() -> int:

wheel_glibc, _arch = parse_wheel_tag(args.wheel)
jre_glibc = max_glibc_in_dir(args.jre)
expected = max(jre_glibc, MANYLINUX_FLOOR)

print(f"Wheel platform tag glibc: {wheel_glibc[0]}.{wheel_glibc[1]}")
print(f"JRE max GLIBC_ symbol: {jre_glibc[0]}.{jre_glibc[1]}")
print(f"Supported floor: {MANYLINUX_FLOOR[0]}.{MANYLINUX_FLOOR[1]}")

if wheel_glibc != jre_glibc:
if wheel_glibc != expected:
print(
f"ERROR: wheel manylinux tag (manylinux_{wheel_glibc[0]}_{wheel_glibc[1]}) "
f"does not match the JRE's actual glibc requirement "
f"(manylinux_{jre_glibc[0]}_{jre_glibc[1]}). "
f"does not match the expected manylinux_{expected[0]}_{expected[1]}: "
f"the higher of the supported floor and the JRE's actual glibc requirement. "
"Update WHEEL_PLAT in scripts/Dockerfile.build (issue #4037).",
file=sys.stderr,
)
return 1

print("OK: wheel platform tag matches the JRE's glibc requirement.")
print("OK: wheel platform tag matches the floor or the JRE's glibc requirement.")
return 0


Expand Down
68 changes: 68 additions & 0 deletions tests/test_wheel_platform_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,74 @@ def test_main_succeeds_when_tag_matches(
assert "OK" in capsys.readouterr().out


def test_main_succeeds_at_the_floor_when_jre_needs_less(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A JRE built for glibc 2.17 (Eclipse Temurin) is tagged with the floor, manylinux_2_28."""
verifier = _load_verifier()
jre_dir = tmp_path / "jre"
jre_dir.mkdir()
_make_elf_with_glibc_symbols(jre_dir / "libjvm.so", ["2.2.5", "2.17"])

wheel = tmp_path / "arcadedb_jvm-26.4.2-py3-none-manylinux_2_28_aarch64.whl"
wheel.write_bytes(b"")

rc = _run_main(verifier, [str(wheel), str(jre_dir)])
assert rc == 0
assert "OK" in capsys.readouterr().out


def test_main_fails_when_tag_below_the_floor(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The JRE would run on glibc 2.17, but JPype publishes manylinux_2_28 wheels only."""
verifier = _load_verifier()
jre_dir = tmp_path / "jre"
jre_dir.mkdir()
_make_elf_with_glibc_symbols(jre_dir / "libjvm.so", ["2.17"])

wheel = tmp_path / "arcadedb_jvm-26.4.2-py3-none-manylinux_2_17_x86_64.whl"
wheel.write_bytes(b"")

rc = _run_main(verifier, [str(wheel), str(jre_dir)])
assert rc != 0
assert "manylinux_2_28" in capsys.readouterr().err


def test_main_fails_when_tag_above_the_floor_and_jre(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Going back to manylinux_2_34 with a 2.17 JRE would lock out glibc 2.28 to 2.33 for no reason."""
verifier = _load_verifier()
jre_dir = tmp_path / "jre"
jre_dir.mkdir()
_make_elf_with_glibc_symbols(jre_dir / "libjvm.so", ["2.17"])

wheel = tmp_path / "arcadedb_jvm-26.4.2-py3-none-manylinux_2_34_x86_64.whl"
wheel.write_bytes(b"")

rc = _run_main(verifier, [str(wheel), str(jre_dir)])
assert rc != 0
assert "manylinux_2_28" in capsys.readouterr().err


def test_jre_only_mode_prints_the_floor_or_the_jre_requirement(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
verifier = _load_verifier()
old = tmp_path / "old"
old.mkdir()
_make_elf_with_glibc_symbols(old / "libjvm.so", ["2.17"])
assert _run_main(verifier, ["--jre", str(old)]) == 0
assert capsys.readouterr().out.strip() == "manylinux_2_28"

new = tmp_path / "new"
new.mkdir()
_make_elf_with_glibc_symbols(new / "libjvm.so", ["2.17", "2.34"])
assert _run_main(verifier, ["--jre", str(new)]) == 0
assert capsys.readouterr().out.strip() == "manylinux_2_34"


def test_main_fails_when_tag_higher_than_jre(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
Expand Down
Loading