Repository navigation
Publish JAR #3
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 }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: darwin-arm64 | |
| runner: macos-15 | |
| - name: linux-x64 | |
| runner: ubuntu-24.04 | |
| - name: windows-x64 | |
| runner: windows-2025 | |
| 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 | |
| # --- Version-anchored cache: keyed by the zvec submodule commit --- | |
| - name: Resolve zvec submodule commit | |
| id: zvec | |
| shell: bash | |
| run: echo "sha=$(git -C zvec rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| # 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 | |
| key: zvec-build-${{ matrix.name }}-${{ steps.zvec.outputs.sha }} | |
| # --- Unix build toolchain --- | |
| - name: Set up Python (for CMake / Ninja) | |
| if: runner.os != 'Windows' && 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: | | |
| python -m pip install --upgrade pip cmake==3.30.0 ninja==1.11.1 | |
| echo "$(python -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 | |
| 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 -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 | |
| run: | | |
| cd zvec | |
| New-Item -ItemType Directory -Force -Path build | Out-Null | |
| cd build | |
| cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_C_BINDINGS=ON -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 }}-${{ steps.zvec.outputs.sha }} | |
| # --- 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 | |
| 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 | |
| # --- Version-anchored cache for the linux zvec build (JNI glue compile) --- | |
| - name: Resolve zvec submodule commit | |
| id: zvec | |
| shell: bash | |
| run: echo "sha=$(git -C zvec rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
| # 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-${{ steps.zvec.outputs.sha }} | |
| - name: Set up Python (for CMake / Ninja) | |
| if: steps.cache-zvec.outputs.cache-hit != 'true' | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.10' | |
| - name: Install build tools | |
| if: steps.cache-zvec.outputs.cache-hit != 'true' | |
| shell: bash | |
| run: | | |
| python -m pip install --upgrade pip cmake==3.30.0 ninja==1.11.1 | |
| echo "$(python -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 | |
| run: | | |
| NPROC=$(nproc 2>/dev/null || echo 2) | |
| cd zvec | |
| mkdir -p build && cd build | |
| cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_C_BINDINGS=ON -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-${{ steps.zvec.outputs.sha }} | |
| - 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 (the linux glue is compiled | |
| # locally by the mvn build below). Each artifact holds a single | |
| # <javacpp-platform>/ directory (e.g. macosx-arm64, windows-x86_64). | |
| # 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-*' -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. | |
| for p in macosx linux windows; do | |
| case "$p" in | |
| macosx) ext='dylib' ;; | |
| linux) ext='so' ;; | |
| windows) ext='dll' ;; | |
| esac | |
| for lib in jniZvecNative zvec_c_api; do | |
| if ! echo "$ENTRIES" | grep -qE "${p}.*${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: 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: 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" |