[PATCH 2/2] image-oci: drop *-initial sysroot deps in do_image_oci to avoid libgcc collision
Koen Kooi <[email protected]> Tue, 28 Jul 2026 13:55:16 +0200
| Newsgroups | org.yoctoproject.lists.meta-virtualization |
|---|---|
| Message-ID | <[email protected]> |
do_image_oci can abort with:
ERROR: <recipe> do_image_oci: The file .../crtbegin.o is installed
by both libgcc and libgcc-initial, aborting
This was seen deterministically (6+ clean-sstate attempts) on an
OCI_LAYER_MODE = "multi" image recipe whose "packages:" content is a
from-source-compiled library with a moderately large DEPENDS. It is
sstate-state-dependent: it only fires once do_image_oci's dependency
walk actually has to populate (rather than skip via sstate/setscene
shortcuts) both libgcc and libgcc-initial into the same recipe
sysroot, so a given recipe/build combination can build clean one run
and hit the collision on the next, depending on what is already
present in sstate. Confirmed in our own testing: instrumenting the
fix below shows it stripping two "*-initial" do_populate_sysroot
nodes out of BB_TASKDEPDATA for a boost-based multi-layer "packages:"
recipe -- i.e. libgcc-initial is a real, reachable node in
do_image_oci's dependency walk for realistic recipes, even though a
minimal single-package recipe does not reach it.
Root cause: do_image_oci[depends] contains several
"...:do_populate_sysroot" entries (umoci-native, jq-native, and for
multi-layer package installs, opkg-native/rsync-native/dnf-native).
Any task whose [depends] flag contains the substring
"populate_sysroot" gets oe-core's extend_recipe_sysroot
(staging.bbclass) auto-attached as a prefunc, which walks the full
transitive do_populate_sysroot closure reachable from those deps and
copies it into the recipe's private sysroot.
oe-core already protects against exposing both libgcc and
libgcc-initial into the same sysroot: setscene_depvalid() excludes
"*-initial" recipes via SSTATE_EXCLUDEDEPS_SYSROOT
(".*->.*-initial.*", meta/conf/layer.conf, "nothing needs to depend
on libc-initial"), and staging_populate_sysroot_dir has its own
"skip libgcc-initial due to file overlap" special case. But the
SSTATE_EXCLUDEDEPS_SYSROOT check inside setscene_depvalid only
applies when the *dependent* task is itself do_populate_sysroot
("taskdependees[task][1] == 'do_populate_sysroot'"). For
do_image_oci, the dependent task is do_image_oci, not
do_populate_sysroot, so that exclusion is skipped entirely and both
libgcc and libgcc-initial fall through to the default "keep it"
path, landing in the same sysroot and colliding on their shared
crtbegin.o.
Fix: add a RecipeTaskPreProcess handler that, for do_image_oci
specifically, prepends a prefunc which strips "*-initial"
do_populate_sysroot nodes out of BB_TASKDEPDATA before
extend_recipe_sysroot runs (extend_recipe_sysroot re-reads
BB_TASKDEPDATA fresh each time, so this is sufficient). This mirrors
the existing "skip *-initial" intent from
staging_populate_sysroot_dir, scoped narrowly to do_image_oci so it
doesn't change behavior for any other task. The *-initial recipes
are bootstrap-only (used to build glibc) and are fully superseded by
their real counterparts by image-assembly time, so nothing an image
needs is lost by excluding them here.
Signed-off-by: Koen Kooi <[email protected]>
---
classes/image-oci.bbclass | 77 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 77 insertions(+)
diff --git a/classes/image-oci.bbclass b/classes/image-oci.bbclass
index ed8926f6..c397e2f9 100644
--- a/classes/image-oci.bbclass
+++ b/classes/image-oci.bbclass
@@ -526,6 +526,83 @@ def oci_install_layer_packages(d, layer_rootfs, layer_packages, layer_name):
bb.note(f"OCI: Package installation complete for layer '{layer_name}'")
+# =============================================================================
+# Work around libgcc vs libgcc-initial sysroot collision in do_image_oci
+# =============================================================================
+#
+# do_image_oci[depends] contains "...:do_populate_sysroot" entries (for the
+# native tools it needs: umoci/jq/opkg/rsync-native). oe-core's
+# staging_taskhandler therefore auto-attaches extend_recipe_sysroot as a
+# prefunc of do_image_oci (any task whose [depends] contains the substring
+# "populate_sysroot" gets it). extend_recipe_sysroot then walks the whole
+# reachable populate_sysroot dependency tree and exposes it into this recipe's
+# private sysroot.
+#
+# For a *do_populate_sysroot* consumer, oe-core's setscene_depvalid() excludes
+# *-initial recipes (SSTATE_EXCLUDEDEPS_SYSROOT ".*->.*-initial.*"), so a normal
+# recipe never sees both libgcc and libgcc-initial. But that exclusion is gated
+# behind "taskdependees[task][1] == 'do_populate_sysroot'", which is false here
+# (the consuming task is do_image_oci), so both libgcc and libgcc-initial get
+# exposed and their shared crtbegin.o collides:
+# ERROR: ... The file .../crtbegin.o is installed by both libgcc and
+# libgcc-initial, aborting
+#
+# oe-core already skips *-initial on its other sysroot-population codepaths
+# (staging_populate_sysroot_dir: "skip libgcc-initial due to file overlap", and
+# the do_sdk_depends/do_populate_sdk_ext skip inside extend_recipe_sysroot
+# itself). We apply the same intent here, narrowly and only for image-oci
+# recipes: before extend_recipe_sysroot runs, drop *-initial do_populate_sysroot
+# nodes from this task's copy of BB_TASKDEPDATA so they never enter the tree.
+# extend_recipe_sysroot re-reads BB_TASKDEPDATA fresh, so this is sufficient and
+# touches nothing else. The *-initial recipes are bootstrap-only (used to build
+# glibc) and are fully superseded by their real counterparts by image-assembly
+# time, so nothing an image needs is lost.
+python oci_strip_initial_from_taskdepdata () {
+ import copy
+
+ taskdepdata = d.getVar("BB_TASKDEPDATA", False)
+ if not taskdepdata:
+ return
+
+ filtered = copy.deepcopy(taskdepdata)
+ drop = set()
+ for k, v in filtered.items():
+ # v[0] = PN, v[1] = taskname
+ if v[1] == "do_populate_sysroot" and v[0].endswith("-initial"):
+ drop.add(k)
+
+ if not drop:
+ return
+
+ for k in drop:
+ del filtered[k]
+ # Remove inbound edges to the dropped nodes. Each entry is a bb.TaskData
+ # namedtuple (immutable), but its dep set (index 3) is a mutable set, so
+ # mutate it in place rather than reassigning the field.
+ for v in filtered.values():
+ v[3].difference_update(drop)
+
+ bb.note("OCI: dropped %d *-initial do_populate_sysroot node(s) from "
+ "BB_TASKDEPDATA to avoid libgcc/libgcc-initial sysroot collision"
+ % len(drop))
+ d.setVar("BB_TASKDEPDATA", filtered)
+}
+oci_strip_initial_from_taskdepdata[vardepsexclude] += "BB_TASKDEPDATA"
+
+# Ensure our prefunc runs *before* extend_recipe_sysroot. staging_taskhandler
+# (a global class) prepends extend_recipe_sysroot on the RecipeTaskPreProcess
+# event; this handler is registered later (image-oci is inherited after the
+# global classes) so its prepend lands first in the prefunc list.
+python oci_taskhandler() {
+ task = "do_image_oci"
+ if task in e.tasklist:
+ deps = d.getVarFlag(task, "depends")
+ if deps and "populate_sysroot" in deps:
+ d.prependVarFlag(task, "prefuncs", "oci_strip_initial_from_taskdepdata ")
+}
+oci_taskhandler[eventmask] = "bb.event.RecipeTaskPreProcess"
+addhandler oci_taskhandler
+
# the IMAGE_CMD:oci comes from the .inc
OCI_IMAGE_BACKEND_INC ?= "${@"image-oci-" + "${OCI_IMAGE_BACKEND}" + ".inc"}"
include ${OCI_IMAGE_BACKEND_INC}
--
2.34.1