use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
ensure_git_hooks_installed();
emit_cli_script_bytecode();
let manifest_dir =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let portal_dist = manifest_dir.join("portal-dist");
let index = portal_dist.join("index.html");
if !index.exists() {
fs::create_dir_all(&portal_dist).expect("create portal-dist");
fs::write(
&index,
"<!doctype html><html><head><title>Harn portal not built</title></head>\
<body><h1>Harn portal not built</h1>\
<p>Run <code>./scripts/dev_setup.sh</code> or <code>make setup</code> \
to install portal dependencies and build the frontend, or run \
<code>npm --prefix crates/harn-cli/portal run build</code> directly, \
to populate \
<code>crates/harn-cli/portal-dist</code>.</p></body></html>",
)
.expect("write placeholder portal index.html");
let assets = portal_dist.join("assets").join("portal");
fs::create_dir_all(&assets).expect("create portal-dist assets dir");
for stub in ["app.js", "api.js", "styles.css"] {
let path = assets.join(stub);
if !path.exists() {
fs::write(&path, b"").expect("write placeholder portal asset");
}
}
}
println!("cargo:rerun-if-changed=portal-dist");
}
fn ensure_git_hooks_installed() {
if std::env::var_os("HARN_DISABLE_AUTO_HOOK_SETUP").is_some() {
return;
}
let Ok(top) = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
else {
return;
};
if !top.status.success() {
return;
}
let toplevel = String::from_utf8_lossy(&top.stdout).trim().to_string();
if toplevel.is_empty() {
return;
}
let hooks_dir = PathBuf::from(&toplevel).join(".githooks");
if !hooks_dir.is_dir() {
return;
}
let current = Command::new("git")
.args(["config", "--get", "core.hooksPath"])
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
if current == ".githooks" {
return;
}
let _ = Command::new("git")
.args(["config", "core.hooksPath", ".githooks"])
.status();
}
fn emit_cli_script_bytecode() {
use harn_vm::bytecode_cache::{serialize_chunk_artifact, CacheKey};
use harn_vm::compile_source;
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR"));
let bytecode_dir = out_dir.join("cli-bytecode");
fs::create_dir_all(&bytecode_dir).expect("create cli-bytecode dir");
let manifest_dir =
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let cli_scripts_dir = manifest_dir
.join("..")
.join("harn-stdlib")
.join("src")
.join("stdlib")
.join("cli");
println!("cargo:rerun-if-changed={}", cli_scripts_dir.display());
let stdlib_lib = manifest_dir
.join("..")
.join("harn-stdlib")
.join("src")
.join("lib.rs");
println!("cargo:rerun-if-changed={}", stdlib_lib.display());
println!("cargo:rerun-if-env-changed=HARN_SKIP_AOT_CLI_BUILD");
let table_path = out_dir.join("cli_bytecode_table.rs");
if std::env::var_os("HARN_SKIP_AOT_CLI_BUILD").is_some() {
write_table(&table_path, &[]);
return;
}
if cfg!(target_os = "windows") {
write_table(&table_path, &[]);
return;
}
let mut entries: Vec<(String, String)> = Vec::new();
for script in harn_stdlib::STDLIB_CLI_SCRIPTS {
let name = script.name;
let source = script.source;
let safe = safe_filename(name);
let chunk = match compile_source(source) {
Ok(chunk) => chunk,
Err(err) => panic!(
"AOT compile failed for CLI script `{name}`: {err}\n\
(this is a build-time failure; the script must compile cleanly \
or be guarded with a skiplist entry in build.rs)"
),
};
let synthetic_path = bytecode_dir.join(format!("{safe}.harn"));
let key = CacheKey::from_source(&synthetic_path, source);
let buf = serialize_chunk_artifact(&key, &chunk).unwrap_or_else(|err| {
panic!("serialize bytecode for CLI script `{name}` failed: {err}");
});
let dest = bytecode_dir.join(format!("{safe}.harnbc"));
fs::write(&dest, &buf).unwrap_or_else(|err| {
panic!(
"write bytecode for CLI script `{name}` to {}: {err}",
dest.display()
);
});
entries.push((name.to_string(), dest.to_string_lossy().into_owned()));
}
write_table(&table_path, &entries);
}
fn safe_filename(name: &str) -> String {
name.replace('/', "-")
}
fn write_table(path: &Path, entries: &[(String, String)]) {
let mut body = String::new();
body.push_str("// @generated by build.rs (harn#2300, G7 AOT bytecode embedding).\n");
body.push_str("// Do not edit by hand. Rerun `cargo build -p harn-cli` to regenerate.\n");
body.push_str("pub(crate) const STDLIB_CLI_SCRIPT_BYTECODE: &[(&str, &[u8])] = &[\n");
for (name, file_path) in entries {
body.push_str(" (\"");
body.push_str(&escape_str(name));
body.push_str("\", include_bytes!(\"");
body.push_str(&escape_str(file_path));
body.push_str("\")),\n");
}
body.push_str("];\n");
fs::write(path, body).expect("write cli_bytecode_table.rs");
}
fn escape_str(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
c => out.push(c),
}
}
out
}