Skip to content

enhance: unify C++ build for all language bindings - #469

Open
tedxu wants to merge 1 commit into
milvus-io:mainfrom
tedxu:enhance/unified-build
Open

enhance: unify C++ build for all language bindings#469
tedxu wants to merge 1 commit into
milvus-io:mainfrom
tedxu:enhance/unified-build

Conversation

@tedxu

@tedxu tedxu commented Apr 1, 2026

Copy link
Copy Markdown
Collaborator

Build libmilvus-storage.so once with all deps shared and WITH_JNI
on by default, eliminating separate python-lib and java-lib targets
that each triggered a full C++ rebuild.

Build system changes:

  • Remove WITH_PYTHON_BINDING option and visibility/version-script
    blocks (Python's RTLD_LOCAL provides the same isolation)
  • Make all deps shared by default in conanfile.py to prevent JVM
    TLS exhaustion from static PT_TLS segments
  • Change WITH_JNI default to ON with FATAL_ERROR for missing
    JAVA_HOME
  • Remove redundant -Wl,-z,lazy on base library and macOS folly
    force-load workaround (both unnecessary with shared deps)
  • Decouple loon CLI from WITH_UT, add RPATH and install target
  • Add make dist target for relocatable C++ distribution tarball
  • Add libs/ to Rust build.rs link search path and rpath

CI changes (new unified ci.yml replaces cpp-ci.yml, python-ci.yml,
java-ci.yml):

  • One build job (with ASAN) shared by parallel test-cpp,
    test-python, and test-java jobs via artifact upload
  • Separate lint-cpp (with JDK for JNI coverage) and lint-python
  • test-java now runs sbt test instead of just sbt compile
  • Incorporates azurite tests from upstream azure FS work

Release changes (release.yml):

  • Single build job fans out to package-python and package-java
  • Publish jobs and GitHub release follow packaging

Test plan:

  • CI workflow validates build + all language tests pass
  • lint-cpp checks JNI code with clang-tidy
  • Azurite tests included alongside minio tests

@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: tedxu

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@tedxu
tedxu force-pushed the enhance/unified-build branch 2 times, most recently from feaf2a8 to 3928e02 Compare April 2, 2026 11:25
@codecov

codecov Bot commented Apr 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.77778% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.32%. Comparing base (f8fe09b) to head (0434edc).

Files with missing lines Patch % Lines
python/milvus_storage/_ffi.py 77.77% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #469      +/-   ##
==========================================
- Coverage   72.84%   70.32%   -2.53%     
==========================================
  Files         133      133              
  Lines       13300    13282      -18     
  Branches     1975     1972       -3     
==========================================
- Hits         9689     9341     -348     
- Misses       3611     3941     +330     
Flag Coverage Δ
cpp 75.25% <ø> (-0.73%) ⬇️
python 26.32% <77.77%> (-18.33%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tedxu
tedxu force-pushed the enhance/unified-build branch 11 times, most recently from b711be2 to 92d3825 Compare April 9, 2026 07:21
@tedxu
tedxu force-pushed the enhance/unified-build branch 11 times, most recently from 1ec1e66 to c9560a2 Compare April 15, 2026 06:54
@tedxu
tedxu force-pushed the enhance/unified-build branch from c9560a2 to ee543d2 Compare April 15, 2026 07:18
@jiaqizho

Copy link
Copy Markdown
Collaborator

why Failing after 1s — 71.00% (target 75.00%) ?

project code coverage is required. is that some of CI have not been running ?

@tedxu

tedxu commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator Author

why Failing after 1s — 71.00% (target 75.00%) ?

project code coverage is required. is that some of CI have not been running ?

The test TestS3Options is disabled, it causes memory issue if ASAN is turned on and some libraries being linked shared. I've already commented inline.

@tedxu
tedxu force-pushed the enhance/unified-build branch 4 times, most recently from 51668c4 to a8dfe81 Compare April 17, 2026 08:28
Consolidate three separate CI workflows (cpp-ci, python-ci, java-ci)
into a single ci.yml with a shared build matrix. The C++ library is
built once in two variants (test and release) and reused by all
downstream jobs.

Make all deps shared by default in conanfile.py to prevent JVM TLS
exhaustion from static PT_TLS segments when loaded via dlopen, and
to avoid duplicate allocator state between Arrow and aws-sdk-cpp.

Add FFI_EXPORT_ONLY option to hide C++ symbols via linker version
script for Python/Java release builds. Skip loon CLI in this mode
since it requires C++ internals.

Move ASAN definitions before test subdirectory in CMakeLists.txt so
that RUNNING_WITH_ASAN is visible to test targets. Skip TestS3Options
and TestClientBuilder's S3Options::Defaults() under ASAN to avoid
false-positive bad-free in shared aws-sdk-cpp.

Add os._exit atexit handler on Linux to prevent C++ static destructor
crashes during Python interpreter shutdown.

Other changes:
- Remove redundant python-lib / java-lib Makefile targets
- Add make dist target for relocatable tarball
- Add loon RPATH and install support
- Add rust/build.rs search path for shared deps in libs/
- Align WITH_FIU Makefile default to False (matches conanfile)

Signed-off-by: Ted Xu <ted.xu@zilliz.com>
@tedxu
tedxu force-pushed the enhance/unified-build branch from a8dfe81 to 0434edc Compare April 17, 2026 09:57
@jiaqizho

Copy link
Copy Markdown
Collaborator

A few concerns about cpp/scripts/make_dist.sh — some are release-correctness bugs worth addressing before this lands, others are quality issues.

🔴 macOS path produces a broken tarball

The RPATH-fixup branch is Linux-only:

if [ "${OS}" = "linux" ] && command -v patchelf &>/dev/null; then
  patchelf --set-rpath '\$ORIGIN/../lib' "${DIST_DIR}/bin/loon" 2>/dev/null || true
  ...
fi

But the script also detects dylib and names the tarball milvus-storage-<ver>-darwin-<arch>.tar.gz, so the intent is clearly to support macOS. On darwin the equivalent is install_name_tool -add_rpath @loader_path/../lib on bin/loon plus install_name_tool -id/-change on the dylibs; without that, the darwin tarball untars into binaries that fail to load their libraries at runtime. Either add the macOS branch or fail fast if OS=darwin.

🔴 Silent failures under set -euo pipefail

Several paths where a missing output yields a "successful" tarball that's actually broken:

  • patchelf ... 2>/dev/null || true — if patchelf is missing or fails, the error is swallowed and we publish a tarball with unfixed RPATH. This is the opposite of what set -e buys us.
  • loon missing → WARNING and continue (tarball has no CLI).
  • libs/ missing → WARNING and continue (tarball has no shared deps).
  • .pc file missing → not even a warning.

A distribution script shouldn't allow partial success. Suggest dropping the || true, asserting patchelf/install_name_tool up front, and converting the WARNING branches into hard errors unless there's a specific reason the component is optional.

🔴 The .pc file has build-machine absolute paths

cpp/src/libstorage.pc.in:

libdir=@CMAKE_INSTALL_FULL_LIBDIR@
includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@

These get expanded at configure time to absolute paths from the CI build machine. Copied verbatim into the tarball by the script, `pkg-config --cflags --libs libstorage` on the consumer side returns those CI paths. Either template with \${pcfiledir} (relative), or let cmake --install regenerate it with the correct prefix (see next point).

🟡 CMake install() rules already exist and are being bypassed

cpp/CMakeLists.txt already declares:

install(TARGETS milvus-storage DESTINATION ...)
install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/milvus-storage" DESTINATION ...)
install(FILES "...${module}.pc" DESTINATION ...)

and this PR adds install(TARGETS loon DESTINATION bin) in cpp/tools/CMakeLists.txt. But the dist script never calls cmake --install — it manually cps everything in bash, which (a) ignores the rules already written, (b) reimplements .pc path substitution incorrectly (per above), and (c) will drift as the CMake rules evolve. Suggest:

cmake --install "${BUILD_DIR}" --prefix "${DIST_DIR}"
# then the script only needs to copy conan's libs/ + produce the tarball

or just use CPack (cpack -G TGZ), which turns the same install() rules into a tarball for free and handles RPATH/symlinks correctly.

🟡 Version extraction is fragile

sed -n 's/^project(milvus-storage VERSION \([^ )]*\).*/\1/p' CMakeLists.txt

Requires project(...) on a single line with exact spacing. If anyone reformats to the multi-line form cmake-format prefers, this silently returns empty and the script exits. Prefer a dedicated VERSION file or a configure_file-generated version.txt as the source of truth.

🟡 Generated headers will be missed

cp -r "${SRC_DIR}/include/milvus-storage" "${DIST_DIR}/include/"

Only copies source-tree headers. Any configure_file-generated or proto-generated headers live in the build tree and will be absent. If the public API references any of them, downstream builds fail with header not found. Using cmake --install avoids this because install(DIRECTORY ...) + target-specific PUBLIC_HEADER rules handle both cases.

🟠 No runtime sanity check

After assembling the tarball, a minimal guard like:

ldd "${DIST_DIR}/lib/libmilvus-storage.so" | grep "not found" && exit 1
# and otool -L equivalent on darwin

would catch cases where a shared dep got added to conanfile.py but not imported to libs/, or where a conan imports symlink is dangling. Right now a tarball "produced successfully" doesn't imply it runs.

🟠 Dangling symlinks from conan imports

cp -a "${LIBS_DIR}/*" preserves symlinks. If conan imports leaves absolute-path symlinks (e.g. libarrow.so -> /home/runner/.conan2/...), they become dangling after copy. Worth adding:

find "${DIST_DIR}/lib" -xtype l -print | grep . && { echo "dangling symlinks"; exit 1; }

🟠 Inconsistent arch naming

uname -m returns aarch64 on Linux arm and arm64 on macOS arm — two names for the same ISA in the tarball filename. Worth normalizing once multi-arch release starts.

🟠 No checksum / signature alongside tarball

Standard release hygiene: a .sha256 next to the tarball (and optionally .asc). Can be a follow-up.


Scope suggestion

The dist flow is orthogonal to the stated thesis of this PR ("unify C++ build for all language bindings"). Given the macOS and silent-failure issues above, would it make sense to split cpp/scripts/make_dist.sh + install(TARGETS loon ...) + the make dist target into a follow-up PR? The main build unification doesn't depend on dist shipping in this one, and dist could use its own review cycle (ideally driven by `cmake --install`/CPack rather than hand-rolled bash).

if sys.platform == "linux":
_orig_exit = sys.exit

def _patched_exit(code=0):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sys.exit / atexit → os._exit hack in _ffi.py concerns me. A few points worth discussing before we merge:

What it actually does. The os._exit in the atexit handler bypasses Py_FinalizeEx and __cxa_finalize, which is what avoids the C++ static-destructor crash. The sys.exit monkey-patch is purely to preserve the exit code through
that path (without it, any sys.exit(N) would get rewritten to exit 0 by the os._exit(0) in the handler). So the "fix" is really two coupled hacks, not one.

Why I think this is the wrong layer to fix it.

  1. It masks a real bug, not a spurious one. A segfault during C++ static destruction means some global singleton in aws-sdk-cpp / Arrow / folly is touching state that was already torn down. That's a real ordering/lifetime bug
    in our shutdown sequence, and it will bite us in any embedding that isn't CPython — most notably the JNI path, which loads the same libmilvus-storage.so and has none of this protection. So we haven't actually fixed the crash,
    we've just hidden it from Python CI while leaving Java users exposed.
  2. It mutates global interpreter state on import. Any process that does import milvus_storage now has its sys.exit rebound and an os._exit atexit handler registered, regardless of whether the user wanted that behavior. This is
    especially unfriendly in notebooks, long-running services, and test runners where milvus_storage is one of many libraries sharing the interpreter.
  3. It silently swallows normal shutdown side effects. os._exit skips stdio flushing, tempfile cleanup, logging.shutdown, thread joins, and every atexit handler registered after ours. For a library that may be imported inside
    someone else's application, this is a pretty invasive default.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the root case here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shared linkage introduced this issue, and we have some known resource allocation left-overs: the entries in loon fs cache, and the clients initialized globally in aws sdk, etc. We need a exit recall anyway. I've tried releasing the mentioned resources but those are not the complete set.

Comment thread cpp/conanfile.py
self.options["zstd"].shared = True
self.options["glog"].shared = True
self.options["gflags"].shared = True
# Ensure aws-c-* are shared to avoid duplicate allocator

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already defined "aws-c-*/*:shared": True, in default_options. And just define all shared libs into default_options. Do not split the same logical into different function.

// DefaultAWSCredentialsProviderChain triggers a bad-free inside the
// AWS CRT TLS init path when aws-sdk-cpp is a shared lib not built
// with ASAN. Skip the entire test under ASAN.
GTEST_SKIP() << "Skipped under ASAN (AWS CRT shared lib false positive)";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

won‘t get this ASAN in linux? This is just a false alarm on OSX.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test breaks only under Linux, not OSX, and the issue is introduced by DefaultAWSCredentialsProviderChain described in the comment.

Comment thread .github/workflows/ci.yml
@@ -0,0 +1,291 @@
name: CI

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can use the orchestrator + reusable workflows to split the different logical into different yaml files?

ex.

  .github/workflows/
    ci.yml              ← orchestration
    _build.yml          ← workflow_call,build debug and release so
    _test-cpp.yml       ← workflow_call,do cpp test 
    _test-python.yml    ← workflow_call,dp python test 
    _test-java.yml      ← workflow_call,do java test 
    _lint-python.yml    ← workflow_call, do python lint

ci.yml

  name: CI
  on:
    push:
      paths: ['cpp/**', 'python/**', 'java/**', '.github/workflows/**']
    pull_request:
      paths: ['cpp/**', 'python/**', 'java/**', '.github/workflows/**']

  jobs:
    build:
      uses: ./.github/workflows/_build.yml

    test-cpp:
      needs: build
      uses: ./.github/workflows/_test-cpp.yml

    test-python:
      needs: build
      uses: ./.github/workflows/_test-python.yml

    test-java:
      needs: build
      uses: ./.github/workflows/_test-java.yml

    lint-python:
      uses: ./.github/workflows/_lint-python.yml
...

Comment thread .github/workflows/ci.yml

- name: Run tests (azurite)
working-directory: ./cpp
run: source ./scripts/azurite_env.sh && ./build/Release/test/milvus_test

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TEST_FFI not running

Comment thread .github/workflows/ci.yml
run: ./build/Release/test/milvus_test && ./build/Release/test/Test_FFI

- name: Run tests (minio)
working-directory: ./cpp

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TEST_FFI not running..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants