Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,26 @@ public Optional<String> parseLayerFromLabel(String label, Set<String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ private Optional<ExternalId> 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)) {
Expand All @@ -144,17 +146,49 @@ private Optional<ExternalId> generateExternalId(String dependencyName, String de
return Optional.ofNullable(externalId);
}

@Nullable
private String chooseRecipeLayer(String dependencyName, @Nullable String dependencyLayer, List<String> 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;
}

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
Comment thread
bd-spratikbharti marked this conversation as resolved.
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
);
Comment thread
bd-spratikbharti marked this conversation as resolved.
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,
authoritativeLayer
);
}
return dependencyLayer;
return authoritativeLayer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,22 @@ void test() {
assertEquals(4, results.getRecipesWithLayers().size());
assertTrue(results.getRecipesWithLayers().containsKey("adcli"));
}

@Test
void preservesRecipeWithEmptyLayerListWhenRecipeDetailsCannotBeParsed() {
List<String> 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"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -32,7 +33,97 @@ void testLayer() {
knownLayers.add("meta");

Optional<String> 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<String> knownLayers = new LinkedHashSet<>();
knownLayers.add("wrlinux");
knownLayers.add("meta");
knownLayers.add("meta-poky");

Optional<String> 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<String> knownLayers = new LinkedHashSet<>();
knownLayers.add("meta");
knownLayers.add("wrlinux");
knownLayers.add("meta-poky");

Optional<String> 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<String> knownLayers = new LinkedHashSet<>();
knownLayers.add("core");
knownLayers.add("meta-custom");
knownLayers.add("meta");

Optional<String> 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<String> knownLayers = new LinkedHashSet<>();
knownLayers.add("wrlinux");
knownLayers.add("poky");
knownLayers.add("meta");

Optional<String> layer = parser.parseLayerFromLabel(labelValue, knownLayers);

assertTrue(layer.isPresent());
assertEquals("meta", layer.get());
}
Expand Down