diff --git a/anda-config/config.rs b/anda-config/config.rs index 5c86bcc..b4cd831 100644 --- a/anda-config/config.rs +++ b/anda-config/config.rs @@ -196,18 +196,20 @@ pub struct Docker { pub image: BTreeMap, // tag, file } +pub fn parse_kv(input: &str) -> impl Iterator> + '_ { + input + .split(',') + .filter(|item| !item.trim().is_empty()) + .map(|item| item.split_once('=').map(|(l, r)| (l.to_owned(), r.to_owned()))) +} + +pub fn parse_filters(filters: &[String]) -> Option>> { + filters.iter().map(std::ops::Deref::deref).map(crate::parse_kv).map(Iterator::collect).collect() +} + /// Turn a string into a BTreeMap -#[must_use] -pub fn parse_map(input: &str) -> Option> { - let mut map = BTreeMap::new(); - for item in input.split(',') { - if item.trim().is_empty() { - continue; - } - let (k, v) = item.split_once('=')?; - map.insert(k.to_owned(), v.to_owned()); - } - Some(map) +pub fn parse_labels<'a, I: Iterator>(labels: I) -> Option> { + labels.flat_map(parse_kv).collect() } #[derive(Deserialize, PartialEq, Eq, Serialize, Debug, Clone, Default)] @@ -425,17 +427,17 @@ mod test_parser { #[test] fn test_map() { - let m: BTreeMap = [("foo".to_owned(), "bar".to_owned())].into(); + let m = [("foo".to_owned(), "bar".to_owned())].into(); - assert_eq!(parse_map("foo=bar"), Some(m)); + assert_eq!(parse_labels(std::iter::once("foo=bar".into())), Some(m)); - let multieq: BTreeMap = [("foo".to_owned(), "bar=baz".to_owned())].into(); + let multieq = [("foo".to_owned(), "bar=baz".to_owned())].into(); - assert_eq!(parse_map("foo=bar=baz"), Some(multieq)); + assert_eq!(parse_labels(std::iter::once("foo=bar=baz".into())), Some(multieq)); - let multi: BTreeMap = + let multi = [("foo".to_owned(), "bar".to_owned()), ("baz".to_owned(), "qux".to_owned())].into(); - assert_eq!(parse_map("foo=bar,baz=qux"), Some(multi)); + assert_eq!(parse_labels(std::iter::once("foo=bar,baz=qux".into())), Some(multi)); } } diff --git a/andax/lib.rs b/andax/lib.rs index 00d5c48..9429681 100644 --- a/andax/lib.rs +++ b/andax/lib.rs @@ -9,9 +9,8 @@ mod fns; mod run; pub use fns::rpm::RPMSpec; -pub use rhai::Map; -pub use run::errhdl; -pub use run::run; +pub use rhai::{self, Map}; +pub use run::{errhdl, run}; /// The usual Error type returned by the Rhai engine. /// Alias for `Box`. diff --git a/andax/run.rs b/andax/run.rs index 26e3142..e39227b 100644 --- a/andax/run.rs +++ b/andax/run.rs @@ -15,7 +15,7 @@ use rhai::{ Engine, EvalAltResult as RhaiE, NativeCallContext as Ctx, Scope, }; use std::fmt::Write; -use std::{collections::BTreeMap, io::BufRead, path::Path}; +use std::{io::BufRead, path::Path}; use tracing::{debug, error, instrument, trace, warn}; /// # Errors @@ -207,18 +207,21 @@ pub fn errhdl(name: &str, scr: &Path, err: EvalAltResult) { } /// Executes an AndaX script. -pub fn run<'a, F: FnOnce(&mut Scope<'a>)>( +pub fn run< + 'a, + F: FnOnce(&mut Scope<'a>), + K: Into, + V: Into, + L: Iterator, +>( name: &'a str, scr: &'a Path, - labels: BTreeMap, + labels: L, f: F, ) -> Option> { let (en, mut sc) = gen_en(); f(&mut sc); - let mut lbls = rhai::Map::new(); - for (k, v) in labels { - lbls.insert(k.into(), v.into()); - } + let lbls: rhai::Map = labels.map(|(k, v)| (k.into(), v.into())).collect(); sc.push("labels", lbls); exec(name, scr, sc, en) } diff --git a/src/builder.rs b/src/builder.rs index d19101b..ea2b891 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -126,7 +126,7 @@ macro_rules! script { let sc = andax::run( $name, &$scr, - std::collections::BTreeMap::new(), + std::iter::empty::<(String, String)>(), |_sc| { $( _sc.push(stringify!($var), $var); )* }, @@ -409,7 +409,7 @@ async fn build_all( .map(|p| p.to_string_lossy().to_string()) .collect::>() .as_slice(), - std::mem::take(&mut project.labels), + project.labels.iter().map(|(a, b)| (a.clone(), b.clone())).collect(), )?; }; Ok(()) diff --git a/src/cli.rs b/src/cli.rs index 8f32e87..76e3b20 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -191,10 +191,31 @@ pub enum Command { Update { /// Labels to pass to the scripts #[clap(short, long)] - labels: Option, + labels: Vec, /// Only run update scripts in project with the specified labels + /// + /// This should be a comma-separated list of filters. + /// Each time `--filters=...` is specified, the comma-separated list of key-values will be + /// checked against a project. If missing or different, the project will be ignored. + /// However, specifying `--filters` multiple times will create an "or" effect --- the + /// project will not be ignored if it satisfies one of the list of `--filters`. For + /// example, `-f a=1,b=2 -f c=3` means the project needs to satisfy either "a=1" and "b=2", + /// or only "c=3". + #[clap(short, long)] + filters: Vec, + /// Exclude update scripts in project with the specified labels + /// + /// This should be a comma-separated list of excludes. + /// Each time `--exclude=...` is specified, the comma-separated list of key-values will be + /// checked against the labels of a project, and it will be ignored if all the key-values + /// are present. In addition, specifying `--exclude` multiple times will create an "or" + /// effect --- a project will be excluded if it satisfies one of the list of `--filters`. + /// For example, `-e a=1,b=2 -e c=3` means projects with "a=1" and "b=2" at the same time, + /// or "c=3", are excluded. Projects with only "a=1" or "b=2" are not excluded. + /// + /// This will always override `--filters`. #[clap(short, long)] - filters: Option, + excludes: Vec, }, /// Run .rhai scripts diff --git a/src/main.rs b/src/main.rs index cfbd768..d0fa306 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,12 +10,12 @@ mod oci; mod rpm_spec; mod update; mod util; -use anda_config::parse_map; +use anda_config::parse_labels; use clap::{CommandFactory, Parser}; use clap_complete::generate; use cli::{Cli, Command}; use color_eyre::{eyre::eyre, Result}; -use std::{collections::BTreeMap, io, mem::take}; +use std::{io, mem::take}; use tracing::{debug, trace}; #[allow(clippy::unwrap_in_result)] @@ -102,24 +102,21 @@ async fn main() -> Result<()> { println!("build_matrix={}", serde_json::to_string(&entries)?); } - Command::Update { labels, filters } => { - let labels = parse_map(&labels.unwrap_or_default()); - let filters = parse_map(&filters.unwrap_or_default()); - update::update( - anda_config::load_from_file(&cli.config)?, - labels.ok_or_else(|| eyre!("Cannot parse --labels"))?, - filters.ok_or_else(|| eyre!("Cannot parse --labels"))?, - )?; + Command::Update { labels, filters, excludes } => { + let labels = parse_labels(labels.iter().map(std::ops::Deref::deref)) + .ok_or_else(|| eyre!("Cannot parse --labels"))?; + let filters = anda_config::parse_filters(&filters) + .ok_or_else(|| eyre!("Cannot parse --filters"))?; + let excludes = anda_config::parse_filters(&excludes) + .ok_or_else(|| eyre!("Cannot parse --excludes"))?; + update::update(anda_config::load_from_file(&cli.config)?, labels, filters, excludes)?; } Command::Run { scripts, labels } => { if scripts.is_empty() { return Err(eyre!("No scripts to run")); } - let labels = if let Some(lbls) = labels { - parse_map(&lbls).ok_or_else(|| eyre!("Cannot parse --labels"))? - } else { - BTreeMap::new() - }; + let labels = parse_labels(labels.iter().map(std::ops::Deref::deref)) + .ok_or_else(|| eyre!("Cannot parse --labels"))?; update::run_scripts(&scripts, labels)?; } } diff --git a/src/update.rs b/src/update.rs index f4a8561..aa6e607 100644 --- a/src/update.rs +++ b/src/update.rs @@ -10,16 +10,22 @@ use std::{ use tracing::{debug, error, instrument, trace}; /// Return true only if the project `lbls` does not have the key or the value does not match. -fn filter_project(lbls: &BTreeMap) -> impl FnMut((&String, &String)) -> bool + '_ { +fn filter_project(lbls: &BTreeMap) -> impl Fn(&(String, String)) -> bool + '_ { |(k, v)| lbls.get(k).map_or(true, |val| val != v) } +/// Return true only if `lbls` have the key and the value matches. +fn exclude_project(lbls: &BTreeMap) -> impl Fn(&(String, String)) -> bool + '_ { + |(k, v)| lbls.get(k).is_some_and(|val| val == v) +} + #[allow(clippy::arithmetic_side_effects)] #[instrument(skip(cfg))] pub fn update( cfg: Manifest, - global_lbls: BTreeMap, - fls: BTreeMap, + global_lbls: Vec<(String, String)>, + fls: Vec>, + excls: Vec>, ) -> Result<()> { let mut handlers = vec![]; let proj_len = cfg.project.len(); @@ -29,7 +35,10 @@ pub fn update( scr_len += 1; let mut lbls = std::mem::take(&mut proj.labels); lbls.extend(global_lbls.clone()); - if fls.iter().any(filter_project(&lbls)) { + if fls.iter().all(|fls| fls.iter().any(filter_project(&lbls))) { + continue; + } + if excls.iter().any(|excls| excls.iter().all(exclude_project(&lbls))) { continue; } trace!(name, scr = scr.to_str(), "Th start"); @@ -39,10 +48,12 @@ pub fn update( let th = thread::current(); let name = th.name().expect("No name for andax thread??"); let start = std::time::Instant::now(); - let sc = run(name, &scr, lbls, |sc| { + let sc = run(name, &scr, lbls.iter(), |sc| { // we have to do it here as `Dynamic` in andax::Map nu Sync impl - let filters = - fls.into_iter().map(|(k, v)| (k.into(), v.into())).collect::(); + let filters = fls + .into_iter() + .flat_map(|fls| fls.into_iter().map(|(k, v)| (k.into(), v.into()))) + .collect::(); sc.push("filters", filters); if let Some(rpm) = &proj.rpm { sc.push("rpm", RPMSpec::new(name.to_owned(), &scr, &rpm.spec)); @@ -113,7 +124,7 @@ pub fn update( } #[instrument] -pub fn run_scripts(scripts: &[String], labels: BTreeMap) -> Result<()> { +pub fn run_scripts(scripts: &[String], labels: Vec<(String, String)>) -> Result<()> { let mut handlers = vec![]; for scr in scripts { trace!(scr, "Th start"); @@ -121,7 +132,7 @@ pub fn run_scripts(scripts: &[String], labels: BTreeMap) -> Resu handlers.push(Builder::new().name(scr.to_owned()).spawn(move || { let th = thread::current(); let name = th.name().expect("No name for andax thread??"); - run(name, &std::path::PathBuf::from(name), labels, |_| {}); + run(name, &std::path::PathBuf::from(name), labels.into_iter(), |_| {}); })?); } @@ -152,10 +163,10 @@ mod tests { let lbls = std::iter::once(("nightly", "1")).map(|(l, r)| (l.into(), r.into())).collect(); let mut test1 = filter_project(&lbls); for (k, v) in transform(&[("nightly", "0"), ("hai", "bai"), ("large", "1")]) { - assert!(test1((&k, &v))); + assert!(test1(&(k, v))); } for (k, v) in transform(&[("nightly", "1")]) { - assert!(!test1((&k, &v))); + assert!(!test1(&(k, v))); } } }