summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Beyer <matthias.beyer@atos.net>2021-09-16 12:38:50 +0200
committerMatthias Beyer <matthias.beyer@atos.net>2021-09-16 12:38:50 +0200
commit555734ea066d11d0b3efb96bff84563847f0757d (patch)
tree6d46f8b8fd0c4c563f401ca326fd425c9a8af53b
parent63566cccba0c2c42af2e38d7ec0a922a60cf128d (diff)
parent1ad7f9d3a2e783a2392b5362e34930032cdffdb1 (diff)
Merge branch 'dependency-if'
-rw-r--r--Cargo.toml3
-rw-r--r--src/cli.rs29
-rw-r--r--src/commands/build.rs8
-rw-r--r--src/commands/tree_of.rs21
-rw-r--r--src/package/dag.rs250
-rw-r--r--src/package/dependency/build.rs148
-rw-r--r--src/package/dependency/condition.rs420
-rw-r--r--src/package/dependency/mod.rs2
-rw-r--r--src/package/dependency/runtime.rs193
-rw-r--r--src/package/package.rs25
-rw-r--r--src/util/docker.rs7
11 files changed, 1049 insertions, 57 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 71ff5e6..3332ce7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
+
diff --git a/src/cli.rs b/src/cli.rs
index dbafd91..bc14341 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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(