diff options
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | src/cli.rs | 29 | ||||
-rw-r--r-- | src/commands/build.rs | 8 | ||||
-rw-r--r-- | src/commands/tree_of.rs | 21 | ||||
-rw-r--r-- | src/package/dag.rs | 250 | ||||
-rw-r--r-- | src/package/dependency/build.rs | 148 | ||||
-rw-r--r-- | src/package/dependency/condition.rs | 420 | ||||
-rw-r--r-- | src/package/dependency/mod.rs | 2 | ||||
-rw-r--r-- | src/package/dependency/runtime.rs | 193 | ||||
-rw-r--r-- | src/package/package.rs | 25 | ||||
-rw-r--r-- | src/util/docker.rs | 7 |
11 files changed, 1049 insertions, 57 deletions
@@ -100,3 +100,6 @@ funty = "=1.1.0" # the pin here, we enforce the build to not use 1.4.0 or newer. zeroize = ">=1.3.0, <1.4.0" +[dev-dependencies] +toml = "0.5" + @@ -1061,6 +1061,35 @@ pub fn cli<'a>() -> App<'a> { .value_name("VERSION_CONSTRAINT") .about("A version constraint to search for (optional), E.G. '=1.0.0'") ) + .arg(Arg::new("image") + .required(false) + .multiple(false) + .takes_value(true) + .value_name("IMAGE NAME") + .short('I') + .long("image") + .about("Name of the docker image to use") + .long_about(indoc::indoc!(r#" + Name of the docker image to use. + + Required because tree might look different on different images because of + conditions on dependencies. + "#)) + ) + .arg(Arg::new("env") + .required(false) + .multiple(true) + .short('E') + .long("env") + .validator(env_pass_validator) + .about("Additional env to be passed when building packages") + .long_about(indoc::indoc!(r#" + Additional env to be passed when building packages. + + Required because tree might look different on different images because of + conditions on dependencies. + "#)) + ) ) .subcommand(App::new("metrics") diff --git a/src/commands/build.rs b/src/commands/build.rs index d0e473b..8148a33 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -43,6 +43,7 @@ use crate::package::Dag; use crate::package::PackageName; use crate::package::PackageVersion; use crate::package::Shebang; +use crate::package::condition::ConditionData; use crate::repository::Repository; use crate::schema; use crate::source::SourceCache; @@ -226,7 +227,12 @@ pub async fn build( let dag = { let bar_tree_building = progressbars.bar(); - let dag = Dag::for_root_package(package.clone(), &repo, Some(&bar_tree_building))?; + let condition_data = ConditionData { + image_name: Some(&image_name), + env: &additional_env, + }; + + let dag = Dag::for_root_package(package.clone(), &repo, Some(&bar_tree_building), &condition_data)?; bar_tree_building.finish_with_message("Finished loading Dag"); dag }; diff --git a/src/commands/tree_of.rs b/src/commands/tree_of.rs index 6b297e3..811b2b7 100644 --- a/src/commands/tree_of.rs +++ b/src/commands/tree_of.rs @@ -20,7 +20,10 @@ use resiter::AndThen; use crate::package::Dag; use crate::package::PackageName; use crate::package::PackageVersionConstraint; +use crate::package::condition::ConditionData; use crate::repository::Repository; +use crate::util::EnvironmentVariableName; +use crate::util::docker::ImageName; /// Implementation of the "tree_of" subcommand pub async fn tree_of( @@ -36,6 +39,22 @@ pub async fn tree_of( .map(PackageVersionConstraint::try_from) .transpose()?; + let image_name = matches + .value_of("image") + .map(String::from) + .map(ImageName::from); + + let additional_env = matches + .values_of("env") + .unwrap_or_default() + .map(crate::util::env::parse_to_env) + .collect::<Result<Vec<(EnvironmentVariableName, String)>>>()?; + + let condition_data = ConditionData { + image_name: image_name.as_ref(), + env: &additional_env, + }; + repo.packages() .filter(|p| pname.as_ref().map(|n| p.name() == n).unwrap_or(true)) .filter(|p| { @@ -44,7 +63,7 @@ pub async fn tree_of( .map(|v| v.matches(p.version())) .unwrap_or(true) }) - .map(|package| Dag::for_root_package(package.clone(), &repo, None)) + .map(|package| Dag::for_root_package(package.clone(), &repo, None, &condition_data)) .and_then_ok(|tree| { let stdout = std::io::stdout(); let mut outlock = stdout.lock(); diff --git a/src/package/dag.rs b/src/package/dag.rs index aea416c..c805121 100644 --- a/src/package/dag.rs +++ b/src/package/dag.rs @@ -17,16 +17,23 @@ use anyhow::Error; use anyhow::Result; use anyhow::anyhow; use daggy::Walker; +use getset::Getters; use indicatif::ProgressBar; +use itertools::Itertools; use log::trace; use ptree::Style; use ptree::TreeItem; use resiter::AndThen; -use getset::Getters; use crate::package::Package; +use crate::package::PackageName; +use crate::package::PackageVersionConstraint; +use crate::package::condition::ConditionCheckable; +use crate::package::condition::ConditionData; +use crate::package::dependency::ParseDependency; use crate::repository::Repository; + #[derive(Debug, Getters)] pub struct Dag { #[getset(get = "pub")] @@ -41,15 +48,69 @@ impl Dag { p: Package, repo: &Repository, progress: Option<&ProgressBar>, + conditional_data: &ConditionData<'_>, // required for selecting packages with conditional dependencies ) -> Result<Self> { + + /// helper fn with bad name to check the dependency condition of a dependency and parse the dependency into a tuple of + /// name and version for further processing + fn process<D: ConditionCheckable + ParseDependency>(d: &D, conditional_data: &ConditionData<'_>) + -> Result<(bool, PackageName, PackageVersionConstraint)> + { + // Check whether the condition of the dependency matches our data + let take = d.check_condition(conditional_data)?; + let (name, version) = d.parse_as_name_and_version()?; + + // (dependency check result, name of the dependency, version of the dependency) + Ok((take, name, version)) + } + + /// Helper fn to get the dependencies of a package + /// + /// This function helps getting the dependencies of a package as an iterator over + /// (Name, Version). + /// + /// It also filters out dependencies that do not match the `conditional_data` passed and + /// makes the dependencies unique over (name, version). + fn get_package_dependencies<'a>(package: &'a Package, conditional_data: &'a ConditionData<'_>) + -> impl Iterator<Item = Result<(PackageName, PackageVersionConstraint)>> + 'a + { + + package.dependencies() + .build() + .iter() + .map(move |d| process(d, conditional_data)) + .chain({ + package.dependencies() + .runtime() + .iter() + .map(move |d| process(d, conditional_data)) + }) + + // Now filter out all dependencies where their condition did not match our + // `conditional_data`. + .filter(|res| match res { + Ok((true, _, _)) => true, + Ok((false, _, _)) => false, + Err(_) => true, + }) + + // Map out the boolean from the condition, because we don't need that later on + .map(|res| res.map(|(_, name, vers)| (name, vers))) + + // Make all dependencies unique, because we don't want to build one dependency + // multiple times + .unique_by(|res| res.as_ref().ok().cloned()) + } + fn add_sub_packages<'a>( repo: &'a Repository, mappings: &mut HashMap<&'a Package, daggy::NodeIndex>, dag: &mut daggy::Dag<&'a Package, i8>, p: &'a Package, - progress: Option<&ProgressBar> + progress: Option<&ProgressBar>, + conditional_data: &ConditionData<'_>, ) -> Result<()> { - p.get_self_packaged_dependencies() + get_package_dependencies(p, conditional_data) .and_then_ok(|(name, constr)| { trace!("Dependency for {} {} found: {:?}", p.name(), p.version(), name); let packs = repo.find_with_version(&name, &constr); @@ -69,7 +130,7 @@ impl Dag { mappings.insert(p, idx); trace!("Recursing for: {:?}", p); - add_sub_packages(repo, mappings, dag, p, progress) + add_sub_packages(repo, mappings, dag, p, progress, conditional_data) }) } else { Ok(()) @@ -78,9 +139,13 @@ impl Dag { .collect::<Result<()>>() } - fn add_edges(mappings: &HashMap<&Package, daggy::NodeIndex>, dag: &mut daggy::Dag<&Package, i8>) -> Result<()> { + fn add_edges(mappings: &HashMap<&Package, daggy::NodeIndex>, + dag: &mut daggy::Dag<&Package, i8>, + conditional_data: &ConditionData<'_>, + ) -> Result<()> + { for (package, idx) in mappings { - package.get_self_packaged_dependencies() + get_package_dependencies(package, conditional_data) .and_then_ok(|(name, constr)| { mappings .iter() @@ -103,8 +168,8 @@ impl Dag { trace!("Making package Tree for {:?}", p); let root_idx = dag.add_node(&p); mappings.insert(&p, root_idx); - add_sub_packages(repo, &mut mappings, &mut dag, &p, progress)?; - add_edges(&mappings, &mut dag)?; + add_sub_packages(repo, &mut mappings, &mut dag, &p, progress, conditional_data)?; + add_edges(&mappings, &mut dag, conditional_data)?; trace!("Finished makeing package Tree"); Ok(Dag { @@ -159,11 +224,14 @@ mod tests { use std::collections::BTreeMap; + use crate::package::Dependencies; + use crate::package::Dependency; + use crate::package::condition::Condition; + use crate::package::condition::OneOrMore; use crate::package::tests::package; use crate::package::tests::pname; use crate::package::tests::pversion; - use crate::package::Dependencies; - use crate::package::Dependency; + use crate::util::docker::ImageName; use indicatif::ProgressBar; @@ -182,7 +250,13 @@ mod tests { let repo = Repository::from(btree); let progress = ProgressBar::hidden(); - let r = Dag::for_root_package(p1, &repo, Some(&progress)); + let condition_data = ConditionData { + image_name: None, + env: &[], + }; + + let r = Dag::for_root_package(p1, &repo, Some(&progress), &condition_data); + assert!(r.is_ok()); } @@ -214,7 +288,12 @@ mod tests { let repo = Repository::from(btree); let progress = ProgressBar::hidden(); - let dag = Dag::for_root_package(p1, &repo, Some(&progress)); + let condition_data = ConditionData { + image_name: None, + env: &[], + }; + + let dag = Dag::for_root_package(p1, &repo, Some(&progress), &condition_data); assert!(dag.is_ok()); let dag = dag.unwrap(); let ps = dag.all_packages(); @@ -303,7 +382,12 @@ mod tests { let repo = Repository::from(btree); let progress = ProgressBar::hidden(); - let r = Dag::for_root_package(p1, &repo, Some(&progress)); + let condition_data = ConditionData { + image_name: None, + env: &[], + }; + + let r = Dag::for_root_package(p1, &repo, Some(&progress), &condition_data); assert!(r.is_ok()); let r = r.unwrap(); let ps = r.all_packages(); @@ -446,7 +530,12 @@ mod tests { let repo = Repository::from(btree); let progress = ProgressBar::hidden(); - let r = Dag::for_root_package(p1, &repo, Some(&progress)); + let condition_data = ConditionData { + image_name: None, + env: &[], + }; + + let r = Dag::for_root_package(p1, &repo, Some(&progress), &condition_data); assert!(r.is_ok()); let r = r.unwrap(); let ps = r.all_packages(); @@ -551,7 +640,12 @@ mod tests { let repo = Repository::from(btree); let progress = ProgressBar::hidden(); - let r = Dag::for_root_package(p1, &repo, Some(&progress)); + let condition_data = ConditionData { + image_name: None, + env: &[], + }; + + let r = Dag::for_root_package(p1, &repo, Some(&progress), &condition_data); assert!(r.is_ok()); let r = r.unwrap(); let ps = r.all_packages(); @@ -560,5 +654,131 @@ mod tests { assert!(ps.iter().any(|p| *p.name() == pname("p3"))); assert!(ps.iter().any(|p| *p.name() == pname("p4"))); } + + + /// Build a repository with two packages and a condition for their dependency + fn repo_with_ab_packages_with_condition(cond: Condition) -> (Package, Repository) { + let mut btree = BTreeMap::new(); + + let mut p1 = { + let name = "a"; + let vers = "1"; + let pack = package(name, vers, "https://rust-lang.org", "123"); + btree.insert((pname(name), pversion(vers)), pack.clone()); + pack + }; + + { + let name = "b"; + let vers = "2"; + let pack = package(name, vers, "https://rust-lang.org", "124"); + btree.insert((pname(name), pversion(vers)), pack); + } + + { + let d = Dependency::new_conditional(String::from("b =2"), cond); + let ds = Dependencies::with_runtime_dependency(d); + p1.set_dependencies(ds); + } + + (p1, Repository::from(btree)) + } + + // Test whether the dependency DAG is correctly build if there is NO conditional data passed + // + // Because the dependency is conditional with "fooimage" required as build-image, the + // dependency DAG should NOT contain package "b" + #[test] + fn test_add_two_dependent_packages_with_image_conditional() { + let condition = { + let in_image = Some(OneOrMore::<String>::One(String::from("fooimage"))); + Condition::new(None, None, in_image) + }; + let (p1, repo) = repo_with_ab_packages_with_condition(condition); + + let condition_data = ConditionData { + image_name: None, + env: &[], + }; + + let progress = ProgressBar::hidden(); + + let dag = Dag::for_root_package(p1, &repo, Some(&progress), &condition_data); + assert!(dag.is_ok()); + let dag = dag.unwrap(); + let ps = dag.all_packages(); + + assert!(ps.iter().any(|p| *p.name() == pname("a"))); + assert!(ps.iter().any(|p| *p.version() == pversion("1"))); + + // Not in the tree: + assert!(!ps.iter().any(|p| *p.name() == pname("b")), "'b' should not be in tree, but is: {:?}", ps); + assert!(!ps.iter().any(|p| *p.version() == pversion("2")), "'2' should not be in tree, but is: {:?}", ps); + } + + // Test whether the dependency DAG is correctly build if a image is used, but not the one + // required + // + // Because the dependency is conditional with "fooimage" required as build-image, but + // "barimage" is used, the dependency DAG should NOT contain package "b" + #[test] + fn test_add_two_dependent_packages_with_image_conditional_but_other_image_provided() { + let condition = { + let in_image = Some(OneOrMore::<String>::One(String::from("fooimage"))); + Condition::new(None, None, in_image) + }; + let (p1, repo) = repo_with_ab_packages_with_condition(condition); + + let img_name = ImageName::from("barimage"); + let condition_data = ConditionData { + image_name: Some(&img_name), + env: &[], + }; + + let progress = ProgressBar::hidden(); + + let dag = Dag::for_root_package(p1, &repo, Some(&progress), &condition_data); + assert!(dag.is_ok()); + let dag = dag.unwrap(); + let ps = dag.all_packages(); + + assert!(ps.iter().any(|p| *p.name() == pname("a"))); + assert!(ps.iter().any(|p| *p.version() == pversion("1"))); + + // Not in the tree: + assert!(!ps.iter().any(|p| *p.name() == pname("b"))); + assert!(!ps.iter().any(|p| *p.version() == pversion("2"))); + } + + // Test whether the dependency DAG is correctly build if the right image name is passed + #[test] + fn test_add_two_dependent_packages_with_image_conditional_and_image_provided() { + let condition = { + let in_image = Some(OneOrMore::<String>::One(String::from("fooimage"))); + Condition::new(None, None, in_image) + }; + let (p1, repo) = repo_with_ab_packages_with_condition(condition); + + let img_name = ImageName::from("fooimage"); + let condition_data = ConditionData { + image_name: Some(&img_name), + env: &[], + }; + + let progress = ProgressBar::hidden(); + + let dag = Dag::for_root_package(p1, &repo, Some(&progress), &condition_data); + assert!(dag.is_ok()); + let dag = dag.unwrap(); + let ps = dag.all_packages(); + + assert!(ps.iter().any(|p| *p.name() == pname("a"))); + assert!(ps.iter().any(|p| *p.version() == pversion("1"))); + + // IN the tree: + assert!(ps.iter().any(|p| *p.name() == pname("b"))); + assert!(ps.iter().any(|p| *p.version() == pversion("2"))); + } + } diff --git a/src/package/dependency/build.rs b/src/package/dependency/build.rs index 8eb8eb3..4d51335 100644 --- a/src/package/dependency/build.rs +++ b/src/package/dependency/build.rs @@ -12,30 +12,164 @@ use anyhow::Result; use serde::Deserialize; use serde::Serialize; -use crate::package::dependency::ParseDependency; -use crate::package::dependency::StringEqual; use crate::package::PackageName; use crate::package::PackageVersionConstraint; +use crate::package::dependency::ParseDependency; +use crate::package::dependency::StringEqual; +use crate::package::dependency::condition::Condition; /// A dependency that is packaged and is only required during build time #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] -#[serde(transparent)] -pub struct BuildDependency(String); +#[serde(untagged)] +pub enum BuildDependency { + Simple(String), + Conditional { + name: String, + condition: Condition, + }, +} impl AsRef<str> for BuildDependency { fn as_ref(&self) -> &str { - self.0.as_ref() + match self { + BuildDependency::Simple(name) => name, + BuildDependency::Conditional { name, .. } => name, + } } } impl StringEqual for BuildDependency { fn str_equal(&self, s: &str) -> bool { - self.0 == s + match self { + BuildDependency::Simple(name) => name == s, + BuildDependency::Conditional { name, .. } => name == s, + } } } impl ParseDependency for BuildDependency { fn parse_as_name_and_version(&self) -> Result<(PackageName, PackageVersionConstraint)> { - crate::package::dependency::parse_package_dependency_string_into_name_and_version(&self.0) + crate::package::dependency::parse_package_dependency_string_into_name_and_version(self.as_ref()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::package::dependency::condition::OneOrMore; + + #[derive(serde::Serialize, serde::Deserialize)] + #[allow(unused)] + pub struct TestSetting { + setting: BuildDependency, + } + + #[test] + fn test_parse_dependency() { + let s: TestSetting = toml::from_str(r#"setting = "foo""#).expect("Parsing TestSetting failed"); + match s.setting { + BuildDependency::Simple(name) => assert_eq!(name, "foo", "Expected 'foo', got {}", name), + other => panic!("Unexpected deserialization to other variant: {:?}", other), + } + } + + #[test] + fn test_parse_conditional_dependency() { + let s: TestSetting = toml::from_str(r#"setting = { name = "foo", condition = { in_image = "bar"} }"#).expect("Parsing TestSetting failed"); + match s.setting { + BuildDependency::Conditional { name, condition } => { + assert_eq!(name, "foo", "Expected 'foo', got {}", name); + assert_eq!(*condition.has_env(), None); + assert_eq!(*condition.env_eq(), None); + assert_eq!(condition.in_image().as_ref(), Some(&OneOrMore::<String>::One(String::from("bar")))); + }, + other => panic!("Unexpected deserialization to other variant: {:?}", other), + } + } + + #[test] + fn test_parse_conditional_dependency_pretty() { + let pretty = r#" + [setting] + name = "foo" + [setting.condition] + in_image = "bar" + "#; + + let s: TestSetting = toml::from_str(pretty).expect("Parsing TestSetting failed"); + + match s.setting { + BuildDependency::Conditional { name, condition } => { + assert_eq!(name, "foo", "Expected 'foo', got {}", name); + assert_eq!(*condition.has_env(), None); + assert_eq!(*condition.env_eq(), None); + assert_eq!(condition.in_image().as_ref(), Some(&OneOrMore::<String>::One(String::from("bar")))); + }, + other => panic!("Unexpected deserialization to other variant: {:?}", other), + } + } + + + #[derive(serde::Serialize, serde::Deserialize)] + #[allow(unused)] + pub struct TestSettings { + settings: Vec<BuildDependency>, + } + + #[test] + fn test_parse_conditional_dependencies() { + let s: TestSettings = toml::from_str(r#"settings = [{ name = "foo", condition = { in_image = "bar"} }]"#).expect("Parsing TestSetting failed"); + match s.settings.get(0).expect("Has not one dependency") { + BuildDependency::Conditional { name, condition } => { + assert_eq!(name, "foo", "Expected 'foo', got {}", name); + assert_eq!(*condition.has_env(), None); + assert_eq!(*condition.env_eq(), None); + assert_eq!(condition.in_image().as_ref(), Some(&OneOrMore::<String>::One(String::from("bar")))); + }, + other => panic!("Unexpected deserialization to other variant: {:?}", other), + } + } + + #[test] + fn test_parse_conditional_dependencies_pretty() { + let pretty = r#" + [[settings]] + name = "foo" + condition = { in_image = "bar" } + "#; + + let s: TestSettings = toml::from_str(pretty).expect("Parsing TestSetting failed"); + + match s.settings.get(0).expect("Has not one dependency") { + BuildDependency::Conditional { name, condition } => { + assert_eq!(name, "foo", "Expected 'foo', got {}", name); + assert_eq!(*condition.has_env(), None); + assert_eq!(*condition.env_eq(), None); + assert_eq!(condition.in_image().as_ref(), Some(&OneOrMore::<String>::One(String::from("bar")))); + }, + other => panic!("Unexpected deserialization to other variant: {:?}", other), + } + } + + #[test] + fn test_parse_conditional_dependencies_pretty_2() { + let pretty = r#" + [[settings]] + name = "foo" + condition.in_image = "bar" + "#; + + let s: TestSettings = toml::from_str(pretty).expect("Parsing TestSetting failed"); + + match s.settings.get(0).expect("Has not one dependency") { + BuildDependency::Conditional { name, condition } => { + assert_eq!(name, "foo", "Expected 'foo', got {}", name); + assert_eq!(*condition.has_env(), None); + assert_eq!(*condition.env_eq(), None); + assert_eq!(condition.in_image().as_ref(), Some(&OneOrMore::<String>::One(String::from("bar")))); + }, + other => panic!("Unexpected deserialization to other variant: {:?}", other), + } + } +} + diff --git a/src/package/dependency/condition.rs b/src/package/dependency/condition.rs new file mode 100644 index 0000000..c39ece6 --- /dev/null +++ b/src/package/dependency/condition.rs @@ -0,0 +1,420 @@ +// +// Copyright (c) 2020-2021 science+computing ag and other contributors +// +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// + +use std::collections::BTreeMap; + +use serde::Deserialize; +use serde::Serialize; +use getset::Getters; +use anyhow::Result; + +use crate::util::EnvironmentVariableName; +use crate::util::docker::ImageName; + +/// The Condition type +/// +/// This type represents a condition whether a dependency should be included in the package tree or +/// not. +/// +/// Right now, we are supporting condition by environment (set or equal) or whether a specific +/// build image is used. +/// All these settings are optional, of course. +/// +#[derive(Serialize, Deserialize, Getters, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct Condition { + #[serde(rename = "has_env", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + pub(super) has_env: Option<OneOrMore<EnvironmentVariableName>>, + + #[serde(rename = "env_eq", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + pub(super) env_eq: Option<BTreeMap<EnvironmentVariableName, String>>, + + #[serde(rename = "in_image", skip_serializing_if = "Option::is_none")] + #[getset(get = "pub")] + pub(super) in_image: Option<OneOrMore<String>>, +} + +impl Condition { + #[cfg(test)] + pub fn new(has_env: Option<OneOrMore<EnvironmentVariableName>>, + env_eq: Option<BTreeMap<EnvironmentVariableName, String>>, + in_image: Option<OneOrMore<String>>) + -> Self + { + Condition { has_env, env_eq, in_image } + } + + /// Check whether the condition matches a certain set of data + /// + /// # Return value + /// + /// Always returns Ok(_) in the current implementation + pub fn matches(&self, data: &ConditionData<'_>) -> Result<bool> { + if !self.matches_env_cond(data)? { + return Ok(false) + } + + if !self.matches_env_eq_cond(data)? { + return Ok(false) + } + + if !self.matches_in_image_cond(data)? { + return Ok(false) + } + + Ok(true) + } + + fn matches_env_cond(&self, data: &ConditionData<'_>) -> Result<bool> { + if let Some(has_env_cond) = self.has_env.as_ref() { + let b = match has_env_cond { + OneOrMore::One(env) => data.env.iter().any(|(name, _)| env == name), + OneOrMore::More(envs) => envs.iter().all(|required_env| { + data.env + .iter() + .any(|(name, _)| name == required_env) + }) + }; + + if !b { + return Ok(false) + } + } + + Ok(true) + } + + fn matches_env_eq_cond(&self, data: &ConditionData<'_>) -> Result<bool> { + if let Some(env_eq_cond) = self.env_eq.as_ref() { + let b = env_eq_cond.iter() + .all(|(req_env_name, req_env_val)| { + data.env + .iter() + .find(|(env_name, _)| env_name == req_env_name) + .map(|(_, env_val)| env_val == req_env_val) + .unwrap_or(false) + }); + + if !b { + return Ok(false) + } + } + + Ok(true) + } + + fn matches_in_image_cond(&self, data: &ConditionData<'_>) -> Result<bool> { + if let Some(in_image_cond) = self.in_image.as_ref() { + let b = match in_image_cond { + OneOrMore::One(req_image) => { + // because the image_name in the ConditionData is Option, + // which is a design-decision because the image can be not-specified (in the + // "tree-of" subcommand), + // we automatically use `false` as value here. + // + // That is because if we need to have a certain image (which is what this + // condition expresses), and there is no image specified in the ConditionData, + // we are by definition are NOT in this image. + data.image_name + .as_ref() + .map(|i| i.as_ref() == req_image) + .unwrap_or(false) + }, + OneOrMore::More(req_images) => { + req_images.iter() + .any(|ri| { + data.image_name + .as_ref() + .map(|inam| inam.as_ref() == ri) + .unwrap_or(false) + }) + }, + }; + + Ok(b) + } else { + Ok(true) + } + } +} + + +/// Helper type for supporting Vec<T> and T in value +/// position of Condition +#[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[serde(untagged)] +pub enum OneOrMore<T: Sized> { + One(T), + More(Vec<T>), +} + +#[allow(clippy::from_over_into)] +impl<T: Sized> Into<Vec<T>> for OneOrMore<T> { + fn into(self) -> Vec<T> { + match self { + OneOrMore::One(o) => vec![o], + OneOrMore::More(m) => m, + } + } +} + +#[cfg(test)] +impl From<Vec<String>> for OneOrMore<String> { + fn from(v: Vec<String>) -> Self { + OneOrMore::More(v) + } +} + +#[cfg(test)] +impl From<String> for OneOrMore<String> { + fn from(s: String) -> Self { + OneOrMore::One(s) + } +} + + +#[derive(Debug)] +pub struct ConditionData<'a> { + pub(crate) image_name: Option<&'a ImageName>, + pub(crate) env: &'a [(EnvironmentVariableName, String)], +} + +/// Trait for all things that have a condition that can be checked against ConditionData. +/// +/// To be implemented by dependency types. +/// +/// # Return value +/// +/// Ok(true) if the dependency is relevant, considering the ConditionData +/// Ok(false) if the dependency should be ignored, considering the ConditionData +/// Err(_) if the condition checking failed (see `Condition::matches`) +/// +pub trait ConditionCheckable { + fn check_condition(&self, data: &ConditionData<'_>) -> Result<bool>; +} + +impl ConditionCheckable for crate::package::BuildDependency { + fn check_condition(&self, data: &ConditionData<'_>) -> Result<bool> { + match self { + // If the dependency is a simple one, e.g. "foo =1.2.3", there is no condition, so the + // dependency has always to be used + crate::package::BuildDependency::Simple(_) => Ok(true), + crate::package::BuildDependency::Conditional { condition, .. } => condition.matches(data), + } + } +} + +impl ConditionCheckable for crate::package::Dependency { + fn check_condition(&self, data: &ConditionData<'_>) -> Result<bool> { + match self { + // If the dependency is a simple one, e.g. "foo =1.2.3", there is no condition, so the + // dependency has always to be used + crate::package::Dependency::Simple(_) => Ok(true), + crate::package::Dependency::Conditional { condition, .. } => condition.matches(data), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_has_env_deserialization() { + let s = r#"has_env = "foo""#; + let c: Condition = toml::from_str(s).expect("Deserializing has_env"); + + assert_eq!(c.has_env.unwrap(), OneOrMore::<EnvironmentVariableName>::One(EnvironmentVariableName::from("foo"))); + assert!(c.env_eq.is_none()); + assert!(c.in_image.is_none()); + } + + #[test] + fn test_has_env_list_deserialization() { + let s = r#"has_env = ["foo", "bar"]"#; + |