use heck::ToUpperCamelCase; use quote::{format_ident, quote};
use std::{env, fs, path::PathBuf};
const LICENSE_DIR: &str = "lic/assets/licenses";
fn is_all_caps_acronym(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c.is_ascii_uppercase())
}
fn main() -> std::io::Result<()> {
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
let generated_file_path = out_dir.join("generated_licenses.rs");
let license_dir_path = PathBuf::from(LICENSE_DIR);
let mut variants = Vec::new();
let mut license_details = Vec::new();
if license_dir_path.is_dir() {
for entry in fs::read_dir(&license_dir_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "txt") {
if let (Some(file_stem), Some(file_name_osstr)) = (
path.with_extension("").file_stem(),
path.with_extension("").with_extension("").file_name(),
) {
let file_stem_str = file_stem.to_string_lossy();
let file_name_str = file_name_osstr.to_string_lossy().to_string();
let mut stem_with_replacements = String::new();
for c in file_stem_str.chars() {
match c {
'0' => stem_with_replacements.push_str("Zero"),
'1' => stem_with_replacements.push_str("One"),
'2' => stem_with_replacements.push_str("Two"),
'3' => stem_with_replacements.push_str("Three"),
'4' => stem_with_replacements.push_str("Four"),
'5' => stem_with_replacements.push_str("Five"),
'6' => stem_with_replacements.push_str("Six"),
'7' => stem_with_replacements.push_str("Seven"),
'8' => stem_with_replacements.push_str("Eight"),
'9' => stem_with_replacements.push_str("Nine"),
'+' => stem_with_replacements.push_str("Plus"),
'.' => stem_with_replacements.push_str("Dot"),
_ => stem_with_replacements.push(c),
}
}
let parts: Vec<&str> = stem_with_replacements
.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|s| !s.is_empty()) .collect();
let mut final_ident_string = String::new();
for part in parts {
if is_all_caps_acronym(part) {
final_ident_string.push_str(part);
} else {
final_ident_string.push_str(&part.to_upper_camel_case());
}
}
if final_ident_string.is_empty() {
eprintln!(
"cargo:warning=Could not generate valid identifier for filename: {}",
file_name_str
);
final_ident_string.push_str("InvalidLicenseName");
}
let variant_ident = format_ident!("{}", final_ident_string);
variants.push(variant_ident.clone()); license_details.push((variant_ident, file_name_str));
}
}
}
} else {
eprintln!(
"cargo:warning=License directory '{}' not found or not a directory. Generating empty License enum.",
LICENSE_DIR
);
}
let variants_with_attrs = license_details.iter().map(|(variant, filename)| {
quote! {
#[value(name = #filename)] #variant
}
});
let name_match_arms = license_details.iter().map(|(variant, filename)| {
quote! { Self::#variant => #filename }
});
let from_str_match_arms = license_details.iter().map(|(variant, filename)| {
quote! { #filename => Ok(Self::#variant), }
});
let template_content_match_arms = license_details.iter().map(|(variant, template_path)| {
quote! {
Self::#variant => include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/licenses/", #template_path, ".template.txt"))
}
});
let variant_idents = license_details.iter().map(|(variant, _)| variant);
let generated_code = quote! {
#![allow(clippy::all)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, clap::ValueEnum)]
pub enum License {
#( #variants_with_attrs ),*
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseLicenseError;
impl std::fmt::Display for ParseLicenseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Provided string does not match any known license filename")
}
}
impl std::error::Error for ParseLicenseError {}
impl License {
pub fn spdx_id(&self) -> &'static str {
match self {
#( #name_match_arms ),* }
}
pub fn iter() -> impl Iterator<Item = Self> {
[ #( Self::#variant_idents ),* ].iter().copied()
}
pub fn template_content(&self) -> &'static str {
match self {
#( #template_content_match_arms ),*
}
}
}
impl std::fmt::Display for License {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.spdx_id())
}
}
impl std::str::FromStr for License {
type Err = ParseLicenseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
#( #from_str_match_arms )*
_ => Err(ParseLicenseError),
}
}
}
};
fs::write(&generated_file_path, generated_code.to_string())?;
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", LICENSE_DIR);
if license_dir_path.is_dir() {
for entry in fs::read_dir(&license_dir_path)?.flatten() {
if entry.path().is_file() {
println!("cargo:rerun-if-changed={}", entry.path().display());
}
}
}
Ok(())
}