Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,40 @@ 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 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.

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 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, 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

Prebuilt SDKs remain cached at `$OUT_DIR/zvec-prebuilt`, under Cargo's `target`
Expand Down
6 changes: 6 additions & 0 deletions zvec-sys/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
98 changes: 86 additions & 12 deletions zvec-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -106,17 +116,30 @@ fn resolve_lib_dir(
prebuilt_cache_dir: &Path,
auto_build_dir: &Path,
) -> Option<PathBuf> {
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
Expand Down Expand Up @@ -165,6 +188,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<PathBuf>) -> String {
let target = env::var("TARGET").unwrap_or_default();
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.";
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<PathBuf>,
submodule_zvec: &Path,
Expand Down Expand Up @@ -201,27 +259,43 @@ 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"],
}
}

/// 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()
.any(|name| dir.join(name).is_file())
}

fn auto_build_zvec(build_dir: &Path) -> Option<PathBuf> {
let zvec_src = build_dir.join("zvec");

Expand Down
Loading
Loading