Let downstream binaries bundle the native library without runtime env vars - #14
Conversation
…t env vars zvec-rust-sys emitted an rpath via `cargo:rustc-link-arg=-Wl,-rpath,...`, but that link-arg only applies to the emitting rlib's own targets and never propagates to a downstream executable. As a result, binaries linking zvec-rust had no LC_RPATH/DT_RUNPATH pointing at libzvec_c_api, forcing users to set DYLD_LIBRARY_PATH / LD_LIBRARY_PATH at runtime. Propagate the resolved library directory through the dependency graph and let the final binary crate configure its own rpath: - zvec-sys: publish the resolved lib dir as `links` metadata (cargo:lib_dir), exposed to direct dependents as DEP_ZVEC_C_API_LIB_DIR; drop the ineffective rpath link-arg. Also fix Windows lib detection to distinguish msvc (.lib import lib required) from gnu/mingw (.dll.a or .dll). - zvec: declare `links = "zvec_rust"` and forward DEP_ZVEC_C_API_LIB_DIR as its own cargo:lib_dir, so direct dependents receive DEP_ZVEC_RUST_LIB_DIR. - zvec-build: new helper crate. From a binary crate's build.rs, configure() emits per-OS rpaths (@executable_path[/../lib] on macOS, $ORIGIN[/../lib] on Linux, none on Windows) and stages the runtime library beside the binary for local development, so the binary runs with no dylib search-path env vars.
chinaux
left a comment
There was a problem hiding this comment.
Thanks for tackling this — the diagnosis is spot-on: cargo:rustc-link-arg emitted from zvec-sys only applies to that package's own targets, and since both crates are rlibs, the old rpath never reached any downstream binary. I verified a few things on my side before reviewing:
Verified OK
- Removing the old rpath link-arg is regression-free — CI and the Makefile have always relied on
DYLD_LIBRARY_PATH/LD_LIBRARY_PATH; the flag never affected anything. zvec_c_apisets noVERSION/SOVERSIONin its CMakeLists, soDT_NEEDEDis exactlylibzvec_c_api.so— staging by that file name is sufficient on Linux.links = "zvec_rust"requires a build script, andzvec/build.rsalready exists on main, so no conflict.- The OUT_DIR → 3-levels-up → binary output dir logic is correct for both native and cross builds.
That said, I'd like two blockers addressed before merging:
Blockers
-
zvec-rust-buildis never published to crates.io.publish-crates.ymlonly publisheszvec-rust-sysandzvec-rust, and its version-verification step doesn't cover the new crate. As-is, downstreamzvec-rust-build = "0.7"build-dependencies won't resolve. Please add dry-run + publish steps (and the version check) for the new crate. -
rpaths are emitted for binaries only.
cargo:rustc-link-arg-binsdoesn't cover tests, examples, or benches, so a downstream crate's integration-test binaries get no rpath at all andcargo teststill requires the env vars. (The verification in the PR description only exercised the binary.) Consider plaincargo:rustc-link-arg— which applies to all target kinds of the emitting package — or additionally emit the-tests/-examples/-benchesvariants.
Should-fix
-
jieba dict is not staged. The auto-registration added in 37e50fa discovers
data/jieba_dict/relative to the loaded library (viadladdr/GetModuleHandleExW) or via a build-time-recorded path. In the recommendedbin/ + lib/deployment layout on another machine, both discovery paths fail becausecopy_runtime_libs_tocopies only the shared library. Suggest stagingdata/jieba_dict/(when present inlib_dir()) next to the library, or at minimum documenting that packagers must ship it. -
Root READMEs still over-promise.
README.md:57/README_CN.md:57claim the bundled feature "sets up the library path viarpath" — that was never true, and remains untrue without the helper crate. Worth correcting here and introducingzvec-rust-buildin the Prerequisites section. -
Linux (
$ORIGIN) and Windows paths are wired but untested (as the PR notes), and workspace CI doesn't cover the downstream-binary scenario at all. Suggest adding a tiny example bin that callsconfigure()and runningenv -u LD_LIBRARY_PATH ./target/debug/<smoke>(plus the macOS equivalent) in CI.
Nits
let _ = target_env(); // reserved for future— drop the placeholder or use it.configure()emitscargo:rerun-if-changed=build.rson behalf of the downstream package, which disables Cargo's default "re-run if any file changed" policy — worth documenting.- The
ZVEC_LIB_DIRfallback inlib_dir()can stage a host-arch library during cross-compilation — worth a doc caveat. - No unit tests in the new crate;
runtime_lib_file_names/binary_output_dirare cheap to test.
Overall this fixes a real long-standing flaw and the design (links metadata + downstream helper crate) is the right pattern — looking forward to the next iteration.
…verage, jieba staging, docs Follow-up to PR review on the downstream rpath bundling change. - copy_runtime_libs_to: guard against copying a file onto itself. fs::copy truncates the destination before reading the source, so when ZVEC_LIB_DIR already points at the binary output directory the staged library was zeroed out (reproduced on macOS). stage_file() now skips when source and destination canonicalize to the same object, which also covers symlinked layouts. Added unit tests including a symlink case. - rpath: emit via cargo:rustc-link-arg (not -bins) so integration tests, examples, and benches of the downstream crate also carry the rpath and run without dylib search-path env vars. - jieba dict: stage data/jieba_dict beside the library so the FTS jieba tokenizer's runtime discovery (<lib_dir>/data/jieba_dict) works in a self-contained bin/+lib/ deployment. - publish-crates.yml: add version check, dry-run, and publish steps for the new zvec-rust-build crate so downstream build-deps resolve on crates.io. - CI: add zvec-rpath-smoke, a downstream binary that wires zvec-rust-build, executed with all dylib search-path vars cleared to exercise the @executable_path / $ORIGIN paths on macOS/Linux/Windows. - README/README_CN: correct the false "bundled sets up the library path via rpath" claim and document the zvec-rust-build helper. - Nits: drop the target_env() placeholder, document that emitting rerun-if-* overrides Cargo's default rerun policy, note the ZVEC_LIB_DIR cross-compile caveat, and add unit tests for runtime_lib_file_names / binary_output_dir.
|
Thanks for the thorough review — all points addressed in 40a6412. Blockers
Should-fix Nits — removed the Additional bug fixed (reported separately): |
Problem
Binaries that link
zvec-rustfail to locatelibzvec_c_apiat runtime unless the user exportsDYLD_LIBRARY_PATH(macOS) /LD_LIBRARY_PATH(Linux).Root cause:
zvec-sys/build.rstried to set the runtime search path withbut
cargo:rustc-link-argonly affects the emitting package's own binary/cdylib targets.zvec-sys(andzvec) compile to rlibs, so the flag is never applied to a downstream executable. The final binary therefore carries noLC_RPATH/DT_RUNPATHpointing at the shared library, and the loader can only find it via environment variables. There is no supported way for a build script to inject an rpath into a binary defined in another crate further down the graph.Solution
Stop trying to set the rpath from the library crates. Instead, propagate the resolved native-library directory through the dependency graph and let the final binary crate set its own rpath (and, for local dev, stage the library beside the binary).
zvec-sys: publish the resolved lib dir aslinksmetadata viacargo:lib_dir=.... Cargo exposes it to direct dependents' build scripts asDEP_ZVEC_C_API_LIB_DIR. Removed the ineffective rpath link-arg. Also fixed Windows library detection to distinguishmsvc(needs the.libimport library) fromgnu/mingw (can link.dll.aor.dlldirectly).zvec: declarelinks = "zvec_rust"and forwardDEP_ZVEC_C_API_LIB_DIRas its owncargo:lib_dir, so crates depending directly onzvec-rustreceive it asDEP_ZVEC_RUST_LIB_DIR.zvec-build(new helper crate,zvec-rust-build): called from a downstream binary crate'sbuild.rs.configure():cargo:rustc-link-arg-bins:@executable_pathand@executable_path/../libon macOS,$ORIGINand$ORIGIN/../libon Linux, none on Windows (the loader searches the executable's own directory);cargo run/ a freshly built binary work with no dylib search-path env vars.The rpath is written at link time (not patched in afterward with
install_name_tool), so macOS code signing is unaffected.Downstream usage
A binary crate depends directly on
zvec-rust, addszvec-rust-buildas a build-dependency, and calls it frombuild.rs:Deployment layout options:
@executable_path/$ORIGIN);bin/<exe>+lib/<shared lib>(@executable_path/../lib/$ORIGIN/../lib).Note:
DEP_*metadata only reaches direct dependents, so the binary crate must depend onzvec-rustdirectly (not merely transitively) forconfigure()to locate the library.Verification
Validated end-to-end from the
zvec-grepconsumer on macOS arm64 (via a local[patch.crates-io], not included here):otool -l zgshows the expectedLC_RPATHentries; install name is@rpath/libzvec_c_api.dylib;env -u DYLD_LIBRARY_PATH -u DYLD_FALLBACK_LIBRARY_PATH ./zg --versionsucceeds;bin/+lib/deploy runs with all dylib search-path vars cleared;Linux (
$ORIGIN) and Windows (loader searches exe dir) paths are wired but were not executed on the macOS host.