use std::path::{Path, PathBuf};
const IREE_SHA: &str = "e4a3b0405d7d23554da26403658d0e8c3c5ecf25";
const RELEASE_TAG: &str = "v0.1.0";
const RELEASE_BASE: &str = "https://github.com/tallamjr/iree-embedded/releases/download";
const CHECKSUMS: &str = include_str!("runtime-checksums.txt");
fn main() {
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let root = manifest.join("../..").canonicalize().unwrap();
let target = std::env::var("TARGET").unwrap_or_default();
let is_mcu = target.starts_with("thumbv7em");
let variant = if is_mcu { "mcu" } else { "host" };
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let env_src = std::env::var_os("IREE_SRC_DIR").map(PathBuf::from);
let env_build = std::env::var_os("IREE_RUNTIME_DIR").map(PathBuf::from);
let in_repo_src = root.join(".iree/src");
let in_repo_build = root
.join(".iree/build")
.join(if is_mcu { "mcu" } else { "host" });
let (src, build_dir) = if env_src.is_some() || env_build.is_some() {
(
env_src.unwrap_or_else(|| in_repo_src.clone()),
env_build.unwrap_or_else(|| in_repo_build.clone()),
)
} else if in_repo_build.is_dir() && in_repo_src.is_dir() {
(in_repo_src, in_repo_build)
} else {
let unpacked = download_runtime(&target, is_mcu, &out);
(unpacked.join("src"), unpacked.join("build"))
};
let inc_src = src.join("runtime/src");
let inc_gen = build_dir.join("runtime/src");
let inc_flatcc = src.join("third_party/flatcc/include");
let bm_config = manifest.join("iree_bm_config.h");
for (dir, lib) in [
("runtime/src/iree/runtime", "iree_runtime_unified"),
(
"runtime/src/iree/hal/drivers/local_sync",
"iree_hal_drivers_local_sync_sync_driver",
),
(
"runtime/src/iree/hal/local/loaders",
"iree_hal_local_loaders_embedded_elf_loader",
),
(
"runtime/src/iree/hal/local/loaders",
"iree_hal_local_loaders_static_library_loader",
),
("build_tools/third_party/flatcc", "flatcc_parsing"),
("build_tools/third_party/printf", "printf_printf"),
] {
println!(
"cargo:rustc-link-search=native={}",
build_dir.join(dir).display()
);
println!("cargo:rustc-link-lib=static={lib}");
}
let extern_c = out.join("extern.c");
let bindings_rs = out.join("bindings.rs");
let gen_dir = manifest.join("generated");
let committed_bindings = gen_dir.join(format!("bindings_{variant}.rs"));
let committed_extern = gen_dir.join(format!("extern_{variant}.c"));
let explicit_regen = std::env::var_os("IREE_EMBEDDED_REGENERATE_BINDINGS").is_some();
let regenerate = explicit_regen || !committed_bindings.exists() || !committed_extern.exists();
let llvm_prefix = if cfg!(target_os = "macos") {
run_capture("brew", &["--prefix", "llvm"])
} else {
None
};
if regenerate {
generate_bindings(
&inc_src,
&inc_gen,
&inc_flatcc,
&bm_config,
is_mcu,
&llvm_prefix,
&bindings_rs,
&extern_c,
);
if explicit_regen {
std::fs::create_dir_all(&gen_dir).unwrap();
std::fs::copy(&bindings_rs, &committed_bindings).expect("save committed bindings");
std::fs::copy(&extern_c, &committed_extern).expect("save committed extern.c");
}
} else {
std::fs::copy(&committed_bindings, &bindings_rs).expect("use committed bindings");
std::fs::copy(&committed_extern, &extern_c).expect("use committed extern.c");
}
let mut wrappers = cc::Build::new();
wrappers
.file(&extern_c)
.include(&manifest)
.include(&inc_src)
.include(&inc_gen)
.include(&inc_flatcc);
if is_mcu {
wrappers.pic(false);
wrappers
.compiler("arm-none-eabi-gcc")
.flag("-include")
.flag(bm_config.to_str().unwrap())
.flag("-mcpu=cortex-m4")
.flag("-mthumb")
.flag("-mfloat-abi=hard")
.flag("-mfpu=fpv4-sp-d16")
.flag("-fno-pic");
} else if cfg!(target_os = "macos") {
let llvm_ar = llvm_prefix
.as_ref()
.map(|p| PathBuf::from(format!("{p}/bin/llvm-ar")))
.filter(|p| p.exists())
.or_else(|| run_capture("which", &["llvm-ar"]).map(PathBuf::from));
if let Some(ar) = llvm_ar {
wrappers.archiver(ar);
}
}
wrappers.compile("iree_static_wrappers");
println!("cargo:rerun-if-changed=wrapper.h");
println!("cargo:rerun-if-changed=iree_bm_config.h");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=generated");
println!("cargo:rerun-if-changed=runtime-checksums.txt");
println!("cargo:rerun-if-env-changed=IREE_RUNTIME_DIR");
println!("cargo:rerun-if-env-changed=IREE_SRC_DIR");
println!("cargo:rerun-if-env-changed=IREE_EMBEDDED_REGENERATE_BINDINGS");
}
#[allow(clippy::too_many_arguments)]
fn generate_bindings(
inc_src: &std::path::Path,
inc_gen: &std::path::Path,
inc_flatcc: &std::path::Path,
bm_config: &std::path::Path,
is_mcu: bool,
llvm_prefix: &Option<String>,
bindings_rs: &std::path::Path,
extern_c: &std::path::Path,
) {
let mut clang_args: Vec<String> = vec![
format!("-I{}", inc_src.display()),
format!("-I{}", inc_gen.display()),
format!("-I{}", inc_flatcc.display()),
];
if cfg!(target_os = "macos") && std::env::var_os("LIBCLANG_PATH").is_none() {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(p) = llvm_prefix {
candidates.push(PathBuf::from(format!("{p}/lib")));
}
if let Some(xc) = run_capture("xcode-select", &["-p"]) {
candidates.push(PathBuf::from(format!(
"{xc}/Toolchains/XcodeDefault.xctoolchain/usr/lib"
)));
candidates.push(PathBuf::from(format!("{xc}/usr/lib")));
}
if let Some(dir) = candidates
.into_iter()
.find(|d| d.join("libclang.dylib").exists())
{
unsafe { std::env::set_var("LIBCLANG_PATH", &dir) };
}
}
if is_mcu {
clang_args.push("--target=thumbv7em-none-eabihf".to_string());
clang_args.push("-include".to_string());
clang_args.push(bm_config.display().to_string());
for dir in arm_none_eabi_include_dirs() {
clang_args.push(format!("-isystem{dir}"));
}
} else if cfg!(target_os = "macos") {
if let Some(sdk) = run_capture("xcrun", &["--show-sdk-path"]) {
clang_args.push(format!("-isysroot{sdk}"));
}
}
let mut builder = bindgen::Builder::default()
.header("wrapper.h")
.use_core()
.ctypes_prefix("core::ffi")
.prepend_enum_name(false)
.layout_tests(false)
.allowlist_function("iree_.*")
.allowlist_type("iree_.*")
.allowlist_var("IREE_.*")
.blocklist_type("iree_allocator_t")
.wrap_unsafe_ops(true)
.wrap_static_fns(true)
.wrap_static_fns_path(extern_c);
for arg in &clang_args {
builder = builder.clang_arg(arg);
}
let bindings = builder.generate().expect("bindgen failed");
bindings.write_to_file(bindings_rs).expect("write bindings");
}
fn arm_none_eabi_include_dirs() -> Vec<String> {
let Ok(out) = std::process::Command::new("arm-none-eabi-gcc")
.args(["-E", "-Wp,-v", "-xc", "/dev/null"])
.output()
else {
return Vec::new();
};
let text = String::from_utf8_lossy(&out.stderr);
let mut dirs = Vec::new();
let mut in_list = false;
for line in text.lines() {
if line.contains("#include <...> search starts here:") {
in_list = true;
} else if line.contains("End of search list.") {
break;
} else if in_list {
dirs.push(line.trim().to_string());
}
}
dirs
}
fn download_runtime(target: &str, is_mcu: bool, out: &Path) -> PathBuf {
let artefact_target = artefact_target(target, is_mcu).unwrap_or_else(|| {
panic!(
"iree-embedded-sys: no prebuilt IREE runtime is published for target `{target}`. \
Build one with scripts/build-runtime-*.sh and point IREE_RUNTIME_DIR (build dir) \
and IREE_SRC_DIR (IREE source) at it."
)
});
let sha9 = &IREE_SHA[..9];
let name = format!("iree-runtime-{artefact_target}-{sha9}");
let file = format!("{name}.tar.gz");
let unpacked = out.join(&name);
if unpacked.join("build").is_dir() && unpacked.join("src").is_dir() {
return unpacked;
}
let expected = expected_sha256(CHECKSUMS, &file).unwrap_or_else(|| {
panic!(
"iree-embedded-sys: no pinned sha256 for `{file}` in runtime-checksums.txt; \
the download cannot be verified. Set IREE_RUNTIME_DIR and IREE_SRC_DIR instead."
)
});
let url = format!("{RELEASE_BASE}/{RELEASE_TAG}/{file}");
let tarball = out.join(&file);
let status = std::process::Command::new("curl")
.args(["--proto", "=https", "--tlsv1.2", "-fsSL", "-o"])
.arg(&tarball)
.arg(&url)
.status()
.unwrap_or_else(|e| {
panic!("iree-embedded-sys: failed to run `curl` to download {url}: {e}")
});
assert!(
status.success(),
"iree-embedded-sys: `curl` failed ({status}) downloading {url}"
);
let actual = sha256_file(&tarball);
assert!(
actual == expected,
"iree-embedded-sys: sha256 mismatch for {file}\n expected {expected}\n got {actual}\n\
Refusing to use a runtime that does not match the pinned checksum."
);
let status = std::process::Command::new("tar")
.arg("-xzf")
.arg(&tarball)
.arg("-C")
.arg(out)
.status()
.unwrap_or_else(|e| panic!("iree-embedded-sys: failed to run `tar` on {file}: {e}"));
assert!(
status.success(),
"iree-embedded-sys: `tar` failed ({status}) extracting {file}"
);
assert!(
unpacked.join("build").is_dir() && unpacked.join("src").is_dir(),
"iree-embedded-sys: unpacked {file} but {} is missing build/ or src/",
unpacked.display()
);
unpacked
}
fn artefact_target(target: &str, is_mcu: bool) -> Option<String> {
if is_mcu {
return Some("cortex-m4f".to_string());
}
let arch = if target.starts_with("x86_64") {
"x86_64"
} else if target.starts_with("aarch64") {
"arm64"
} else {
return None;
};
let os = if target.contains("apple") || target.contains("darwin") {
"darwin"
} else if target.contains("linux") {
"linux"
} else {
return None;
};
Some(format!("host-{os}-{arch}"))
}
fn expected_sha256(checksums: &str, filename: &str) -> Option<String> {
for line in checksums.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.split_whitespace();
let hash = parts.next()?;
let name = parts.next()?;
if name == filename {
return Some(hash.to_lowercase());
}
}
None
}
fn sha256_file(path: &Path) -> String {
let out = std::process::Command::new("sha256sum")
.arg(path)
.output()
.ok()
.filter(|o| o.status.success())
.or_else(|| {
std::process::Command::new("shasum")
.args(["-a", "256"])
.arg(path)
.output()
.ok()
.filter(|o| o.status.success())
})
.unwrap_or_else(|| {
panic!(
"iree-embedded-sys: need `sha256sum` or `shasum` on PATH to verify the \
runtime download. Set IREE_RUNTIME_DIR and IREE_SRC_DIR to skip the download."
)
});
String::from_utf8_lossy(&out.stdout)
.split_whitespace()
.next()
.expect("sha256 tool produced no output")
.to_lowercase()
}
fn run_capture(cmd: &str, args: &[&str]) -> Option<String> {
let out = std::process::Command::new(cmd).args(args).output().ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() { None } else { Some(s) }
}