use std::env;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
#[path = "src/manifest.rs"]
#[allow(dead_code)] mod manifest;
const PLACEHOLDER_PREAMBLE: &[u8] = b"CG_STUB_PLACEHOLDER\n";
fn main() {
println!("cargo:rerun-if-changed=src/manifest.rs");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=CG_STUB_OFFLINE");
println!("cargo:rerun-if-env-changed=CG_STUB_BINARY_PATH");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
let embedded = out_dir.join("embedded_binary");
if let Ok(path) = env::var("CG_STUB_BINARY_PATH") {
println!("cargo:warning=using local binary from CG_STUB_BINARY_PATH={path}");
let bytes = fs::read(&path)
.unwrap_or_else(|err| panic!("CG_STUB_BINARY_PATH={path}: read failed: {err}"));
fs::write(&embedded, &bytes).expect("write embedded_binary failed");
return;
}
if env::var("CG_STUB_OFFLINE").as_deref() == Ok("1") {
write_placeholder(&embedded, "CG_STUB_OFFLINE=1");
return;
}
if manifest::SHA256_BY_TARGET.is_empty() {
write_placeholder(&embedded, "SHA256_BY_TARGET is empty (development build)");
return;
}
let bytes = download_archive_and_extract();
fs::write(&embedded, &bytes).expect("write embedded_binary failed");
}
fn write_placeholder(path: &Path, reason: &str) {
let mut content = PLACEHOLDER_PREAMBLE.to_vec();
content.extend_from_slice(format!("reason: {reason}\n").as_bytes());
content.extend_from_slice(
b"This build is a development placeholder. Install via:\n \
cargo install renso-code-graph renso-code-graph-mcp\n \
curl -fsSL https://cg.renso.ai/install.sh | sh\n",
);
fs::write(path, content).expect("write placeholder failed");
}
fn target_triple() -> String {
env::var("TARGET").expect("TARGET not set by cargo")
}
fn expected_sha_for(target: &str) -> &'static str {
for (t, sha) in manifest::SHA256_BY_TARGET {
if *t == target {
return sha;
}
}
let known: Vec<&str> = manifest::SHA256_BY_TARGET.iter().map(|(t, _)| *t).collect();
panic!(
"no prebuilt binary for target `{target}` in this release.\n\
Supported targets: {known:?}.\n\
Workarounds:\n \
- Set CG_STUB_OFFLINE=1 and exec the binary yourself.\n \
- Set CG_STUB_BINARY_PATH=/path/to/code_graph-mcp and \
rerun cargo install."
);
}
fn archive_format(target: &str) -> &'static str {
if target.contains("windows") {
"zip"
} else {
"tar.gz"
}
}
fn archive_url(target: &str) -> String {
let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set");
let ext = archive_format(target);
format!(
"{base}/v{version}/{prefix}-{target}.{ext}",
base = manifest::RELEASE_URL_BASE,
prefix = manifest::ARCHIVE_PREFIX,
version = version,
target = target,
ext = ext,
)
}
fn download_archive_and_extract() -> Vec<u8> {
let target = target_triple();
let expected_sha = expected_sha_for(&target);
let url = archive_url(&target);
println!("cargo:warning=downloading {url}");
let resp = ureq::get(&url)
.call()
.unwrap_or_else(|err| panic!("download failed: {url}: {err}"));
let mut archive_bytes = Vec::new();
resp.into_reader()
.read_to_end(&mut archive_bytes)
.unwrap_or_else(|err| panic!("download read failed: {url}: {err}"));
verify_sha256(&archive_bytes, expected_sha, &url);
let binary_name = if target.contains("windows") {
format!("{}.exe", manifest::BINARY_NAME)
} else {
manifest::BINARY_NAME.to_string()
};
if archive_format(&target) == "zip" {
extract_zip_entry(&archive_bytes, &binary_name)
} else {
extract_tar_gz_entry(&archive_bytes, &binary_name)
}
}
fn extract_tar_gz_entry(bytes: &[u8], name: &str) -> Vec<u8> {
let gz = flate2::read::GzDecoder::new(bytes);
let mut ar = tar::Archive::new(gz);
for entry in ar.entries().expect("tar entries") {
let mut entry = entry.expect("tar entry");
let path = entry.path().expect("tar entry path").into_owned();
if path.file_name().map(|f| f == name).unwrap_or(false) {
let mut out = Vec::new();
entry.read_to_end(&mut out).expect("tar entry read");
return out;
}
}
panic!("binary `{name}` not found in tarball");
}
fn extract_zip_entry(bytes: &[u8], name: &str) -> Vec<u8> {
use std::io::{Cursor, Read};
let mut archive = zip::ZipArchive::new(Cursor::new(bytes))
.unwrap_or_else(|err| panic!("zip parse failed: {err}"));
for i in 0..archive.len() {
let mut entry = archive
.by_index(i)
.unwrap_or_else(|err| panic!("zip entry {i}: {err}"));
let entry_path = entry.enclosed_name().map(|p| p.to_path_buf());
let matches = entry_path
.as_ref()
.and_then(|p| p.file_name())
.map(|f| f == name)
.unwrap_or(false);
if matches {
let mut out = Vec::with_capacity(entry.size() as usize);
entry
.read_to_end(&mut out)
.unwrap_or_else(|err| panic!("zip read: {err}"));
return out;
}
}
panic!("binary `{name}` not found in zip archive");
}
fn verify_sha256(bytes: &[u8], expected: &str, url: &str) {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(bytes);
let actual = hex::encode(hasher.finalize());
if actual != expected {
panic!(
"SHA256 mismatch:\n expected: {expected}\n actual: {actual}\n \
URL: {url}\n\nThe published crate references an \
archive whose hash does not match what was downloaded."
);
}
}