use std::env;
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
#[allow(clippy::needless_doctest_main)]
pub fn configure(icons: &[(&str, &str)]) {
let target_os = env::var("CARGO_CFG_TARGET_OS")
.expect("CARGO_CFG_TARGET_OS is always set for Cargo build scripts");
if target_os != "windows" {
write_generated(&generate_stub());
return;
}
let manifest_dir = PathBuf::from(
env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is always set for build scripts"),
);
let resolved = resolve(icons, &manifest_dir);
let default = resolved
.iter()
.find(|entry| entry.name == "Default")
.expect("the icon set must contain an entry named `Default`");
for entry in &resolved {
println!("cargo:rerun-if-changed={}", entry.path.display());
}
bake_default(&default.path);
write_generated(&generate(&resolved));
}
struct Entry {
name: String,
path: PathBuf,
}
fn resolve(icons: &[(&str, &str)], manifest_dir: &Path) -> Vec<Entry> {
assert!(!icons.is_empty(), "the icon set must not be empty");
let mut resolved: Vec<Entry> = Vec::with_capacity(icons.len());
for (name, path) in icons {
assert!(
is_ident(name),
"`{name}` is not a valid Rust identifier, so it cannot be an AppIcon variant"
);
assert!(
!resolved.iter().any(|entry| entry.name == *name),
"duplicate icon name `{name}`"
);
let joined = manifest_dir.join(path);
let canonical = fs::canonicalize(&joined)
.unwrap_or_else(|e| panic!("icon `{}` not found at {}: {e}", name, joined.display()));
resolved.push(Entry {
name: (*name).to_owned(),
path: strip_unc(canonical),
});
}
resolved
}
fn bake_default(ico: &Path) {
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set for build scripts"));
let rc_path = out_dir.join("alt_icons.rc");
let escaped = ico.display().to_string().replace('\\', "\\\\");
fs::write(&rc_path, format!("LANGUAGE 0, 0\n1 ICON \"{escaped}\"\n"))
.unwrap_or_else(|e| panic!("could not write {}: {e}", rc_path.display()));
embed_resource::compile(&rc_path, embed_resource::NONE)
.manifest_required()
.unwrap_or_else(|e| panic!("could not compile {}: {e}", rc_path.display()));
}
fn generate(entries: &[Entry]) -> String {
let mut out = String::new();
out.push_str(HEADER);
out.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\npub enum AppIcon {\n");
for entry in entries {
let _ = writeln!(out, " {},", entry.name);
}
out.push_str("}\n\nimpl AppIcon {\n /// Every icon in the set, in declaration order.\n");
out.push_str(" pub const ALL: &'static [AppIcon] = &[\n");
for entry in entries {
let _ = writeln!(out, " AppIcon::{},", entry.name);
}
out.push_str(" ];\n}\n\nimpl alt_icons::Icon for AppIcon {\n");
out.push_str(" fn name(&self) -> &'static str {\n match self {\n");
for entry in entries {
let _ = writeln!(
out,
" AppIcon::{} => \"{}\",",
entry.name, entry.name
);
}
out.push_str(" }\n }\n\n");
out.push_str(" fn bytes(&self) -> &'static [u8] {\n match self {\n");
for entry in entries {
let _ = writeln!(
out,
" AppIcon::{} => include_bytes!(r\"{}\"),",
entry.name,
entry.path.display()
);
}
out.push_str(" }\n }\n}\n");
out
}
fn generate_stub() -> String {
let mut out = String::from(HEADER);
out.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\npub enum AppIcon {}\n\n");
out.push_str("impl AppIcon {\n pub const ALL: &'static [AppIcon] = &[];\n}\n\n");
out.push_str("impl alt_icons::Icon for AppIcon {\n");
out.push_str(" fn name(&self) -> &'static str {\n match *self {}\n }\n\n");
out.push_str(" fn bytes(&self) -> &'static [u8] {\n match *self {}\n }\n}\n");
out
}
fn write_generated(code: &str) {
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set for build scripts"));
let path = out_dir.join("alt_icons.rs");
fs::write(&path, code).unwrap_or_else(|e| panic!("could not write {}: {e}", path.display()));
}
const HEADER: &str = "// Generated by alt-icons::build. Do not edit.\n\n";
fn is_ident(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn strip_unc(path: PathBuf) -> PathBuf {
let text = path.to_string_lossy();
match text.strip_prefix(r"\\?\") {
Some(stripped) => PathBuf::from(stripped),
None => path,
}
}