diff --git a/.github/workflows/core-main-artifact-check.yml b/.github/workflows/core-main-artifact-check.yml new file mode 100644 index 0000000..153759c --- /dev/null +++ b/.github/workflows/core-main-artifact-check.yml @@ -0,0 +1,130 @@ +name: Check chdb-core main artifacts + +# A one-off, pre-release consumer check. The normal Rust workflow continues to +# test its release pin; this one tests four CI artifacts from the same core SHA. +on: + pull_request: + branches: [main] + paths: + - .github/workflows/core-main-artifact-check.yml + - tests/bundled_runtime_hidden.rs + - tests/runtime_faces.rs + +permissions: + contents: read + +jobs: + main_artifacts: + if: github.head_ref == 'codex/chdb-core-26.9.2-rc.2' + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + run_id: '38427250828' + artifact: chdb-artifacts-linux-x86_64 + asset_prefix: linux-x86_64 + - os: ubuntu-24.04-arm + run_id: '38429301339' + artifact: chdb-artifacts-linux-aarch64 + asset_prefix: linux-aarch64 + - os: macos-latest + run_id: '38427008431' + artifact: chdb-artifacts-macos-arm64 + asset_prefix: macos-arm64 + test_min_macos: '12.0' + - os: macos-15-intel + run_id: '38428913578' + artifact: chdb-artifacts-macos-x86_64 + asset_prefix: macos-x86_64 + test_min_macos: '12.0' + runs-on: ${{ matrix.os }} + env: + CARGO_TERM_COLOR: always + CORE_SHA: dec391dcdf788cbecede338a0f305b399d8495e1 + CORE_RUN_ID: ${{ matrix.run_id }} + CORE_ARTIFACT: ${{ matrix.artifact }} + ASSET_PREFIX: ${{ matrix.asset_prefix }} + CHDB_TEST_MIN_MACOS: ${{ matrix.test_min_macos }} + steps: + - uses: actions/checkout@v4 + + - name: Verify source and fetch the exact CI artifact + env: + # A read-only fine-grained token for chdb-core Actions can be added + # as this secret if the Rust repository's token cannot read it. + GH_TOKEN: ${{ secrets.CHDB_CORE_ARTIFACT_READ_TOKEN || github.token }} + run: | + set -euo pipefail + actual=$(gh run view "$CORE_RUN_ID" -R chdb-io/chdb-core \ + --json headSha,conclusion,headBranch \ + --jq '[.headSha,.conclusion,.headBranch] | join(" ")') + if [ "$actual" != "$CORE_SHA success main" ]; then + echo "::error::core run $CORE_RUN_ID is '$actual', expected '$CORE_SHA success main'" + exit 1 + fi + gh run download "$CORE_RUN_ID" -R chdb-io/chdb-core \ + -n "$CORE_ARTIFACT" -D "$RUNNER_TEMP/core-artifact" + + - name: Unpack matching native libraries and headers + run: | + set -euo pipefail + DYNAMIC_DIR="$RUNNER_TEMP/core-dynamic" + STATIC_DIR="$RUNNER_TEMP/core-static" + mkdir -p "$DYNAMIC_DIR" "$STATIC_DIR" + tar -xzf "$RUNNER_TEMP/core-artifact/$ASSET_PREFIX-libchdb.tar.gz" -C "$DYNAMIC_DIR" + tar -xzf "$RUNNER_TEMP/core-artifact/$ASSET_PREFIX-libchdb-static.tar.gz" -C "$STATIC_DIR" + test -s "$DYNAMIC_DIR/libchdb.so" + test -s "$STATIC_DIR/libchdb.a" + cmp "$DYNAMIC_DIR/chdb.h" "$STATIC_DIR/chdb.h" + echo "Using chdb-core $CORE_SHA: $CORE_ARTIFACT from run $CORE_RUN_ID" + + - name: Check formatting and dynamic Clippy + if: matrix.os == 'ubuntu-latest' + env: + CHDB_LIB_DIR: ${{ runner.temp }}/core-dynamic + CHDB_INCLUDE_DIR: ${{ runner.temp }}/core-dynamic + run: | + cargo fmt --check + cargo clippy --all-targets -- --deny warnings + + - name: Build and test dynamic library + env: + CHDB_LIB_DIR: ${{ runner.temp }}/core-dynamic + CHDB_INCLUDE_DIR: ${{ runner.temp }}/core-dynamic + LD_LIBRARY_PATH: ${{ runner.temp }}/core-dynamic + DYLD_LIBRARY_PATH: ${{ runner.temp }}/core-dynamic + RUST_BACKTRACE: full + run: | + cargo build --verbose + cargo test -- --test-threads=1 + cargo test --features durable -- --test-threads=1 + if [ "$RUNNER_OS" = Linux ] && [ "$RUNNER_ARCH" = X64 ]; then + cargo test --features durable-s3 --lib -- --test-threads=1 durable::backends + fi + + - name: Check all-feature Clippy against the static archive + if: matrix.os == 'ubuntu-latest' + env: + CHDB_LIB_DIR: ${{ runner.temp }}/core-static + CHDB_INCLUDE_DIR: ${{ runner.temp }}/core-static + run: cargo clippy --all-targets --all-features -- --deny warnings + + - name: Build, inspect and run statically linked tests + env: + CHDB_LIB_DIR: ${{ runner.temp }}/core-static + CHDB_INCLUDE_DIR: ${{ runner.temp }}/core-static + CHDB_CHECK_HOST_UNWIND: '1' + RUST_BACKTRACE: full + run: | + set -euo pipefail + cargo test --features static --no-run --tests -j 2 --message-format=json > "$RUNNER_TEMP/artifacts.json" + jq -r 'select(.reason == "compiler-artifact" and .profile.test == true and .executable != null) + | .executable' "$RUNNER_TEMP/artifacts.json" > "$RUNNER_TEMP/test-binaries.txt" + test -s "$RUNNER_TEMP/test-binaries.txt" + xargs .github/scripts/check-static-link.sh < "$RUNNER_TEMP/test-binaries.txt" + while read -r binary <&3; do + echo "== $binary" + "$binary" --test-threads=1 < /dev/null + done 3< "$RUNNER_TEMP/test-binaries.txt" + cargo test --features static --doc diff --git a/tests/bundled_runtime_hidden.rs b/tests/bundled_runtime_hidden.rs index 21bcf91..1e78f21 100644 --- a/tests/bundled_runtime_hidden.rs +++ b/tests/bundled_runtime_hidden.rs @@ -1,15 +1,15 @@ -//! The bundled C++ runtime must not be visible from outside the archive. +//! The bundled C++ runtime must not capture symbols used by the host runtime. //! -//! This is the cause the runtime cases in `runtime_faces.rs` observe the effect -//! of, checked directly and without linking anything. +//! The runtime cases in `runtime_faces.rs` check the effect in a linked binary; +//! this checks the archive's symbols directly. //! //! `libchdb.so` / `.dylib` gate their exports at link time, with a version //! script on Linux and an exported-symbols list on macOS, which is why dynamic //! linking never had this problem. An archive has no link step of its own: every //! symbol it carries reaches the final artifact and takes part in resolution. So -//! for a static build the gate has to be compiled into the objects, and this -//! asserts that it was — that the three bundled runtime targets have no -//! externally visible definitions at all. +//! for a static build the gate must be baked into the archive. Older archives +//! hide the bundled runtime's definitions, while newer ones prelink and localize +//! them on Linux or rename them on macOS. //! //! Two reasons to check the cause and not only the effect. It holds on Linux, //! where the visibility can regress with no symptom to observe, so no runtime @@ -20,9 +20,8 @@ //! over its own build output (`chdb/build/check_static_lib_hermetic.sh`, gate //! 2). That one guards the release; this one guards what actually arrived. //! -//! Measured across the engines this crate has been pinned to: v26.7.0 shows -//! 2365 externally visible definitions among 58 runtime members, and -//! v26.7.2-rc.2 and v26.7.3 show 0 among the same 58. +//! Measured across older engines: v26.7.0 shows 2365 externally visible +//! definitions among 58 runtime members, and v26.7.2-rc.2/v26.7.3 show 0. mod common; @@ -37,7 +36,7 @@ const RUNTIME_MEMBER_PREFIXES: [&str; 3] = ["libcxx__", "libcxxabi__", "libunwin const OFFENDERS_SHOWN: usize = 20; #[test] -fn bundled_runtime_objects_export_nothing() { +fn bundled_runtime_symbols_cannot_bind_host() { let artifact = Path::new(env!("CHDB_LINKED_ARTIFACT")); assert!( !artifact.as_os_str().is_empty(), @@ -54,13 +53,33 @@ fn bundled_runtime_objects_export_nothing() { } let members = runtime_members(artifact); - // Zero would mean the naming changed and this test had quietly stopped - // testing anything, which is worse than a failure. - assert!( - !members.is_empty(), - "no {RUNTIME_MEMBER_PREFIXES:?} members among the archive's members: either the \ - bundled runtime is no longer a separate set of objects, or chdb-core renamed them" - ); + if members.is_empty() { + // New Linux archives prelink all objects into one member and localize + // everything outside the C API. The old per-runtime-member check no + // longer describes that layout; verify its exported surface here and + // exercise the host unwinder in runtime_faces. + let all_members = run("ar", &[OsStr::new("t"), artifact.as_os_str()], None); + let names: Vec<_> = all_members.lines().collect(); + assert!( + cfg!(target_os = "linux") + && names.len() == 1 + && names[0].starts_with("libchdb") + && names[0].ends_with(".o"), + "unknown static archive layout: {names:?}" + ); + let (defined, visible) = symbols(&[artifact.to_path_buf()]); + assert!(defined > 0 && visible.iter().any(|name| name == "chdb_connect")); + assert!( + visible.len() <= 100 + && !visible.iter().any(|name| { + name.starts_with("_Unwind_") + || name.starts_with("unw_") + || name.starts_with("__cxa_") + }), + "prelinked archive exports unexpected runtime symbols: {visible:?}" + ); + return; + } let scratch = common::tempdir(); let objects = extract(artifact, &members, scratch.path()); @@ -91,15 +110,14 @@ fn bundled_runtime_objects_export_nothing() { } } - // Asserted at zero rather than "disjoint from this host's system runtime". - // These targets are built to have no externally visible definitions at all, - // and a symbol that merely happens to be absent from the running OS version - // would otherwise slip through. + // The old layout hides these definitions; the new macOS layout renames + // every one so a host can never resolve a runtime reference to it. assert!( - visible.is_empty(), + visible.is_empty() + || (cfg!(target_os = "macos") && visible.iter().all(|s| s.ends_with(".chdb"))), "{} of {} definitions in the bundled runtime are visible from outside the \ - archive; a static build can then bind a call made inside the engine to \ - another copy of the C++ runtime", + archive under an unrenamed name; a static build can then bind a host \ + runtime call to the bundled C++ runtime", visible.len(), defined ); diff --git a/tests/runtime_faces.rs b/tests/runtime_faces.rs index b419411..01a72c1 100644 --- a/tests/runtime_faces.rs +++ b/tests/runtime_faces.rs @@ -166,6 +166,21 @@ fn cases() -> Vec { run: arrow_release_callback, }); + // chdb-io/chdb-rust#53: an archive may keep the engine working while + // breaking the host Rust runtime's unwinder. Exercise the real consumer + // after a failing engine query, isolated so a SIGSEGV cannot hide the + // outcome of the other runtime cases. + #[cfg(feature = "static")] + if std::env::var_os("CHDB_CHECK_HOST_UNWIND").is_some() { + // The current pin predates chdb-core's fix. Opt in when validating a + // candidate archive, and enable by default only after adopting it. + cases.push(Case { + name: "host_backtrace_and_panic", + expect: "backtrace=ok|panic=ok\n", + run: host_backtrace_and_panic, + }); + } + cases } @@ -489,6 +504,25 @@ fn signal_handlers_before_connect() { println!("ok"); } +#[cfg(feature = "static")] +fn host_backtrace_and_panic() { + chdb_rust::runtime::signal_handlers(false); + let conn = Connection::open_in_memory().expect("open"); + assert!(conn + .query("SELECT * FROM chdb_rust_missing_table", OutputFormat::CSV) + .is_err()); + + let trace = std::backtrace::Backtrace::force_capture(); + assert_eq!(trace.status(), std::backtrace::BacktraceStatus::Captured); + assert!(!trace.to_string().trim().is_empty()); + print!("backtrace=ok|"); + + std::panic::set_hook(Box::new(|_| {})); + assert!(std::panic::catch_unwind(|| panic!("expected panic")).is_err()); + drop(conn); + println!("panic=ok"); +} + /// The deadly-signal disposition for `sig`, read via `sigaction` with a null /// `act` — a query, not a change. fn signal_disposition(sig: libc::c_int) -> libc::sighandler_t {