From b3c30a30fd021a33ed6badb6ef3182d48a43fef7 Mon Sep 17 00:00:00 2001 From: firestar99 Date: Wed, 3 Dec 2025 15:10:29 +0100 Subject: [PATCH 1/4] generate: read placeholders with choices from `cargo-generate.toml` --- xtask/Cargo.lock | 25 +- xtask/Cargo.toml | 6 +- xtask/src/cargo_generate_config.rs | 28 +++ xtask/src/generate.rs | 363 +++++++++++++++++------------ xtask/src/main.rs | 1 + 5 files changed, 255 insertions(+), 168 deletions(-) create mode 100644 xtask/src/cargo_generate_config.rs diff --git a/xtask/Cargo.lock b/xtask/Cargo.lock index e952cd5..7d65b19 100644 --- a/xtask/Cargo.lock +++ b/xtask/Cargo.lock @@ -1866,27 +1866,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "syn" version = "2.0.111" @@ -2482,8 +2461,10 @@ dependencies = [ "cargo-generate", "clap", "env_logger", + "indexmap", "log", - "strum", + "serde", + "toml", ] [[package]] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index d07fb0b..86d66ce 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -7,7 +7,11 @@ edition = "2024" [dependencies] anyhow = "1.0.100" clap = { version = "4.5.53", features = ["derive"] } -strum = { version = "0.27.2", features = ["derive"] } log = "0.4.28" env_logger = "0.11.8" + +# deps below are copied from `cargo-generate` cargo-generate = { version = "0.23.7", default-features = false, features = [] } +serde = { version = "~1.0", features = ["derive"] } +toml = { version = "~0.9", features = ["preserve_order"] } +indexmap = { version = "~2", features = ["serde"] } diff --git a/xtask/src/cargo_generate_config.rs b/xtask/src/cargo_generate_config.rs new file mode 100644 index 0000000..e5da2a2 --- /dev/null +++ b/xtask/src/cargo_generate_config.rs @@ -0,0 +1,28 @@ +//! These definitions are copied from `cargo_generate::config`, since that interface isn't pub. +//! Only keys we need are kept. + +use indexmap::IndexMap; +use serde::Deserialize; +use std::collections::HashMap; + +pub const CONFIG_FILE_NAME: &str = "cargo-generate.toml"; + +#[derive(Deserialize, Debug, PartialEq, Default, Clone)] +pub struct Config { + pub template: Option, + pub placeholders: Option, + pub conditional: Option>, +} + +#[derive(Deserialize, Debug, PartialEq, Eq, Default, Clone)] +pub struct TemplateConfig { + pub sub_templates: Option>, +} + +#[derive(Deserialize, Debug, PartialEq, Clone)] +pub struct ConditionalConfig { + pub placeholders: Option, +} + +#[derive(Deserialize, Debug, PartialEq, Clone, Default)] +pub struct TemplateSlotsTable(pub IndexMap); diff --git a/xtask/src/generate.rs b/xtask/src/generate.rs index 423b629..151b0f3 100644 --- a/xtask/src/generate.rs +++ b/xtask/src/generate.rs @@ -1,69 +1,15 @@ -use anyhow::Context; +use crate::cargo_generate_config::{CONFIG_FILE_NAME, Config}; +use anyhow::{Context, anyhow, bail}; use cargo_generate::GenerateArgs; use clap::Parser; -use clap::builder::PossibleValue; +use indexmap::IndexMap; use log::{debug, info}; use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; use std::path::{Path, PathBuf}; -use strum::{Display, EnumString, IntoStaticStr, VariantArray}; pub const TEMPLATE_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../graphics"); -/// All possible placeholder *values*. -/// -/// We assume there are no duplicate values for placeholders, so we don't need to type out the key / placeholder name, -/// but can derive the key from the value directly. -#[repr(u32)] -#[derive(Copy, Clone, Eq, PartialEq, Hash, Display, EnumString, IntoStaticStr, VariantArray)] -pub enum Values { - #[strum(to_string = "cargo-gpu")] - CargoGpu, - #[strum(to_string = "spirv-builder")] - SpirvBuilder, -} - -impl Debug for Values { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Display::fmt(self, f) - } -} - -impl clap::ValueEnum for Values { - fn value_variants<'a>() -> &'a [Self] { - Values::VARIANTS - } - - fn to_possible_value(&self) -> Option { - Some(PossibleValue::new(self.value())) - } -} - -impl Values { - pub fn key(&self) -> Placeholders { - match self { - Values::CargoGpu | Values::SpirvBuilder => Placeholders::Integration, - } - } - - pub fn value(&self) -> &'static str { - <&'static str>::from(self) - } -} - -#[repr(u32)] -#[derive(Copy, Clone, Eq, PartialEq, Hash, Display, EnumString, IntoStaticStr, VariantArray)] -pub enum Placeholders { - #[strum(to_string = "integration")] - Integration, -} - -impl Debug for Placeholders { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Display::fmt(self, f) - } -} - #[derive(Parser, Debug, Default)] pub struct Generate { /// Directory where to place the generated templates. @@ -78,53 +24,147 @@ pub struct Generate { /// If a command fails, this process will fail as well, allowing you to test the template output. #[clap(long, short = 'x')] execute: Option, - values: Vec, + /// Filter for values that any placeholder accepts + /// + /// We assume there are no values that two different placeholders match, within a single template, so we don't have + /// to specify the placeholder the value is associated to. + filter: Vec, } -impl Generate { +#[derive(Clone, Debug)] +struct Template { + name: String, + template_dir: PathBuf, + placeholders: IndexMap>, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +struct Define<'a> { + key: &'a str, + value: &'a str, +} + +impl Display for Define<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}={}", self.key, self.value) + } +} + +impl Debug for Define<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(self, f) + } +} + +impl Template { + fn graphics() -> anyhow::Result { + Self::parse("graphics".to_string(), PathBuf::from(TEMPLATE_PATH)) + } + + fn parse(name: String, template_dir: PathBuf) -> anyhow::Result { + let config_file = template_dir.join(CONFIG_FILE_NAME); + let config: Config = toml::from_str(&std::fs::read_to_string(&config_file)?)?; + let placeholders = config + .placeholders + .with_context(|| format!("Expected `placeholders` in `{}`", config_file.display()))? + .0; + let placeholders = placeholders + .into_iter() + .map(|(p, toml)| { + let choices = toml.get("choices").with_context(|| { + format!( + "Expected `placeholders.{p}` in `{}` to have `choices` set", + config_file.display() + ) + })?; + let choices = choices.as_array().with_context(|| { + format!( + "Expected `placeholders.{p}.choices` in `{}` to be an array", + config_file.display() + ) + })?; + let choices = + choices + .iter() + .enumerate() + .map(|(i, c)| { + let c = c.as_str().with_context(|| format!( + "Expected `placeholders.{p}.choices[{i}]` in `{}` to be a string", + config_file.display() + ))?; + Ok(c.to_string()) + }) + .collect::>>()?; + Ok((p, choices)) + }) + .collect::>>()?; + Ok(Self { + name, + template_dir, + placeholders, + }) + } + + fn value_to_placeholder(&self) -> HashMap<&str, &str> { + self.placeholders + .iter() + .flat_map(|(key, values)| values.iter().map(|v| (v.as_str(), key.as_str()))) + .collect() + } + /// Computes all template variants to expand to and returns them in a Vec. /// The inner vec is guaranteed to contain no duplicate [`Placeholders`] and is sorted by order of the - /// [`Placeholders`] in the enum. - fn variants(&self) -> Vec> { + /// [`Placeholders`] in the enum. Returns an error if a filter was not found. + fn variants<'a>( + &'a self, + filter: impl Iterator, + ) -> Result>>, &'a str> { // insert all Placeholders with empty Vec for possible values - let mut variant_map: HashMap> = Placeholders::VARIANTS + let mut variant_map: IndexMap<&str, Vec<&str>> = self + .placeholders .iter() - .map(|p| (*p, Vec::new())) + .map(|(p, _)| (p.as_str(), Vec::new())) .collect(); // push all values supplied by args into their respective Placeholder's Vec - for v in self.values.iter().copied() { - variant_map.get_mut(&v.key()).unwrap().push(v); + let mut filter = filter.peekable(); + if filter.peek().is_some() { + let value_to_key = self.value_to_placeholder(); + for v in filter { + let key = value_to_key.get(v).ok_or(v)?; + variant_map.get_mut(key).unwrap().push(v); + } } // cross product of all Placeholder keys - let mut variants: Vec> = Vec::from(&[Vec::new()]); + let mut variants: Vec> = Vec::from(&[Vec::new()]); for (p, mut values) in variant_map.into_iter() { // if some Placeholders don't have any values -> all possible values if values.is_empty() { - values = Values::VARIANTS - .iter() - .copied() - .filter(|v| v.key() == p) - .collect(); + let all = self.placeholders.get(p).unwrap(); + values = all.iter().map(|s| s.as_str()).collect::>(); } assert!(!values.is_empty()); - values.sort_by_key(|v| v.key() as u32); variants = values .into_iter() .flat_map(|add| { - variants - .iter() - .map(move |v| v.iter().copied().chain([add]).collect::>()) + variants.iter().map(move |v| { + v.iter() + .copied() + .chain([Define { key: p, value: add }]) + .collect::>() + }) }) .collect::>(); } - debug!("Computed variants: {variants:?}"); - variants + debug!("Variants for template `{}`: {variants:?}", self.name); + Ok(variants) } +} +impl Generate { fn out_base_dir(&self) -> anyhow::Result { let out = self .out @@ -138,6 +178,8 @@ impl Generate { Ok(out) } + /// Some params can't be normalized with `--define` + /// https://github.com/cargo-generate/cargo-generate/issues/1602 fn normalize_env(&self) { // Safety: xtask generate is not multithreaded unsafe { @@ -146,11 +188,16 @@ impl Generate { } } - fn generate(&self, out_base_dir: &Path, variant: &[Values]) -> anyhow::Result { + fn generate( + &self, + out_base_dir: &Path, + template: &Template, + variant: &[Define], + ) -> anyhow::Result { let out_dir = { let mut out_dir = PathBuf::from(out_base_dir); for value in variant { - out_dir.push(value.value()); + out_dir.push(value.value); } std::fs::create_dir_all(&out_dir)?; out_dir @@ -158,13 +205,11 @@ impl Generate { debug!("Generating `{variant:?}` at `{}`", out_dir.display()); let mut args = GenerateArgs::default(); - args.template_path.path = Some(TEMPLATE_PATH.to_string()); + args.template_path.path = Some(template.template_dir.to_string_lossy().into_owned()); args.init = true; args.overwrite = true; - args.define = variant - .iter() - .map(|v| format!("{}={}", v.key(), v.value())) - .collect(); + args.silent = true; + args.define = variant.iter().map(|v| v.to_string()).collect(); args.name = Some("name-is-ignored".to_string()); args.destination = Some(out_dir.clone()); cargo_generate::generate(args)?; @@ -188,7 +233,7 @@ impl Generate { success &= status.success(); } if !success { - anyhow::bail!("Some processes spawned by `--execute` failed"); + bail!("Some processes spawned by `--execute` failed"); } } Ok(()) @@ -197,15 +242,16 @@ impl Generate { pub fn run(&self) -> anyhow::Result<()> { self.normalize_env(); let out_base_dir = self.out_base_dir()?; - let variants = self.variants(); + + let template = Template::graphics()?; + let variants = template + .variants(self.filter.iter().map(|f| f.as_str())) + .map_err(|filter| anyhow!("Unknown filter `{filter}`"))?; let results = variants - .into_iter() - .map(|variant| { - let out_dir = self.generate(&out_base_dir, &variant)?; - Ok((variant, out_dir)) - }) + .iter() + .map(|variant| self.generate(&out_base_dir, &template, variant)) .collect::>>()?; - self.execute(results.iter().map(|(_, a)| a.as_path()))?; + self.execute(results.iter().map(|a| a.as_path()))?; Ok(()) } } @@ -214,56 +260,80 @@ impl Generate { mod tests { use super::*; - pub fn placeholder_map() -> HashMap> { - let mut variant_map: HashMap> = Placeholders::VARIANTS - .iter() - .copied() - .map(|p| (p, Vec::new())) - .collect(); - for v in Values::VARIANTS.iter().copied() { - variant_map.get_mut(&v.key()).unwrap().push(v); + const CARGO_GPU: Define = Define { + key: "integration", + value: "cargo-gpu", + }; + const SPIRV_BUILDER: Define = Define { + key: "integration", + value: "spirv-builder", + }; + const ASH: Define = Define { + key: "api", + value: "ash", + }; + const WGPU: Define = Define { + key: "api", + value: "wgpu", + }; + const CPU: Define = Define { + key: "api", + value: "cpu", + }; + + pub fn test_template() -> Template { + Template { + name: "my-template".to_string(), + template_dir: PathBuf::from("./my_template/"), + placeholders: IndexMap::from( + [ + ("integration", ["cargo-gpu", "spirv-builder"].as_slice()), + ("api", ["ash", "wgpu", "cpu"].as_slice()), + ] + .map(|(k, v)| { + ( + k.to_string(), + v.iter().map(|v| v.to_string()).collect::>(), + ) + }), + ), } - debug!("placeholder_map: {variant_map:?}"); - variant_map } #[test] - pub fn test_variants_all() { - let v_all = Generate { - values: Vec::new(), - ..Default::default() - } - .variants(); - for (p, values) in placeholder_map() { - debug!("{p}: {values:?}"); - assert_eq!(v_all.len() % values.len(), 0); - let each_type_count = v_all.len() / values.len(); + pub fn variants_all() { + let template = test_template(); + let all = template.variants(std::iter::empty()).unwrap(); + debug!("all: {all:?}"); - for value in values { - let vec = v_all - .iter() - .filter(|v| v.contains(&value)) - .collect::>(); - assert_eq!( - vec.len(), - each_type_count, - "Expected {each_type_count} variants with `{value}`, got {}: {vec:?}", - vec.len() - ); - } - } + // order *in the outer slice* is arbitrary + // order within the inner slice is not, and will affect `generated/` folder structure + let expected = [ + [CARGO_GPU, ASH], + [SPIRV_BUILDER, ASH], + [CARGO_GPU, WGPU], + [SPIRV_BUILDER, WGPU], + [CARGO_GPU, CPU], + [SPIRV_BUILDER, CPU], + ]; + assert_eq!(all, expected); } #[test] - pub fn test_variants_no_duplicates() { - let generate = Generate { - values: Vec::new(), - ..Default::default() - }; - let variants = generate.variants(); + pub fn variants_cross_product_test() { + variants_cross_product(test_template()); + } + + #[test] + pub fn variants_cross_product_repo() { + variants_cross_product(Template::graphics().unwrap()); + } + + pub fn variants_cross_product(template: Template) { + let variants = template.variants(std::iter::empty()).unwrap(); for variant in variants { - for value in variant.iter().copied() { - let value_count = variant.iter().copied().filter(|o| *o == value).count(); + for value in variant.iter() { + let value_count = variant.iter().filter(|o| Define::eq(o, value)).count(); assert_eq!( value_count, 1, "Variant `{variant:?}` contains value `{value}` more than once!" @@ -273,23 +343,26 @@ mod tests { } #[test] - pub fn test_variants_filter() { - let v_all = Generate { - values: Vec::new(), - ..Default::default() - } - .variants(); - for (p, values) in placeholder_map() { + pub fn variants_filter_test() { + variants_filter(test_template()) + } + + #[test] + pub fn variants_filter_repo() { + variants_filter(Template::graphics().unwrap()) + } + + pub fn variants_filter(template: Template) { + let v_all = template.variants(std::iter::empty()).unwrap(); + for (p, values) in &template.placeholders { debug!("{p}: {values:?}"); assert_eq!(v_all.len() % values.len(), 0); let each_type_count = v_all.len() / values.len(); for i in 1..values.len() { - let v_one = Generate { - values: Vec::from(&values[..i]), - ..Default::default() - } - .variants(); + let v_one = template + .variants(values[..i].iter().map(|s| s.as_str())) + .unwrap(); assert_eq!(v_one.len(), each_type_count * i); } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index c4460f3..da952cb 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -2,6 +2,7 @@ use crate::generate::Generate; use clap::Parser; use log::debug; +pub mod cargo_generate_config; mod generate; #[derive(Parser, Debug)] From e2a33e962602e6064ff31e1a59a986c39e60f6ab Mon Sep 17 00:00:00 2001 From: firestar99 Date: Wed, 3 Dec 2025 15:10:50 +0100 Subject: [PATCH 2/4] generate: template discovery --- xtask/src/generate.rs | 145 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 124 insertions(+), 21 deletions(-) diff --git a/xtask/src/generate.rs b/xtask/src/generate.rs index 151b0f3..3ca7cba 100644 --- a/xtask/src/generate.rs +++ b/xtask/src/generate.rs @@ -1,15 +1,13 @@ use crate::cargo_generate_config::{CONFIG_FILE_NAME, Config}; -use anyhow::{Context, anyhow, bail}; +use anyhow::{Context, bail}; use cargo_generate::GenerateArgs; use clap::Parser; use indexmap::IndexMap; use log::{debug, info}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; use std::path::{Path, PathBuf}; -pub const TEMPLATE_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../graphics"); - #[derive(Parser, Debug, Default)] pub struct Generate { /// Directory where to place the generated templates. @@ -31,6 +29,64 @@ pub struct Generate { filter: Vec, } +#[derive(Clone, Debug)] +struct TemplateDiscovery { + templates: Vec