//! Build-time bundling of the bun runtime. //! //! - Reads `BUN_VARIANT` ("default" | "baseline"; default = "default") env //! and `[package.metadata.nomifun-runtime] bun_version` from Cargo.toml. //! - For win-arm64: emits HAS_EMBEDDED_BUN=false and exits early. //! - Otherwise: downloads `bun--[-baseline].zip`, //! extracts the `bun` binary, zstd-compresses it, emits constants //! into `$OUT_DIR/bun_meta.rs`. //! - Caches decompressed+compressed artifacts in `$CARGO_HOME/nomifun-bun-cache/`. #[path = "build_support.rs"] mod build_support; use std::fs; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use sha2::{Digest, Sha256}; fn main() { println!("cargo:rerun-if-env-changed=BUN_VARIANT"); println!("cargo:rerun-if-env-changed=BUN_VERSION_OVERRIDE"); println!("cargo:rerun-if-env-changed=NOMIFUN_EMBED_BUN"); println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=build_support.rs"); println!("cargo:rerun-if-changed=Cargo.toml"); let target = std::env::var("TARGET").expect("TARGET env set by cargo"); let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR")); let variant = std::env::var("BUN_VARIANT").unwrap_or_else(|_| "default".into()); let version = read_bun_version(); let meta_path = out_dir.join("bun_meta.rs"); // Default: dev builds skip bun download (no network). CI release // sets NOMIFUN_EMBED_BUN=1 to actually embed. if std::env::var("NOMIFUN_EMBED_BUN").as_deref() != Ok("1") { write_meta_stub(&meta_path, &out_dir); return; } if !build_support::has_embedded_bun(&target) { write_meta_stub(&meta_path, &out_dir); return; } let Some(asset) = build_support::asset_name_for(&target, &variant) else { println!("cargo:warning=No bun asset mapped for target {target}; emitting stub"); write_meta_stub(&meta_path, &out_dir); return; }; let cache_root = cargo_cache_root().join(&version).join(format!("{target}-{variant}")); fs::create_dir_all(&cache_root).expect("create cache dir"); let bun_exe_path = cache_root.join(build_support::bun_exe_name(&target)); let compressed_path = cache_root.join("bun.blob.zst"); let sha_path = cache_root.join("bun.sha256"); // Step 1: ensure decompressed bun is cached. if !bun_exe_path.is_file() { let url = build_support::download_url(&version, &asset); println!("cargo:info=Downloading {url}"); let zip_path = cache_root.join(&asset); download(&url, &zip_path); unzip_bun(&zip_path, &cache_root, build_support::bun_exe_name(&target)); } // Step 2: compute sha256 if missing. let sha_hex = if sha_path.is_file() { fs::read_to_string(&sha_path).expect("read sha").trim().to_string() } else { let hex = sha256_of(&bun_exe_path); fs::write(&sha_path, &hex).expect("write sha"); hex }; // Step 3: compress if missing. if !compressed_path.is_file() { zstd_compress_file(&bun_exe_path, &compressed_path); } // Step 4: copy compressed blob into OUT_DIR so include_bytes! can find it. let out_blob = out_dir.join("bun.blob.zst"); fs::copy(&compressed_path, &out_blob).expect("copy blob to OUT_DIR"); // Step 5: emit bun_meta.rs. let meta = format!( "// @generated by build.rs — do not edit\n\ pub const BUN_BLOB: &[u8] = include_bytes!(\"bun.blob.zst\");\n\ pub const BUN_SHA256: &str = \"{sha_hex}\";\n\ pub const BUN_VERSION: &str = \"{version}\";\n\ pub const HAS_EMBEDDED_BUN: bool = true;\n" ); fs::write(&meta_path, meta).expect("write bun_meta.rs"); } fn read_bun_version() -> String { if let Ok(v) = std::env::var("BUN_VERSION_OVERRIDE") && !v.trim().is_empty() { return v; } let toml_src = fs::read_to_string("Cargo.toml").expect("read Cargo.toml"); let parsed: toml::Value = toml::from_str(&toml_src).expect("parse Cargo.toml"); parsed .get("package") .and_then(|p| p.get("metadata")) .and_then(|m| m.get("nomifun-runtime")) .and_then(|r| r.get("bun_version")) .and_then(|v| v.as_str()) .expect("[package.metadata.nomifun-runtime] bun_version missing") .to_string() } fn cargo_cache_root() -> PathBuf { let home = std::env::var("CARGO_HOME").map(PathBuf::from).unwrap_or_else(|_| { dirs::home_dir() .map(|h| h.join(".cargo")) .unwrap_or_else(|| PathBuf::from(".cargo")) }); home.join("nomifun-bun-cache") } fn write_meta_stub(meta_path: &Path, out_dir: &Path) { let stub_blob = out_dir.join("bun.blob.zst"); if !stub_blob.is_file() { fs::write(&stub_blob, []).expect("write stub blob"); } let body = "// @generated by build.rs — do not edit\n\ pub const BUN_BLOB: &[u8] = include_bytes!(\"bun.blob.zst\");\n\ pub const BUN_SHA256: &str = \"\";\n\ pub const BUN_VERSION: &str = \"\";\n\ pub const HAS_EMBEDDED_BUN: bool = false;\n"; fs::write(meta_path, body).expect("write stub bun_meta.rs"); } fn download(url: &str, out: &Path) { let bytes = reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(600)) .build() .expect("build http client") .get(url) .send() .unwrap_or_else(|e| panic!("GET {url} failed: {e}")) .error_for_status() .unwrap_or_else(|e| panic!("non-2xx from {url}: {e}")) .bytes() .unwrap_or_else(|e| panic!("read body: {e}")); fs::write(out, &bytes).unwrap_or_else(|e| panic!("write {}: {e}", out.display())); } fn unzip_bun(zip_path: &Path, out_dir: &Path, exe_name: &str) { let f = fs::File::open(zip_path).expect("open zip"); let mut archive = zip::ZipArchive::new(f).expect("read zip"); for i in 0..archive.len() { let mut entry = archive.by_index(i).expect("zip entry"); let name = entry.name().to_string(); if name.ends_with(exe_name) && !entry.is_dir() { let out = out_dir.join(exe_name); let mut w = fs::File::create(&out).expect("create exe"); std::io::copy(&mut entry, &mut w).expect("extract exe"); return; } } panic!("bun executable {exe_name} not found in zip {}", zip_path.display()); } fn sha256_of(path: &Path) -> String { let mut f = fs::File::open(path).expect("open for sha"); let mut hasher = Sha256::new(); let mut buf = [0u8; 64 * 1024]; loop { let n = f.read(&mut buf).expect("read chunk"); if n == 0 { break; } hasher.update(&buf[..n]); } hex::encode(hasher.finalize()) } fn zstd_compress_file(input: &Path, output: &Path) { let mut src = fs::File::open(input).expect("open src"); let dst = fs::File::create(output).expect("create dst"); let mut enc = zstd::stream::write::Encoder::new(dst, 19).expect("zstd encoder"); std::io::copy(&mut src, &mut enc).expect("compress"); let mut out = enc.finish().expect("finish encoder"); out.write_all(&[]).ok(); }