From 6fcdd773737fe03f9e379a5c32fc0ea8b86de111 Mon Sep 17 00:00:00 2001 From: bd-spratikbharti Date: Tue, 5 May 2026 12:48:23 +0530 Subject: [PATCH 1/4] fix(bitbake): resolve layer misidentification with deepest match - Match deepest folder in path instead of first occurrence - Validate against authoritative show-recipes source - Add comprehensive test coverage --- .../bitbake/parse/GraphNodeLabelParser.java | 20 +++- .../BitbakeDependencyGraphTransformer.java | 18 +++- .../unit/GraphNodeLabelParserTest.java | 93 ++++++++++++++++++- 3 files changed, 123 insertions(+), 8 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/parse/GraphNodeLabelParser.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/parse/GraphNodeLabelParser.java index 36c9c209f5..f5d87439c4 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/parse/GraphNodeLabelParser.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/parse/GraphNodeLabelParser.java @@ -29,14 +29,26 @@ public Optional parseLayerFromLabel(String label, Set knownLayer if (!recipeSpec.isPresent()) { return Optional.empty(); } + + // Find the deepest (rightmost) matching layer in the path + // This ensures we match the actual layer folder, not parent folders + // that happen to share a name with a known layer + String deepestMatch = null; + int deepestPosition = -1; + for (String candidateLayerName : knownLayerNames) { String possibleLayerPathSubstring = LABEL_PATH_SEPARATOR + candidateLayerName + LABEL_PATH_SEPARATOR; - if (recipeSpec.get().contains(possibleLayerPathSubstring)) { - return Optional.of(candidateLayerName); + int position = recipeSpec.get().lastIndexOf(possibleLayerPathSubstring); + if (position > deepestPosition) { + deepestPosition = position; + deepestMatch = candidateLayerName; } } - logger.warn("Graph Node recipe '{}' does not correspond to any known layer ({})", label, knownLayerNames); - return Optional.empty(); + + if (deepestMatch == null) { + logger.warn("Graph Node recipe '{}' does not correspond to any known layer ({})", label, knownLayerNames); + } + return Optional.ofNullable(deepestMatch); } @NotNull diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java index 5b58b1ad94..fa2fe62875 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java @@ -145,16 +145,28 @@ private Optional generateExternalId(String dependencyName, String de } private String chooseRecipeLayer(String dependencyName, @Nullable String dependencyLayer, List recipeLayerNames) { + // Validate: path-matched layer must exist in authoritative show-recipes list + if (dependencyLayer != null && recipeLayerNames.contains(dependencyLayer)) { + logger.trace("For dependency recipe {}: using layer {} parsed from task-depends.dot", dependencyName, dependencyLayer); + return dependencyLayer; + } + + // Path-matched layer is null or invalid - fall back to authoritative source if (dependencyLayer == null) { logger.warn( "Did not parse a layer for dependency {} from task-depends.dot; falling back to layer {} (first from show-recipes output)", dependencyName, recipeLayerNames.get(0) ); - dependencyLayer = recipeLayerNames.get(0); } else { - logger.trace("For dependency recipe {}: using layer {} parsed from task-depends.dot", dependencyName, dependencyLayer); + logger.debug( + "Path-matched layer '{}' not valid for recipe '{}' (valid layers: {}); using '{}'", + dependencyLayer, + dependencyName, + recipeLayerNames, + recipeLayerNames.get(0) + ); } - return dependencyLayer; + return recipeLayerNames.get(0); } } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/GraphNodeLabelParserTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/GraphNodeLabelParserTest.java index 6153b9d469..28aebc8581 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/GraphNodeLabelParserTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/GraphNodeLabelParserTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Optional; import java.util.Set; @@ -32,7 +33,97 @@ void testLayer() { knownLayers.add("meta"); Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); - + + assertTrue(layer.isPresent()); + assertEquals("meta", layer.get()); + } + + /** + * Path: /home/user/wrlinux/project/poky/meta/recipes-support/openssl.bb + * ^^^^^^^^ ^^^^ + * User folder Actual layer (deepest) + * + * Verifies: Returns "meta" (deepest), not "wrlinux" (shallow parent folder). + */ + @Test + void testLayerWithParentFolderCollision() { + String labelValue = "openssl do_compile\\n:3.0.8-r0\\n/home/user/wrlinux/project/poky/meta/recipes-support/openssl_3.0.8.bb"; + GraphNodeLabelParser parser = new GraphNodeLabelParser(); + + Set knownLayers = new LinkedHashSet<>(); + knownLayers.add("wrlinux"); + knownLayers.add("meta"); + knownLayers.add("meta-poky"); + + Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); + + assertTrue(layer.isPresent()); + assertEquals("meta", layer.get()); + } + + /** + * Same path as above, but layers in different order. + * + * Verifies: Result is deterministic regardless of iteration order. + */ + @Test + void testLayerParsingDeterministic() { + String labelValue = "openssl do_compile\\n:3.0.8-r0\\n/home/user/wrlinux/project/poky/meta/recipes-support/openssl_3.0.8.bb"; + GraphNodeLabelParser parser = new GraphNodeLabelParser(); + + Set knownLayers = new LinkedHashSet<>(); + knownLayers.add("meta"); + knownLayers.add("wrlinux"); + knownLayers.add("meta-poky"); + + Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); + + assertTrue(layer.isPresent()); + assertEquals("meta", layer.get()); + } + + /** + * Path: /builds/core/yocto/meta-custom/recipes-support/curl.bb + * ^^^^ ^^^^^^^^^^^ + * Parent Actual layer (deepest) + * + * Verifies: Any parent folder collision is handled, not just specific names. + */ + @Test + void testLayerIgnoresArbitraryParentFolder() { + String labelValue = "curl do_compile\\n:7.88.0-r0\\n/builds/core/yocto/meta-custom/recipes-support/curl_7.88.0.bb"; + GraphNodeLabelParser parser = new GraphNodeLabelParser(); + + Set knownLayers = new LinkedHashSet<>(); + knownLayers.add("core"); + knownLayers.add("meta-custom"); + knownLayers.add("meta"); + + Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); + + assertTrue(layer.isPresent()); + assertEquals("meta-custom", layer.get()); + } + + /** + * Path: /home/user/wrlinux/project/poky/meta/recipes-support/openssl.bb + * ^^^^^^^^ ^^^^ ^^^^ + * Pos 10 23 31 (deepest wins) + * + * Verifies: With multiple matches, deepest is selected. + */ + @Test + void testLayerSelectsDeepestMatch() { + String labelValue = "openssl do_compile\\n:3.0.8-r0\\n/home/user/wrlinux/project/poky/meta/recipes-support/openssl_3.0.8.bb"; + GraphNodeLabelParser parser = new GraphNodeLabelParser(); + + Set knownLayers = new LinkedHashSet<>(); + knownLayers.add("wrlinux"); + knownLayers.add("poky"); + knownLayers.add("meta"); + + Optional layer = parser.parseLayerFromLabel(labelValue, knownLayers); + assertTrue(layer.isPresent()); assertEquals("meta", layer.get()); } From 4b54f68f2c60eae5f3220198645afa09ea48e740 Mon Sep 17 00:00:00 2001 From: bd-spratikbharti Date: Fri, 8 May 2026 15:18:38 +0530 Subject: [PATCH 2/4] bitbake: guard layer selection and add fallback tests Prevent crash on empty layer lists, validate graph layer against authoritative source, and cover new fallback paths in tests. --- .../BitbakeDependencyGraphTransformer.java | 30 ++++++- ...BitbakeDependencyGraphTransformerTest.java | 80 +++++++++++++++++++ .../unit/BitbakeRecipesParserTest.java | 18 +++++ 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java index fa2fe62875..a03e4f3032 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/bitbake/transform/BitbakeDependencyGraphTransformer.java @@ -125,7 +125,9 @@ private Optional generateExternalId(String dependencyName, String de ExternalId externalId = null; if (recipeLayerNames != null) { dependencyLayer = chooseRecipeLayer(dependencyName, dependencyLayer, recipeLayerNames); - externalId = ExternalId.FACTORY.createYoctoExternalId(dependencyLayer, dependencyName, dependencyVersion); + if (dependencyLayer != null) { + externalId = ExternalId.FACTORY.createYoctoExternalId(dependencyLayer, dependencyName, dependencyVersion); + } } else { logger.debug("Failed to find component '{}' in component layer map. [dependencyVersion: {}; dependencyLayer: {}", dependencyName, dependencyVersion, dependencyLayer); if (dependencyName.endsWith(NATIVE_SUFFIX)) { @@ -144,6 +146,7 @@ private Optional generateExternalId(String dependencyName, String de return Optional.ofNullable(externalId); } + @Nullable private String chooseRecipeLayer(String dependencyName, @Nullable String dependencyLayer, List recipeLayerNames) { // Validate: path-matched layer must exist in authoritative show-recipes list if (dependencyLayer != null && recipeLayerNames.contains(dependencyLayer)) { @@ -151,12 +154,31 @@ private String chooseRecipeLayer(String dependencyName, @Nullable String depende return dependencyLayer; } + if (recipeLayerNames.isEmpty()) { + if (dependencyLayer != null) { + logger.warn( + "No authoritative show-recipes layer was parsed for dependency {}; using layer '{}' from task-depends.dot instead", + dependencyName, + dependencyLayer + ); + return dependencyLayer; + } + + logger.warn( + "No authoritative show-recipes layer or task-depends.dot layer was available for dependency {}; skipping external ID creation", + dependencyName + ); + return null; + } + + String authoritativeLayer = recipeLayerNames.get(0); + // Path-matched layer is null or invalid - fall back to authoritative source if (dependencyLayer == null) { logger.warn( "Did not parse a layer for dependency {} from task-depends.dot; falling back to layer {} (first from show-recipes output)", dependencyName, - recipeLayerNames.get(0) + authoritativeLayer ); } else { logger.debug( @@ -164,9 +186,9 @@ private String chooseRecipeLayer(String dependencyName, @Nullable String depende dependencyLayer, dependencyName, recipeLayerNames, - recipeLayerNames.get(0) + authoritativeLayer ); } - return recipeLayerNames.get(0); + return authoritativeLayer; } } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeDependencyGraphTransformerTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeDependencyGraphTransformerTest.java index 233c06db6b..5c1f30e969 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeDependencyGraphTransformerTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeDependencyGraphTransformerTest.java @@ -80,4 +80,84 @@ public void ignoredNoVersion() { graphAssert.hasNoDependency(externalIdFactory.createYoctoExternalId("meta", "example", null)); graphAssert.hasRootSize(0); } + + @Test + public void fallsBackToDependencyLayerWhenAuthoritativeLayerListIsEmpty() { + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + BitbakeGraph bitbakeGraph = new BitbakeGraph(); + bitbakeGraph.addNode("example", "75", "meta-from-graph"); + + Map> recipeToLayerMap = new HashMap<>(); + recipeToLayerMap.put("example", Collections.emptyList()); + + BitbakeDependencyGraphTransformer bitbakeDependencyGraphTransformer = new BitbakeDependencyGraphTransformer(EnumListFilter.excludeNone()); + DependencyGraph dependencyGraph = bitbakeDependencyGraphTransformer.transform(bitbakeGraph, recipeToLayerMap, null); + + NameVersionGraphAssert graphAssert = new NameVersionGraphAssert(Forge.YOCTO, dependencyGraph); + graphAssert.hasRootSize(1); + graphAssert.hasDependency(externalIdFactory.createYoctoExternalId("meta-from-graph", "example", "75")); + } + + @Test + public void skipsDependencyWhenNoLayerIsAvailable() { + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + BitbakeGraph bitbakeGraph = new BitbakeGraph(); + bitbakeGraph.addNode("example", "75", null); + + Map> recipeToLayerMap = new HashMap<>(); + recipeToLayerMap.put("example", Collections.emptyList()); + + BitbakeDependencyGraphTransformer bitbakeDependencyGraphTransformer = new BitbakeDependencyGraphTransformer(EnumListFilter.excludeNone()); + DependencyGraph dependencyGraph = bitbakeDependencyGraphTransformer.transform(bitbakeGraph, recipeToLayerMap, null); + + GraphAssert graphAssert = new GraphAssert(Forge.YOCTO, dependencyGraph); + graphAssert.hasNoDependency(externalIdFactory.createYoctoExternalId("meta", "example", "75")); + graphAssert.hasRootSize(0); + } + + /** + * Graph layer parsed from task-depends.dot does not appear in the authoritative show-recipes list. + * Expected: transformer ignores the invalid graph layer and uses the first authoritative layer instead. + */ + @Test + public void usesAuthoritativeLayerWhenGraphLayerIsInvalid() { + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + BitbakeGraph bitbakeGraph = new BitbakeGraph(); + // node carries "wrong-layer" which is NOT in the authoritative map + bitbakeGraph.addNode("example", "75", "wrong-layer"); + + Map> recipeToLayerMap = new HashMap<>(); + recipeToLayerMap.put("example", Collections.singletonList("meta-authoritative")); + + BitbakeDependencyGraphTransformer bitbakeDependencyGraphTransformer = new BitbakeDependencyGraphTransformer(EnumListFilter.excludeNone()); + DependencyGraph dependencyGraph = bitbakeDependencyGraphTransformer.transform(bitbakeGraph, recipeToLayerMap, null); + + NameVersionGraphAssert graphAssert = new NameVersionGraphAssert(Forge.YOCTO, dependencyGraph); + graphAssert.hasRootSize(1); + // must use the authoritative layer, not the invalid graph layer + graphAssert.hasDependency(externalIdFactory.createYoctoExternalId("meta-authoritative", "example", "75")); + graphAssert.hasNoDependency(externalIdFactory.createYoctoExternalId("wrong-layer", "example", "75")); + } + + /** + * No layer is parsed from task-depends.dot for the node (null). + * Expected: transformer falls back to the first authoritative layer from show-recipes. + */ + @Test + public void usesAuthoritativeLayerWhenGraphLayerIsNull() { + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + BitbakeGraph bitbakeGraph = new BitbakeGraph(); + // node has no layer from task-depends.dot + bitbakeGraph.addNode("example", "75", null); + + Map> recipeToLayerMap = new HashMap<>(); + recipeToLayerMap.put("example", Collections.singletonList("meta-authoritative")); + + BitbakeDependencyGraphTransformer bitbakeDependencyGraphTransformer = new BitbakeDependencyGraphTransformer(EnumListFilter.excludeNone()); + DependencyGraph dependencyGraph = bitbakeDependencyGraphTransformer.transform(bitbakeGraph, recipeToLayerMap, null); + + NameVersionGraphAssert graphAssert = new NameVersionGraphAssert(Forge.YOCTO, dependencyGraph); + graphAssert.hasRootSize(1); + graphAssert.hasDependency(externalIdFactory.createYoctoExternalId("meta-authoritative", "example", "75")); + } } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeRecipesParserTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeRecipesParserTest.java index 8ac7589b9d..bfaeb5bb35 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeRecipesParserTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/bitbake/unit/BitbakeRecipesParserTest.java @@ -35,4 +35,22 @@ void test() { assertEquals(4, results.getRecipesWithLayers().size()); assertTrue(results.getRecipesWithLayers().containsKey("adcli")); } + + @Test + void preservesRecipeWithEmptyLayerListWhenRecipeDetailsCannotBeParsed() { + List malformedShowRecipesOutputLines = Arrays.asList( + "=== Available recipes: ===\n", + "broken-recipe:\n", + " malformed-layer-line\n", + "valid-recipe:\n", + " meta 1.0\n" + ); + + BitbakeRecipesParser parser = new BitbakeRecipesParser(); + ShowRecipesResults results = parser.parseShowRecipes(malformedShowRecipesOutputLines); + + assertTrue(results.getRecipesWithLayers().containsKey("broken-recipe")); + assertTrue(results.getRecipesWithLayers().get("broken-recipe").isEmpty()); + assertEquals(Arrays.asList("meta"), results.getRecipesWithLayers().get("valid-recipe")); + } } From bc11ea79a9baf7cdec0914cdde904a274e55e1e2 Mon Sep 17 00:00:00 2001 From: blackduck-serv-builder Date: Thu, 28 May 2026 00:43:00 -0400 Subject: [PATCH 3/4] Release 11.5.0-SIGQA3-IDETECT-5131 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0118212609..25d25c01ec 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { group = 'com.blackduck.integration' -version = '11.5.0-SIGQA3-shanty.merge_11.4.z_to_main-SNAPSHOT' +version = '11.5.0-SIGQA3-IDETECT-5131' apply plugin: 'com.blackduck.integration.solution' apply plugin: 'org.springframework.boot' From 5df7a87fd1b64b3c6d0d6a47838695a4959b4c8d Mon Sep 17 00:00:00 2001 From: blackduck-serv-builder Date: Thu, 28 May 2026 00:54:52 -0400 Subject: [PATCH 4/4] Using the next snapshot post release 11.5.0-SIGQA4-IDETECT-5131-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 25d25c01ec..4c07011005 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ buildscript { group = 'com.blackduck.integration' -version = '11.5.0-SIGQA3-IDETECT-5131' +version = '11.5.0-SIGQA4-IDETECT-5131-SNAPSHOT' apply plugin: 'com.blackduck.integration.solution' apply plugin: 'org.springframework.boot'