use std::fs;
use std::path::Path;
fn main() {
generate_version();
generate_platform_template_assets();
generate_source_library_assets();
generate_skill_assets();
generate_skill_extra_assets();
generate_node_bundle_assets();
generate_help_index();
}
fn generate_version() {
let cargo_version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.0.0".to_string());
println!("cargo:rerun-if-env-changed=BUILD_VERSION");
let app_version = match std::env::var("BUILD_VERSION") {
Ok(v) if !v.trim().is_empty() => v.trim().to_string(),
_ => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!("{cargo_version}.{}", format_build_timestamp(now))
}
};
let sekejap_version = fs::read_to_string("Cargo.lock")
.ok()
.and_then(|lock| {
let at = lock.find("name = \"sekejap\"\nversion = \"")?;
let rest = &lock[at + "name = \"sekejap\"\nversion = \"".len()..];
Some(rest[..rest.find('"')?].to_string())
})
.unwrap_or_else(|| "unknown".to_string());
println!("cargo:rerun-if-changed=Cargo.lock");
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest = std::path::Path::new(&out_dir).join("version_gen.rs");
fs::write(
&dest,
format!(
"pub const APP_VERSION: &str = \"{app_version}\";\npub const SEKEJAP_ENGINE_VERSION: &str = \"{sekejap_version}\";\n"
),
)
.expect("failed writing version_gen.rs");
println!("cargo:warning=🔖 Build version: {app_version}");
println!("cargo:rerun-if-changed=Cargo.toml");
}
fn format_build_timestamp(secs: u64) -> String {
let mut days = secs / 86400;
let time_of_day = secs % 86400;
let hour = time_of_day / 3600;
let minute = (time_of_day % 3600) / 60;
let mut year = 1970u64;
loop {
let leap = is_leap(year);
let days_in_year = if leap { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let leap = is_leap(year);
let month_days: [u64; 12] = [
31,
if leap { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut month = 1u64;
for &md in &month_days {
if days < md {
break;
}
days -= md;
month += 1;
}
let day = days + 1;
format!("{year}{month:02}{day:02}{hour:02}{minute:02}")
}
fn is_leap(year: u64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn generate_platform_template_assets() {
let templates_dir = "src/platform/web/templates";
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest = Path::new(&out_dir).join("platform_templates_gen.rs");
println!("cargo:rerun-if-changed=build.rs");
let mut rel_paths: Vec<String> = Vec::new();
collect_files(Path::new(templates_dir), templates_dir, &mut rel_paths);
rel_paths.sort();
let mut code = String::from("pub const PLATFORM_TEMPLATE_ASSETS: &[EmbeddedAsset] = &[\n");
for rel in &rel_paths {
code.push_str(&format!(
" EmbeddedAsset {{ \
path: {rel:?}, \
bytes: include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/src/platform/web/templates/{rel}\")) \
}},\n"
));
}
code.push_str("];\n");
fs::write(&dest, code).expect("failed writing platform_templates_gen.rs");
println!("cargo:rerun-if-changed={templates_dir}");
}
fn generate_source_library_assets() {
let root = "blessed/source-libraries";
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest = Path::new(&out_dir).join("source_libraries_gen.rs");
let mut rel_paths: Vec<String> = Vec::new();
collect_files(Path::new(root), root, &mut rel_paths);
rel_paths.sort();
let mut code = String::from("pub const PLATFORM_SOURCE_LIBRARY_ASSETS: &[EmbeddedAsset] = &[\n");
for rel in &rel_paths {
if !rel.ends_with(".tsx") && !rel.ends_with(".ts") {
continue;
}
code.push_str(&format!(
" EmbeddedAsset {{ \
path: \"zeb/{rel}\", \
bytes: include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{root}/{rel}\")) \
}},\n"
));
}
code.push_str("];\n");
fs::write(&dest, code).expect("failed writing source_libraries_gen.rs");
println!("cargo:rerun-if-changed={root}");
}
fn generate_skill_assets() {
let root = "blessed/skills";
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest = Path::new(&out_dir).join("skills_gen.rs");
let mut rel_paths: Vec<String> = Vec::new();
collect_files(Path::new(root), root, &mut rel_paths);
rel_paths.sort();
assert!(
rel_paths.iter().any(|p| p.ends_with("/SKILL.md")),
"no SKILL.md under {root} — is the build context excluding markdown?"
);
let text = [".md", ".json", ".yaml", ".yml", ".txt", ".ts", ".tsx", ".mjs", ".js", ".py", ".sh", ".sql", ".css"];
let mut code = String::from("pub const PLATFORM_SKILL_ASSETS: &[EmbeddedAsset] = &[\n");
for rel in &rel_paths {
if !text.iter().any(|ext| rel.ends_with(ext)) {
continue;
}
code.push_str(&format!(
" EmbeddedAsset {{ \
path: {rel:?}, \
bytes: include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{root}/{rel}\")) \
}},\n"
));
}
code.push_str("];\n");
fs::write(&dest, code).expect("failed writing skills_gen.rs");
println!("cargo:rerun-if-changed={root}");
}
fn generate_skill_extra_assets() {
let root = "blessed/skill-extras";
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest = Path::new(&out_dir).join("skill_extras_gen.rs");
let mut rel_paths: Vec<String> = Vec::new();
collect_files(Path::new(root), root, &mut rel_paths);
rel_paths.sort();
assert!(
rel_paths.iter().any(|p| p.ends_with("/SKILL.md")),
"no SKILL.md under {root} — is the build context excluding markdown?"
);
let text = [".md", ".json", ".yaml", ".yml", ".txt", ".ts", ".tsx", ".mjs", ".js", ".py", ".sh", ".sql", ".css"];
let mut code = String::from("pub const PLATFORM_SKILL_EXTRA_ASSETS: &[EmbeddedAsset] = &[\n");
for rel in &rel_paths {
if !text.iter().any(|ext| rel.ends_with(ext)) {
continue;
}
code.push_str(&format!(
" EmbeddedAsset {{ \
path: {rel:?}, \
bytes: include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/{root}/{rel}\")) \
}},\n"
));
}
code.push_str("];\n");
fs::write(&dest, code).expect("failed writing skill_extras_gen.rs");
println!("cargo:rerun-if-changed={root}");
}
fn generate_node_bundle_assets() {
let bundles_dir = "src/pipeline/nodes/bundled";
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest = Path::new(&out_dir).join("node_bundles_gen.rs");
println!("cargo:rerun-if-changed=build.rs");
let mut rel_paths: Vec<String> = Vec::new();
collect_files(Path::new(bundles_dir), bundles_dir, &mut rel_paths);
rel_paths.sort();
let mut code =
String::from("pub const PLATFORM_COMPOSITE_NODE_ASSETS: &[EmbeddedAsset] = &[\n");
for rel in &rel_paths {
code.push_str(&format!(
" EmbeddedAsset {{ \
path: {rel:?}, \
bytes: include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/src/pipeline/nodes/bundled/{rel}\")) \
}},\n"
));
}
code.push_str("];\n");
fs::write(&dest, code).expect("failed writing node_bundles_gen.rs");
println!("cargo:rerun-if-changed={bundles_dir}");
}
fn generate_help_index() {
let help_root = "src/platform/help";
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let dest = Path::new(&out_dir).join("help_generated.rs");
println!("cargo:rerun-if-changed=build.rs");
let mut all_files: Vec<String> = Vec::new();
collect_files(Path::new(help_root), help_root, &mut all_files);
all_files.retain(|p| p.ends_with(".md"));
all_files.sort();
let mut code = String::from(
"/// Auto-generated help node array. Do not edit — regenerated by build.rs.\n\
pub static HELP: &[HelpNode] = &[\n",
);
for rel_from_help_root in &all_files {
let manifest_rel = format!("{}/{}", help_root, rel_from_help_root);
let help_path = rel_from_help_root.trim_end_matches(".md");
let content = match fs::read_to_string(&manifest_rel) {
Ok(c) => c,
Err(e) => {
eprintln!("cargo:warning=Could not read {}: {}", manifest_rel, e);
continue;
}
};
let title = parse_title(&content);
let excerpt = parse_excerpt(&content);
let last_updated = git_last_updated(&manifest_rel);
let include_path = format!("/{}", manifest_rel);
code.push_str(&format!(
" HelpNode {{\n\
path: {help_path:?},\n\
title: {title:?},\n\
excerpt: {excerpt:?},\n\
last_updated: {last_updated:?},\n\
content: include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), {include_path:?})),\n\
}},\n",
));
println!("cargo:rerun-if-changed={}", manifest_rel);
}
code.push_str("];\n");
fs::write(&dest, &code).expect("failed writing help_generated.rs");
println!("cargo:rerun-if-changed={}", help_root);
}
fn parse_title(content: &str) -> String {
for line in content.lines() {
let trimmed = line.trim();
if let Some(title) = trimmed.strip_prefix("# ") {
return title.trim().to_string();
}
}
"Untitled".to_string()
}
fn parse_excerpt(content: &str) -> String {
let mut past_title = false;
let mut paragraph_lines: Vec<&str> = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if !past_title {
if trimmed.starts_with("# ") {
past_title = true;
}
continue;
}
if trimmed.is_empty() {
if !paragraph_lines.is_empty() {
break;
}
continue;
}
if trimmed.starts_with('#') || trimmed == "---" || trimmed.starts_with("```") {
if !paragraph_lines.is_empty() {
break;
}
continue;
}
paragraph_lines.push(trimmed);
}
let raw = paragraph_lines.join(" ");
let cleaned = raw
.replace("**", "")
.replace('*', "")
.replace('`', "")
.replace("__", "");
let cleaned = cleaned.trim().to_string();
if cleaned.len() <= 150 {
cleaned
} else {
let truncated = &cleaned[..150];
let end = truncated.rfind(' ').unwrap_or(150);
format!("{}…", cleaned[..end].trim())
}
}
fn git_last_updated(manifest_rel: &str) -> String {
let output = std::process::Command::new("git")
.args(["log", "--format=%as", "-1", "--", manifest_rel])
.output();
match output {
Ok(out) if out.status.success() => {
let date = String::from_utf8_lossy(&out.stdout).trim().to_string();
if date.is_empty() {
"unknown".to_string()
} else {
date
}
}
_ => "unknown".to_string(),
}
}
fn collect_files(dir: &Path, root: &str, out: &mut Vec<String>) {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files(&path, root, out);
} else if let Ok(rel) = path.strip_prefix(root) {
let rel_str = rel.to_string_lossy().replace('\\', "/");
out.push(rel_str);
}
}
}