use std::{env, path::Path};
#[allow(dead_code)]
fn win_target() -> bool {
std::env::var("CARGO_CFG_WINDOWS").is_ok()
}
#[allow(dead_code)]
fn is_compiler(compiler_name: &str) -> bool {
std::env::var("CARGO_CFG_TARGET_ENV").is_ok_and(|v| v == compiler_name)
}
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let out_path = Path::new(&out_dir).join("bindgen.rs");
#[cfg(feature = "bundled")]
build_bundled_backend::main(&out_dir, &out_path);
#[cfg(not(feature = "bundled"))]
build_linked::main(&out_dir, &out_path)
}
#[cfg(all(feature = "bundled", not(feature = "bundled-cmake")))]
mod build_bundled_cc;
#[cfg(all(feature = "bundled", feature = "bundled-cmake"))]
mod build_bundled_cmake;
#[cfg(all(feature = "bundled", not(feature = "bundled-cmake")))]
use crate::build_bundled_cc as build_bundled_backend;
#[cfg(all(feature = "bundled", feature = "bundled-cmake"))]
use crate::build_bundled_cmake as build_bundled_backend;
#[cfg(feature = "bundled")]
#[allow(unused_variables)]
pub(crate) fn write_bindings(header_dir: &Path, out_path: &Path) {
#[cfg(feature = "buildtime_bindgen")]
{
use crate::{HeaderLocation, bindings};
let header = HeaderLocation::FromPath(header_dir.to_string_lossy().into_owned());
bindings::write_to_out_dir(header, out_path);
}
#[cfg(not(feature = "buildtime_bindgen"))]
{
use std::fs;
fs::copy(
#[cfg(not(feature = "loadable-extension"))]
"src/bindgen_bundled_version.rs",
#[cfg(feature = "loadable-extension")]
"src/bindgen_bundled_version_loadable.rs",
out_path,
)
.expect("Could not copy bindings to output directory");
}
}
pub enum HeaderLocation {
FromEnvironment,
Wrapper,
FromPath(String),
}
impl From<HeaderLocation> for String {
fn from(header: HeaderLocation) -> Self {
match header {
HeaderLocation::FromEnvironment => {
let mut header = env::var("DUCKDB_INCLUDE_DIR").unwrap_or_else(|_| env::var("DUCKDB_LIB_DIR").unwrap());
header.push_str(if cfg!(feature = "loadable-extension") {
"/duckdb_extension.h"
} else {
"/duckdb.h"
});
header
}
HeaderLocation::Wrapper => {
if cfg!(feature = "loadable-extension") {
"wrapper_ext.h".into()
} else {
"wrapper.h".into()
}
}
HeaderLocation::FromPath(path) => format!(
"{}/{}",
path,
if cfg!(feature = "loadable-extension") {
"duckdb_extension.h"
} else {
"duckdb.h"
}
),
}
}
}
#[cfg(not(feature = "bundled"))]
mod build_linked {
#[cfg(feature = "vcpkg")]
extern crate vcpkg;
#[cfg(feature = "buildtime_bindgen")]
use super::bindings;
use super::{HeaderLocation, is_compiler, win_target};
use std::{
env, fs, io,
path::{Path, PathBuf},
};
pub fn main(out_dir: &str, out_path: &Path) {
#[allow(unused_variables)]
let header = find_duckdb(out_dir);
if let Some(include_dir) = include_dir_for(&header) {
println!("cargo:include={include_dir}");
}
#[cfg(not(feature = "buildtime_bindgen"))]
{
std::fs::copy(
#[cfg(not(feature = "loadable-extension"))]
"src/bindgen_bundled_version.rs",
#[cfg(feature = "loadable-extension")]
"src/bindgen_bundled_version_loadable.rs",
out_path,
)
.expect("Could not copy bindings to output directory");
}
#[cfg(feature = "buildtime_bindgen")]
{
bindings::write_to_out_dir(header, out_path);
}
}
fn include_dir_for(header: &HeaderLocation) -> Option<String> {
match header {
HeaderLocation::FromEnvironment => env::var("DUCKDB_INCLUDE_DIR")
.or_else(|_| {
env::var("DUCKDB_LIB_DIR").and_then(|dir| {
if Path::new(&dir).join("duckdb.h").exists() {
Ok(dir)
} else {
Err(env::VarError::NotPresent)
}
})
})
.ok(),
HeaderLocation::FromPath(path) => Some(path.clone()),
HeaderLocation::Wrapper => None,
}
}
fn link_directive() -> &'static str {
match env::var("DUCKDB_STATIC") {
Ok(v) if v != "0" => "static=duckdb_static",
_ => "dylib=duckdb",
}
}
fn find_duckdb(out_dir: &str) -> HeaderLocation {
println!("cargo:rerun-if-env-changed=DUCKDB_DOWNLOAD_LIB");
if !cfg!(feature = "loadable-extension") {
println!("cargo:rerun-if-env-changed=DUCKDB_INCLUDE_DIR");
println!("cargo:rerun-if-env-changed=DUCKDB_LIB_DIR");
println!("cargo:rerun-if-env-changed=DUCKDB_STATIC");
if cfg!(feature = "vcpkg") && is_compiler("msvc") {
println!("cargo:rerun-if-env-changed=VCPKGRS_DYNAMIC");
}
println!("cargo:link-target=duckdb");
}
if win_target() && cfg!(feature = "winduckdb") {
if !cfg!(feature = "loadable-extension") {
println!("cargo:rustc-link-lib=dylib=duckdb");
}
return HeaderLocation::Wrapper;
}
if let Ok(dir) = env::var("DUCKDB_LIB_DIR") {
println!("cargo:rustc-env=LD_LIBRARY_PATH={dir}");
let pkgconfig_path = Path::new(&dir).join("pkgconfig");
unsafe { env::set_var("PKG_CONFIG_PATH", pkgconfig_path) };
#[cfg(feature = "pkg-config")]
let lib_found = pkg_config::Config::new().probe("duckdb").is_ok();
#[cfg(not(feature = "pkg-config"))]
let lib_found = false;
if !lib_found {
println!("cargo:rustc-link-lib={}", link_directive());
println!("cargo:rustc-link-search={dir}");
}
return HeaderLocation::FromEnvironment;
}
if should_download_libduckdb() {
return download_libduckdb(out_dir).unwrap_or_else(|err| panic!("Failed to download libduckdb: {err}"));
}
if let Some(header) = try_vcpkg() {
return header;
}
#[cfg(feature = "pkg-config")]
{
match pkg_config::Config::new().print_system_libs(false).probe("duckdb") {
Ok(mut lib) => {
if let Some(header) = lib.include_paths.pop() {
HeaderLocation::FromPath(header.to_string_lossy().into())
} else {
HeaderLocation::Wrapper
}
}
Err(_) => {
if !cfg!(feature = "loadable-extension") {
println!("cargo:rustc-link-lib={}", link_directive());
}
HeaderLocation::Wrapper
}
}
}
#[cfg(not(feature = "pkg-config"))]
{
if !cfg!(feature = "loadable-extension") {
println!("cargo:rustc-link-lib={}", link_directive());
}
HeaderLocation::Wrapper
}
}
fn try_vcpkg() -> Option<HeaderLocation> {
#[cfg(feature = "vcpkg")]
if is_compiler("msvc") {
if let Ok(mut lib) = vcpkg::Config::new().probe("duckdb") {
if let Some(header) = lib.include_paths.pop() {
return Some(HeaderLocation::FromPath(header.to_string_lossy().into()));
}
}
}
None
}
fn should_download_libduckdb() -> bool {
env::var("DUCKDB_DOWNLOAD_LIB")
.map(|value| matches!(value.to_ascii_lowercase().as_str(), "1" | "true"))
.unwrap_or(false)
}
fn download_libduckdb(out_dir: &str) -> Result<HeaderLocation, Box<dyn std::error::Error>> {
let target = env::var("TARGET")?;
let archive = LibduckdbArchive::for_target(&target)
.ok_or_else(|| format!("No pre-built libduckdb available for target '{target}'"))?;
let version = duckdb_version_from_pkg_version(env!("CARGO_PKG_VERSION"));
let download_dir = workspace_download_dir(out_dir)?.join(&target).join(&version);
fs::create_dir_all(&download_dir)?;
let archive_path = download_dir.join(archive.archive_name);
let lib_marker = download_dir.join(archive.dynamic_lib);
if lib_marker.exists() {
println!("cargo:warning=Reusing libduckdb from {}", download_dir.display());
} else {
let client = http_client()?;
let url = archive.download_url(&version);
ensure_libduckdb(&client, &url, &archive_path)?;
extract_libduckdb(&archive_path, &download_dir)?;
if !lib_marker.exists() {
return Err(format!(
"Downloaded archive did not contain expected library '{}'",
archive.dynamic_lib
)
.into());
}
}
configure_link_search(&download_dir);
copy_libduckdb(&download_dir, archive.dynamic_lib, out_dir)?;
Ok(HeaderLocation::FromPath(download_dir.to_string_lossy().into_owned()))
}
fn configure_link_search(lib_dir: &Path) {
println!("cargo:rustc-link-search=native={}", lib_dir.display());
if !cfg!(feature = "loadable-extension") {
println!("cargo:rustc-link-lib={}", link_directive());
}
if !win_target() {
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display());
}
}
fn ensure_libduckdb(
client: &reqwest::blocking::Client,
url: &str,
archive_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
if archive_path.exists() {
println!("cargo:warning=libduckdb already present at {}", archive_path.display());
return Ok(());
}
let tmp_path = archive_path.with_extension("download");
if let Some(parent) = archive_path.parent() {
fs::create_dir_all(parent)?;
}
let mut response = client.get(url).send()?.error_for_status()?;
let mut tmp_file = fs::File::create(&tmp_path)?;
io::copy(&mut response, &mut tmp_file)?;
fs::rename(&tmp_path, archive_path)?;
println!("cargo:warning=Downloaded libduckdb from {url}");
Ok(())
}
fn extract_libduckdb(archive_path: &Path, destination: &Path) -> Result<(), Box<dyn std::error::Error>> {
let file = fs::File::open(archive_path)?;
let mut archive = zip::ZipArchive::new(file)?;
archive.extract(destination)?;
println!("cargo:warning=Extracted libduckdb to {}", destination.display());
Ok(())
}
fn copy_libduckdb(
download_dir: &Path,
lib_filename: &str,
out_dir: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let Some(deps_dir) = profile_deps_dir(out_dir) else {
return Err("Could not determine target/deps directory for runtime lib copy".into());
};
fs::create_dir_all(&deps_dir)?;
let source = download_dir.join(lib_filename);
let dest = deps_dir.join(lib_filename);
if dest.exists() {
fs::remove_file(&dest)?;
}
fs::copy(&source, &dest)?;
println!("cargo:warning=Copied libduckdb to {}", dest.display());
Ok(())
}
fn workspace_download_dir(out_dir: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Ok(dir) = env::var("CARGO_TARGET_DIR") {
return Ok(PathBuf::from(dir).join("duckdb-download"));
}
let target_root = Path::new(out_dir)
.ancestors()
.find(|ancestor| {
ancestor
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name == "target")
})
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("target"));
Ok(target_root.join("duckdb-download"))
}
fn duckdb_version_from_pkg_version(pkg_version: &str) -> String {
let encoded = pkg_version
.split('.')
.nth(1)
.expect("CARGO_PKG_VERSION should use the documented 1.MAJOR_MINOR_PATCH.x format")
.parse::<u32>()
.expect("CARGO_PKG_VERSION should encode the DuckDB version as an integer in its second component");
let duckdb_major = encoded / 10_000;
let duckdb_minor = (encoded / 100) % 100;
let duckdb_patch = encoded % 100;
format!("{duckdb_major}.{duckdb_minor}.{duckdb_patch}")
}
fn profile_deps_dir(out_dir: &str) -> Option<PathBuf> {
let build_dir = Path::new(out_dir).ancestors().find(|ancestor| {
ancestor
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name == "build")
})?;
let profile_dir = build_dir.parent()?;
Some(profile_dir.join("deps"))
}
struct LibduckdbArchive {
archive_name: &'static str,
dynamic_lib: &'static str,
}
impl LibduckdbArchive {
fn for_target(target: &str) -> Option<Self> {
match target {
t if t.ends_with("apple-darwin") => Some(Self {
archive_name: "libduckdb-osx-universal.zip",
dynamic_lib: "libduckdb.dylib",
}),
"x86_64-unknown-linux-gnu" => Some(Self {
archive_name: "libduckdb-linux-amd64.zip",
dynamic_lib: "libduckdb.so",
}),
"aarch64-unknown-linux-gnu" => Some(Self {
archive_name: "libduckdb-linux-arm64.zip",
dynamic_lib: "libduckdb.so",
}),
"x86_64-pc-windows-msvc" => Some(Self {
archive_name: "libduckdb-windows-amd64.zip",
dynamic_lib: "duckdb.dll",
}),
"aarch64-pc-windows-msvc" => Some(Self {
archive_name: "libduckdb-windows-arm64.zip",
dynamic_lib: "duckdb.dll",
}),
_ => None,
}
}
fn download_url(&self, version: &str) -> String {
format!(
"https://github.com/duckdb/duckdb/releases/download/v{version}/{}",
self.archive_name
)
}
}
fn http_client() -> Result<reqwest::blocking::Client, reqwest::Error> {
let timeout = env::var("CARGO_HTTP_TIMEOUT")
.or_else(|_| env::var("HTTP_TIMEOUT"))
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(90);
reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(timeout))
.build()
}
}
#[cfg(feature = "buildtime_bindgen")]
mod bindings {
use super::HeaderLocation;
use std::{fs::OpenOptions, io::Write, path::Path};
#[cfg(feature = "loadable-extension")]
fn extract_method(ty: &syn::Type) -> Option<&syn::TypeBareFn> {
match ty {
syn::Type::Path(tp) => tp.path.segments.last(),
_ => None,
}
.map(|seg| match &seg.arguments {
syn::PathArguments::AngleBracketed(args) => args.args.first(),
_ => None,
})?
.map(|arg| match arg {
syn::GenericArgument::Type(t) => Some(t),
_ => None,
})?
.map(|ty| match ty {
syn::Type::BareFn(r) => Some(r),
_ => None,
})?
}
#[cfg(feature = "loadable-extension")]
fn generate_functions(output: &mut String) {
let ast: syn::File = syn::parse_str(output).expect("could not parse bindgen output");
let duckdb_ext_api_v1: syn::ItemStruct = ast
.items
.into_iter()
.find_map(|i| {
if let syn::Item::Struct(s) = i {
if s.ident == "duckdb_ext_api_v1" { Some(s) } else { None }
} else {
None
}
})
.expect("could not find duckdb_ext_api_v1");
let p_api = quote::format_ident!("p_api");
let mut stores = Vec::new();
for field in duckdb_ext_api_v1.fields {
let ident = field.ident.expect("unnamed field");
let span = ident.span();
let function_name = ident.to_string();
let ptr_name = syn::Ident::new(format!("__{}", function_name.to_uppercase()).as_ref(), span);
let duckdb_fn_name = syn::Ident::new(&function_name, span);
let method = extract_method(&field.ty).unwrap_or_else(|| panic!("unexpected type for {function_name}"));
let arg_names: syn::punctuated::Punctuated<&syn::Ident, syn::token::Comma> =
method.inputs.iter().map(|i| &i.name.as_ref().unwrap().0).collect();
let args = &method.inputs;
let ty = &method.output;
let tokens = quote::quote! {
static #ptr_name: ::std::sync::atomic::AtomicPtr<()> = ::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
pub unsafe fn #duckdb_fn_name(#args) #ty {
let function_ptr = #ptr_name.load(::std::sync::atomic::Ordering::Acquire);
assert!(!function_ptr.is_null(), "DuckDB API not initialized or DuckDB feature omitted");
let fun: unsafe extern "C" fn(#args) #ty = ::std::mem::transmute(function_ptr);
(fun)(#arg_names)
}
};
output.push_str(&prettyplease::unparse(
&syn::parse2(tokens).expect("could not parse quote output"),
));
output.push('\n');
stores.push(quote::quote! {
if let Some(fun) = (*#p_api).#ident {
#ptr_name.store(
fun as usize as *mut (),
::std::sync::atomic::Ordering::Release,
);
}
});
}
let tokens = quote::quote! {
pub unsafe fn duckdb_rs_extension_api_init(info: duckdb_extension_info, access: *const duckdb_extension_access, version: &str) -> ::std::result::Result<bool, &'static str> {
let version_c_string = std::ffi::CString::new(version).unwrap();
let #p_api = (*access).get_api.unwrap()(info, version_c_string.as_ptr()) as *const duckdb_ext_api_v1;
if #p_api.is_null() {
return Ok(false);
}
#(#stores)*
Ok(true)
}
};
output.push_str(&prettyplease::unparse(
&syn::parse2(tokens).expect("could not parse quote output"),
));
output.push('\n');
}
pub fn write_to_out_dir(header: HeaderLocation, out_path: &Path) {
let header: String = header.into();
let mut output = Vec::new();
let mut builder = bindgen::builder();
if cfg!(feature = "loadable-extension") {
builder = builder.ignore_functions();
}
builder
.trust_clang_mangling(false)
.header(header.clone())
.allowlist_item(r#"(\w*duckdb\w*)"#)
.allowlist_type("idx_t")
.layout_tests(false) .clang_arg("-DDUCKDB_EXTENSION_API_VERSION_UNSTABLE")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.unwrap_or_else(|_| panic!("could not run bindgen on header {header}"))
.write(Box::new(&mut output))
.expect("could not write output of bindgen");
#[allow(unused_mut)]
let mut output = String::from_utf8(output).expect("bindgen output was not UTF-8?!");
#[cfg(feature = "loadable-extension")]
generate_functions(&mut output);
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(out_path)
.unwrap_or_else(|_| panic!("Could not write to {out_path:?}"));
file.write_all(output.as_bytes())
.unwrap_or_else(|_| panic!("Could not write to {out_path:?}"));
}
}