From 36e20a45aab2f7d211a1d3fa5d71482413f71442 Mon Sep 17 00:00:00 2001 From: Techassi Date: Wed, 19 Aug 2026 23:55:40 +0200 Subject: [PATCH 1/2] feat(boil): Add circular local image dependency detection --- rust/boil/src/core/bakefile.rs | 48 ++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/rust/boil/src/core/bakefile.rs b/rust/boil/src/core/bakefile.rs index da33286b9..aa4cfc493 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,12 @@ 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 target node once we are done with the current image name + image + // version combination. + chain.pop(); } } From 1cbb02fed4b00046f4078c54451eab83141d3e9e Mon Sep 17 00:00:00 2001 From: Techassi Date: Thu, 20 Aug 2026 08:52:44 +0200 Subject: [PATCH 2/2] docs(boil): Improve dev comment about chain.pop() --- rust/boil/src/core/bakefile.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rust/boil/src/core/bakefile.rs b/rust/boil/src/core/bakefile.rs index aa4cfc493..9d7588cd0 100644 --- a/rust/boil/src/core/bakefile.rs +++ b/rust/boil/src/core/bakefile.rs @@ -284,8 +284,15 @@ impl Targets { self.insert_targets(image_name.clone(), image_config, options, false, chain)?; } - // Remove the last target node once we are done with the current image name + image - // version combination. + // 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(); } }