From d3feea0454942e6e9b2efa6d286a50a338b00e94 Mon Sep 17 00:00:00 2001 From: mrcs64 <9069178+mrcs64@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:21:23 +0000 Subject: [PATCH 1/2] feat(sys): require the configured native library with ZVEC_STRICT_LIB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ZVEC_LIB_DIR` is documented as the highest priority source for the native library, but an unusable value only prints a warning and resolution continues with the sibling checkout, the submodule, `vendor/lib`, a cached or downloaded prebuilt SDK, or a source build. A typo, a stale path, or a directory holding an incomplete SDK therefore links a different copy of the library than the one the environment selected, without failing. `ZVEC_STRICT_LIB=1` makes the explicit configuration the only accepted source: `ZVEC_LIB_DIR` must be set and must contain the platform library, every substitute is skipped, and a missing or invalid directory fails the build script with the expected file name and target instead of falling back. The mode is off by default, is enabled only by `1` or `true`, and never acquires anything. `has_zvec_lib` now shares its platform file-name table with the failure message so both report the same artifacts for the current target. Tests: `zvec-sys/tests/build_script.rs` gains six cases — explicit directory required, incomplete directory rejected, valid directory accepted, every other value leaving the mode off, acquisition unreachable, and the platform file-name table for Linux, macOS and Windows. --- README.md | 26 +++++ zvec-sys/README.md | 6 ++ zvec-sys/build.rs | 88 ++++++++++++--- zvec-sys/tests/build_script.rs | 189 +++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 7c042f7..c37d4a9 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,32 @@ The build script resolves the C library in this order: Set `ZVEC_AUTO_BUILD=0` to disable steps 5 and 6. +### Strict External Library Mode + +`ZVEC_STRICT_LIB=1` requires the native library to come from an explicitly +configured `ZVEC_LIB_DIR` and disables every substitute: + +1. `ZVEC_LIB_DIR` must be set and must contain the platform library: + `libzvec_c_api.so` on Linux, `libzvec_c_api.dylib` on macOS/iOS, and the + import library (`zvec_c_api.lib` for MSVC, or `libzvec_c_api.dll.a`, the + import library, or the DLL for MinGW) on Windows. +2. Steps 2–6 above are skipped: no sibling checkout, no submodule, no vendor + directory, no cached or downloaded prebuilt SDK, and no source build. +3. A missing, invalid or incomplete `ZVEC_LIB_DIR` fails the build script with + the expected file name and target instead of falling back to another copy. + +Use it when a build must link exactly the library the environment provides — +reproducible packaging, auditing, or offline builds — and a silent fallback to a +different copy would be a defect. Because the failure is raised when the build +script runs, `cargo check` fails as well while strict mode is on and the library +is absent. + +The mode is opt-in, and only `1` or `true` enable it: any other value, including +an empty one, keeps the resolution order and behavior above unchanged. `DOCS_RS` +builds still skip native library resolution entirely, because they do not link. +Strict mode constrains the library only; `ZVEC_INCLUDE_DIR` resolution is +unchanged. + ### Prebuilt Downloads and Failure Handling Prebuilt SDKs remain cached at `$OUT_DIR/zvec-prebuilt`, under Cargo's `target` diff --git a/zvec-sys/README.md b/zvec-sys/README.md index 14923f8..33cf55c 100644 --- a/zvec-sys/README.md +++ b/zvec-sys/README.md @@ -25,6 +25,12 @@ This crate requires the zvec C library (`libzvec_c_api`) to be available at link 3. **Vendor directory**: `vendor/lib` 4. **Auto-build** (experimental): Clones and builds zvec from source +Set `ZVEC_STRICT_LIB=1` to accept step 1 only: the library must then come from a +`ZVEC_LIB_DIR` that contains the platform library, and the build script fails +with the expected file name instead of falling back to steps 2–4, a cached or +downloaded prebuilt SDK, or a source build. See the +[main README](../README.md) for details. + See the [main README](../README.md) for detailed setup instructions. ## Usage diff --git a/zvec-sys/build.rs b/zvec-sys/build.rs index 0cd7d0b..1e0b9d0 100644 --- a/zvec-sys/build.rs +++ b/zvec-sys/build.rs @@ -11,6 +11,7 @@ fn main() { "ZVEC_LIB_DIR", "ZVEC_INCLUDE_DIR", "ZVEC_AUTO_BUILD", + "ZVEC_STRICT_LIB", "ZVEC_REPO_URL", "ZVEC_PREBUILT_URL", "DOCS_RS", @@ -49,6 +50,15 @@ fn main() { &auto_build_dir, ); if lib_dir.is_none() { + // A strict build accepts no substitute, so the explicit configuration + // was the only thing that could have resolved: report what is missing + // instead of letting the acquisition settings decide the outcome. + if strict_lib_dir_required() { + panic!( + "{}", + strict_lib_failure_message(env::var_os("ZVEC_LIB_DIR").map(PathBuf::from)) + ); + } // Only fail the build when the script was allowed to acquire the // library itself and every method failed. With acquisition disabled the // caller is expected to supply the library (for example a system @@ -121,17 +131,30 @@ fn resolve_lib_dir( prebuilt_cache_dir: &Path, auto_build_dir: &Path, ) -> Option { + let strict = strict_lib_dir_required(); + // 1. Environment variable (highest priority — for advanced users) if let Some(custom) = env::var_os("ZVEC_LIB_DIR") { let path = PathBuf::from(&custom); if has_zvec_lib(&path) { return Some(path); } - println!( - "cargo:warning=ZVEC_LIB_DIR={} does not contain the required native library; \ - continuing library resolution.", - path.display() - ); + if !strict { + println!( + "cargo:warning=ZVEC_LIB_DIR={} does not contain the required native library; \ + continuing library resolution.", + path.display() + ); + } + } + + // Everything below substitutes another copy of the native library for the + // configured one: a sibling checkout, the submodule, the vendor directory, + // a cached or downloaded prebuilt SDK, or a source build. A strict build + // forbids exactly that, so it stops here and leaves the reporting to the + // caller, which knows whether the configuration was explicit. + if strict { + return None; } // 2. Sibling zvec checkout @@ -180,6 +203,41 @@ fn acquisition_enabled() -> bool { cfg!(feature = "bundled") && env::var("ZVEC_AUTO_BUILD").unwrap_or_default() != "0" } +/// Whether the build script must use the explicitly configured native library +/// and nothing else. +/// +/// Opt in with `ZVEC_STRICT_LIB=1` (`true` is accepted as well). Every other +/// value, including an empty one, leaves the resolution order unchanged, so the +/// mode cannot be enabled by accident. +fn strict_lib_dir_required() -> bool { + let value = env::var("ZVEC_STRICT_LIB").unwrap_or_default(); + value == "1" || value.eq_ignore_ascii_case("true") +} + +/// Explains why a strict build could not resolve the configured native +/// library. +fn strict_lib_failure_message(configured: Option) -> String { + let target = env::var("TARGET").unwrap_or_default(); + let expected = expected_lib_file_names().join(", "); + let no_fallback = "A strict build never falls back to a sibling zvec/ checkout, the \ + submodule, the vendor directory, a cached or downloaded prebuilt SDK, or \ + a source build."; + match configured { + Some(dir) => format!( + "ZVEC_STRICT_LIB is set, but ZVEC_LIB_DIR={} does not contain the native library for \ + {target}: expected {expected}. {no_fallback} Point ZVEC_LIB_DIR at a directory that \ + contains {expected}, or unset ZVEC_STRICT_LIB to use the normal resolution order.", + dir.display() + ), + None => format!( + "ZVEC_STRICT_LIB is set, but ZVEC_LIB_DIR is not set. The native library for {target} \ + must be configured explicitly, in a directory that contains {expected}. \ + {no_fallback} Set ZVEC_LIB_DIR, or unset ZVEC_STRICT_LIB to use the normal \ + resolution order." + ), + } +} + fn resolve_include_dir( sibling_zvec: &Option, submodule_zvec: &Path, @@ -216,27 +274,33 @@ fn resolve_include_dir( None } -fn has_zvec_lib(dir: &Path) -> bool { +/// Names of the native library files the linker can use for the current +/// target. +fn expected_lib_file_names() -> &'static [&'static str] { let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); match target_os.as_str() { - "macos" | "ios" => dir.join("libzvec_c_api.dylib").is_file(), + "macos" | "ios" => &["libzvec_c_api.dylib"], "windows" => { // MSVC dynamic linking requires the .lib import library; the .dll // alone is not enough for the linker. The GNU/MinGW toolchain can // link directly against the .dll (or a .dll.a import lib). let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); if target_env == "msvc" { - dir.join("zvec_c_api.lib").is_file() + &["zvec_c_api.lib"] } else { - dir.join("libzvec_c_api.dll.a").is_file() - || dir.join("zvec_c_api.lib").is_file() - || dir.join("zvec_c_api.dll").is_file() + &["libzvec_c_api.dll.a", "zvec_c_api.lib", "zvec_c_api.dll"] } } - _ => dir.join("libzvec_c_api.so").is_file(), + _ => &["libzvec_c_api.so"], } } +fn has_zvec_lib(dir: &Path) -> bool { + expected_lib_file_names() + .iter() + .any(|name| dir.join(name).is_file()) +} + fn auto_build_zvec(build_dir: &Path) -> Option { let zvec_src = build_dir.join("zvec"); diff --git a/zvec-sys/tests/build_script.rs b/zvec-sys/tests/build_script.rs index 7cb137c..718559f 100644 --- a/zvec-sys/tests/build_script.rs +++ b/zvec-sys/tests/build_script.rs @@ -18,12 +18,14 @@ mod build_script { "TARGET", "CARGO_PKG_VERSION", "CARGO_CFG_TARGET_OS", + "CARGO_CFG_TARGET_ENV", "CARGO_MANIFEST_DIR", "OUT_DIR", "ZVEC_PREBUILT_URL", "ZVEC_LIB_DIR", "ZVEC_INCLUDE_DIR", "ZVEC_AUTO_BUILD", + "ZVEC_STRICT_LIB", "ZVEC_REPO_URL", "DOCS_RS", "ZVEC_TEST_LOG", @@ -114,6 +116,23 @@ mod build_script { .map(|entry| entry.unwrap().path()) .collect() } + + /// A valid native library in the vendor directory the build script + /// resolves for this fixture, without being told about it. + fn install_vendor_library(&self) -> PathBuf { + let vendor_lib = self.root.join("workspace/vendor/lib"); + fs::create_dir_all(&vendor_lib).unwrap(); + fs::write(vendor_lib.join("libzvec_c_api.so"), "native library").unwrap(); + vendor_lib + } + + /// A valid native library directory to configure explicitly. + fn install_explicit_library(&self) -> PathBuf { + let lib_dir = self.root.join("native"); + fs::create_dir_all(&lib_dir).unwrap(); + fs::write(lib_dir.join("libzvec_c_api.so"), "native library").unwrap(); + lib_dir + } } impl Drop for Fixture { @@ -308,6 +327,176 @@ mod build_script { assert!(fixture.log().is_empty()); } + /// Text of a panic payload raised by the build script. + fn panic_message(payload: Box) -> String { + if let Some(message) = payload.downcast_ref::() { + message.clone() + } else if let Some(message) = payload.downcast_ref::<&str>() { + (*message).to_string() + } else { + String::new() + } + } + + #[test] + fn strict_mode_requires_an_explicit_library_directory() { + let fixture = Fixture::new(); + env::set_var("ZVEC_STRICT_LIB", "1"); + let vendor_lib = fixture.install_vendor_library(); + + let message = panic_message(std::panic::catch_unwind(main).unwrap_err()); + + assert!(message.contains("ZVEC_STRICT_LIB is set")); + assert!(message.contains("ZVEC_LIB_DIR is not set")); + assert!(message.contains("libzvec_c_api.so")); + // A substitute exists but was neither used nor touched, and nothing + // tried to download or build one. + assert!(!fixture.cache_dir().exists()); + assert!(fixture.log().is_empty()); + assert_eq!( + fs::read_to_string(vendor_lib.join("libzvec_c_api.so")).unwrap(), + "native library" + ); + assert_eq!( + resolve_lib_dir( + &None, + &fixture.root.join("submodule"), + &fixture.root.join("workspace/vendor"), + &fixture.cache_dir(), + &fixture.root.join("auto-build"), + ), + None + ); + } + + #[test] + fn strict_mode_rejects_an_incomplete_directory_and_never_falls_back() { + let fixture = Fixture::new(); + env::set_var("ZVEC_STRICT_LIB", "1"); + let vendor_lib = fixture.install_vendor_library(); + let incomplete = fixture.root.join("incomplete"); + fs::create_dir_all(&incomplete).unwrap(); + fs::write(incomplete.join("libzvec_c_api.so.tmp"), "partial").unwrap(); + env::set_var("ZVEC_LIB_DIR", &incomplete); + + let message = panic_message(std::panic::catch_unwind(main).unwrap_err()); + + assert!(message.contains("ZVEC_STRICT_LIB is set")); + assert!(message.contains(&incomplete.display().to_string())); + assert!(!fixture.cache_dir().exists()); + assert!(fixture.log().is_empty()); + assert!(vendor_lib.join("libzvec_c_api.so").is_file()); + } + + #[test] + fn strict_mode_accepts_an_explicit_library_directory() { + let fixture = Fixture::new(); + env::set_var("ZVEC_STRICT_LIB", "1"); + let lib_dir = fixture.install_explicit_library(); + env::set_var("ZVEC_LIB_DIR", &lib_dir); + + main(); + + assert_eq!( + resolve_lib_dir( + &None, + &fixture.root.join("submodule"), + &fixture.root.join("workspace/vendor"), + &fixture.cache_dir(), + &fixture.root.join("auto-build"), + ), + Some(lib_dir) + ); + assert!(!fixture.cache_dir().exists()); + assert!(fixture.log().is_empty()); + } + + #[test] + fn strict_mode_stays_off_for_every_other_value() { + let fixture = Fixture::new(); + let vendor_lib = fixture.install_vendor_library(); + let incomplete = fixture.root.join("incomplete"); + fs::create_dir_all(&incomplete).unwrap(); + env::set_var("ZVEC_LIB_DIR", &incomplete); + + for value in ["", "0", "false", "no"] { + env::set_var("ZVEC_STRICT_LIB", value); + assert!( + !strict_lib_dir_required(), + "value {value:?} must not enable strict mode" + ); + main(); + assert_eq!( + resolve_lib_dir( + &None, + &fixture.root.join("submodule"), + &fixture.root.join("workspace/vendor"), + &fixture.cache_dir(), + &fixture.root.join("auto-build"), + ), + Some(vendor_lib.clone()) + ); + } + assert!(fixture.log().is_empty()); + } + + #[test] + fn strict_mode_never_reaches_acquisition() { + let fixture = Fixture::new(); + env::set_var("ZVEC_STRICT_LIB", "1"); + // Acquisition is enabled and both a working download path and a + // substitute that would otherwise resolve are in place; the clone + // URL keeps a wrong implementation offline instead of fetching a + // real repository. + env::set_var("ZVEC_AUTO_BUILD", "1"); + env::set_var("ZVEC_REPO_URL", "/nonexistent/zvec.git"); + fixture.successful_download("curl", "-o"); + fixture.successful_extraction(); + fixture.install_vendor_library(); + + let message = panic_message(std::panic::catch_unwind(main).unwrap_err()); + + assert!(message.contains("ZVEC_STRICT_LIB is set")); + assert!(!message.contains("every acquisition method failed")); + // No download tool ran, nothing was published, and no source + // checkout was created. + assert!(fixture.log().is_empty()); + assert!(!fixture.cache_dir().exists()); + assert!(!fixture.root.join("out/zvec-build/zvec").exists()); + } + + #[test] + fn platform_library_names_are_honoured() { + let fixture = Fixture::new(); + let lib_dir = fixture.root.join("native"); + fs::create_dir(&lib_dir).unwrap(); + + // Linux, the fixture default. + assert_eq!(expected_lib_file_names(), &["libzvec_c_api.so"]); + + env::set_var("CARGO_CFG_TARGET_OS", "macos"); + assert_eq!(expected_lib_file_names(), &["libzvec_c_api.dylib"]); + assert!(!has_zvec_lib(&lib_dir)); + fs::write(lib_dir.join("libzvec_c_api.dylib"), "native library").unwrap(); + assert!(has_zvec_lib(&lib_dir)); + + env::set_var("CARGO_CFG_TARGET_OS", "windows"); + env::set_var("CARGO_CFG_TARGET_ENV", "msvc"); + assert_eq!(expected_lib_file_names(), &["zvec_c_api.lib"]); + assert!(!has_zvec_lib(&lib_dir)); + + env::set_var("CARGO_CFG_TARGET_ENV", "gnu"); + assert_eq!( + expected_lib_file_names(), + &["libzvec_c_api.dll.a", "zvec_c_api.lib", "zvec_c_api.dll"] + ); + fs::write(lib_dir.join("zvec_c_api.dll"), "native library").unwrap(); + assert!(has_zvec_lib(&lib_dir)); + + env::set_var("CARGO_CFG_TARGET_OS", "linux"); + assert_eq!(expected_lib_file_names(), &["libzvec_c_api.so"]); + } + #[test] fn docs_rs_skips_native_library_acquisition() { let fixture = Fixture::new(); From 12b1bd093ec2855dfbf2964597db7629d663fc03 Mon Sep 17 00:00:00 2001 From: mrcs64 <9069178+mrcs64@users.noreply.github.com> Date: Fri, 16 Oct 2026 19:42:24 +0000 Subject: [PATCH 2/2] test(sys): cover the true casing, the dictionary, and the message wording Review findings on the strict-artifact mode: - `ZVEC_STRICT_LIB=true` (and `True`/`TRUE`) is now exercised, and the values other toolchains read as "yes" (`yes`, `on`) are pinned as off, so the accepted word is exactly `true`, case-insensitively, which the README now states. - Strict mode no longer takes the cppjieba dictionary from a sibling checkout or the submodule: a dictionary from a different copy of the SDK is the same silent substitution the mode refuses. `resolve_jieba_dict_dir` stops after the configured directory in strict mode, and the test asserts the non-strict sibling pick and the strict refusal on one fixture. - The failure message reads "one of a, b, c" where the platform accepts several artifact names, so the MinGW message no longer reads as if all three were required. - README: the `true` casing, the dictionary rule, and the include-path consequence stated outright -- headers may still come from a sibling checkout, which decides what downstream build scripts see rather than what the linker uses. Verified: build-script tests 19 passed (three new), workspace 232 passed and 0 failed, `clippy -D warnings` clean, and five real build runs -- strict with `1` and with `TRUE`, missing directory, unset directory, and the non-strict fallback. --- README.md | 27 ++++++++--- zvec-sys/build.rs | 47 ++++++++++++++----- zvec-sys/tests/build_script.rs | 85 ++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c37d4a9..3b5025e 100644 --- a/README.md +++ b/README.md @@ -140,24 +140,37 @@ configured `ZVEC_LIB_DIR` and disables every substitute: 1. `ZVEC_LIB_DIR` must be set and must contain the platform library: `libzvec_c_api.so` on Linux, `libzvec_c_api.dylib` on macOS/iOS, and the - import library (`zvec_c_api.lib` for MSVC, or `libzvec_c_api.dll.a`, the - import library, or the DLL for MinGW) on Windows. + import library (`zvec_c_api.lib` for MSVC, or one of `libzvec_c_api.dll.a`, + the import library, or the DLL for MinGW) on Windows. 2. Steps 2–6 above are skipped: no sibling checkout, no submodule, no vendor directory, no cached or downloaded prebuilt SDK, and no source build. 3. A missing, invalid or incomplete `ZVEC_LIB_DIR` fails the build script with the expected file name and target instead of falling back to another copy. +The cppjieba dictionary the `jieba` FTS tokenizer needs follows the library: in +strict mode it must come from the configured directory (`data/jieba_dict` next to +the library, or `/data/jieba_dict`), never from a sibling checkout or the +submodule, because that dictionary belongs to a different copy of the SDK. + Use it when a build must link exactly the library the environment provides — reproducible packaging, auditing, or offline builds — and a silent fallback to a different copy would be a defect. Because the failure is raised when the build script runs, `cargo check` fails as well while strict mode is on and the library is absent. -The mode is opt-in, and only `1` or `true` enable it: any other value, including -an empty one, keeps the resolution order and behavior above unchanged. `DOCS_RS` -builds still skip native library resolution entirely, because they do not link. -Strict mode constrains the library only; `ZVEC_INCLUDE_DIR` resolution is -unchanged. +The mode is opt-in, and only `1` or the word `true` enable it; `true` is matched +case-insensitively (`TRUE`, `True`), because those are plainly the same word, and +every other value — including an empty one and the `yes`/`on` that other +toolchains accept — keeps the resolution order and behavior above unchanged. +`DOCS_RS` builds still skip native library resolution entirely, because they do +not link. + +Strict mode constrains the library and its dictionary, not the include path: +`ZVEC_INCLUDE_DIR` resolution is unchanged, so a strict build can still take its +headers from a sibling checkout or the submodule. That decides what downstream +build scripts see rather than what the linker uses, which is why it is not part +of this mode — set `ZVEC_INCLUDE_DIR` explicitly when the headers have to be +pinned to the same directory. ### Prebuilt Downloads and Failure Handling diff --git a/zvec-sys/build.rs b/zvec-sys/build.rs index 1e0b9d0..fe9895b 100644 --- a/zvec-sys/build.rs +++ b/zvec-sys/build.rs @@ -110,9 +110,13 @@ fn main() { // near the resolved library so the high-level crate can auto-register the // default jieba dict dir for the `jieba` FTS tokenizer. if let Some(ref dir) = lib_dir { - if let Some(jieba_dir) = - resolve_jieba_dict_dir(&sibling_zvec, &submodule_zvec, &auto_build_dir, dir) - { + if let Some(jieba_dir) = resolve_jieba_dict_dir( + &sibling_zvec, + &submodule_zvec, + &auto_build_dir, + dir, + strict_lib_dir_required(), + ) { println!( "cargo:rustc-env=ZVEC_RUST_JIEBA_DICT_DIR={}", jieba_dir.display() @@ -218,7 +222,7 @@ fn strict_lib_dir_required() -> bool { /// library. fn strict_lib_failure_message(configured: Option) -> String { let target = env::var("TARGET").unwrap_or_default(); - let expected = expected_lib_file_names().join(", "); + let expected = describe_expected_lib_file_names(); let no_fallback = "A strict build never falls back to a sibling zvec/ checkout, the \ submodule, the vendor directory, a cached or downloaded prebuilt SDK, or \ a source build."; @@ -295,6 +299,16 @@ fn expected_lib_file_names() -> &'static [&'static str] { } } +/// The expected library names as a phrase the linker's rule matches: one name +/// where the platform accepts one artifact, and "one of a, b, c" where it +/// accepts several, so the message cannot read as if all of them are required. +fn describe_expected_lib_file_names() -> String { + match expected_lib_file_names() { + [only] => (*only).to_owned(), + several => format!("one of {}", several.join(", ")), + } +} + fn has_zvec_lib(dir: &Path) -> bool { expected_lib_file_names() .iter() @@ -720,25 +734,34 @@ fn num_cpus() -> String { /// (`/lib` + `/data`, as produced by zvec's CMake install). /// 3. The `thirdparty/cppjieba` dict of a sibling / submodule / auto-built /// zvec source checkout. +/// +/// A strict build stops after the first two. A dictionary taken from a sibling +/// checkout or the submodule belongs to a different copy of the SDK than the +/// configured library, which is the substitution strict mode exists to refuse; +/// the dictionary is part of the SDK's own layout, so the configured directory +/// has to provide it the same way it provides the library. fn resolve_jieba_dict_dir( sibling_zvec: &Option, submodule_zvec: &Path, auto_build_dir: &Path, lib_dir: &Path, + strict: bool, ) -> Option { let mut candidates: Vec = vec![ lib_dir.join("data").join("jieba_dict"), lib_dir.join("..").join("data").join("jieba_dict"), ]; - let dict_suffix = Path::new("thirdparty") - .join("cppjieba") - .join("cppjieba-5.6.7") - .join("dict"); - if let Some(ref sibling) = sibling_zvec { - candidates.push(sibling.join(&dict_suffix)); + if !strict { + let dict_suffix = Path::new("thirdparty") + .join("cppjieba") + .join("cppjieba-5.6.7") + .join("dict"); + if let Some(sibling) = sibling_zvec { + candidates.push(sibling.join(&dict_suffix)); + } + candidates.push(submodule_zvec.join(&dict_suffix)); + candidates.push(auto_build_dir.join("zvec").join(&dict_suffix)); } - candidates.push(submodule_zvec.join(&dict_suffix)); - candidates.push(auto_build_dir.join("zvec").join(&dict_suffix)); candidates.into_iter().find(|c| has_jieba_dict(c)) } diff --git a/zvec-sys/tests/build_script.rs b/zvec-sys/tests/build_script.rs index 718559f..fa1885d 100644 --- a/zvec-sys/tests/build_script.rs +++ b/zvec-sys/tests/build_script.rs @@ -440,6 +440,91 @@ mod build_script { assert!(fixture.log().is_empty()); } + #[test] + fn strict_is_enabled_by_one_or_the_word_true_however_it_is_cased() { + // Held for its environment isolation and restored on drop; the + // assertions below read process environment, not fixture state. + let _fixture = Fixture::new(); + + for value in ["1", "true", "TRUE", "True"] { + env::set_var("ZVEC_STRICT_LIB", value); + assert!( + strict_lib_dir_required(), + "value {value:?} must enable strict mode" + ); + } + // Values that read as "yes" in some other toolchains stay off, so the + // word this contract accepts is exactly `true`. + for value in ["yes", "on", "2", " true"] { + env::set_var("ZVEC_STRICT_LIB", value); + assert!( + !strict_lib_dir_required(), + "value {value:?} must not enable strict mode" + ); + } + } + + #[test] + fn strict_mode_keeps_the_jieba_dict_inside_the_configured_directory() { + let fixture = Fixture::new(); + let explicit = fixture.install_explicit_library(); + + // A dictionary in the sibling checkout, which non-strict resolution + // takes. + let dict_suffix = Path::new("thirdparty") + .join("cppjieba") + .join("cppjieba-5.6.7") + .join("dict"); + let sibling_dict = fixture.root.join("zvec").join(&dict_suffix); + fs::create_dir_all(&sibling_dict).unwrap(); + fs::write(sibling_dict.join("jieba.dict.utf8"), "dict").unwrap(); + fs::write(sibling_dict.join("hmm_model.utf8"), "model").unwrap(); + + let sibling = Some(fixture.root.join("zvec")); + let submodule = fixture.root.join("submodule"); + let auto_build = fixture.root.join("auto-build"); + + // Non-strict: the sibling dictionary is used. Strict mode has to + // refuse exactly this, because that dictionary belongs to a different + // copy of the SDK than the configured library. + assert_eq!( + resolve_jieba_dict_dir(&sibling, &submodule, &auto_build, &explicit, false), + Some(sibling_dict) + ); + // Strict: only the configured directory may supply it, and this one + // does not, so the crate registers no dictionary rather than one from + // another copy. + assert_eq!( + resolve_jieba_dict_dir(&sibling, &submodule, &auto_build, &explicit, true), + None + ); + } + + #[test] + fn the_expected_names_read_as_one_of_where_several_are_valid() { + // Held for its environment isolation and restored on drop. + let _fixture = Fixture::new(); + + env::set_var("CARGO_CFG_TARGET_OS", "linux"); + assert_eq!(describe_expected_lib_file_names(), "libzvec_c_api.so"); + + env::set_var("CARGO_CFG_TARGET_OS", "windows"); + env::set_var("CARGO_CFG_TARGET_ENV", "gnu"); + assert_eq!( + describe_expected_lib_file_names(), + "one of libzvec_c_api.dll.a, zvec_c_api.lib, zvec_c_api.dll" + ); + let message = strict_lib_failure_message(None); + assert!( + message.contains("one of libzvec_c_api.dll.a, zvec_c_api.lib, zvec_c_api.dll"), + "the message must not read as if every name were required: {message}" + ); + + // MSVC accepts exactly one artifact, so it reads as a plain name. + env::set_var("CARGO_CFG_TARGET_ENV", "msvc"); + assert_eq!(describe_expected_lib_file_names(), "zvec_c_api.lib"); + } + #[test] fn strict_mode_never_reaches_acquisition() { let fixture = Fixture::new();