quack-rs 0.10.0

Production-grade Rust SDK for building DuckDB loadable extensions
Documentation
// SPDX-License-Identifier: MIT
// Copyright 2026 Tom F. <https://github.com/tomtom215/>

//! Build script for quack-rs.
//!
//! When the `bundled-test` feature is active this compiles a tiny C++ shim
//! (`src/testing/bundled_api_init.cpp`) that exposes `DuckDB`'s internal
//! `CreateAPIv1()` function as a C-linkage symbol.  The Rust side calls this
//! at test startup to populate the `loadable-extension` dispatch table from
//! the bundled `DuckDB` symbols, enabling `InMemoryDb` to work in `cargo test`.

use std::env;
use std::path::{Path, PathBuf};

fn main() {
    // On Windows, bundled DuckDB uses the Restart Manager API (RmStartSession,
    // RmEndSession, RmRegisterResources, RmGetList) in its AdditionalLockInfo()
    // function.  libduckdb-sys's build script does not emit a link directive for
    // rstrtmgr.lib, so we add it here whenever we're building for Windows.
    if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
        println!("cargo:rustc-link-lib=rstrtmgr");
    }

    // Only needed when bundled-test is enabled.
    if env::var("CARGO_FEATURE_BUNDLED_TEST").is_err() {
        return;
    }

    let duckdb_include = find_duckdb_include();

    cc::Build::new()
        .cpp(true)
        .file("src/testing/bundled_api_init.cpp")
        .include(&duckdb_include)
        // DuckDB headers use C++11 features; keep it minimal.
        .flag_if_supported("-std=c++11")
        // Suppress warnings from DuckDB headers that we don't own.
        .flag_if_supported("-w")
        // On Windows/MSVC the DuckDB headers declare all public symbols with
        // __declspec(dllimport) unless DUCKDB_STATIC_BUILD is defined.  Without
        // this flag the compiler emits __imp_duckdb_* references, but the
        // bundled static library exports plain duckdb_* symbols, causing
        // LNK2019 "unresolved external symbol __imp_duckdb_*" errors at link.
        .define("DUCKDB_STATIC_BUILD", None)
        .compile("quack_rs_bundled_init");

    println!("cargo:rerun-if-changed=src/testing/bundled_api_init.cpp");
}

/// Locates the `DuckDB` include directory from `libduckdb-sys`'s build output.
///
/// Cargo places all crate build outputs under `target/{profile}/build/`.  We
/// navigate up from our own `OUT_DIR` to that shared `build/` directory and
/// search for a `libduckdb-sys-*` subdirectory that contains the extracted
/// `DuckDB` source tree (present only when the `bundled` feature is active).
///
/// **Build-order caveat**: Cargo runs build scripts as soon as their
/// `[build-dependencies]` are ready — *before* regular `[dependencies]` are
/// compiled.  This means `libduckdb-sys` (a regular dependency) may still be
/// compiling when this function executes.  We therefore poll with a timeout
/// to wait for the headers to appear.
fn find_duckdb_include() -> PathBuf {
    // OUT_DIR  = .../target/{profile}/build/quack-rs-{hash}/out
    // We want  = .../target/{profile}/build/
    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
    let build_dir = out_dir
        .parent() // .../build/quack-rs-{hash}
        .and_then(Path::parent) // .../build
        .expect("could not navigate to Cargo build directory from OUT_DIR");

    // Poll for up to ~10 minutes (libduckdb-sys compiles the full DuckDB C++
    // source when the `bundled` feature is active, which can take several
    // minutes on CI runners).
    for attempt in 0..120 {
        if let Some(path) = scan_for_duckdb_headers(build_dir) {
            return path;
        }
        if attempt < 119 {
            std::thread::sleep(std::time::Duration::from_secs(5));
        }
    }

    panic!(
        "Could not find DuckDB headers from libduckdb-sys build output.\n\
         Ensure that the `duckdb` dependency is resolved with `features = [\"bundled\"]`\n\
         and that `libduckdb-sys` has been built before this crate."
    );
}

/// Scans the Cargo build directory for `libduckdb-sys-*` subdirectories
/// that contain the extracted `DuckDB` include tree.
fn scan_for_duckdb_headers(build_dir: &Path) -> Option<PathBuf> {
    for entry in std::fs::read_dir(build_dir).ok()?.flatten() {
        if !entry
            .file_name()
            .to_string_lossy()
            .starts_with("libduckdb-sys-")
        {
            continue;
        }

        let candidate = entry.path().join("out/duckdb/src/include");
        if candidate.is_dir() {
            return Some(candidate);
        }
    }
    None
}