use std::{
path::{Path, PathBuf},
process::Command,
};
fn main() {
let rid = target_rid();
run(rid, Some(env!("CARGO_PKG_VERSION")));
}
fn target_rid() -> &'static str {
let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
match (os.as_str(), arch.as_str()) {
("linux", "x86_64") => "linux-x64",
("linux", "aarch64") => "linux-arm64",
("macos", "aarch64") => "osx-arm64",
("windows", "x86_64") => "win-x64",
_ => panic!(
"spicedb-embedded-sys does not support target {}-{}. \
Supported: linux-x64, linux-arm64, osx-arm64, win-x64.",
os, arch
),
}
}
fn run(rid: &str, release_version: Option<&str>) {
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR set by Cargo"));
let manifest_dir = PathBuf::from(
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by Cargo"),
);
let (lib_filename, lib_path) = lib_artifact_name_and_path(rid, &out_dir);
ensure_go_source_in_crate(&manifest_dir);
let prebuild_dir = manifest_dir.join("prebuilds").join(rid);
let prebuild_crate = prebuild_dir.join(&lib_filename);
#[allow(unused_variables)]
let (prebuild, prebuild_dir) = if prebuild_crate.exists()
&& file_size_at_least(&prebuild_crate, MIN_LIB_SIZE_BYTES)
{
(prebuild_crate.clone(), prebuild_dir)
} else if try_build_from_source(&manifest_dir, &out_dir, &lib_filename) {
let prebuild = out_dir.join(&lib_filename);
if !prebuild.exists() {
panic!(
"Build from source reported success but library not found at {}.",
prebuild.display()
);
}
(prebuild.clone(), out_dir.clone())
} else {
let env_version = std::env::var("SPICEDB_EMBEDDED_RELEASE_VERSION").ok();
let version = release_version.or(env_version.as_deref());
if let Some(version) = version {
validate_release_version(version);
download_from_release(rid, version, &out_dir);
let prebuild = out_dir.join(&lib_filename);
if !prebuild.exists() {
panic!(
"Download completed but library not found at {}. Tarball may have wrong layout.",
prebuild.display()
);
}
(prebuild.clone(), out_dir.clone())
} else {
panic!(
"Prebuild not found for {}. Looked in {}. \
Build from source: install Go (CGO enabled) and build from repo (mise run shared-c-build), \
or run ./scripts/stage-all-prebuilds.sh to stage into spicedb-embedded-sys/prebuilds/<rid>. \
When using the published crate, the build script uses CARGO_PKG_VERSION to download the lib from GitHub Release.",
rid,
prebuild_crate.display()
);
}
};
if !prebuild.exists() {
panic!(
"Expected prebuilt library at {} after attempting local prebuild, build-from-source, or download, \
but it was not found. Target rid: {}. Expected path in OUT_DIR: {}.",
prebuild.display(),
rid,
lib_path.display()
);
}
println!("cargo:rerun-if-changed={}", prebuild.display());
if prebuild != lib_path {
std::fs::copy(&prebuild, &lib_path).expect("copy prebuild to OUT_DIR");
}
if !file_size_at_least(&lib_path, MIN_LIB_SIZE_BYTES) {
let size = std::fs::metadata(&lib_path).map(|m| m.len()).unwrap_or(0);
panic!(
"spicedb-embedded-sys: library at {} is {} bytes (need at least {}). The linker would fail. \
Build-from-source or download produced an invalid or truncated file.",
lib_path.display(),
size,
MIN_LIB_SIZE_BYTES
);
}
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "windows" {
generate_import_lib_windows(&prebuild_dir, &out_dir);
}
println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rustc-link-lib=dylib=spicedb");
copy_lib_to_target(&lib_path, &lib_filename);
if let Some(target_dir) = out_dir.ancestors().nth(3) {
emit_rpath(target_dir);
}
}
fn ensure_go_source_in_crate(manifest_dir: &Path) {
let repo_root = match manifest_dir.ancestors().nth(2) {
Some(r) => r.to_path_buf(),
None => return,
};
let repo_shared_c = repo_root.join("shared").join("c");
if !repo_shared_c.join("go.mod").exists() {
return;
}
println!("cargo:rerun-if-changed={}", repo_shared_c.display());
let dest = manifest_dir.join("shared").join("c");
if let Err(e) = copy_dir_recursive(&repo_shared_c, &dest) {
eprintln!("cargo:warning=copy shared/c into -sys crate: {}", e);
}
}
fn try_build_from_source(manifest_dir: &Path, out_dir: &Path, lib_filename: &str) -> bool {
let shared_c = manifest_dir.join("shared").join("c");
if !shared_c.join("go.mod").exists() {
return false;
}
let _ = std::fs::create_dir_all(out_dir);
let out_lib = out_dir.join(lib_filename);
let output = match Command::new("go")
.args(["build", "-buildmode=c-shared", "-o"])
.arg(&out_lib)
.arg(".")
.current_dir(&shared_c)
.env("CGO_ENABLED", "1")
.output()
{
Ok(o) => o,
Err(e) => {
println!(
"cargo:warning=spicedb-embedded-sys: go build failed to run: {}",
e
);
return false;
}
};
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
println!(
"cargo:warning=spicedb-embedded-sys: go build exited with {:?}",
output.status
);
if !stdout.is_empty() {
for line in stdout.lines() {
println!("cargo:warning=spicedb-embedded-sys: go stdout: {}", line);
}
}
if !stderr.is_empty() {
for line in stderr.lines() {
println!("cargo:warning=spicedb-embedded-sys: go stderr: {}", line);
}
}
return false;
}
if lib_filename.ends_with(".dll") {
let def_src = shared_c.join("spicedb.def");
if def_src.exists() {
let _ = std::fs::copy(&def_src, out_dir.join("spicedb.def"));
}
}
let size = std::fs::metadata(&out_lib).map(|m| m.len()).unwrap_or(0);
if out_lib.exists() && file_size_at_least(&out_lib, MIN_LIB_SIZE_BYTES) {
println!("cargo:rerun-if-changed={}", shared_c.display());
true
} else {
println!(
"cargo:warning=spicedb-embedded-sys: go build succeeded but {} is {} bytes (need >= {}), skipping build-from-source",
out_lib.display(),
size,
MIN_LIB_SIZE_BYTES
);
false
}
}
const MIN_LIB_SIZE_BYTES: u64 = 1_000_000;
fn file_size_at_least(path: &Path, min_bytes: u64) -> bool {
std::fs::metadata(path)
.map(|m| m.len() >= min_bytes)
.unwrap_or(false)
}
fn copy_dir_recursive(src: &Path, dest: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dest)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if ty.is_dir() {
copy_dir_recursive(&src_path, &dest_path)?;
} else {
std::fs::copy(&src_path, &dest_path)?;
}
}
Ok(())
}
fn validate_release_version(version: &str) {
if version.is_empty()
|| !version
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-')
{
panic!(
"Invalid SPICEDB_EMBEDDED_RELEASE_VERSION '{}': only alphanumeric, dot, hyphen allowed",
version
);
}
}
fn download_from_release(rid: &str, version: &str, out_dir: &Path) {
let url = format!(
"https://github.com/borkfork/spicedb-embedded/releases/download/v{}/libspicedb-{}.tar.gz",
version, rid
);
let archive = out_dir.join("libspicedb.tar.gz");
let status = Command::new("curl")
.args(["-L", "-f", "-s", "-o"])
.arg(&archive)
.arg(&url)
.status();
let status = status.unwrap_or_else(|e| {
panic!(
"Failed to run curl to download {}: {}. Install curl or use a local prebuild.",
url, e
)
});
if !status.success() {
panic!(
"curl failed ({}). Check network and that release v{} exists with asset libspicedb-{}.tar.gz",
status, version, rid
);
}
let meta = std::fs::metadata(&archive).unwrap_or_else(|e| {
panic!(
"failed to stat downloaded file {}: {}",
archive.display(),
e
)
});
let len = meta.len();
let mut magic = [0u8; 2];
let ok = len >= 100
&& std::fs::File::open(&archive)
.and_then(|mut f| std::io::Read::read_exact(&mut f, &mut magic))
.is_ok()
&& magic == [0x1f, 0x8b];
if !ok {
let _ = std::fs::remove_file(&archive);
panic!(
"Downloaded file from {} is {} bytes and does not look like gzip (expected release asset libspicedb-{}.tar.gz). \
If the URL returned a 404/HTML page, check that release v{} exists and has that asset. \
Try clearing the build: cargo clean -p spicedb-embedded-sys",
url, len, rid, version
);
}
let status = Command::new("tar")
.args(["-xzf"])
.arg(&archive)
.args(["-C"])
.arg(out_dir)
.status();
let status = status
.unwrap_or_else(|e| panic!("Failed to run tar to extract {}: {}", archive.display(), e));
if !status.success() {
panic!("tar failed ({}) extracting {}", status, archive.display());
}
let _ = std::fs::remove_file(&archive);
let (lib_filename, _) = lib_artifact_name_and_path(rid, out_dir);
let lib_path = out_dir.join(&lib_filename);
if !lib_path.exists() {
panic!(
"After extracting {}, {} was not found. Tarball may have wrong layout (expected ./{} at root).",
archive.display(),
lib_path.display(),
lib_filename
);
}
if !file_size_at_least(&lib_path, MIN_LIB_SIZE_BYTES) {
let size = std::fs::metadata(&lib_path).map(|m| m.len()).unwrap_or(0);
panic!(
"After extracting {}, {} is {} bytes (need at least {}). Delete target and retry, or set SPICEDB_EMBEDDED_RELEASE_VERSION.",
archive.display(),
lib_path.display(),
size,
MIN_LIB_SIZE_BYTES
);
}
}
fn lib_artifact_name_and_path(rid: &str, out_dir: &Path) -> (String, PathBuf) {
let (name, ext) = if rid.starts_with("osx-") {
("libspicedb", "dylib")
} else if rid.starts_with("win-") {
("spicedb", "dll")
} else {
("libspicedb", "so")
};
let lib_filename = format!("{name}.{ext}");
(lib_filename.clone(), out_dir.join(&lib_filename))
}
fn copy_lib_to_target(lib_path: &Path, lib_filename: &str) {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR set by Cargo");
let out_path = Path::new(&out_dir);
if let Some(target_dir) = out_path.ancestors().nth(3) {
let _ = std::fs::copy(lib_path, target_dir.join(lib_filename));
}
}
fn emit_rpath(runtime_lib_dir: &Path) {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "macos" {
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
println!(
"cargo:rustc-link-arg=-Wl,-rpath,{}",
runtime_lib_dir.display()
);
}
if target_os == "linux" {
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
println!(
"cargo:rustc-link-arg=-Wl,-rpath,{}",
runtime_lib_dir.display()
);
}
}
fn generate_import_lib_windows(def_dir: &Path, out_dir: &Path) {
let def_file = def_dir.join("spicedb.def");
let lib_file = out_dir.join("spicedb.lib");
if !def_file.exists() {
return;
}
if try_generate_import_lib_win(&def_file, &lib_file).is_err() {
panic!(
"Failed to generate spicedb.lib. MSVC needs the import library to link the DLL. \
Install Visual Studio Build Tools. From Git Bash: scripts/generate-dll-import-lib.sh {} {}",
def_file.display(),
out_dir.display()
);
}
}
fn try_generate_import_lib_win(def_file: &Path, lib_file: &Path) -> Result<(), ()> {
let lib_exe = find_msvc_lib_exe().ok_or(())?;
let status = Command::new(&lib_exe)
.arg(format!("/def:{}", def_file.display()))
.arg(format!("/out:{}", lib_file.display()))
.arg("/machine:x64")
.status();
match status {
Ok(s) if s.success() => Ok(()),
_ => Err(()),
}
}
fn find_msvc_lib_exe() -> Option<PathBuf> {
#[cfg(not(windows))]
return None;
#[cfg(windows)]
{
let vswhere =
Path::new(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe");
if !vswhere.exists() {
return None;
}
let out = Command::new(vswhere)
.args(["-latest", "-property", "installationPath"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let vs_path = std::str::from_utf8(&out.stdout).ok()?.trim_end();
let vc_tools = Path::new(vs_path).join("VC").join("Tools").join("MSVC");
for entry in std::fs::read_dir(&vc_tools).ok()?.flatten() {
let path = entry
.path()
.join("bin")
.join("Hostx64")
.join("x64")
.join("lib.exe");
if path.exists() {
return Some(path);
}
}
None
}
}