Repository navigation
Publish JAR #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish JAR | |
| # Builds (and uploads) a TRUE multi-platform zvec-java fat JAR. | |
| # | |
| # Strategy (mirrors, and improves on, the seqeralabs/zvec-java packaging model): | |
| # 1. build-natives — a matrix job builds the zvec C-API library and the | |
| # JavaCPP JNI glue on every supported platform, then stages the per-platform | |
| # native bundle (target/classes/<javacpp-platform>/) as an artifact. The | |
| # zvec C build is cached with a key anchored to the zvec submodule commit, | |
| # so a submodule bump automatically invalidates the cache. | |
| # 2. build-jar — a single job builds the linux zvec C library (version-anchored | |
| # cache), regenerates the git-ignored ZvecNative.java via the JavaCPP parser | |
| # and compiles the linux JNI glue, then drops the macOS/Windows native | |
| # bundles from step 1 into src/main/resources/<platform>/ and packages ONE | |
| # fat JAR containing the libraries for all platforms. | |
| # | |
| # The workflow is manual-only (workflow_dispatch) by design: the Release | |
| # workflow publishes the pre-built C libraries first, a human verifies the | |
| # release, and only then is this run by hand with the same tag to build the | |
| # JAR family and (opt-in via deploy_central) deploy to Maven Central. | |
| # Its final artifact is "zvec-java-jars". | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Release tag / version (e.g., v0.7.0)' | |
| required: true | |
| type: string | |
| deploy_central: | |
| description: 'Also deploy the full artifact matrix (all-platform + per-platform + nolib JARs) to Maven Central' | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| # =========================================================================== | |
| # Build the native bundle (zvec_c_api + jnizvec glue) for each platform | |
| # =========================================================================== | |
| build-natives: | |
| name: Natives (${{ matrix.name }}) | |
| runs-on: ${{ matrix.runner }} | |
| # The Linux leg runs inside the same manylinux_2_28 image upstream zvec uses | |
| # for its prebuilt SDK (see zvec/.github/workflows/_build_prebuilt.yml), so | |
| # the .so we ship still loads on glibc 2.28 distros - CentOS 8, RHEL 8, | |
| # Ubuntu 20.04. Building on the bare ubuntu-24.04 runner links against glibc | |
| # 2.39 and yields a library that demands GLIBC_2.38, which only Ubuntu | |
| # 23.10+, Debian 13 and Fedora 38+ can satisfy. macOS and Windows need no | |
| # container: macos-15 and windows-2025 are exactly what upstream builds on. | |
| container: ${{ matrix.container || null }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: darwin-arm64 | |
| runner: macos-15 | |
| container: '' | |
| cache_abi: host | |
| - name: linux-x64 | |
| runner: ubuntu-24.04 | |
| # Pinned dated tag, the same batch upstream uses. | |
| container: 'quay.io/pypa/manylinux_2_28_x86_64:2026.03.01-1' | |
| cache_abi: manylinux_2_28 | |
| - name: linux-arm64 | |
| runner: ubuntu-24.04-arm | |
| # Same image family upstream builds its aarch64 SDK in. | |
| container: 'quay.io/pypa/manylinux_2_28_aarch64:2026.03.01-1' | |
| cache_abi: manylinux_2_28 | |
| - name: windows-x64 | |
| runner: windows-2025 | |
| container: '' | |
| cache_abi: host | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| - name: Set up JDK 11 | |
| uses: actions/setup-java@v6 | |
| with: | |
| distribution: temurin | |
| java-version: '11' | |
| cache: maven | |
| # The manylinux image ships no Maven, and the runner's preinstalled | |
| # /usr/share/maven is not visible inside a container job. | |
| - name: Install Maven (Linux container) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| MVN_VERSION=3.9.16 | |
| curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/apache-maven-${MVN_VERSION}-bin.tar.gz" \ | |
| | tar -xz -C /opt | |
| echo "/opt/apache-maven-${MVN_VERSION}/bin" >> "$GITHUB_PATH" | |
| # --- Version-anchored cache: keyed by the zvec submodule commit and the | |
| # release tag, since the tag is compiled into the library --- | |
| - name: Resolve zvec submodule commit and version | |
| id: zvec | |
| shell: bash | |
| run: | | |
| echo "sha=$(git -C zvec rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| # zvec/cmake/version.cmake derives the version from `git describe | |
| # --tags` and otherwise falls back to a deliberately obvious dummy | |
| # v0.0.0. In CI that fallback always wins, because the submodule is | |
| # checked out detached and without tags - so the published library | |
| # used to report v0.0.0-g<sha> through Zvec.getVersion(), and 0.0.0 | |
| # through getVersionMajor/Minor/Patch. Feed the release tag in | |
| # explicitly, the way upstream zvec does in _build_prebuilt.yml. | |
| # Anything that is not a vX.Y.Z string is dropped rather than passed | |
| # on: version.cmake raises FATAL_ERROR on a malformed override. | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| case "$TAG" in v*) ;; *) TAG="v$TAG" ;; esac | |
| if printf '%s' "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::'$TAG' is not a vX.Y.Z version; the native build will report zvec's fallback version" | |
| echo "tag=" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Restore-only; see the gated save step after the build steps below. | |
| - name: Restore zvec build cache | |
| id: cache-zvec | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: zvec/build | |
| # cache_abi is part of the key so switching the Linux build into the | |
| # manylinux container cannot reuse a host-glibc build tree, and the | |
| # tag is part of it because OVERRIDE_GIT_DESCRIBE is baked into the | |
| # compiled library - without it a cache hit would keep serving a | |
| # tree built against the old version string. | |
| key: zvec-build-${{ matrix.name }}-${{ matrix.cache_abi }}-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }} | |
| # --- Unix build toolchain --- | |
| - name: Set up Python (for CMake / Ninja) | |
| if: runner.os == 'macOS' && steps.cache-zvec.outputs.cache-hit != 'true' | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.10' | |
| - name: Install build tools (Unix) | |
| if: runner.os != 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true' | |
| shell: bash | |
| run: | | |
| if [ "$(uname -s)" = "Linux" ]; then | |
| # setup-python is skipped inside the container: the CPython it | |
| # downloads is built for the host glibc and will not run in a | |
| # glibc 2.28 image. Use the bundled CPython instead, the same way | |
| # upstream zvec's _build_prebuilt.yml does. | |
| PYBIN=$(ls -d /opt/python/cp3*/bin | head -1) | |
| echo "$PYBIN" >> "$GITHUB_PATH" | |
| export PATH="$PYBIN:$PATH" | |
| fi | |
| python3 -m pip install --upgrade pip cmake==3.30.0 ninja==1.11.1 | |
| echo "$(python3 -c 'import site; print(site.USER_BASE)')/bin" >> "$GITHUB_PATH" | |
| # --- Windows build toolchain --- | |
| - name: Set up MSVC (Windows) | |
| if: runner.os == 'Windows' | |
| uses: ilammy/msvc-dev-cmd@v1 | |
| - name: Install CMake and Ninja (Windows) | |
| if: runner.os == 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true' | |
| shell: pwsh | |
| run: pip install cmake==3.30.0 ninja==1.11.1 | |
| # --- Build the zvec C-API library (skipped on cache hit) --- | |
| - name: Build C-API library (Unix) | |
| if: runner.os != 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true' | |
| shell: bash | |
| env: | |
| ZVEC_TAG: ${{ steps.zvec.outputs.tag }} | |
| run: | | |
| NPROC=$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2) | |
| cd zvec | |
| mkdir -p build && cd build | |
| cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_C_BINDINGS=ON \ | |
| -DOVERRIDE_GIT_DESCRIBE="$ZVEC_TAG" -G Ninja | |
| cmake --build . -j$NPROC --target zvec_c_api | |
| touch .zvec-build-ok | |
| - name: Build C-API library (Windows) | |
| if: runner.os == 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true' | |
| shell: pwsh | |
| env: | |
| ZVEC_TAG: ${{ steps.zvec.outputs.tag }} | |
| run: | | |
| cd zvec | |
| New-Item -ItemType Directory -Force -Path build | Out-Null | |
| cd build | |
| cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_C_BINDINGS=ON ` | |
| "-DOVERRIDE_GIT_DESCRIBE=$env:ZVEC_TAG" -G Ninja | |
| cmake --build . --target zvec_c_api -j $env:NUMBER_OF_PROCESSORS | |
| New-Item -ItemType File -Force -Path .zvec-build-ok | Out-Null | |
| # Save as soon as the build is known good: the marker file is written only | |
| # by a successful build, so a later failure (JNI glue, staging, upload) | |
| # still leaves a reusable cache and an incomplete tree is never stored. | |
| # continue-on-error absorbs the 409 raised when a concurrent run already | |
| # stored this key. | |
| - name: Save zvec build cache | |
| if: always() && steps.cache-zvec.outputs.cache-hit != 'true' && hashFiles('zvec/build/.zvec-build-ok') != '' | |
| uses: actions/cache/save@v6 | |
| continue-on-error: true | |
| with: | |
| path: zvec/build | |
| key: zvec-build-${{ matrix.name }}-${{ matrix.cache_abi }}-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }} | |
| # --- Locate the directory that holds the built library / import lib --- | |
| # JavaCPP links against <linkPaths> and copies the runtime library out of | |
| # that very same directory (copyLibs=true in pom.xml), so the link library | |
| # and the runtime library must sit side by side. Windows splits the CMake | |
| # output: the import library zvec_c_api.lib lands in build/lib while the | |
| # runtime zvec_c_api.dll lands in build/bin. Resolving build/lib alone | |
| # therefore produces a Windows bundle holding only the JNI glue, which | |
| # links fine but fails to load at runtime, so pull the DLL across. | |
| - name: Resolve zvec lib directory | |
| shell: bash | |
| run: | | |
| find_runtime_lib() { | |
| find "$1" -maxdepth 1 -type f \ | |
| \( -name '*zvec_c_api*.dll' -o -name '*zvec_c_api*.so*' -o -name '*zvec_c_api*.dylib' \) \ | |
| 2>/dev/null | head -n1 | |
| } | |
| for d in \ | |
| "$GITHUB_WORKSPACE/zvec/build/lib/Release" \ | |
| "$GITHUB_WORKSPACE/zvec/build/lib" \ | |
| "$GITHUB_WORKSPACE/zvec/build/bin/Release" \ | |
| "$GITHUB_WORKSPACE/zvec/build/bin"; do | |
| if [ -d "$d" ] && ls "$d"/*zvec_c_api* >/dev/null 2>&1; then | |
| ZVEC_LIB_DIR="$d" | |
| echo "Using zvec lib dir: $d" | |
| break | |
| fi | |
| done | |
| if [ -z "$ZVEC_LIB_DIR" ]; then | |
| echo "::error::No zvec_c_api build output found under zvec/build" | |
| exit 1 | |
| fi | |
| if [ -z "$(find_runtime_lib "$ZVEC_LIB_DIR")" ]; then | |
| for b in \ | |
| "$GITHUB_WORKSPACE/zvec/build/bin/Release" \ | |
| "$GITHUB_WORKSPACE/zvec/build/bin"; do | |
| if [ -d "$b" ] && [ -n "$(find_runtime_lib "$b")" ]; then | |
| echo "Copying runtime library from $b into $ZVEC_LIB_DIR" | |
| find "$b" -maxdepth 1 -type f -name '*zvec_c_api*' \ | |
| \( -name '*.dll' -o -name '*.so*' -o -name '*.dylib' \) \ | |
| -exec cp {} "$ZVEC_LIB_DIR"/ \; | |
| break | |
| fi | |
| done | |
| fi | |
| if [ -z "$(find_runtime_lib "$ZVEC_LIB_DIR")" ]; then | |
| echo "::error::zvec_c_api runtime library (.dll/.so/.dylib) is not present in $ZVEC_LIB_DIR" | |
| find "$GITHUB_WORKSPACE/zvec/build" -maxdepth 2 -name '*zvec_c_api*' -print | |
| exit 1 | |
| fi | |
| echo "ZVEC_LIB_DIR=$ZVEC_LIB_DIR" >> "$GITHUB_ENV" | |
| echo "=== zvec_c_api files in $ZVEC_LIB_DIR ===" | |
| find "$ZVEC_LIB_DIR" -maxdepth 1 -name '*zvec_c_api*' -exec ls -la {} \; | |
| # --- Compile the JavaCPP JNI glue and copy libs into target/classes --- | |
| - name: Build JNI glue (mvn package) | |
| shell: bash | |
| run: mvn package -DskipTests -Dzvec.lib.path="$ZVEC_LIB_DIR" -B | |
| # --- Stage the JavaCPP platform bundle for aggregation --- | |
| - name: Stage native bundle | |
| id: stage | |
| shell: bash | |
| run: | | |
| # The javacpp compiler mojo emits the JNI glue + copied libs under | |
| # the bound class' package path (org/zvec/binding/<platform>/). | |
| PLATDIR=$(find target/classes/org/zvec/binding -mindepth 1 -maxdepth 1 -type d -name '*-*' | head -n1) | |
| if [ -z "$PLATDIR" ]; then | |
| # Fallback: root-level platform directory. | |
| PLATDIR=$(find target/classes -mindepth 1 -maxdepth 1 -type d -name '*-*' | head -n1) | |
| fi | |
| if [ -z "$PLATDIR" ]; then | |
| echo "::error::No JavaCPP platform directory found under target/classes" | |
| ls -la target/classes || true | |
| exit 1 | |
| fi | |
| PLATNAME=$(basename "$PLATDIR") | |
| echo "JavaCPP platform: $PLATNAME" | |
| mkdir -p "staging/$PLATNAME" | |
| # Only runtime shared libraries belong in the JAR; the MSVC import | |
| # library and export file the compiler drops next to the glue would | |
| # just add dead weight to every artifact. | |
| find "$PLATDIR" -maxdepth 1 -type f \ | |
| \( -name '*.dll' -o -name '*.so*' -o -name '*.dylib' \) \ | |
| -exec cp {} "staging/$PLATNAME/" \; | |
| if [ -z "$(ls -A "staging/$PLATNAME")" ]; then | |
| echo "::error::No runtime native library staged out of $PLATDIR" | |
| ls -la "$PLATDIR" || true | |
| exit 1 | |
| fi | |
| echo "=== Staged native bundle ($PLATNAME) ===" | |
| ls -la "staging/$PLATNAME/" | |
| - name: Upload native bundle | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: natives-${{ matrix.name }} | |
| path: staging/ | |
| # =========================================================================== | |
| # Aggregate every platform bundle into a single multi-platform fat JAR | |
| # =========================================================================== | |
| build-jar: | |
| name: Build JAR | |
| needs: build-natives | |
| runs-on: ubuntu-24.04 | |
| # This job compiles the linux zvec_c_api and the linux JNI glue that end up | |
| # in every JAR variant, so it has to run in the same manylinux_2_28 image as | |
| # the build-natives linux leg - otherwise the shipped .so files demand | |
| # GLIBC_2.38 and refuse to load on CentOS 8 / RHEL 8 / Ubuntu 20.04. | |
| container: 'quay.io/pypa/manylinux_2_28_x86_64:2026.03.01-1' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| with: | |
| # Submodules are needed: the JavaCPP parser reads the zvec headers to | |
| # regenerate the git-ignored ZvecNative.java, and the compiler links | |
| # the linux JNI glue against the built zvec_c_api. | |
| submodules: recursive | |
| - name: Set up JDK 11 | |
| uses: actions/setup-java@v6 | |
| with: | |
| distribution: temurin | |
| java-version: '11' | |
| cache: maven | |
| # The manylinux image ships no Maven, and the runner's preinstalled | |
| # /usr/share/maven is not visible inside a container job. | |
| - name: Install Maven (Linux container) | |
| shell: bash | |
| run: | | |
| MVN_VERSION=3.9.16 | |
| curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/apache-maven-${MVN_VERSION}-bin.tar.gz" \ | |
| | tar -xz -C /opt | |
| echo "/opt/apache-maven-${MVN_VERSION}/bin" >> "$GITHUB_PATH" | |
| # --- Version-anchored cache for the linux zvec build (JNI glue compile) --- | |
| - name: Resolve zvec submodule commit and version | |
| id: zvec | |
| shell: bash | |
| run: | | |
| echo "sha=$(git -C zvec rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| # zvec/cmake/version.cmake derives the version from `git describe | |
| # --tags` and otherwise falls back to a deliberately obvious dummy | |
| # v0.0.0. In CI that fallback always wins, because the submodule is | |
| # checked out detached and without tags - so the published library | |
| # used to report v0.0.0-g<sha> through Zvec.getVersion(), and 0.0.0 | |
| # through getVersionMajor/Minor/Patch. Feed the release tag in | |
| # explicitly, the way upstream zvec does in _build_prebuilt.yml. | |
| # Anything that is not a vX.Y.Z string is dropped rather than passed | |
| # on: version.cmake raises FATAL_ERROR on a malformed override. | |
| TAG="${{ inputs.tag || github.ref_name }}" | |
| case "$TAG" in v*) ;; *) TAG="v$TAG" ;; esac | |
| if printf '%s' "$TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::'$TAG' is not a vX.Y.Z version; the native build will report zvec's fallback version" | |
| echo "tag=" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Restore-only; see the gated save step after the build step below. | |
| - name: Restore zvec build cache | |
| id: cache-zvec | |
| uses: actions/cache/restore@v6 | |
| with: | |
| path: zvec/build | |
| key: zvec-build-linux-x64-manylinux_2_28-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }} | |
| - name: Install build tools | |
| if: steps.cache-zvec.outputs.cache-hit != 'true' | |
| shell: bash | |
| run: | | |
| # setup-python cannot be used here: the CPython it downloads is built | |
| # for the host glibc and will not run inside this glibc 2.28 image. | |
| # Use the image's bundled CPython, as upstream zvec does. | |
| PYBIN=$(ls -d /opt/python/cp3*/bin | head -1) | |
| echo "$PYBIN" >> "$GITHUB_PATH" | |
| export PATH="$PYBIN:$PATH" | |
| python3 -m pip install --upgrade pip cmake==3.30.0 ninja==1.11.1 | |
| echo "$(python3 -c 'import site; print(site.USER_BASE)')/bin" >> "$GITHUB_PATH" | |
| - name: Build C-API library (linux) | |
| if: steps.cache-zvec.outputs.cache-hit != 'true' | |
| shell: bash | |
| env: | |
| ZVEC_TAG: ${{ steps.zvec.outputs.tag }} | |
| run: | | |
| NPROC=$(nproc 2>/dev/null || echo 2) | |
| cd zvec | |
| mkdir -p build && cd build | |
| cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_C_BINDINGS=ON \ | |
| -DOVERRIDE_GIT_DESCRIBE="$ZVEC_TAG" -G Ninja | |
| cmake --build . -j$NPROC --target zvec_c_api | |
| touch .zvec-build-ok | |
| # Save as soon as the build is known good, so a later failure (fat JAR | |
| # assembly, deploy) still leaves a reusable cache for the next run. | |
| # continue-on-error absorbs the 409 raised when a concurrent run already | |
| # stored this key. | |
| - name: Save zvec build cache | |
| if: always() && steps.cache-zvec.outputs.cache-hit != 'true' && hashFiles('zvec/build/.zvec-build-ok') != '' | |
| uses: actions/cache/save@v6 | |
| continue-on-error: true | |
| with: | |
| path: zvec/build | |
| key: zvec-build-linux-x64-manylinux_2_28-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }} | |
| - name: Download prebuilt native bundles | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: natives-* | |
| path: native-artifacts | |
| - name: Stage foreign-platform native libraries into resources | |
| shell: bash | |
| run: | | |
| mkdir -p src/main/resources/org/zvec/binding | |
| # Stage every platform bundle EXCEPT linux-x86_64, whose glue the mvn | |
| # build below compiles locally. linux-arm64 cannot be built here - the | |
| # job runs on an x86_64 runner - so it arrives prebuilt from the | |
| # build-natives leg like the macOS and Windows bundles do. Each | |
| # artifact holds a single <javacpp-platform>/ directory, and natives | |
| # land under the JavaCPP package path so the JAR layout is uniform | |
| # across platforms (org/zvec/binding/<platform>/). | |
| find native-artifacts -mindepth 2 -maxdepth 2 -type d \ | |
| ! -name 'linux-x86_64' -exec cp -r {} src/main/resources/org/zvec/binding/ \; | |
| echo "=== Foreign platforms staged into resources ===" | |
| ls -la src/main/resources/org/zvec/binding/ || true | |
| - name: Set project version from tag | |
| shell: bash | |
| run: | | |
| TAG="${{ inputs.tag }}" | |
| VERSION="${TAG#v}" | |
| mvn versions:set -DnewVersion="$VERSION" -DgenerateBackupPoms=false -B | |
| echo "Set Maven version to $VERSION" | |
| # Full build: the JavaCPP parser regenerates ZvecNative.java (git-ignored) | |
| # from the zvec headers, and the compiler builds the linux JNI glue into | |
| # target/classes/linux-x86_64/. The macOS/Windows libraries staged into | |
| # resources above are bundled alongside, yielding a multi-platform fat JAR. | |
| - name: Build multi-platform fat JAR | |
| shell: bash | |
| run: mvn package -DskipTests -Dzvec.lib.path="$GITHUB_WORKSPACE/zvec/build/lib" -B | |
| - name: Verify JAR bundles all platforms | |
| shell: bash | |
| run: | | |
| JAR=$(ls target/zvec-java-*-with-dependencies.jar | head -n1) | |
| ENTRIES=$(jar tf "$JAR") | |
| echo "=== Native libraries inside $JAR ===" | |
| echo "$ENTRIES" | grep -E '(macosx|linux|windows).*\.(dylib|so|dll)$' | sort | |
| # Both halves of the native stack have to be there for every platform: | |
| # the JavaCPP JNI glue and the zvec C-API runtime library. Asserting | |
| # only "some native file exists" lets a Windows bundle that ships | |
| # jniZvecNative.dll without zvec_c_api.dll pass and then blow up with | |
| # an UnsatisfiedLinkError on the consumer's machine. | |
| # Exact JavaCPP platform directory names: a bare "linux" prefix would | |
| # let a JAR carrying only linux-x86_64 satisfy the linux-arm64 check. | |
| for entry in macosx-arm64:dylib linux-x86_64:so linux-arm64:so windows-x86_64:dll; do | |
| p="${entry%%:*}" | |
| ext="${entry##*:}" | |
| for lib in jniZvecNative zvec_c_api; do | |
| if ! echo "$ENTRIES" | grep -qE "${p}/(lib)?${lib}\.${ext}(\.[0-9.]+)?$"; then | |
| echo "::error::${lib} runtime library for platform '$p' is missing from the fat JAR" | |
| exit 1 | |
| fi | |
| done | |
| done | |
| echo "=== Bundled jieba FTS dict inside $JAR ===" | |
| echo "$ENTRIES" | grep 'zvec/jieba_dict/' || true | |
| for f in jieba.dict.utf8 hmm_model.utf8; do | |
| if ! echo "$ENTRIES" | grep -q "zvec/jieba_dict/$f"; then | |
| echo "::error::jieba dict file zvec/jieba_dict/$f missing from the fat JAR" | |
| exit 1 | |
| fi | |
| done | |
| - name: Verify the linux natives stay glibc 2.28 compatible | |
| shell: bash | |
| run: | | |
| # The linux libraries are built in manylinux_2_28 so they keep loading | |
| # on CentOS 8 / RHEL 8 / Ubuntu 20.04. Building them on the bare | |
| # runner instead links glibc 2.39 and produces a library that demands | |
| # GLIBC_2.38 - loadable only on Ubuntu 23.10+, Debian 13 and Fedora | |
| # 38+. Check the exact files every JAR variant packs: the main JAR, | |
| # the per-platform classifier JARs and the shaded one. | |
| LIMIT=2.28 | |
| if ! command -v readelf >/dev/null 2>&1; then | |
| echo "::warning::readelf is unavailable, skipping the glibc floor check" | |
| exit 0 | |
| fi | |
| # linux-x86_64 is compiled by this job; linux-arm64 arrives prebuilt | |
| # from its build-natives leg. readelf rather than objdump because | |
| # objdump in this x86_64 image has no aarch64 backend. | |
| for LIBDIR in \ | |
| target/classes/org/zvec/binding/linux-x86_64 \ | |
| src/main/resources/org/zvec/binding/linux-arm64; do | |
| if [ -z "$(find "$LIBDIR" -maxdepth 1 -name '*.so' -print -quit 2>/dev/null)" ]; then | |
| echo "::error::no linux native library found under $LIBDIR" | |
| exit 1 | |
| fi | |
| for so in "$LIBDIR"/*.so; do | |
| MAX=$(readelf -V "$so" 2>/dev/null | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sed 's/GLIBC_//' | sort -uV | tail -1) | |
| echo "$LIBDIR/$(basename "$so"): highest versioned GLIBC symbol = ${MAX:-none}" | |
| if [ -n "$MAX" ] && [ "$(printf '%s\n%s\n' "$MAX" "$LIMIT" | sort -V | tail -1)" != "$LIMIT" ]; then | |
| echo "::error::$so requires GLIBC_$MAX but the floor is GLIBC_$LIMIT - it was not built inside a manylinux_2_28 container" | |
| exit 1 | |
| fi | |
| done | |
| done | |
| - name: Upload JAR artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: zvec-java-jars | |
| path: | | |
| target/zvec-java-*.jar | |
| # ------------------------------------------------------------------ | |
| # Maven Central deployment: all-platform main JAR + per-platform | |
| # classifier JARs + nolib JAR (layout follows org.duckdb:duckdb_jdbc), | |
| # opt-in via the deploy_central input. Requires repository secrets: | |
| # CENTRAL_TOKEN_USER / CENTRAL_TOKEN_PASSWORD (portal user token) | |
| # GPG_SIGNING_KEY (base64 armored private key) / GPG_SIGNING_PASSPHRASE | |
| # The deploy re-runs the build with the release profiles, attaching | |
| # sources + javadoc JARs, per-platform classifier JARs, the nolib JAR | |
| # and GPG signatures, then uploads one deployment bundle to the | |
| # Sonatype Central Portal. | |
| # ------------------------------------------------------------------ | |
| - name: Import GPG signing key | |
| if: inputs.deploy_central | |
| env: | |
| GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }} | |
| run: | | |
| # This job now runs inside the manylinux image, which is not | |
| # guaranteed to ship gpg; maven-gpg-plugin needs it to sign. | |
| command -v gpg >/dev/null 2>&1 || dnf install -y gnupg2 >/dev/null \ | |
| || yum install -y gnupg2 >/dev/null | |
| echo "$GPG_SIGNING_KEY" | base64 -d | gpg --batch --import | |
| - name: Write Central Portal settings.xml | |
| if: inputs.deploy_central | |
| run: | | |
| cat > central-settings.xml <<'XML' | |
| <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"> | |
| <servers> | |
| <server> | |
| <id>central</id> | |
| <username>${env.CENTRAL_TOKEN_USER}</username> | |
| <password>${env.CENTRAL_TOKEN_PASSWORD}</password> | |
| </server> | |
| </servers> | |
| </settings> | |
| XML | |
| - name: Deploy to Maven Central | |
| if: inputs.deploy_central | |
| env: | |
| CENTRAL_TOKEN_USER: ${{ secrets.CENTRAL_TOKEN_USER }} | |
| CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} | |
| GPG_SIGNING_PASSPHRASE: ${{ secrets.GPG_SIGNING_PASSPHRASE }} | |
| run: | | |
| mvn -s central-settings.xml -B deploy -Prelease,release-natives \ | |
| -DskipTests \ | |
| -Dzvec.lib.path="$GITHUB_WORKSPACE/zvec/build/lib" \ | |
| -Dgpg.passphrase="$GPG_SIGNING_PASSPHRASE" | |
| # =========================================================================== | |
| # Load the built JAR on the glibc floor we advertise, before anyone publishes | |
| # =========================================================================== | |
| # build-jar uploads the deployment bundle to the Central Portal, but the | |
| # release profile sets autoPublish=false with waitUntil=validated, so the | |
| # bundle stops at VALIDATED and nothing reaches Maven Central until a human | |
| # clicks "Publish". This job runs inside that window: it is the last signal | |
| # before that button gets pressed. | |
| # | |
| # It runs inside manylinux_2_28 on both linux architectures rather than on a | |
| # bare runner, deliberately. The runners' own glibc 2.39 is newer than | |
| # anything the README claims to support, so a pass there would prove nothing; | |
| # glibc 2.28 is the documented floor, and a .so linked against something | |
| # newer dies here with exactly the UnsatisfiedLinkError a consumer would hit. | |
| # | |
| # Linux only: macOS and Windows have no equivalent ABI floor to defend, and | |
| # their runners already match upstream zvec's. | |
| smoke-test: | |
| name: Smoke test (${{ matrix.name }}) | |
| needs: build-jar | |
| runs-on: ${{ matrix.runner }} | |
| container: ${{ matrix.container }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: linux-x86_64 | |
| runner: ubuntu-24.04 | |
| container: 'quay.io/pypa/manylinux_2_28_x86_64:2026.03.01-1' | |
| - name: linux-arm64 | |
| runner: ubuntu-24.04-arm | |
| container: 'quay.io/pypa/manylinux_2_28_aarch64:2026.03.01-1' | |
| steps: | |
| # No submodules here: the smoke test runs against the built JAR, not | |
| # against the sources, so the zvec checkout is not needed. | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Set up JDK 11 | |
| uses: actions/setup-java@v6 | |
| with: | |
| distribution: temurin | |
| java-version: '11' | |
| - name: Download the built JARs | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: zvec-java-jars | |
| path: jars | |
| - name: Report the runtime under test | |
| shell: bash | |
| run: | | |
| echo "arch : $(uname -m)" | |
| echo "glibc : $(getconf GNU_LIBC_VERSION 2>/dev/null || ldd --version | head -1)" | |
| java -version | |
| - name: Load the JAR and exercise the database | |
| shell: bash | |
| run: | | |
| MAIN_JAR=$(find jars -maxdepth 1 -type f -name 'zvec-java-*.jar' \ | |
| ! -name '*-with-dependencies.jar' | head -1) | |
| if [ -z "$MAIN_JAR" ]; then | |
| echo "::error::no main zvec-java JAR in the downloaded artifact" | |
| ls -la jars | |
| exit 1 | |
| fi | |
| # Test the multi-platform JAR rather than a classifier: that is what a | |
| # consumer gets with no classifier, and JavaCPP has to pick this | |
| # runner's own platform directory out of it, so the resolution path | |
| # under test is the real one. The script resolves javacpp itself and | |
| # falls through to a direct repo1 download, since the image has no | |
| # ~/.m2 and ships no Maven. | |
| # The tag is the version the native libraries were compiled with, so | |
| # this also guards the OVERRIDE_GIT_DESCRIBE plumbing above: a build | |
| # that silently fell back to zvec's v0.0.0 dummy fails right here. | |
| bash scripts/smoke-test.sh --jar "$MAIN_JAR" \ | |
| --expect-version "${{ inputs.tag }}" |