diff --git a/.github/workflows/kernel-modules-extension.yml b/.github/workflows/kernel-modules-extension.yml new file mode 100644 index 000000000..4d913c4c9 --- /dev/null +++ b/.github/workflows/kernel-modules-extension.yml @@ -0,0 +1,143 @@ +name: Kernel Modules Extension + +on: + push: + branches: + - 'alexgg/os-blocks-kernel-modules' + + workflow_call: + inputs: + machine: + required: true + type: string + os-version: + description: 'balenaOS version (e.g. from git tag)' + required: true + type: string + deploy-environment: + required: false + type: string + default: balena-staging.com + secrets: + BALENA_API_DEPLOY_KEY: + description: balena API key for the deploy environment + required: false + + workflow_dispatch: + inputs: + machine: + description: 'Device type' + required: true + type: choice + options: + - raspberrypi4-64 + os-version: + description: 'balenaOS version (e.g. from git tag)' + required: true + type: string + deploy-environment: + description: 'Deploy environment' + required: false + type: string + default: balena-staging.com + +env: + SHARED_BUILD_DIR: ${{ github.workspace }}/shared + +jobs: + build-extension: + name: Build kernel modules extension (${{ matrix.machine }}) + runs-on: + - self-hosted + - X64 + - yocto + environment: ${{ inputs.deploy-environment || 'balena-staging.com' }} + + strategy: + fail-fast: false + matrix: + machine: ${{ inputs.machine && fromJSON(format('["{0}"]', inputs.machine)) || fromJSON('["raspberrypi4-64"]') }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: true + + - name: Build kernel modules via Yocto + run: | + mkdir -p "${SHARED_BUILD_DIR}" + + ./balena-yocto-scripts/build/balena-build.sh \ + -d "${{ matrix.machine }}" \ + -s "${SHARED_BUILD_DIR}" \ + -i "balena-kernel-modules-block" \ + -g "-t layers/meta-kernel-modules-block/conf/samples" \ + | tee build.log + + if ! grep -q "Build for ${{ matrix.machine }} suceeded" build.log; then + exit 1 + fi + + - name: Prepare deployment + id: prepare + run: | + mkdir -p kernel-modules-deploy/contents + + # Find and extract the tarball + targz=$(find build/tmp/deploy/images/${{ matrix.machine }}/ -name "balena-kernel-modules-block*.tar.gz" -type l) + if [ -z "${targz}" ]; then + echo "ERROR: No balena-kernel-modules-block tarball found" + exit 1 + fi + if [ "$(echo "${targz}" | wc -l)" -ne 1 ]; then + echo "ERROR: Expected exactly one tarball, found:" + echo "${targz}" + exit 1 + fi + tar xf "${targz}" -C kernel-modules-deploy/contents + + # Extract kernel version from modules tree + kernel_version=$(ls kernel-modules-deploy/contents/lib/modules/) + echo "kernel_version=${kernel_version}" >> "${GITHUB_OUTPUT}" + + - name: Setup balena CLI + uses: balena-io-examples/setup-balena-action@v0.0.30 + with: + balena-token: ${{ secrets.BALENA_API_DEPLOY_KEY }} + env: + BALENARC_BALENA_URL: ${{ inputs.deploy-environment || 'balena-staging.com' }} + + - name: Deploy to balenaCloud + env: + BALENARC_BALENA_URL: ${{ inputs.deploy-environment || 'balena-staging.com' }} + FLEET: kernel-modules-${{ matrix.machine }} + ORG: balena_os + run: | + # Create docker-compose.yml with extension labels + cat > kernel-modules-deploy/docker-compose.yml < /dev/null 2>&1; then + balena fleet create "${FLEET}" --type "${{ matrix.machine }}" --organization "${ORG}" + fi + + # Deploy + balena deploy "${ORG}/${FLEET}" --source kernel-modules-deploy diff --git a/.github/workflows/raspberrypi4-64.yml b/.github/workflows/raspberrypi4-64.yml index 4cde61ec8..a1071d58f 100644 --- a/.github/workflows/raspberrypi4-64.yml +++ b/.github/workflows/raspberrypi4-64.yml @@ -35,6 +35,11 @@ on: required: false type: string default: '' + extensions: + description: Space-separated list of OS block extension images to include + required: false + type: string + default: '' permissions: id-token: write # This is required for requesting the JWT #https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services#requesting-the-access-token @@ -46,7 +51,7 @@ permissions: jobs: yocto: name: Yocto - uses: balena-os/balena-yocto-scripts/.github/workflows/yocto-build-deploy.yml@cc83969226e96a3d22652ba5340135b697e366bb + uses: balena-os/balena-yocto-scripts/.github/workflows/yocto-build-deploy.yml@6abbbd645c9b78a211fdcb509de5259f9475f741 # Prevent duplicate workflow executions for pull_request (PR) and pull_request_target (PRT) events. # Both PR and PRT will be triggered for the same pull request, whether it is internal or from a fork. # This condition will prevent the workflow from running twice for the same pull request while @@ -69,3 +74,15 @@ jobs: deploy-environment: ${{ inputs.deploy-environment || 'balena-cloud.com' }} # Allow overriding the meta-balena ref for workflow dispatch events meta-balena-ref: ${{ inputs.meta-balena-ref || '' }} + # OS block extensions to include in the OS composition + extensions: ${{ inputs.extensions || '' }} + + kernel-modules: + name: Kernel Modules Extension + needs: yocto + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + uses: ./.github/workflows/kernel-modules-extension.yml + secrets: inherit + with: + machine: raspberrypi4-64 + os-version: ${{ github.ref_name }} diff --git a/balena-yocto-scripts b/balena-yocto-scripts index cc8396922..6abbbd645 160000 --- a/balena-yocto-scripts +++ b/balena-yocto-scripts @@ -1 +1 @@ -Subproject commit cc83969226e96a3d22652ba5340135b697e366bb +Subproject commit 6abbbd645c9b78a211fdcb509de5259f9475f741 diff --git a/kernel-modules-deploy/Dockerfile b/kernel-modules-deploy/Dockerfile new file mode 100644 index 000000000..f3b68a759 --- /dev/null +++ b/kernel-modules-deploy/Dockerfile @@ -0,0 +1,12 @@ +FROM scratch + +ARG KERNEL_VERSION +ARG OS_VERSION +COPY contents/ / + +LABEL io.balena.image.class=overlay +LABEL io.balena.image.requires-reboot=1 +LABEL io.balena.image.store=data +LABEL io.balena.image.profiles=kernel-modules +LABEL io.balena.image.kernel=${KERNEL_VERSION} +LABEL io.balena.image.os-version=${OS_VERSION} diff --git a/layers/meta-balena b/layers/meta-balena index 5ceed2308..024f7ae21 160000 --- a/layers/meta-balena +++ b/layers/meta-balena @@ -1 +1 @@ -Subproject commit 5ceed2308a1ce8a2e453f0f2890039106c1ee7b0 +Subproject commit 024f7ae2110c741efde329fab7ba0aa8d3195dfc diff --git a/layers/meta-kernel-modules-block/classes/kernel-modules-extension.bbclass b/layers/meta-kernel-modules-block/classes/kernel-modules-extension.bbclass new file mode 100644 index 000000000..98e424712 --- /dev/null +++ b/layers/meta-kernel-modules-block/classes/kernel-modules-extension.bbclass @@ -0,0 +1,250 @@ +# kernel-modules-extension.bbclass +# +# Builds a kernel extension block with additional loadable modules using +# targeted enablement from a curated config fragment (modules-selection.cfg). +# +# Strategy: +# 1. Let normal kernel config flow run (defconfig + BALENA_CONFIGS) +# 2. Save the base .config, normalize via olddefconfig +# 3. Apply modules-selection.cfg via merge_config.sh +# 4. Run olddefconfig to resolve dependencies +# 5. Config stability check: verify no =y options were added or removed +# 6. Compile kernel + modules (normal do_compile flow) +# 7. Install all modules (base + new) +# 8. After install, rebuild with base config to get base symvers for CRC +# verification +# +# Usage: +# inherit kernel-modules-extension +# # Place modules-selection.cfg in FILESEXTRAPATHS-reachable directory + + +def kernel_modules_read_config(config_path): + """Read a kernel .config into a dict of CONFIG_OPTION -> full line.""" + import re + + values = {} + with open(config_path) as f: + for line in f: + line = line.strip() + m = re.match(r"^(CONFIG_\w+)=", line) + if m: + values[m.group(1)] = line + continue + m = re.match(r"^# (CONFIG_\w+) is not set$", line) + if m: + values[m.group(1)] = line + + return values + + +python do_kernel_extend_config() { + import os + import shutil + import subprocess + + S = d.getVar("S") + B = d.getVar("B") + + base_config = os.path.join(B, ".config") + base_saved = os.path.join(B, ".config.base") + + make_cmd = d.getVar("KERNEL_MAKE_CMD") or "make" + make_opts = d.getVar("EXTRA_OEMAKE") or "" + arch = d.getVar("ARCH") + if not arch: + bb.fatal("kernel-modules-extension: ARCH variable not set") + + # 1. Normalize base config via olddefconfig to get a clean baseline. + # The .config may contain stale/renamed kconfig symbols, or may be + # leftover from a previous extended build if the build dir was reused. + # Normalizing ensures a clean starting point for comparison. + cmd = f'{make_cmd} {make_opts} -C {S} O={B} ARCH={arch} olddefconfig' + bb.note("Normalizing base config via olddefconfig") + ret = subprocess.run(cmd, shell=True, capture_output=True, text=True) + if ret.returncode != 0: + bb.fatal(f"olddefconfig (base normalization) failed:\n{ret.stderr}") + + # 2. Save the normalized .config as .config.base AFTER normalization. + # This ensures .config.base is always a clean, normalized base config + # regardless of any stale state in the build directory. + shutil.copy(base_config, base_saved) + bb.note(f"Saved normalized base config to {base_saved}") + + base_values = kernel_modules_read_config(base_config) + base_y = {k for k, v in base_values.items() if v.endswith("=y")} + base_m = {k for k, v in base_values.items() if v.endswith("=m")} + bb.note(f"Base config (normalized): {len(base_y)} =y, {len(base_m)} =m") + + # 3. Find modules-selection.cfg via FILESEXTRAPATHS + selection_cfg = None + filesextrapaths = d.getVar("FILESEXTRAPATHS") or "" + for path in filesextrapaths.split(":"): + candidate = os.path.join(path, "modules-selection.cfg") + if os.path.exists(candidate): + selection_cfg = candidate + break + + if not selection_cfg: + bb.fatal( + "kernel-modules-extension: modules-selection.cfg not found.\n" + "Place it in a directory reachable via FILESEXTRAPATHS.\n" + f"Searched paths: {filesextrapaths}" + ) + + bb.note(f"Using module selection fragment: {selection_cfg}") + + # 4. Apply modules-selection.cfg via merge_config.sh + merge_script = os.path.join(S, "scripts", "kconfig", "merge_config.sh") + cmd = f'{merge_script} -m -O {B} {base_config} {selection_cfg}' + bb.note("Merging modules-selection.cfg") + ret = subprocess.run(cmd, shell=True, capture_output=True, text=True) + if ret.returncode != 0: + bb.fatal(f"merge_config.sh failed:\n{ret.stderr}") + + # 5. Run olddefconfig to resolve dependencies + cmd = f'{make_cmd} {make_opts} -C {S} O={B} ARCH={arch} olddefconfig' + bb.note("Running olddefconfig") + ret = subprocess.run(cmd, shell=True, capture_output=True, text=True) + if ret.returncode != 0: + bb.fatal(f"olddefconfig failed:\n{ret.stderr}") + + # 6. Config stability check: compare =y sets + extended_values = kernel_modules_read_config(base_config) + extended_y = {k for k, v in extended_values.items() if v.endswith("=y")} + extended_m = {k for k, v in extended_values.items() if v.endswith("=m")} + + added_y = sorted(extended_y - base_y) + removed_y = sorted(base_y - extended_y) + + if added_y or removed_y: + msg = "Config stability check FAILED — =y options changed after applying modules-selection.cfg.\n" + if added_y: + msg += f"\nNew =y options ({len(added_y)}):\n" + for opt in added_y: + msg += f" {opt}=y\n" + if removed_y: + msg += f"\nLost =y options ({len(removed_y)}):\n" + for opt in removed_y: + msg += f" {opt}=y (was in base, now missing)\n" + msg += ( + "\nTo fix: remove the offending CONFIG_*=m lines from modules-selection.cfg " + "that trigger these =y dependencies, or add the required =y options to the " + "base kernel config first." + ) + bb.fatal(msg) + + new_m = extended_m - base_m + bb.note(f"Config extension summary:") + bb.note(f" Base: {len(base_y)} =y, {len(base_m)} =m") + bb.note(f" Extended: {len(extended_y)} =y, {len(extended_m)} =m") + bb.note(f" New =m (additional modules): {len(new_m)}") + bb.note(f" Config stability: PASSED (zero =y changes)") +} + +addtask kernel_extend_config after do_kernel_resin_injectconfig before do_compile + + +python do_check_module_compat() { + """Check CRC compatibility between extended and base kernel. + + Runs AFTER do_install so the extended .ko files are safely in ${D}. + Rebuilds with the base config to get base symvers, then verifies that + all shared symbol CRCs match — ensuring modules will load on the base kernel. + """ + import os + import shutil + import subprocess + + B = d.getVar("B") + S = d.getVar("S") + base_saved = os.path.join(B, ".config.base") + base_config = os.path.join(B, ".config") + + if not os.path.exists(base_saved): + bb.fatal("do_check_module_compat: .config.base not found") + + make_cmd = d.getVar("KERNEL_MAKE_CMD") or "make" + make_opts = d.getVar("EXTRA_OEMAKE") or "" + arch = d.getVar("ARCH") + + # 1. Save extended symvers + extended_symvers = os.path.join(B, "vmlinux.symvers") + if not os.path.exists(extended_symvers): + extended_symvers = os.path.join(B, "Module.symvers") + if not os.path.exists(extended_symvers): + bb.fatal( + "do_check_module_compat: neither vmlinux.symvers nor Module.symvers " + f"found in {B}. Did do_compile run successfully?" + ) + extended_symvers_saved = extended_symvers + ".extended" + shutil.copy(extended_symvers, extended_symvers_saved) + + # 2. Save extended config, restore base, and build base modules for symvers comparison. + extended_config_saved = base_config + ".extended" + shutil.copy(base_config, extended_config_saved) + shutil.copy(base_saved, base_config) + bb.note("Building base modules for symvers comparison") + + cmd = (f'{make_cmd} {make_opts} -C {S} O={B} ARCH={arch} ' + f'KBUILD_MODPOST_NOFINAL=1 modules') + ret = subprocess.run(cmd, shell=True, capture_output=True, text=True) + if ret.returncode != 0: + bb.fatal(f"Base module build failed:\n{ret.stderr}") + + # 3. CRC compatibility check using symvers + def parse_symvers(path): + syms = {} + with open(path) as f: + for line in f: + parts = line.strip().split("\t") + if len(parts) >= 2: + syms[parts[1]] = parts[0] # symbol -> CRC + return syms + + base_symvers = os.path.join(B, os.path.basename(extended_symvers)) + base_syms = parse_symvers(base_symvers) + extended_syms = parse_symvers(extended_symvers_saved) + + mismatched = [] + for sym, ext_crc in extended_syms.items(): + if sym in base_syms: + base_crc = base_syms[sym] + if ext_crc != base_crc and base_crc != "0x00000000" and ext_crc != "0x00000000": + mismatched.append((sym, base_crc, ext_crc)) + + bb.note(f"Module compatibility check:") + bb.note(f" Base symbols: {len(base_syms)}") + bb.note(f" Extended symbols: {len(extended_syms)}") + bb.note(f" Shared symbols: {len(set(base_syms) & set(extended_syms))}") + bb.note(f" CRC mismatches: {len(mismatched)}") + + if mismatched: + sample = mismatched[:20] + lines = [f" {sym}: base={bcrc} ext={ecrc}" for sym, bcrc, ecrc in sample] + bb.fatal( + f"{len(mismatched)} symbol CRC mismatches between base and extended kernel.\n" + f"Modules using these symbols will fail to load.\n" + + "\n".join(lines) + + (f"\n ... and {len(mismatched) - 20} more" if len(mismatched) > 20 else "") + ) + + # 4. Restore extended config and symvers so subsequent tasks see the correct state. + shutil.copy(extended_config_saved, base_config) + shutil.copy(extended_symvers_saved, extended_symvers) + bb.note("Restored extended .config and symvers") + + bb.note("Module compatibility check passed — all CRCs match") +} + +addtask check_module_compat after do_install before do_package + +# Remove files not needed for the loadable modules block +do_install:append() { + rm -f ${D}${nonarch_base_libdir}/modules/${KERNEL_VERSION}/modules.builtin.ranges +} + +# Skip kernel_configcheck since we're using a non-standard config flow +do_kernel_configcheck() { + : +} diff --git a/layers/meta-kernel-modules-block/conf/layer.conf b/layers/meta-kernel-modules-block/conf/layer.conf new file mode 100644 index 000000000..59eb11bc9 --- /dev/null +++ b/layers/meta-kernel-modules-block/conf/layer.conf @@ -0,0 +1,5 @@ +BBPATH .= ":${LAYERDIR}" +BBFILES += "${LAYERDIR}/recipes-*/*/*.bb ${LAYERDIR}/recipes-*/*/*.bbappend" +BBFILE_COLLECTIONS += "meta-kernel-modules-block" +BBFILE_PATTERN_meta-kernel-modules-block = "^${LAYERDIR}/" +LAYERSERIES_COMPAT_meta-kernel-modules-block = "kirkstone scarthgap" diff --git a/layers/meta-kernel-modules-block/conf/samples/bblayers.conf.sample b/layers/meta-kernel-modules-block/conf/samples/bblayers.conf.sample new file mode 100644 index 000000000..76629d00b --- /dev/null +++ b/layers/meta-kernel-modules-block/conf/samples/bblayers.conf.sample @@ -0,0 +1,23 @@ +# LAYER_CONF_VERSION is increased each time build/conf/bblayers.conf +# changes incompatibly +POKY_BBLAYERS_CONF_VERSION = "2" + +BBPATH = "${TOPDIR}" +BBFILES ?= "" + +BBLAYERS ?= " \ + ${TOPDIR}/../layers/meta-balena/meta-balena-rust \ + ${TOPDIR}/../layers/meta-balena/meta-balena-common \ + ${TOPDIR}/../layers/meta-balena/meta-balena-kirkstone \ + ${TOPDIR}/../layers/meta-balena-raspberrypi \ + ${TOPDIR}/../layers/poky/meta \ + ${TOPDIR}/../layers/poky/meta-poky \ + ${TOPDIR}/../layers/meta-openembedded/meta-oe \ + ${TOPDIR}/../layers/meta-openembedded/meta-filesystems \ + ${TOPDIR}/../layers/meta-openembedded/meta-networking \ + ${TOPDIR}/../layers/meta-openembedded/meta-python \ + ${TOPDIR}/../layers/meta-openembedded/meta-perl \ + ${TOPDIR}/../layers/meta-raspberrypi \ + ${TOPDIR}/../layers/meta-cyclonedx \ + ${TOPDIR}/../layers/meta-kernel-modules-block \ + " diff --git a/layers/meta-kernel-modules-block/conf/samples/local.conf.sample b/layers/meta-kernel-modules-block/conf/samples/local.conf.sample new file mode 100644 index 000000000..d4e0b14c6 --- /dev/null +++ b/layers/meta-kernel-modules-block/conf/samples/local.conf.sample @@ -0,0 +1,108 @@ +# Supported machines +#MACHINE ?= "raspberrypi" +#MACHINE ?= "raspberrypi2" +#MACHINE ?= "raspberrypi3" +#MACHINE ?= "raspberrypi3-64" +#MACHINE ?= "revpi-connect" +#MACHINE ?= "revpi-connect-s" +#MACHINE ?= "revpi-connect-4" +#MACHINE ?= "revpi-core-3" +#MACHINE ?= "raspberrypi4-64" +#MACHINE ?= "raspberrypi400-64" +#MACHINE ?= "raspberrypicm4-ioboard" +#MACHINE ?= "npe-x500-m3" +#MACHINE ?= "rt-rpi-300" +#MACHINE ?= "raspberrypi3-unipi-neuron" +#MACHINE ?= "raspberrypi4-superhub" +#MACHINE ?= "raspberrypi4-unipi-neuron" +#MACHINE ?= "raspberrypi5" + +# More info meta-resin/README.md +#TARGET_REPOSITORY ?= "" +#TARGET_TAG ?= "" + +# RaspberryPi specific variables +GPU_MEM = "16" + +# for the moment, we disable vc4 graphics for all but the 64 bits machines +DISABLE_VC4GRAPHICS = "1" +DISABLE_VC4GRAPHICS:remove:raspberrypi3-64 = "1" +DISABLE_VC4GRAPHICS:remove:raspberrypi4-64 = "1" +DISABLE_VC4GRAPHICS:remove:raspberrypi5 = "1" +DISABLE_VC4GRAPHICS:remove:raspberrypi0-2w-64 = "1" +DISABLE_VC4GRAPHICS:remove:revpi-connect-s = "1" + +# RPI BSP uses uncompressed kernel images by default +KERNEL_IMAGETYPE="zImage" +KERNEL_BOOTCMD="bootz" + +# When u-boot is enabled we need to use the "Image" format and the "booti" +# command to load the kernel for 64 bits machines +KERNEL_IMAGETYPE:raspberrypi3-64="Image.gz" +KERNEL_BOOTCMD:raspberrypi3-64 = "booti" +KERNEL_IMAGETYPE:raspberrypi4-64="Image.gz" +KERNEL_BOOTCMD:raspberrypi4-64 = "booti" +KERNEL_IMAGETYPE:raspberrypi5="Image.gz" +KERNEL_BOOTCMD:raspberrypi5 = "booti" +KERNEL_IMAGETYPE:raspberrypi0-2w-64 = "Image.gz" +KERNEL_BOOTCMD:raspberrypi0-2w-64 = "booti" + +# RPI Use u-boot. This needs to be 1 as we use u-boot +RPI_USE_U_BOOT = "1" + +# Set this to 1 to disable quiet boot and allow bootloader shell access +#OS_DEVELOPMENT = "1" + +# Set this to make build system generate resinhup bundles +#RESINHUP ?= "yes" + +# Set this to change the supervisor tag used +#SUPERVISOR_TAG ?= "master" + +# Compress final raw image +#BALENA_RAW_IMG_COMPRESSION ?= "xz" + +# Parallelism Options +BB_NUMBER_THREADS ?= "${@oe.utils.cpu_count()}" +PARALLEL_MAKE ?= "-j ${@oe.utils.cpu_count()}" + +# Resin specific distros +DISTRO ?= "resin-systemd" + +# Custom downloads directory +#DL_DIR ?= "${TOPDIR}/downloads" + +# Custom sstate directory +#SSTATE_DIR ?= "${TOPDIR}/sstate-cache" + +# Inheriting this class has shown to speed up builds due to significantly lower +# amounts of data stored in the data cache as well as on disk. +# http://www.yoctoproject.org/docs/latest/mega-manual/mega-manual.html#ref-classes-rm-work +#INHERIT += "rm_work" + +# Remove the old image before the new one is generated to save disk space when RM_OLD_IMAGE is set to 1, this is an easy way to keep the DEPLOY_DIR_IMAGE clean. +RM_OLD_IMAGE = "1" + +# Additional image features +USER_CLASSES ?= "buildstats" + +# By default disable interactive patch resolution (tasks will just fail instead): +PATCHRESOLVE = "noop" + +# Disk Space Monitoring during the build +BB_DISKMON_DIRS = "\ + STOPTASKS,${TMPDIR},1G,100K \ + STOPTASKS,${DL_DIR},1G,100K \ + STOPTASKS,${SSTATE_DIR},1G,100K \ + HALT,${TMPDIR},100M,1K \ + HALT,${DL_DIR},100M,1K \ + HALT,${SSTATE_DIR},100M,1K" + +CONF_VERSION = "2" + +HOSTTOOLS += "docker iptables" + +LICENSE_FLAGS_ACCEPTED = "synaptics-killswitch" + +# CycloneDX SBOM and VEX generation +INHERIT += "cyclonedx-export" diff --git a/layers/meta-kernel-modules-block/recipes-core/images/balena-kernel-modules-block.bb b/layers/meta-kernel-modules-block/recipes-core/images/balena-kernel-modules-block.bb new file mode 100644 index 000000000..774ca3c89 --- /dev/null +++ b/layers/meta-kernel-modules-block/recipes-core/images/balena-kernel-modules-block.bb @@ -0,0 +1,31 @@ +DESCRIPTION = "Kernel modules block image containing all modules" +LICENSE = "MIT" + +inherit image + +IMAGE_INSTALL = "kernel-modules" + +# Exclude kernel-image - we only need modules, not the kernel itself +PACKAGE_EXCLUDE = "kernel-image kernel-image-*" + +IMAGE_LINGUAS = "" +VIRTUAL-RUNTIME_init_manager = "" +INITRAMFS_IMAGE = "" +IMAGE_FSTYPES = "tar.gz" + +# Remove unnecessary files from the image - we only need modules +remove_unnecessary_files() { + # Remove empty symlinks + rm -f ${IMAGE_ROOTFS}/bin ${IMAGE_ROOTFS}/lib ${IMAGE_ROOTFS}/sbin + + # Remove etc files (not needed for modules) + rm -rf ${IMAGE_ROOTFS}/etc + + # Remove run directory + rm -rf ${IMAGE_ROOTFS}/run + + # Remove empty usr/bin created by usrmerge symlinks preprocess + rm -rf ${IMAGE_ROOTFS}/usr/bin +} + +IMAGE_PREPROCESS_COMMAND += "remove_unnecessary_files;" diff --git a/layers/meta-kernel-modules-block/recipes-kernel/linux/files/modules-selection.cfg b/layers/meta-kernel-modules-block/recipes-kernel/linux/files/modules-selection.cfg new file mode 100644 index 000000000..a2255fdab --- /dev/null +++ b/layers/meta-kernel-modules-block/recipes-kernel/linux/files/modules-selection.cfg @@ -0,0 +1,57 @@ +# Curated kernel module selection for balena kernel-modules-block +# +# Only list modules that are DISABLED (# not set) in the base kernel config. +# Only tristate options with minimal =y dependencies should be listed here. +# If adding a module causes the config stability check to fail (new =y options +# appear), either remove that module or add its =y dependencies to the base +# kernel config first. + +# CAN bus - additional controllers (platform, SPI, USB) +CONFIG_CAN_M_CAN_PLATFORM=m +CONFIG_CAN_M_CAN_TCAN4X5X=m +CONFIG_CAN_CAN327=m +CONFIG_CAN_ESD_USB=m +CONFIG_CAN_ETAS_ES58X=m +CONFIG_CAN_MCBA_USB=m +CONFIG_CAN_UCAN=m + +# GNSS/GPS receiver drivers (u-blox, MediaTek, SiRF, USB) +CONFIG_GNSS_UBX_SERIAL=m +CONFIG_GNSS_MTK_SERIAL=m +CONFIG_GNSS_SIRF_SERIAL=m +CONFIG_GNSS_USB=m + +# IIO Sensors - newer Bosch IMUs +CONFIG_BMI088_ACCEL=m +CONFIG_BMI323_I2C=m +CONFIG_BMI323_SPI=m + +# IIO Sensors - ST gyroscope, magnetometer, pressure, 9-axis +CONFIG_IIO_ST_GYRO_3AXIS=m +CONFIG_IIO_ST_MAGN_3AXIS=m +CONFIG_IIO_ST_LSM9DS0=m +CONFIG_IIO_ST_PRESS=m + +# IEEE 802.15.4 / Zigbee/Thread (additional hardware) +CONFIG_IEEE802154_CA8210=m +CONFIG_IEEE802154_MCR20A=m +CONFIG_IEEE802154_ADF7242=m +CONFIG_IEEE802154_HWSIM=m + +# 1-Wire slave devices +CONFIG_W1_SLAVE_DS2405=m +CONFIG_W1_SLAVE_DS2805=m +CONFIG_W1_SLAVE_DS250X=m +CONFIG_W1_MASTER_UART=m + +# NFC (NXP PN5xx family) +CONFIG_NFC_PN533_USB=m +CONFIG_NFC_PN533_I2C=m +CONFIG_NFC_PN532_UART=m + +# GPIO expanders (I2C) +CONFIG_GPIO_PCA9570=m + +# HID - USB-to-I2C/SPI bridge (useful for sensor prototyping) +CONFIG_HID_CP2112=m +CONFIG_HID_FT260=m diff --git a/layers/meta-kernel-modules-block/recipes-kernel/linux/linux-raspberrypi_%.bbappend b/layers/meta-kernel-modules-block/recipes-kernel/linux/linux-raspberrypi_%.bbappend new file mode 100644 index 000000000..5f8d6f29b --- /dev/null +++ b/layers/meta-kernel-modules-block/recipes-kernel/linux/linux-raspberrypi_%.bbappend @@ -0,0 +1,13 @@ +# Kernel modules extension block configuration +# +# Enables a curated set of additional kernel modules while preserving ABI +# compatibility with the base OS kernel. See kernel-modules-extension.bbclass. + +FILESEXTRAPATHS:prepend := "${THISDIR}/files:" + +# Track modules-selection.cfg content in the task hash so changes trigger +# re-execution. NOT in SRC_URI because Yocto auto-merges .cfg files into +# the kernel config — the bbclass applies it manually after saving the base. +do_kernel_extend_config[file-checksums] += "${THISDIR}/files/modules-selection.cfg:True" + +inherit kernel-modules-extension