diff --git a/rust/boil/src/core/bakefile.rs b/rust/boil/src/core/bakefile.rs index da33286b9..9d7588cd0 100644 --- a/rust/boil/src/core/bakefile.rs +++ b/rust/boil/src/core/bakefile.rs @@ -86,6 +86,12 @@ pub enum TargetsError { versions: Vec, image_name: String, }, + + #[snafu(display( + "failed to resolve local image chain. Circular dependency found: {}", + chain.iter().map(|(n, v)| format!("{n}={v}")).collect::>().join(" -> ") + ))] + CircularDependency { chain: Vec<(String, String)> }, } #[derive(Debug, Default)] @@ -169,7 +175,13 @@ impl Targets { .to_string_lossy() .into_owned(); - targets.insert_targets(image_name.to_owned(), image_config, &options, true)?; + targets.insert_targets( + image_name.to_owned(), + image_config, + &options, + true, + &mut Vec::new(), + )?; } Ok(targets) @@ -205,7 +217,13 @@ impl Targets { } ); - targets.insert_targets(image.name.clone(), image_config, &options, true)?; + targets.insert_targets( + image.name.clone(), + image_config, + &options, + true, + &mut Vec::new(), + )?; } Ok(targets) @@ -217,10 +235,26 @@ impl Targets { config: ImageConfig, options: &TargetsOptions, is_entry: bool, + chain: &mut Vec<(String, String)>, ) -> Result<(), TargetsError> { - for image_options in (*config.versions).values() { + for (version, image_options) in (*config.versions).iter() { if !options.only_entry { - // TODO (@Techassi): Add cycle detection + let dependency = (image_name.clone(), version.clone()); + + // If the current image name and image version combination is already in the current + // dependency chain, we hit a circular dependency and abort immediately. + ensure!( + !chain.contains(&dependency), + CircularDependencySnafu { + chain: chain + .iter() + .cloned() + .chain(std::iter::once(dependency)) + .collect::>(), + } + ); + chain.push(dependency); + for (image_name, image_version) in &image_options.local_images { if self .get(image_name) @@ -247,8 +281,19 @@ impl Targets { ); // Wowzers, recursion! - self.insert_targets(image_name.clone(), image_config, options, false)?; + self.insert_targets(image_name.clone(), image_config, options, false, chain)?; } + + // Remove the last dependency as soon as we are done looking at that particular + // dependency (name+version combination). We do this because we are not decending + // down the chain for this particular dependency anymore, but instead move to the + // next dependency at the same level of depth. Illustration: + // + // foo + // bar + // (no further deps, pop "bar") + // baz + chain.pop(); } }