libduckdb-sys 1.10502.0

Native bindings to the libduckdb library, C API
Documentation
use crate::{win_target, write_bindings};
use std::{
    collections::{HashMap, HashSet},
    path::Path,
};

#[derive(serde::Deserialize)]
struct Sources {
    cpp_files: HashSet<String>,
    include_dirs: HashSet<String>,
}

#[derive(serde::Deserialize)]
struct Manifest {
    base: Sources,
    extensions: HashMap<String, Sources>,
}

fn add_extension(
    manifest: &Manifest,
    extension: &str,
    cpp_files: &mut HashSet<String>,
    include_dirs: &mut HashSet<String>,
) {
    let sources = manifest.extensions.get(extension).unwrap_or_else(|| {
        let mut available = manifest.extensions.keys().cloned().collect::<Vec<_>>();
        available.sort();
        panic!(
            "extension `{extension}` is missing from duckdb manifest; available extensions: {}",
            available.join(", ")
        );
    });
    cpp_files.extend(sources.cpp_files.clone());
    include_dirs.extend(sources.include_dirs.clone());
}

fn rewrite_generated_extension_loader(enabled_extensions: &[String], loader_path: &Path) -> std::io::Result<()> {
    // DuckDB 1.5.0's package_build.py generates this translation unit from the full packaged
    // extension list, not the subset that duckdb-rs enables through Cargo features. That means
    // the generated loader can unconditionally include/load extensions like parquet/json even
    // when we deliberately excluded their sources, which breaks `bundled` builds. We cannot
    // influence package_build.py from here without patching the DuckDB submodule, so the least
    // fragile option is to regenerate the entire TU from a vendored copy of DuckDB's tiny
    // upstream template.
    let template = include_str!("generated_extension_loader.cpp.in").replace('\r', "");
    for placeholder in [
        "${EXT_LOADER_BODY}",
        "${EXT_NAME_VECTOR_INITIALIZER}",
        "${EXT_TEST_PATH_INITIALIZER}",
    ] {
        assert!(
            template.contains(placeholder),
            "generated extension loader template is missing placeholder {placeholder}; DuckDB upstream likely changed"
        );
    }

    let mut extension_headers = String::new();
    let mut extension_loader_body = String::new();
    for ext in enabled_extensions {
        extension_headers.push_str(&format!("#include \"{ext}_extension.hpp\"\n"));
        extension_loader_body.push_str(&format!(
            "    if (extension==\"{ext}\") {{\n        db.LoadStaticExtension<{}Extension>();\n        return ExtensionLoadResult::LOADED_EXTENSION;\n    }}\n",
            to_camel_case(ext)
        ));
    }

    let linked_extensions = enabled_extensions
        .iter()
        .map(|ext| format!("\"{ext}\""))
        .collect::<Vec<_>>()
        .join(", ");

    let loader_code = template
        .replace("${EXT_LOADER_BODY}", &extension_loader_body)
        .replace("${EXT_NAME_VECTOR_INITIALIZER}", &linked_extensions)
        .replace("${EXT_TEST_PATH_INITIALIZER}", "");

    let generated_banner = concat!(
        "// Generated by crates/libduckdb-sys/build_bundled_cc.rs from\n",
        "// crates/libduckdb-sys/generated_extension_loader.cpp.in.\n",
        "// Do not edit this file directly.\n\n"
    );

    std::fs::write(
        loader_path,
        format!("{generated_banner}{extension_headers}{loader_code}"),
    )
}

fn extension_enabled(extension: &str) -> bool {
    extension == "core_functions"
        || (extension == "parquet" && cfg!(feature = "parquet"))
        || (extension == "json" && cfg!(feature = "json"))
}

fn to_camel_case(extension: &str) -> String {
    extension
        .split('_')
        .filter(|part| !part.is_empty())
        .map(|part| {
            let (first, rest) = part.split_at(1);
            format!("{}{}", first.to_ascii_uppercase(), rest)
        })
        .collect()
}

fn untar_archive(out_dir: &str) {
    let path = "duckdb.tar.gz";

    let tar_gz = std::fs::File::open(path).expect("archive file");
    let tar = flate2::read::GzDecoder::new(tar_gz);
    let mut archive = tar::Archive::new(tar);
    archive.unpack(out_dir).expect("archive");
}

pub fn main(out_dir: &str, out_path: &Path) {
    untar_archive(out_dir);

    write_bindings(&Path::new(out_dir).join("duckdb/src/include"), out_path);

    let manifest_file = std::fs::File::open(format!("{out_dir}/duckdb/manifest.json")).expect("manifest file");
    let manifest: Manifest = serde_json::from_reader(manifest_file).expect("reading manifest file");

    let mut cpp_files = HashSet::new();
    let mut include_dirs = HashSet::new();
    let mut extensions = manifest
        .extensions
        .keys()
        .filter(|name| extension_enabled(name))
        .cloned()
        .collect::<Vec<String>>();
    extensions.sort();

    cpp_files.extend(manifest.base.cpp_files.clone());
    include_dirs.extend(manifest.base.include_dirs.iter().cloned());

    let mut cfg = cc::Build::new();

    for extension in &extensions {
        add_extension(&manifest, extension, &mut cpp_files, &mut include_dirs);
        cfg.define(
            &format!("DUCKDB_EXTENSION_{}_LINKED", extension.to_uppercase()),
            Some("1"),
        );
    }
    let loader_path = Path::new(out_dir).join("duckdb/generated_extension_loader_package_build.cpp");
    rewrite_generated_extension_loader(&extensions, &loader_path)
        .expect("failed to rewrite generated extension loader");
    println!("cargo:warning=generated extension loader: {}", loader_path.display());

    cfg.define("DUCKDB_EXTENSION_AUTOINSTALL_DEFAULT", "1");
    cfg.define("DUCKDB_EXTENSION_AUTOLOAD_DEFAULT", "1");

    println!("cargo:rerun-if-changed=duckdb.tar.gz");

    cfg.include("duckdb");
    cfg.includes(include_dirs.iter().map(|dir| format!("{out_dir}/duckdb/{dir}")));

    let mut cpp_files_vec: Vec<String> = cpp_files.into_iter().collect();
    cpp_files_vec.sort();
    for f in cpp_files_vec.into_iter().map(|file| format!("{out_dir}/{file}")) {
        cfg.file(f);
    }

    cfg.cpp(true)
        .flag_if_supported("-std=c++11")
        .flag_if_supported("/utf-8")
        .flag_if_supported("/bigobj")
        .warnings(false)
        .flag_if_supported("-w");

    let is_debug = match std::env::var("DEBUG") {
        Ok(v) => v != "false" && v != "0",
        Err(_) => false,
    };
    if !is_debug {
        cfg.define("NDEBUG", None);
    }

    if win_target() {
        cfg.define("DUCKDB_BUILD_LIBRARY", None);
    }
    cfg.compile("duckdb");

    println!("cargo:lib_dir={out_dir}");
}