cad-editor/build.rs
Hakan Seven 084dfb1bdb fix(windows): embed the app icon into the .exe
The Windows executable carried no icon, so Explorer, the taskbar, the
Start-menu tile and file-association entries showed a generic icon. Embed
AppIcon.ico via winresource in build.rs, and generate the .ico from the logo
before the build (it was produced after the build, too late to embed). Local
and non-Windows builds skip the step when the .ico is absent.

Fixes #107.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 04:10:01 +03:00

109 lines
4.1 KiB
Rust

// build.rs — auto-generates src/modules/registry.rs
//
// Scans src/modules/ for subdirectories containing a mod.rs file.
// Each subdirectory name like "home" maps to a struct "HomeModule"
// defined in src/modules/home/mod.rs.
//
// Rules for a directory to be picked up:
// • Located directly in src/modules/
// • Contains a mod.rs file
// • Does NOT contain plugin.toml (add-ons register via BuiltinPlugin::ribbon)
// • Defines a pub struct {PascalCase}Module implementing CadModule
// (unit struct — no ::new() needed)
//
// Tab display order: the PRIORITY list below controls order.
// Unlisted modules appear after priority modules, sorted alphabetically.
//
// Adding a new module:
// 1. Create src/modules/my_name/mod.rs (implement CadModule as MyNameModule)
// 2. Add `pub mod my_name;` to src/modules/mod.rs
// 3. cargo build → module appears in the ribbon
use std::fs;
use std::path::Path;
// Display order for known modules; unknown modules follow alphabetically.
const PRIORITY: &[&str] = &["home", "model", "insert", "annotate", "view", "manage"];
fn main() {
// Windows: embed AppIcon.ico into the .exe so the executable carries its
// own icon (Explorer, taskbar, Start-menu tile, file associations). The
// .ico is produced from assets/logo.svg by the release workflow before the
// build; when it is absent (local/dev builds) this is skipped. See #107.
#[cfg(windows)]
{
println!("cargo:rerun-if-changed=packaging/windows/AppIcon.ico");
if Path::new("packaging/windows/AppIcon.ico").exists() {
let mut res = winresource::WindowsResource::new();
res.set_icon("packaging/windows/AppIcon.ico");
if let Err(e) = res.compile() {
println!("cargo:warning=failed to embed Windows icon: {e}");
}
}
}
let mods_dir = Path::new("src/modules");
println!("cargo:rerun-if-changed=src/modules");
// Collect subdirectories that contain mod.rs.
let mut modules: Vec<String> = fs::read_dir(mods_dir)
.expect("src/modules/ not found")
.filter_map(|e| {
let entry = e.ok()?;
if !entry.file_type().ok()?.is_dir() {
return None;
}
let name = entry.file_name().into_string().ok()?;
let mod_file = mods_dir.join(&name).join("mod.rs");
let plugin_toml = mods_dir.join(&name).join("plugin.toml");
if mod_file.exists() && !plugin_toml.exists() {
Some(name)
} else {
None
}
})
.collect();
// Sort: priority order first, then alphabetical for the rest.
modules.sort_by_key(|name| {
let pri = PRIORITY.iter().position(|p| *p == name.as_str());
(pri.map(|i| i as u32).unwrap_or(u32::MAX), name.clone())
});
// snake_case → PascalCase + "Module"
let to_struct = |s: &str| -> String {
s.split('_')
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect::<String>()
+ "Module"
};
let mut out = String::from(
"// AUTO-GENERATED by build.rs — do not edit by hand.\n\
// To add a module: create src/modules/my_name/mod.rs + add `pub mod my_name;` to modules/mod.rs.\n\n\
use crate::modules::CadModule;\n\n\
/// Returns one boxed instance of every registered CAD module.\n\
/// Called once at startup by `Ribbon::new()`.\n\
pub fn all_modules() -> Vec<Box<dyn CadModule>> {\n\
\tvec![\n",
);
for module in &modules {
let s = to_struct(module);
out.push_str(&format!("\t\tBox::new(super::{module}::{s}),\n"));
}
out.push_str("\t]\n}\n");
let out_path = mods_dir.join("registry.rs");
let current = fs::read_to_string(&out_path).unwrap_or_default();
if current != out {
fs::write(&out_path, &out).expect("failed to write registry.rs");
}
}