use std::collections::BTreeSet;
use std::fs;
use std::path::{Component, Path, PathBuf};
use anyhow::{bail, Context, Result};
include!(concat!(env!("OUT_DIR"), "/templates_gen.rs"));
pub fn execute(name: &str, template: &str) -> Result<()> {
let normalized = template.replace('-', "_");
let prefix = format!("{normalized}/");
let files: Vec<(&str, &str)> = TEMPLATE_FILES
.iter()
.filter_map(|(path, contents)| {
path.strip_prefix(prefix.as_str())
.map(|rel| (rel, *contents))
})
.collect();
if files.is_empty() {
bail!(
"template '{template}' not found; available: {}",
available_templates().join(", ")
);
}
let dest = Path::new(name);
if dest.exists() {
bail!("directory '{}' already exists", name);
}
for (rel, contents) in &files {
let path = dest.join(rel);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, contents).with_context(|| format!("write {}", path.display()))?;
}
replace_in_tree(dest, "{{PROJECT_NAME}}", name)?;
let clankers_dep = clankers_dependency_line(dest)?;
replace_in_tree(dest, "{{CLANKERS_DEPENDENCY}}", &clankers_dep)?;
println!("clankeRS project created: {name}\n\nNext steps:\n cd {name}\n clankers run");
Ok(())
}
fn available_templates() -> Vec<String> {
TEMPLATE_FILES
.iter()
.filter_map(|(path, _)| path.split('/').next())
.collect::<BTreeSet<_>>()
.into_iter()
.map(|name| name.replace('_', "-"))
.collect()
}
fn clankers_dependency_line(dest: &Path) -> Result<String> {
if let Some(workspace) = find_workspace_root() {
let clankers_crate = workspace.join("crates/clankers");
if clankers_crate.exists() {
let dest_abs = std::env::current_dir()?.join(dest);
if let Some(rel) = relative_path(&dest_abs, &clankers_crate) {
return Ok(format!(
"clankers = {{ path = \"{}\" }}",
rel.to_string_lossy()
));
}
}
}
Ok(r#"clankers = "0.1""#.to_string())
}
fn relative_path(from: &Path, to: &Path) -> Option<PathBuf> {
let from = from.components().collect::<Vec<_>>();
let to = to.components().collect::<Vec<_>>();
let mut i = 0;
while i < from.len() && i < to.len() && from[i] == to[i] {
i += 1;
}
let mut rel = PathBuf::new();
for _ in i..from.len() {
if matches!(from[i], Component::Normal(_)) {
rel.push("..");
}
}
for part in &to[i..] {
rel.push(part.as_os_str());
}
if rel.as_os_str().is_empty() {
rel.push(".");
}
Some(rel)
}
fn find_workspace_root() -> Option<PathBuf> {
let cwd = std::env::current_dir().ok()?;
for ancestor in cwd.ancestors() {
if ancestor.join("crates/clankers").exists() {
return Some(ancestor.to_path_buf());
}
}
None
}
fn replace_in_tree(dir: &Path, from: &str, to: &str) -> Result<()> {
for entry in walkdir::WalkDir::new(dir) {
let entry = entry?;
if entry.file_type().is_file() {
let path = entry.path();
let content = fs::read_to_string(path).context("read template file")?;
if content.contains(from) {
fs::write(path, content.replace(from, to))?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn relative_path_to_sibling_crate() {
let from = Path::new("/workspace/clankeRS/hello_clanker");
let to = Path::new("/workspace/clankeRS/crates/clankers");
assert_eq!(
relative_path(from, to).unwrap(),
PathBuf::from("../crates/clankers")
);
}
#[test]
fn all_templates_are_embedded_with_manifests() {
for template in [
"basic_node",
"perception_node",
"ml_inference_node",
"replay_test_node",
] {
let manifest = format!("{template}/Cargo.toml");
assert!(
TEMPLATE_FILES.iter().any(|(path, _)| *path == manifest),
"embedded table is missing {manifest}"
);
let main = format!("{template}/src/main.rs");
assert!(
TEMPLATE_FILES.iter().any(|(path, _)| *path == main),
"embedded table is missing {main}"
);
}
}
}