Skip to content

ci(release): find the Windows DLL where CMake actually puts it #2

ci(release): find the Windows DLL where CMake actually puts it

ci(release): find the Windows DLL where CMake actually puts it #2

Workflow file for this run

name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (e.g., v1.0.0)'
required: true
type: string
permissions:
contents: write
jobs:
# ===========================================================================
# Build pre-compiled C libraries for all supported platforms
# ===========================================================================
build-libs:
name: Build (${{ matrix.name }})
runs-on: ${{ matrix.runner }}
# The Linux leg runs inside the same manylinux_2_28 image upstream zvec uses
# for its own prebuilt SDK (zvec/.github/workflows/_build_prebuilt.yml), so
# the libzvec_c_api.so attached to the release still loads on glibc 2.28
# distros - CentOS 8, RHEL 8, Ubuntu 20.04. On the bare ubuntu-24.04 runner
# it links glibc 2.39 and ends up demanding GLIBC_2.38.
container: ${{ matrix.container || null }}
# Longest observed leg is windows-x64 at ~1h11m.
timeout-minutes: 150
strategy:
fail-fast: false
matrix:
include:
- name: darwin-arm64
runner: macos-15
platform: darwin_arm64
lib_name: libzvec_c_api.dylib
artifact_name: zvec-libs-darwin-arm64
container: ''
cache_abi: host
- name: linux-x64
runner: ubuntu-24.04
platform: linux_amd64
lib_name: libzvec_c_api.so
artifact_name: zvec-libs-linux-x64
# 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
platform: linux_arm64
lib_name: libzvec_c_api.so
artifact_name: zvec-libs-linux-arm64
# 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
platform: windows_amd64
lib_name: zvec_c_api.dll
artifact_name: zvec-libs-windows-x64
container: ''
cache_abi: host
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
# A tag push already checks out the tag; a workflow_dispatch run
# would otherwise check out the default branch and build libraries
# that do not correspond to the tag they get attached to.
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
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. The linux leg
# needs it for the "Verify Maven compile with vendor libs" step below.
- 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 (same key family as publish-jar.yml, so
# both workflows share one cache): keyed by the zvec submodule commit
# and the release tag, which 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 released 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
- name: Restore zvec build cache
id: cache-zvec
uses: actions/cache/restore@v6
with:
path: zvec/build
# cache_abi keeps the host-glibc and manylinux build trees apart, and
# the tag keeps builds with different OVERRIDE_GIT_DESCRIBE values
# apart, since that string is compiled into the library.
key: zvec-build-${{ matrix.name }}-${{ matrix.cache_abi }}-${{ steps.zvec.outputs.sha }}-${{ steps.zvec.outputs.tag }}
# --- Unix build tools ---
- 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'
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"
shell: bash
# --- Windows build tools ---
- name: Set up MSVC (Windows)
if: runner.os == 'Windows'
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 (pinned by SHA; dependabot keeps it current)
- name: Install CMake and Ninja (Windows)
if: runner.os == 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true'
run: pip install cmake==3.30.0 ninja==1.11.1
shell: pwsh
# --- Build C-API library (skipped on cache hit) ---
- name: Build C-API library (Unix)
if: runner.os != 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true'
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
shell: bash
- name: Build C-API library (Windows)
if: runner.os == 'Windows' && steps.cache-zvec.outputs.cache-hit != 'true'
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
shell: pwsh
# Save as soon as the build is known good; the marker file is written
# only by a successful build, so a later failure still leaves a reusable
# cache. continue-on-error absorbs the 409 raised when a concurrent run
# (e.g. a manual Publish JAR run sharing the same key) already stored it.
- 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 }}
# --- Package libraries ---
- name: Package libraries (Unix)
if: runner.os != 'Windows'
run: |
PLATFORM="${{ matrix.platform }}"
mkdir -p staging/$PLATFORM staging/include/zvec
# Copy shared library
cp zvec/build/lib/${{ matrix.lib_name }} staging/$PLATFORM/
echo "✓ Copied ${{ matrix.lib_name }}"
# Copy public header
cp zvec/src/include/zvec/c_api.h staging/include/zvec/
echo "✓ Copied c_api.h"
# Jieba dict: the jieba FTS tokenizer needs these files at runtime;
# ship them under data/jieba_dict, mirroring the zvec SDK layout.
mkdir -p staging/data/jieba_dict
cp zvec/thirdparty/cppjieba/cppjieba-5.6.7/dict/jieba.dict.utf8 staging/data/jieba_dict/
cp zvec/thirdparty/cppjieba/cppjieba-5.6.7/dict/hmm_model.utf8 staging/data/jieba_dict/
echo "✓ Copied jieba dict"
# Apache-2.0 4(d): this archive redistributes the zvec library and the
# cppjieba dictionary, so it carries zvec's own license and attribution
# notices along with them.
cp zvec/LICENSE staging/LICENSE
cp zvec/NOTICE staging/NOTICE
echo "✓ Copied LICENSE + NOTICE"
ls -la staging/$PLATFORM/
du -h staging/$PLATFORM/${{ matrix.lib_name }}
shell: bash
- name: Verify the linux library stays glibc 2.27 compatible
if: runner.os == 'Linux'
shell: bash
run: |
# The README advertises a glibc 2.27 floor (Ubuntu 18.04+, Debian 10+,
# RHEL/CentOS 8+, Fedora 28+) and the manylinux_2_28 toolchain only
# references symbols up to GLIBC_2.27 today, so pin exactly that.
LIMIT=2.27
SO="staging/${{ matrix.platform }}/${{ matrix.lib_name }}"
MAX=$(readelf -V "$SO" | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sed 's/GLIBC_//' | sort -uV | tail -1)
echo "$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 documented floor is GLIBC_$LIMIT - either it was not built inside the manylinux_2_28 container, or zvec started using a newer symbol (raise LIMIT and the README floor together)"
exit 1
fi
- name: Package libraries (Windows)
if: runner.os == 'Windows'
# bash rather than pwsh: the same library is resolved in bash by the
# Publish JAR workflow's "Resolve zvec lib directory" step, and Git Bash
# is present on every Windows runner (the license guard below already
# relies on it). One idiom for both workflows, and a failure here prints
# what is actually on disk - under pwsh the runner's
# ErrorActionPreference=stop made Write-Error end the step before the
# diagnostic listing could run, so the first failure said only
# "DLL not found".
shell: bash
run: |
PLATFORM="${{ matrix.platform }}"
mkdir -p "staging/$PLATFORM" staging/include/zvec
# zvec's CMake sends RUNTIME output (.dll plus executables) to
# build/bin and ARCHIVE/LIBRARY output (.lib, .so, .dylib) to
# build/lib - see zvec/cmake/bazel.cmake - while a Release/
# subdirectory only exists with a multi-config generator. This
# workflow builds with Ninja, so the DLL is in build/bin; search every
# layout anyway and match on a wildcard so a suffixed name resolves.
find_c_api() {
for d in zvec/build/bin zvec/build/bin/Release zvec/build/lib zvec/build/lib/Release; do
[ -d "$d" ] || continue
find "$d" -maxdepth 1 -type f -name "*zvec_c_api*$1" 2>/dev/null | head -n1
done
}
DLL=$(find_c_api .dll | head -n1)
if [ -z "$DLL" ]; then
echo "::error::zvec_c_api DLL not found under zvec/build"
find zvec/build \( -name '*.dll' -o -name '*.lib' \) -print 2>/dev/null | head -40
exit 1
fi
cp "$DLL" "staging/$PLATFORM/"
echo "✓ Copied DLL from $DLL"
# Import library (.lib) - optional but useful for downstream C/C++ consumers
LIB=$(find_c_api .lib | head -n1)
if [ -n "$LIB" ]; then
cp "$LIB" "staging/$PLATFORM/"
echo "✓ Copied import lib from $LIB"
fi
# Public header
cp zvec/src/include/zvec/c_api.h staging/include/zvec/
echo "✓ Copied c_api.h"
# Jieba dict (mirrors the zvec SDK layout data/jieba_dict)
mkdir -p staging/data/jieba_dict
cp zvec/thirdparty/cppjieba/cppjieba-5.6.7/dict/jieba.dict.utf8 staging/data/jieba_dict/
cp zvec/thirdparty/cppjieba/cppjieba-5.6.7/dict/hmm_model.utf8 staging/data/jieba_dict/
echo "✓ Copied jieba dict"
# Apache-2.0 4(d): carry zvec's license and attribution notices.
cp zvec/LICENSE staging/LICENSE
cp zvec/NOTICE staging/NOTICE
echo "✓ Copied LICENSE + NOTICE"
ls -la "staging/$PLATFORM/"
# --- License guard ---
# The archives attached to the GitHub Release carry the same zvec_c_api
# binary as the Maven Central JARs, so they get the same check.
- name: Verify no GPL-only RocksDB code is linked into the library
shell: bash
run: |
# RocksDB is dual-licensed Apache-2.0 / GPLv2 and is shipped here under
# Apache-2.0. Its PerconaFT-derived range_tree lock manager
# (utilities/transactions/lock/range/) is the only GPL-exclusive part,
# and zvec never uses pessimistic transactions, so the linker drops
# those objects. grep -a rather than strings/readelf/objdump: grep is
# available on every runner (Git Bash on Windows) and works on ELF,
# Mach-O and PE alike.
SO="staging/${{ matrix.platform }}/${{ matrix.lib_name }}"
GPL_PATTERN='range_tree|locktree|RangeTree|PessimisticTransaction|percona'
if [ ! -f "$SO" ]; then
echo "::error::$SO is missing - nothing to license-check"
exit 1
fi
HITS=$(grep -a -i -c -E "$GPL_PATTERN" "$SO" || true)
echo "$SO: GPL-only RocksDB symbol hits = $HITS"
if [ "$HITS" != "0" ]; then
echo "::error::$SO links the GPL-only RocksDB range_tree code ($HITS hits) and cannot ship under Apache-2.0"
exit 1
fi
# Positive control: prove the scan really looked at the RocksDB-bearing
# runtime library, so a renamed or empty file cannot pass as clean.
ROCKS=$(grep -a -c -E 'rocksdb' "$SO" || true)
echo "$SO: rocksdb hits = $ROCKS (expected > 0)"
if [ "$ROCKS" -eq 0 ]; then
echo "::error::$SO carries no RocksDB symbols - the scan is not looking at the real library"
exit 1
fi
# --- Verify Maven build compiles against staged vendor libs ---
- name: Verify Maven compile with vendor libs (Unix)
if: runner.os != 'Windows'
run: |
PLATFORM="${{ matrix.platform }}"
mkdir -p lib/$PLATFORM lib/include/zvec
cp staging/$PLATFORM/* lib/$PLATFORM/
cp staging/include/zvec/c_api.h lib/include/zvec/
LIB_PATH="${{ github.workspace }}/lib/$PLATFORM"
mvn compile -Dzvec.lib.path="$LIB_PATH" -B
echo "✓ Maven compile succeeded with vendor libraries"
shell: bash
- name: Verify Maven compile with vendor libs (Windows)
if: runner.os == 'Windows'
run: |
$platform = "${{ matrix.platform }}"
New-Item -ItemType Directory -Force -Path "lib/$platform" | Out-Null
New-Item -ItemType Directory -Force -Path "lib/include/zvec" | Out-Null
Copy-Item "staging/$platform/*" "lib/$platform/"
Copy-Item "staging/include/zvec/c_api.h" "lib/include/zvec/"
$libPath = "${{ github.workspace }}\lib\$platform"
$env:PATH = "$libPath;$env:PATH"
mvn compile "-Dzvec.lib.path=$libPath" -B
Write-Host "✓ Maven compile succeeded with vendor libraries"
shell: pwsh
# --- Create release archive ---
- name: Create archive (Unix)
if: runner.os != 'Windows'
run: |
cd staging
tar -czf ../${{ matrix.artifact_name }}.tar.gz .
cd ..
sha256sum ${{ matrix.artifact_name }}.tar.gz > ${{ matrix.artifact_name }}.tar.gz.sha256
echo "=== Archive ==="
ls -la ${{ matrix.artifact_name }}.tar.gz
cat ${{ matrix.artifact_name }}.tar.gz.sha256
shell: bash
- name: Create archive (Windows)
if: runner.os == 'Windows'
run: |
cd staging
Compress-Archive -Path * -DestinationPath ..\${{ matrix.artifact_name }}.zip
cd ..
$hash = (Get-FileHash "${{ matrix.artifact_name }}.zip" -Algorithm SHA256).Hash.ToLower()
"$hash ${{ matrix.artifact_name }}.zip" | Out-File -Encoding ascii "${{ matrix.artifact_name }}.zip.sha256"
Write-Host "=== Archive ==="
Get-ChildItem "${{ matrix.artifact_name }}.zip"
Get-Content "${{ matrix.artifact_name }}.zip.sha256"
shell: pwsh
# --- Upload artifact for the release job ---
- name: Upload artifact (Unix)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact_name }}
path: |
${{ matrix.artifact_name }}.tar.gz
${{ matrix.artifact_name }}.tar.gz.sha256
- name: Upload artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact_name }}
path: |
${{ matrix.artifact_name }}.zip
${{ matrix.artifact_name }}.zip.sha256
# ===========================================================================
# NOTE: the Maven JAR build & Central deployment intentionally live in the
# standalone "Publish JAR" workflow (.github/workflows/publish-jar.yml),
# which is manual-only. Once this release looks good, run it by hand with
# the same tag to build the JAR family and (opt-in) deploy to Maven Central.
# ===========================================================================
# ===========================================================================
# Create GitHub Release with all platform libraries
# ===========================================================================
release:
name: Create Release
needs: [ build-libs ]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
# Same ref as build-libs, so the release notes and the checksums are
# generated from the commit that was actually built.
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
- name: Determine tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
else
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
path: ./artifacts
- name: List artifacts
run: |
echo "=== Downloaded artifacts ==="
find ./artifacts -type f | sort
shell: bash
- name: Generate release notes
id: notes
run: |
TAG="${{ steps.tag.outputs.tag }}"
VERSION="${TAG#v}"
cat > release-notes.md << EOF
## zvec-java ${TAG}
Industrial-grade Java bindings for the [Zvec](https://github.com/alibaba/zvec) vector database C API.
### Pre-built C Libraries
Download the archive for your platform and extract it to the \`lib/\` directory of your project:
| Platform | Archive | Architecture |
|----------|---------|--------------|
| **macOS** | \`zvec-libs-darwin-arm64.tar.gz\` | Apple Silicon (arm64) |
| **Linux** | \`zvec-libs-linux-x64.tar.gz\` | x86-64 |
| **Windows** | \`zvec-libs-windows-x64.zip\` | x86-64 |
### Maven JAR
The JAR family (multi-platform fat JAR, per-platform classifier JARs, nolib JAR) is
published separately from this release: run the **Publish JAR** workflow manually with
the tag \`${TAG}\` once this release has been verified. Afterwards the artifacts are
available from Maven Central.
### Maven Dependency
\`\`\`xml
<dependency>
<groupId>org.zvec</groupId>
<artifactId>zvec-java</artifactId>
<version>${VERSION}</version>
</dependency>
\`\`\`
### Quick Start
\`\`\`bash
# Extract pre-built library to lib/<platform>/
tar -xzf zvec-libs-linux-x64.tar.gz -C lib/
# Run with vendor library
mvn test -Dzvec.lib.path=lib/linux_amd64
\`\`\`
### SHA-256 Checksums
See the \`.sha256\` files attached to this release for verification.
EOF
echo "notes_file=release-notes.md" >> $GITHUB_OUTPUT
shell: bash
- name: Collect release assets
run: |
mkdir -p release-assets
# Platform libraries
find ./artifacts -name "*.tar.gz" -o -name "*.tar.gz.sha256" \
-o -name "*.zip" -o -name "*.zip.sha256" | while read f; do
cp "$f" release-assets/
done
echo "=== Release assets ==="
ls -lh release-assets/
shell: bash
- name: Create GitHub Release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3 (pinned by SHA; it publishes with contents:write)
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: "zvec-java ${{ steps.tag.outputs.tag }}"
body_path: release-notes.md
draft: false
prerelease: ${{ contains(steps.tag.outputs.tag, '-') }}
files: release-assets/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}