diff mbox series

oeqa/selftest/kernelmodulesplit: test cross-recipe kernel module dependencies

Message ID 20260619130323.2640105-1-sivakumar.bs@gmail.com
State New
Headers show
Series oeqa/selftest/kernelmodulesplit: test cross-recipe kernel module dependencies | expand

Commit Message

Siva Balasubramanian June 19, 2026, 1:03 p.m. UTC
Add a selftest that exercises kernel module dependency generation across
recipes. Two external module recipes are added to meta-selftest:
kernel-module-testfoo exports a symbol and kernel-module-testbar consumes
it (DEPENDS on testfoo). The test builds both and asserts that the
consumer's real, KERNEL_VERSION-suffixed package RDEPENDS the exporter's
suffixed package - and not the empty unsuffixed kernel-module-testfoo
package - and that the suffixed package RPROVIDES the unsuffixed virtual.

This is the failure mode originally reported in [YOCTO #12290], where the
generated dependency resolved to the empty package after the KERNEL_VERSION
suffix was introduced. It works on current master; this test guards against
regressing it again.

The kernel version is derived from the produced package list rather than
KERNEL_VERSION, which is unset at parse time when the modules are restored
from sstate.

Signed-off-by: Siva Balasubramanian <sivakumar.bs@gmail.com>
---
 .../kernel-module-cross/files/Makefile.bar    | 14 ++++
 .../kernel-module-cross/files/Makefile.foo    | 14 ++++
 .../kernel-module-cross/files/testbar.c       | 19 +++++
 .../kernel-module-cross/files/testfoo.c       | 23 ++++++
 .../kernel-module-testbar_0.1.bb              | 20 ++++++
 .../kernel-module-testfoo_0.1.bb              | 16 +++++
 .../oeqa/selftest/cases/kernelmodulesplit.py  | 72 +++++++++++++++++++
 7 files changed, 178 insertions(+)
 create mode 100644 meta-selftest/recipes-test/kernel-module-cross/files/Makefile.bar
 create mode 100644 meta-selftest/recipes-test/kernel-module-cross/files/Makefile.foo
 create mode 100644 meta-selftest/recipes-test/kernel-module-cross/files/testbar.c
 create mode 100644 meta-selftest/recipes-test/kernel-module-cross/files/testfoo.c
 create mode 100644 meta-selftest/recipes-test/kernel-module-cross/kernel-module-testbar_0.1.bb
 create mode 100644 meta-selftest/recipes-test/kernel-module-cross/kernel-module-testfoo_0.1.bb
 create mode 100644 meta/lib/oeqa/selftest/cases/kernelmodulesplit.py
diff mbox series

Patch

diff --git a/meta-selftest/recipes-test/kernel-module-cross/files/Makefile.bar b/meta-selftest/recipes-test/kernel-module-cross/files/Makefile.bar
new file mode 100644
index 0000000000..1d4409bf3a
--- /dev/null
+++ b/meta-selftest/recipes-test/kernel-module-cross/files/Makefile.bar
@@ -0,0 +1,14 @@ 
+obj-m := testbar.o
+
+SRC := $(shell pwd)
+
+all:
+	$(MAKE) -C $(KERNEL_SRC) M=$(SRC)
+
+modules_install:
+	$(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install
+
+clean:
+	rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod
+	rm -f Module.markers Module.symvers modules.order
+	rm -rf .tmp_versions Modules.symvers
diff --git a/meta-selftest/recipes-test/kernel-module-cross/files/Makefile.foo b/meta-selftest/recipes-test/kernel-module-cross/files/Makefile.foo
new file mode 100644
index 0000000000..1720b239c0
--- /dev/null
+++ b/meta-selftest/recipes-test/kernel-module-cross/files/Makefile.foo
@@ -0,0 +1,14 @@ 
+obj-m := testfoo.o
+
+SRC := $(shell pwd)
+
+all:
+	$(MAKE) -C $(KERNEL_SRC) M=$(SRC)
+
+modules_install:
+	$(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install
+
+clean:
+	rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c *.mod
+	rm -f Module.markers Module.symvers modules.order
+	rm -rf .tmp_versions Modules.symvers
diff --git a/meta-selftest/recipes-test/kernel-module-cross/files/testbar.c b/meta-selftest/recipes-test/kernel-module-cross/files/testbar.c
new file mode 100644
index 0000000000..98470c407f
--- /dev/null
+++ b/meta-selftest/recipes-test/kernel-module-cross/files/testbar.c
@@ -0,0 +1,19 @@ 
+#include <linux/module.h>
+#include <linux/init.h>
+
+extern int testfoo_value(void);
+
+static int __init testbar_init(void)
+{
+	pr_info("testbar loaded, foo says %d\n", testfoo_value());
+	return 0;
+}
+
+static void __exit testbar_exit(void)
+{
+	pr_info("testbar unloaded\n");
+}
+
+module_init(testbar_init);
+module_exit(testbar_exit);
+MODULE_LICENSE("GPL");
diff --git a/meta-selftest/recipes-test/kernel-module-cross/files/testfoo.c b/meta-selftest/recipes-test/kernel-module-cross/files/testfoo.c
new file mode 100644
index 0000000000..e438db49cb
--- /dev/null
+++ b/meta-selftest/recipes-test/kernel-module-cross/files/testfoo.c
@@ -0,0 +1,23 @@ 
+#include <linux/module.h>
+#include <linux/init.h>
+
+int testfoo_value(void)
+{
+	return 42;
+}
+EXPORT_SYMBOL(testfoo_value);
+
+static int __init testfoo_init(void)
+{
+	pr_info("testfoo loaded\n");
+	return 0;
+}
+
+static void __exit testfoo_exit(void)
+{
+	pr_info("testfoo unloaded\n");
+}
+
+module_init(testfoo_init);
+module_exit(testfoo_exit);
+MODULE_LICENSE("GPL");
diff --git a/meta-selftest/recipes-test/kernel-module-cross/kernel-module-testbar_0.1.bb b/meta-selftest/recipes-test/kernel-module-cross/kernel-module-testbar_0.1.bb
new file mode 100644
index 0000000000..bf465169f5
--- /dev/null
+++ b/meta-selftest/recipes-test/kernel-module-cross/kernel-module-testbar_0.1.bb
@@ -0,0 +1,20 @@ 
+SUMMARY = "Cross-recipe kernel module dependency test: consumer (Bug 12290)"
+LICENSE = "GPL-2.0-only"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-2.0-only;md5=801f80980d171dd6425610833a22dbe6"
+
+inherit module
+
+# This pulls in foo's Module.symvers via KBUILD_EXTRA_SYMBOLS (module.bbclass
+# __anonymous) so modpost records testfoo as a dependency of testbar.
+DEPENDS = "kernel-module-testfoo"
+
+SRC_URI = "\
+    file://testbar.c \
+    file://Makefile.bar \
+"
+
+S = "${UNPACKDIR}"
+
+do_configure:prepend() {
+    cp ${UNPACKDIR}/Makefile.bar ${UNPACKDIR}/Makefile
+}
diff --git a/meta-selftest/recipes-test/kernel-module-cross/kernel-module-testfoo_0.1.bb b/meta-selftest/recipes-test/kernel-module-cross/kernel-module-testfoo_0.1.bb
new file mode 100644
index 0000000000..8ea3ad3618
--- /dev/null
+++ b/meta-selftest/recipes-test/kernel-module-cross/kernel-module-testfoo_0.1.bb
@@ -0,0 +1,16 @@ 
+SUMMARY = "Cross-recipe kernel module dependency test: exporter (Bug 12290)"
+LICENSE = "GPL-2.0-only"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-2.0-only;md5=801f80980d171dd6425610833a22dbe6"
+
+inherit module
+
+SRC_URI = "\
+    file://testfoo.c \
+    file://Makefile.foo \
+"
+
+S = "${UNPACKDIR}"
+
+do_configure:prepend() {
+    cp ${UNPACKDIR}/Makefile.foo ${UNPACKDIR}/Makefile
+}
diff --git a/meta/lib/oeqa/selftest/cases/kernelmodulesplit.py b/meta/lib/oeqa/selftest/cases/kernelmodulesplit.py
new file mode 100644
index 0000000000..1627748a98
--- /dev/null
+++ b/meta/lib/oeqa/selftest/cases/kernelmodulesplit.py
@@ -0,0 +1,72 @@ 
+#
+# Copyright OpenEmbedded Contributors
+#
+# SPDX-License-Identifier: MIT
+#
+
+import re
+
+from oeqa.selftest.case import OESelftestTestCase
+from oeqa.utils.commands import bitbake, runCmd
+
+
+class KernelModuleSplit(OESelftestTestCase):
+    """
+    Tests for cross-recipe kernel module dependency generation.
+    """
+
+    def real_module_package(self, recipe):
+        """
+        Return the name of the real (.ko-containing) package for a kernel
+        module recipe, i.e. the one suffixed with ${KERNEL_VERSION}. The version
+        is read from the produced package list rather than from KERNEL_VERSION,
+        which can be unset at parse time when the module is restored from sstate.
+        """
+        packages = runCmd("oe-pkgdata-util list-pkgs -p %s" % recipe).output.split()
+        version_re = re.compile(r"^%s-(\d[^/]*)$" % re.escape(recipe))
+        matches = [p for p in packages if version_re.match(p)]
+        self.assertEqual(len(matches), 1,
+            "Expected exactly one %s-<version> package, found: %s" % (recipe, matches))
+        return matches[0]
+
+    def test_cross_recipe_module_dependency(self):
+        """
+        Summary:    Regression test for Yocto bug 12290. When one external
+                    kernel module recipe consumes an exported symbol from
+                    another, the consumer's auto-generated RDEPENDS must point
+                    at the exporter's real (-${KERNEL_VERSION}) package, not at
+                    the empty unsuffixed kernel-module-* package.
+        Expected:   kernel-module-testbar-${KERNEL_VERSION} RDEPENDS
+                    kernel-module-testfoo-${KERNEL_VERSION}, does not depend on
+                    the bare kernel-module-testfoo package, and RPROVIDES the
+                    unsuffixed kernel-module-testbar virtual.
+        Product:    OE-Core
+        """
+        consumer_recipe = "kernel-module-testbar"
+        exporter_recipe = "kernel-module-testfoo"
+
+        bitbake("%s %s" % (consumer_recipe, exporter_recipe))
+
+        consumer_pkg = self.real_module_package(consumer_recipe)
+        exporter_pkg = self.real_module_package(exporter_recipe)
+
+        rdepends = runCmd("oe-pkgdata-util read-value RDEPENDS %s" % consumer_pkg).output.split()
+
+        # The consumer must depend on the real (.ko-containing) exporter
+        # package, i.e. the one suffixed with KERNEL_VERSION ...
+        self.assertIn(exporter_pkg, rdepends,
+            "%s does not RDEPEND the real exporter package %s (got: %s)"
+            % (consumer_pkg, exporter_pkg, rdepends))
+
+        # ... and never on the bare, empty unsuffixed exporter package, which
+        # was the failure mode of bug 12290.
+        self.assertNotIn(exporter_recipe, rdepends,
+            "%s RDEPENDS the empty unsuffixed package %s (bug 12290 regression)"
+            % (consumer_pkg, exporter_recipe))
+
+        # The real package must virtual-provide the unsuffixed name so that
+        # existing references to kernel-module-testbar still resolve.
+        rprovides = runCmd("oe-pkgdata-util read-value RPROVIDES %s" % consumer_pkg).output.split()
+        self.assertIn(consumer_recipe, rprovides,
+            "%s does not RPROVIDE the virtual %s (got: %s)"
+            % (consumer_pkg, consumer_recipe, rprovides))