use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
const SKIPPED_DIRECTORIES: [&str; 4] = ["target", "dist", "node_modules", ".git"];
fn main() {
let manifest = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir"));
let templates = manifest.join("../../examples/templates");
let out = PathBuf::from(std::env::var_os("OUT_DIR").expect("out dir")).join("templates.rs");
println!("cargo:rerun-if-changed={}", templates.display());
let mut generated = String::from(
"// @generated by crates/cli/build.rs from examples/templates. Do not edit.\n\
pub(crate) struct TemplateFile {\n pub(crate) path: &'static str,\n pub(crate) contents: &'static str,\n}\n\n\
pub(crate) struct TemplateSource {\n pub(crate) directory: &'static str,\n pub(crate) files: &'static [TemplateFile],\n /// Directories the scaffold creates but leaves empty. A template marks one\n /// with a `.gitkeep`, which git needs and a scaffolded project must not\n /// inherit: the file set a user sees has to be the file set the template\n /// means.\n pub(crate) directories: &'static [&'static str],\n}\n\n\
pub(crate) const TEMPLATE_SOURCES: &[TemplateSource] = &[\n",
);
let mut directories = fs::read_dir(&templates)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", templates.display()))
.map(|entry| entry.expect("template entry").path())
.filter(|path| path.is_dir())
.collect::<Vec<_>>();
directories.sort();
assert!(
!directories.is_empty(),
"examples/templates must contain at least one template project"
);
for directory in directories {
let name = directory
.file_name()
.and_then(|name| name.to_str())
.expect("template directory name")
.to_string();
let mut files = BTreeMap::new();
let mut directories = BTreeSet::new();
collect(&directory, &directory, &mut files, &mut directories);
assert!(
files.contains_key("Noxid.toml"),
"template {name} has no Noxid.toml"
);
generated.push_str(&format!(
" TemplateSource {{ directory: {name:?}, files: &[\n"
));
for (relative, absolute) in files {
println!("cargo:rerun-if-changed={}", absolute.display());
generated.push_str(&format!(
" TemplateFile {{ path: {relative:?}, contents: include_str!({:?}) }},\n",
absolute.display().to_string()
));
}
generated.push_str(" ], directories: &[");
for relative in directories {
generated.push_str(&format!("{relative:?}, "));
}
generated.push_str("] },\n");
}
generated.push_str("];\n");
generated.push_str(&vendored_plugins(&manifest));
fs::write(&out, generated).unwrap_or_else(|error| panic!("cannot write templates: {error}"));
}
const VENDORED_PLUGIN_SOURCES: [&str; 6] = [
"plugins/drizzle-orm/VETTING.md",
"plugins/drizzle-orm/adapter.js",
"plugins/drizzle-orm/data-scopes.test.mjs",
"plugins/postgres/VETTING.md",
"tools/database-url.mjs",
"tools/node-sqlite.mjs",
];
fn vendored_plugins(manifest: &Path) -> String {
let repository = manifest.join("../..");
let mut generated = String::from(
"\npub(crate) struct VendoredFile {\n pub(crate) path: &'static str,\n pub(crate) contents: &'static str,\n}\n\n\
pub(crate) const VENDORED_PLUGIN_FILES: &[VendoredFile] = &[\n",
);
for relative in VENDORED_PLUGIN_SOURCES {
let absolute = repository.join(relative);
assert!(
absolute.is_file(),
"vendored plugin source {} is missing",
absolute.display()
);
println!("cargo:rerun-if-changed={}", absolute.display());
generated.push_str(&format!(
" VendoredFile {{ path: {relative:?}, contents: include_str!({:?}) }},\n",
absolute.display().to_string()
));
}
generated.push_str("];\n\n");
generated.push_str(&format!(
"pub(crate) const VENDORED_PLUGIN_COMMIT: &str = {:?};\n",
vendored_plugin_commit(&repository)
));
generated
}
fn vendored_plugin_commit(repository: &Path) -> String {
let mut command = std::process::Command::new("git");
command
.arg("-C")
.arg(repository)
.args(["log", "-1", "--format=%H", "--"]);
for relative in VENDORED_PLUGIN_SOURCES {
command.arg(relative);
}
let Ok(output) = command.output() else {
return "unknown".into();
};
if !output.status.success() {
return "unknown".into();
}
let commit = String::from_utf8_lossy(&output.stdout).trim().to_string();
if commit.is_empty() {
"unknown".into()
} else {
commit
}
}
fn collect(
root: &Path,
directory: &Path,
files: &mut BTreeMap<String, PathBuf>,
directories: &mut BTreeSet<String>,
) {
let mut entries = fs::read_dir(directory)
.unwrap_or_else(|error| panic!("cannot read {}: {error}", directory.display()))
.map(|entry| entry.expect("template entry").path())
.collect::<Vec<_>>();
entries.sort();
for entry in entries {
let name = entry
.file_name()
.and_then(|name| name.to_str())
.expect("template file name")
.to_string();
if entry.is_dir() {
if SKIPPED_DIRECTORIES.contains(&name.as_str()) {
continue;
}
collect(root, &entry, files, directories);
continue;
}
if name == ".DS_Store" {
continue;
}
if name == ".gitkeep" {
let parent = entry
.parent()
.expect("template marker has a parent")
.strip_prefix(root)
.expect("template directory inside its template")
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/");
assert!(
!parent.is_empty(),
"a template root cannot be marked with .gitkeep"
);
println!("cargo:rerun-if-changed={}", entry.display());
directories.insert(parent);
continue;
}
let relative = entry
.strip_prefix(root)
.expect("template file inside its template")
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/");
files.insert(relative, entry);
}
}