From 18c2cfeb41d26e26d53ba53ca9b51416421823b1 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 28 Aug 2026 20:06:39 -0600 Subject: [PATCH] fix: Prefer GNU linux builds in install scripts for duckdb support, and gate duckdb support on linux hosts that are MUSL until MUSL support is added --- scripts/install_coyote.ps1 | 8 +++++++- src/config/update.rs | 23 +++++++++++++++++++++++ src/rag/mod.rs | 6 ++++++ src/rag/providers/duckdb.rs | 21 ++++++++++++++++++++- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/scripts/install_coyote.ps1 b/scripts/install_coyote.ps1 index e342667..3995532 100644 --- a/scripts/install_coyote.ps1 +++ b/scripts/install_coyote.ps1 @@ -62,7 +62,13 @@ if ($os -eq 'windows') { else { $candidates += 'coyote-aarch64-apple-darwin.tar.gz' } } elseif ($os -eq 'linux') { if ($arch -eq 'x86_64') { - $candidates += 'coyote-x86_64-unknown-linux-gnu.tar.gz' + # Mirror install_coyote.sh: only offer the gnu build when glibc is detected. + $libc = 'musl' + try { getconf GNU_LIBC_VERSION *> $null; if ($LASTEXITCODE -eq 0) { $libc = 'gnu' } } catch { } + try { if ((ldd --version 2>&1 | Out-String) -imatch 'glibc') { $libc = 'gnu' } } catch { } + if ($libc -eq 'gnu') { + $candidates += 'coyote-x86_64-unknown-linux-gnu.tar.gz' + } $candidates += 'coyote-x86_64-unknown-linux-musl.tar.gz' } else { $candidates += 'coyote-aarch64-unknown-linux-musl.tar.gz' diff --git a/src/config/update.rs b/src/config/update.rs index ce9f514..ae7c4c3 100644 --- a/src/config/update.rs +++ b/src/config/update.rs @@ -68,9 +68,32 @@ fn normalize_version(requested: Option) -> Option { } } +fn host_has_glibc() -> bool { + if process::Command::new("getconf") + .arg("GNU_LIBC_VERSION") + .output() + .is_ok_and(|out| out.status.success()) + { + return true; + } + process::Command::new("ldd") + .arg("--version") + .output() + .is_ok_and(|out| { + let combined = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + combined.to_lowercase().contains("glibc") + }) +} + fn preferred_update_target() -> Option<&'static str> { match (env::consts::OS, env::consts::ARCH) { + ("linux", "x86_64") if host_has_glibc() => Some("x86_64-unknown-linux-gnu"), ("linux", "x86_64") => Some("x86_64-unknown-linux-musl"), + // No aarch64 gnu asset is published; musl is the only option. ("linux", "aarch64") => Some("aarch64-unknown-linux-musl"), _ => None, } diff --git a/src/rag/mod.rs b/src/rag/mod.rs index a5ab8ce..05ceaf1 100644 --- a/src/rag/mod.rs +++ b/src/rag/mod.rs @@ -1869,6 +1869,12 @@ fn select_embedding_model(models: &[&Model]) -> Result { } pub(crate) fn select_rag_driver() -> Result { + // Statically linked musl builds cannot dlopen DuckDB extensions, so duckdb is not offered there. + #[cfg(target_env = "musl")] + let options = vec![ + "yaml — portable, in-memory HNSW; usable from several Coyote processes at once (default)", + ]; + #[cfg(not(target_env = "musl"))] let options = vec![ "yaml — portable, in-memory HNSW; usable from several Coyote processes at once (default)", "duckdb — persistent on-disk store; vectors and content survive restarts; HNSW approximate search.", diff --git a/src/rag/providers/duckdb.rs b/src/rag/providers/duckdb.rs index 05b34f2..47ac154 100644 --- a/src/rag/providers/duckdb.rs +++ b/src/rag/providers/duckdb.rs @@ -85,6 +85,21 @@ pub struct DuckDbProvider { } impl DuckDbProvider { + /// Fail fast on musl builds, before any filesystem or DuckDB work. Coyote's musl + /// builds are statically linked, and a statically linked binary cannot dlopen the + /// `vss`/`fts` extension shared objects, so this driver can never work there. + fn reject_musl() -> Result<()> { + #[cfg(target_env = "musl")] + bail!( + "The duckdb RAG driver is unavailable in this build of coyote: musl builds \ + are statically linked, and a statically linked binary cannot load DuckDB \ + extensions like `vss`/`fts` (dlopen is unsupported). Use the `qdrant` RAG \ + driver instead, or a gnu-libc build of coyote." + ); + #[cfg(not(target_env = "musl"))] + Ok(()) + } + /// Open (or create) the DuckDB file. `dim` is the embedding vector dimension, /// supplied by the caller who knows the model. /// @@ -95,6 +110,7 @@ impl DuckDbProvider { /// created or initialized here, or lazily through `ensure_writable` on the rebuild /// path. pub fn open(db_path: &Path, dim: usize) -> Result { + Self::reject_musl()?; let (conn, writable) = Self::open_for_workload(db_path, dim)?; // A reopened file may already carry a live FTS index from a previous session, // in which case keyword search works immediately. @@ -173,6 +189,7 @@ impl DuckDbProvider { } pub fn introspect_dim(db_path: &Path) -> Result> { + Self::reject_musl()?; if !db_path.exists() { return Ok(None); } @@ -819,7 +836,9 @@ impl RagProvider for DuckDbProvider { } } -#[cfg(test)] +// Not compiled for musl: `open` bails there (statically linked binaries cannot dlopen +// the extension shared objects), so every test in this module would fail at setup. +#[cfg(all(test, not(target_env = "musl")))] mod tests { use super::*; use crate::rag::provider::RagProvider;