new file mode 100644
@@ -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
new file mode 100644
@@ -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
new file mode 100644
@@ -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");
new file mode 100644
@@ -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");
new file mode 100644
@@ -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
+}
new file mode 100644
@@ -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
+}
new file mode 100644
@@ -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))
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