use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn emit_darkly_version() {
let version = Command::new("git")
.args(["describe", "--tags", "--long"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "0.0.0-0-gunknown".to_string());
println!("cargo:rustc-env=DARKLY_VERSION={version}");
let git_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("../../.git");
let mut candidates = vec![
git_dir.join("HEAD"),
git_dir.join("packed-refs"),
git_dir.join("refs/tags"),
];
if let Ok(head) = fs::read_to_string(git_dir.join("HEAD")) {
if let Some(r) = head.trim().strip_prefix("ref: ") {
candidates.push(git_dir.join(r));
}
}
for path in candidates {
if path.exists() {
println!("cargo:rerun-if-changed={}", path.display());
}
}
}
fn main() {
emit_darkly_version();
let src = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("src");
generate_grouped_registry(
&src.join("engine/protocol/handlers"),
"crate::engine::protocol::RequestRegistration",
);
generate_registry(&src.join("gpu/veils"), "crate::gpu::veil::VeilRegistration");
generate_registry(&src.join("gpu/voids"), "crate::gpu::void::VoidRegistration");
generate_registry(
&src.join("gpu/filters"),
"crate::gpu::filter::FilterPipelineRegistration",
);
generate_registry(&src.join("tools"), "crate::tool::ToolRegistration");
generate_registry(
&src.join("brush/nodes"),
"crate::brush::BrushNodeRegistration",
);
generate_registry(
&src.join("brush/stabilizers"),
"crate::brush::stabilizer::StabilizerRegistration",
);
generate_registry(
&src.join("config/sections"),
"crate::config::schema::SchemaSection",
);
generate_registry(
&src.join("document/filters"),
"crate::document::filter::FilterEntityRegistration",
);
generate_registry(
&src.join("document/layer_kinds"),
"crate::document::layer_kind::LayerKindRegistration",
);
generate_registry(
&src.join("gpu/blend_modes"),
"crate::gpu::blend_mode::BlendModeRegistration",
);
generate_yaml_presets(&PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("presets"));
generate_builtin_brushes(
&PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("brushes"),
);
generate_texture_registry(
&PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("resources/textures"),
);
}
fn generate_registry(dir: &Path, registration_type: &str) {
let mut modules = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "rs") {
let stem = path.file_stem().unwrap().to_str().unwrap().to_string();
if stem != "mod" {
modules.push(stem);
}
}
}
}
modules.sort();
let type_name = registration_type.rsplit("::").next().unwrap();
let mut code = String::new();
code.push_str("// @generated by build.rs — do not edit manually.\n");
code.push_str("// To add a new module, create a .rs file in this directory\n");
code.push_str(&format!(
"// that exports `pub fn register() -> {registration_type}`.\n\n"
));
for m in &modules {
code.push_str(&format!("pub mod {m};\n"));
}
code.push_str(&format!("\nuse {registration_type};\n\n"));
code.push_str("#[rustfmt::skip]\n");
code.push_str(&format!("pub fn registrations() -> Vec<{type_name}> {{\n"));
code.push_str(" vec![\n");
for m in &modules {
code.push_str(&format!(" {m}::register(),\n"));
}
code.push_str(" ]\n");
code.push_str("}\n");
let mod_path = dir.join("mod.rs");
let existing = fs::read_to_string(&mod_path).unwrap_or_default();
if existing != code {
fs::write(&mod_path, code).unwrap();
}
println!("cargo:rerun-if-changed={}", dir.display());
}
fn generate_grouped_registry(dir: &Path, registration_type: &str) {
let mut modules = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "rs") {
let stem = path.file_stem().unwrap().to_str().unwrap().to_string();
if stem != "mod" {
modules.push(stem);
}
}
}
}
modules.sort();
let type_name = registration_type.rsplit("::").next().unwrap();
let mut code = String::new();
code.push_str("// @generated by build.rs — do not edit manually.\n");
code.push_str("// To add request kinds, create or edit a domain .rs file in this\n");
code.push_str(&format!(
"// directory that exports `pub fn registrations() -> Vec<{registration_type}>`.\n\n"
));
for m in &modules {
code.push_str(&format!("pub mod {m};\n"));
}
code.push_str(&format!("\nuse {registration_type};\n\n"));
code.push_str("#[rustfmt::skip]\n");
code.push_str(&format!("pub fn registrations() -> Vec<{type_name}> {{\n"));
code.push_str(" let mut all = Vec::new();\n");
for m in &modules {
code.push_str(&format!(" all.extend({m}::registrations());\n"));
}
code.push_str(" all\n");
code.push_str("}\n");
let mod_path = dir.join("mod.rs");
let existing = fs::read_to_string(&mod_path).unwrap_or_default();
if existing != code {
fs::write(&mod_path, code).unwrap();
}
println!("cargo:rerun-if-changed={}", dir.display());
}
fn generate_yaml_presets(dir: &Path) {
let mut defaults_path: Option<PathBuf> = None;
let mut overlays: Vec<(String, PathBuf)> = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.extension().is_some_and(|e| e == "yaml" || e == "yml") {
continue;
}
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if stem == "defaults" {
defaults_path = Some(path);
} else if !stem.is_empty() {
overlays.push((stem, path));
}
}
}
let defaults_path =
defaults_path.unwrap_or_else(|| panic!("presets/defaults.yaml is required"));
overlays.sort_by(|a, b| a.0.cmp(&b.0));
let mut display_names: Vec<(String, String)> = Vec::new();
for (stem, path) in &overlays {
let yaml = fs::read_to_string(path).unwrap_or_default();
let name = parse_yaml_display_name(&yaml).unwrap_or_else(|| titlecase(stem));
display_names.push((stem.clone(), name));
}
let mut code = String::new();
code.push_str("// @generated by build.rs — do not edit manually.\n");
code.push_str(
"// To add a new editor overlay, drop `<name>.yaml` in `crates/darkly/presets/`.\n\n",
);
code.push_str(&format!(
"pub const DEFAULTS_YAML: &str = include_str!({:?});\n\n",
defaults_path.display().to_string()
));
for (stem, path) in &overlays {
code.push_str(&format!(
"const {}_YAML: &str = include_str!({:?});\n",
stem.to_uppercase().replace('-', "_"),
path.display().to_string()
));
}
code.push('\n');
code.push_str("pub const OVERLAYS: &[(&str, &str)] = &[\n");
for (stem, name) in &display_names {
code.push_str(&format!(
" ({:?}, {}_YAML),\n",
name,
stem.to_uppercase().replace('-', "_")
));
}
code.push_str("];\n\n");
code.push_str("pub const BASE_SETTINGS_OPTIONS: &[(&str, &str)] = &[\n");
for (_, name) in &display_names {
code.push_str(&format!(" ({:?}, {:?}),\n", name, name));
}
code.push_str("];\n");
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
let out_path = PathBuf::from(out_dir).join("presets_gen.rs");
fs::write(&out_path, code).unwrap();
println!("cargo:rerun-if-changed={}", dir.display());
}
fn parse_yaml_display_name(yaml: &str) -> Option<String> {
for line in yaml.lines() {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("name:") {
let v = rest.trim();
let v = v.trim_matches('"').trim_matches('\'');
if !v.is_empty() {
return Some(v.to_string());
}
}
}
None
}
fn titlecase(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().chain(chars).collect(),
None => String::new(),
}
}
fn generate_builtin_brushes(dir: &Path) {
let mut brushes: Vec<(String, PathBuf)> = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.extension().is_some_and(|e| e == "yaml" || e == "yml") {
continue;
}
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if stem.is_empty() {
continue;
}
brushes.push((stem, path));
}
}
brushes.sort_by(|a, b| a.0.cmp(&b.0));
let mut code = String::new();
code.push_str("// @generated by build.rs — do not edit manually.\n");
code.push_str("// To add a new built-in brush, drop `<name>.yaml` in\n");
code.push_str("// `crates/darkly/brushes/`. It is loaded automatically.\n\n");
for (stem, path) in &brushes {
code.push_str(&format!(
"const {}_YAML: &str = include_str!({:?});\n",
stem.to_uppercase().replace('-', "_"),
path.display().to_string()
));
}
code.push('\n');
code.push_str("pub const BUILTIN_BRUSHES_YAML: &[(&str, &str)] = &[\n");
for (stem, _) in &brushes {
let filename = format!("{stem}.yaml");
code.push_str(&format!(
" ({:?}, {}_YAML),\n",
filename,
stem.to_uppercase().replace('-', "_"),
));
}
code.push_str("];\n");
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
let out_path = PathBuf::from(out_dir).join("builtin_brushes_gen.rs");
fs::write(&out_path, code).unwrap();
println!("cargo:rerun-if-changed={}", dir.display());
}
fn generate_texture_registry(dir: &Path) {
let mut textures: Vec<(String, PathBuf)> = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if stem.is_empty() || stem.starts_with('.') {
continue;
}
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.unwrap_or_default();
if !matches!(ext.as_str(), "jpg" | "jpeg" | "png" | "webp") {
continue;
}
textures.push((stem, path));
}
}
textures.sort_by(|a, b| a.0.cmp(&b.0));
let mut code = String::new();
code.push_str("// @generated by build.rs — do not edit manually.\n");
code.push_str("// To add a new built-in texture, drop an image into\n");
code.push_str("// `crates/darkly/resources/textures/`. It is registered\n");
code.push_str("// under its file basename (sans extension).\n\n");
code.push_str("pub const BUILTIN_TEXTURES: &[(&str, &[u8])] = &[\n");
for (stem, path) in &textures {
let file_name = path
.file_name()
.and_then(|s| s.to_str())
.expect("texture path must have a file name");
let rel = format!("/resources/textures/{file_name}");
code.push_str(&format!(
" ({stem:?}, include_bytes!(concat!(env!(\"CARGO_MANIFEST_DIR\"), {rel:?}))),\n"
));
}
code.push_str("];\n");
let out_dir = env::var("OUT_DIR").expect("OUT_DIR not set");
let out_path = PathBuf::from(out_dir).join("textures_gen.rs");
fs::write(&out_path, code).unwrap();
println!("cargo:rerun-if-changed={}", dir.display());
}