Skip to content

packaging: ship DKMS modules in their own binary packages - #123

Open
Christopher Obbard (obbardc) wants to merge 7 commits into
mainfrom
wip/obbardc/dkms-modules-split
Open

Christopher Obbard (obbardc) wants to merge 7 commits into
mainfrom
wip/obbardc/dkms-modules-split

Conversation

@obbardc

@obbardc Christopher Obbard (obbardc) commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Every module named in DKMS_MODULES is currently built at dpkg-buildpackage
time and copied into linux-image-<KVER> under /lib/modules/<KVER>/extra/. A
system that installs the kernel gets kgsl, camx, iris-vpu and
audioreach whether or not it has the hardware for any of them, and the rootfs
loads them when matching hardware appears.

The only way to decline one is a global /etc/modprobe.d blacklist in the
rootfs. That file is not tied to any kernel package, so it survives kernel
upgrades and kernel switches and goes on suppressing the out-of-tree modules
for kernels that wanted them. The unit of choice should be a package, not a
rootfs-wide config file.

Each module now ships in its own <name>-modules-<KVER> package with a
matching -dbg, an unversioned <BINPKG>-modules-<name> metapackage tracking
the newest build of the variant, and one <BINPKG>-modules covering the whole
set. DKMS_MODULES=kgsl,camx yields 12 binary packages where it used to yield
5; an empty list still yields the same 5 with an unchanged Build-Depends.

The modules also move from extra/ to updates/qli/. extra/ is a Fedora
convention and is not in depmod's default search order on Debian at all, so the
bundled modules have had the lowest precedence rather than the highest.
updates/qli/ is the structural sibling of the updates/dkms/ a real DKMS
install would use, which is also why the new packages Conflicts/Replaces
the corresponding -dkms package: two copies at equal precedence would leave
which one loads up to depmod's ordering.

System.map comes first in the series and is a prerequisite, not a drive-by:
the depmod snippet debhelper generates for every package shipping a module is
guarded by [ -e /boot/System.map-<KVER> ], and the image did not ship one, so
nothing on the target ever regenerated modules.dep. That is already wrong
today — make modules_install writes modules.dep before the bundler adds
anything under extra/ — and it would leave the new packages unloadable.

Not in this PR, deliberately

  • No config files. The module packages ship .ko files and nothing else.
  • No matrix changes.

The image ships /boot/vmlinuz-<kver> and /boot/config-<kver> but not
System.map. That is not just a missing convenience file: the depmod snippet
debhelper generates for every package shipping a kernel module is

    if [ -e /boot/System.map-#KVERS# ]; then
            depmod -a -F /boot/System.map-#KVERS# #KVERS# || true
    fi

so with no System.map installed the snippet does nothing at all.

Today that is already wrong. make modules_install writes modules.dep before
bundle-dkms-modules.sh adds anything under /lib/modules/<kver>/extra/, so the
modules.dep we ship has never listed the bundled DKMS modules, and nothing on
the target regenerates it. modprobe kgsl cannot work on an installed image.

Install System.map-<kver> alongside the other boot artifacts, and treat a
missing one as an error rather than copying it if it happens to be there: it
is produced by every kernel build that produced the vmlinuz next to it, so its
absence means something is wrong with the build tree, not with the kernel's
configuration.

This is also what Debian and Ubuntu kernels ship, and what crash,
makedumpfile and perf look for.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The bundler writes into two staging trees named on the command line,
--image-pkg-dir and --dbg-pkg-dir. Once each module ships in its own binary
package that stops being enough: the set of trees to write into is derived
from the module list, not known when the script is invoked, so the caller can
only name the directory the trees are created in.

Add --stage-root for that directory -- debian/ in a source package -- and
require it. Nothing reads it yet; this commit only establishes the option, its
validation and its plumbing from debian/rules, so the commit that moves the
staging is about the move alone.

Required rather than optional, and with no default derived from the other
paths: every caller already passes four staging arguments, a fifth is no extra
burden, and guessing debian/ from the parent of --image-pkg-dir would be
wrong for the standalone developer path the script documents.

Validated the way --headers-dir is: absolute, because the script is
documented as callable from anywhere, and existing, because it is a directory
the caller staged rather than one we create.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
Every module named in DKMS_MODULES was copied into linux-image-<kver> under
/lib/modules/<kver>/extra/, so a system installing the kernel got kgsl, camx,
iris-vpu and audioreach whether or not it had the hardware for any of them,
and the only way to decline one was a rootfs-wide /etc/modprobe.d blacklist.
That blacklist is not tied to any kernel package, so it survives kernel
upgrades and kernel switches and goes on suppressing the modules for kernels
that wanted them.

Ship each module in its own <name>-modules-<kver> package instead, with its
debug symbols in a matching -dbg. The modules are still built from the same
-dkms source against the kernel produced by the same build, and a listed
module is still a presence contract; only where they land changes.

'prepare' appends a pair of stanzas per entry in DKMS_LIST, substituted from
the new debian/control-dkms.in. They cannot be spelled in control.in because
there is one per element of a list that is a build input, and they come from
the same DKMS_LIST that drives Build-Depends and the dkms-modules manifest, so
a module cannot be declared in control without being built or vice versa.

Conflicts and Replaces, both unversioned, against the -dkms package. The two
forms are deliberately mutually exclusive: a -dkms install builds a second
copy into updates/dkms/, which sits at the same depmod precedence as ours, so
allowing both would leave which module loads up to depmod's ordering. Replaces
costs nothing and makes the overlap declaratively legal rather than
incidentally avoided.

The modules move from extra/ to updates/qli/. extra/ is not in depmod's
default search order on Debian at all -- it is Fedora's convention, and dkms
picks it only there -- so the bundled modules have had the *lowest* precedence
rather than the highest. updates/qli/ is the exact structural sibling of the
updates/dkms/ a real DKMS install would use.

Three consequences handled here:

- The in-tree collision guard matters more, not less. Under extra/ an in-tree
  module of the same name won; under updates/ ours silently wins. The guard
  stays and its message now says which way the precedence runs.

- The duplicate-basename guard had to change shape. It tested [[ -e "$dest" ]]
  against the one shared image tree; with a tree per package that test sees
  only the current module, while on the target they all unpack into a single
  updates/qli/ and still collide. It is now a run-scoped associative array
  keyed on basename, naming the manifest entry that claimed it.

- A generated stanza with nothing staged under it produces a silently empty
  .deb -- dh_installdeb, dh_gencontrol and dh_builddeb are all happy with an
  empty tree and dh_missing does not look here. Assert a non-zero staged count
  per module package, so a wrong --stage-root fails where it happened.

--dbg-pkg-dir is dropped: the debug trees are derived from --stage-root now.
--image-pkg-dir stays, read-only, for the in-tree module list and the
boot/config-<kver> that BUILD_EXCLUSIVE_CONFIG analysis reads.

Only the modules are shipped. Whatever modprobe.d snippets, udev rules or
initramfs hooks a -dkms package carries are not carried over with them; that
is a separate change, and the package description says so rather than leaving
it to be discovered.

Verified by running prepare over a stub kernel tree: DKMS_MODULES=kgsl,camx
yields 9 binary packages, all parsing clean through Dpkg::Control::Info; an
empty list yields the same 5 packages as before with an unchanged
Build-Depends.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The .ko in <name>-modules-<kver> is compiled from source that lives in a
different source package entirely, and nothing in the Debian metadata says so.
The binary is emitted by src:linux-qcom-next, which does not contain a line of
the module's code, so an archive holding the binary has no reason to keep the
source that produced it.

Built-Using names that source and its exact version, which is what makes the
archive retain it alongside the binary. The bundler is the only place that can
know the value: it is a property of the -dkms package installed in the build
chroot, not of anything in this source tree, so it cannot be spelled in the
template and reaches the stanza as a substvar.

${source:Package} / ${source:Version} rather than the binary's own name and
version. dpkg parses these out of the binary's "Source: name (ver)" field when
it has one and falls back to the binary version otherwise, epoch included.

Writing the file during override_dh_auto_install is safe: dh_prep is the only
thing that truncates a .substvars and it runs before dh_auto_install, while
every writer after that point merges through addsubstvar. The write filters
any previous key out rather than appending, because this script is documented
as callable by hand outside the dh sequence, where nothing has cleared the
file and a second run would otherwise leave two copies of the key.

A failed dpkg-query is fatal with our own message rather than dpkg's. The
package resolved through dpkg -L a few lines earlier, so a failure here means
something genuinely strange rather than a missing build dependency.

The generated files are added to .gitignore and debian/clean alongside the
other build-time artifacts, so a dirty tree does not carry them.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The modules a variant builds are only installable by their versioned names,
<name>-modules-<KVER>. That name changes on every snapshot, so nothing can
depend on "the kgsl modules for this kernel" the way linux-image-qcom-next
already lets something depend on "the newest image for this variant": an
image metapackage exists, a module one does not, and a user who wants the
modules to follow the kernel has to reinstall by hand after each build.

Append a third stanza per DKMS_LIST entry, <BINPKG>-modules-<name>, depending
on that entry's versioned package at (= ${binary:Version}). It is generated in
the same loop, from the same debian/control-dkms.in, so a module cannot gain a
metapackage without being built or be built without gaining one.

Named off BINPKG rather than from a stem derived out of it. The variant
already has exactly one unversioned name the matrix gives it, and extending it
means every unversioned package a variant publishes shares one prefix --
linux-image-qcom-next, linux-image-qcom-next-modules-kgsl -- so they list
together and no second naming scheme has to be kept in step with the first.
The substitution is @binpkg@, which control.in already spells, so the loop
only had to pass it through.

No -dbg metapackage. Debug symbols are fetched for a specific build, which is
the case the versioned name already serves.

Verified by running prepare over a stub kernel tree: DKMS_MODULES=kgsl,camx
now yields 11 binary packages, parsing clean through Dpkg::Control::Info, and
an empty list still yields the same 5 with an unchanged Build-Depends.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
A variant now has one metapackage per module, but nothing names the set. An
image or a rootfs recipe that wants everything this kernel was built with has
to enumerate the modules itself, which means the list lives in two places --
the dkms field in ci/build-matrix.json and whatever consumes the packages --
and drifts the first time one of them gains an entry.

Append one <BINPKG>-modules stanza, generated from the new
debian/control-dkms-all.in, depending on every <BINPKG>-modules-<name> the
loop above just emitted. Installing that one name pulls in the whole
out-of-tree module set for the variant, and the set is described where it is
decided rather than by whoever installs it.

It depends on the metapackages, not on the versioned packages, so what it
names survives a snapshot: only the metapackages underneath it have to
re-point at new versioned names, and this stanza's Depends list changes only
when DKMS_MODULES itself does.

Emitted only when DKMS_LIST is non-empty. A variant that bundles nothing would
otherwise publish an empty metapackage depending on nothing, which is not a
thing anyone would install on purpose. @DKMS_META_DEPENDS@ is appended after
${misc:Depends} the same way @DKMS_BUILD_DEPENDS@ is appended to
Build-Depends, so the field never carries a dangling comma.

Verified by running prepare over a stub kernel tree: DKMS_MODULES=kgsl,camx
now yields 12 binary packages, parsing clean through Dpkg::Control::Info, with
linux-image-qcom-next-modules depending on both per-module metapackages;
BINPKG=linux-image-qcom-next-debug names them all off that prefix instead; and
an empty list still yields the same 5 packages with an unchanged Build-Depends.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
The docs still described the previous arrangement throughout: modules bundled
into linux-image-<KVER>, five binary packages per variant, DKMS debug symbols
under the image's -dbg in extra/. All of that changed.

debian/README.md carries the substance. The "DKMS module bundling" section
becomes "Out-of-tree DKMS modules" and gains what the arrangement now is: the
packages each list entry produces, why they Conflict with the -dkms package
they came from, what Built-Using records and why the module source is never
copied into src:linux-qcom-next.

Two things there are worth writing down because they are not visible in any
one file. Why updates/qli/ rather than extra/: extra/ is not in depmod's
default search order on Debian at all, so the modules had the lowest
precedence rather than the highest. And why /boot/System.map-<KVER> matters to
these packages: the depmod snippet debhelper generates for each of them is
guarded by its presence.

The section also states plainly that only the modules are shipped -- the
modprobe.d snippets, udev rules and initramfs hooks a -dkms package carries
are not carried over with them -- so a reader does not have to infer it from
the absence of any mention.

The remaining edits are corrections rather than additions: the package naming
table and count, the -dbg package's installed paths, the matrix's dkms field
description and artifact table in the top-level README, and the --dkms help in
prepare-source.sh and build-kernel.sh.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
@obbardc

Christopher Obbard (obbardc) commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Initial testing so far so good on trixie && forky:

base kernel package - OK

  • kernel package doesn't ship out-of-tree modules; lsmod | grep kgsl returns nothing.

kgsl - OK

  • installing ./kgsl-modules-7.2.0+qcom-next-20260826-gd49c33864d06-pr123_7.2.0+git20260826~gd49c33864d06-0qli1~bpo13+1~_arm64.deb contains /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/msm_kgsl.o; after rebooting:
debian@rb3gen2:~$ lsmod | grep kgsl                                             
msm_kgsl             2060288  0                                                 
ubwc_config            24576  3 qcom_iris,msm,msm_kgsl                          
llcc_qcom              53248  2 msm,msm_kgsl                                    
mdt_loader             12288  4 qcom_iris,msm,msm_kgsl,qcom_q6v5_pas            
  • removing kgsl-modules package doesn't cause any issues; lsmod | grep kgsl returns nothing again.

camx - OK

$ ls /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/camera* -la
-rw-r--r-- 1 root root 3636608 Sep 18 00:21 /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/camera_qcm6490.ko
-rw-r--r-- 1 root root 5950288 Sep 18 00:21 /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/camera_qcs615.ko
-rw-r--r-- 1 root root 5685616 Sep 18 00:21 /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/camera_qcs9100.ko
-rw-r--r-- 1 root root 5840160 Sep 18 00:21 /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/camera_x1e80100.ko

$ modprobe camera_qcm6490
[   77.432363] camera_qcm6490: loading out-of-tree module taints kernel.
[   77.445443] CAM_INFO: CAM-UTIL: camera_init: 367: No matching device found for camera_kt driver = -19 

iris-vpu - OK

shipped files:

$ ls -la /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/iris_vpu.ko
-rw-r--r-- 1 root root 1547584 Sep 18 00:21 /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/iris_vpu.ko

before reboot:

$ lsmod | grep iris
qcom_iris             204800  0
videobuf2_dma_contig    20480  1 qcom_iris
v4l2_mem2mem           45056  1 qcom_iris
videobuf2_v4l2         32768  2 qcom_iris,v4l2_mem2mem
videodev              327680  3 qcom_iris,videobuf2_v4l2,v4l2_mem2mem
videobuf2_common       73728  5 qcom_iris,videobuf2_dma_contig,videobuf2_v4l2,v4l2_mem2mem,videobuf2_memops
ubwc_config            24576  2 qcom_iris,msm
mdt_loader             12288  3 qcom_iris,msm,qcom_q6v5_pas

after reboot:

$ lsmod | grep iris
iris_vpu              913408  0
qcom_iris             204800  0
videobuf2_dma_contig    20480  1 qcom_iris
v4l2_mem2mem           45056  2 qcom_iris,iris_vpu
videobuf2_v4l2         32768  3 qcom_iris,iris_vpu,v4l2_mem2mem
videodev              327680  4 qcom_iris,videobuf2_v4l2,iris_vpu,v4l2_mem2mem
videobuf2_common       73728  6 qcom_iris,videobuf2_dma_contig,videobuf2_v4l2,iris_vpu,v4l2_mem2mem,videobuf2_memops
mc                     69632  5 videodev,videobuf2_v4l2,videobuf2_common,iris_vpu,v4l2_mem2mem
ubwc_config            24576  3 qcom_iris,msm,iris_vpu
llcc_qcom              53248  2 msm,iris_vpu
mdt_loader             12288  4 qcom_iris,msm,qcom_q6v5_pas,iris_vpu

audioreach - OK

root@rb3gen2:/# ls /lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/audioreach_driver.ko 
/lib/modules/7.2.0+qcom-next-20260826-gd49c33864d06-pr123/updates/qli/audioreach_driver.ko

root@rb3gen2:/# lsmod | grep audioreach

after reboot:

# lsmod | grep audioreach
audioreach_driver     102400  16
snd_q6dsp_common       49152  3 q6apm_lpass_dais,audioreach_driver,q6prm_clocks
apr                    20480  3 snd_q6apm,q6prm,audioreach_driver
snd_soc_core          311296  12 snd_q6apm,q6apm_dai,snd_soc_sc8280xp,snd_soc_qcom_common,q6apm_lpass_dais,snd_soc_hdmi_codec,snd_soc_lpass_wsa_macro,snd_soc_wsa883x,snd_soc_qcom_sdw,snd_soc_lpass_va_macro,audioreach_driver,soundwire_qcom
soundwire_bus        1150976  5 regmap_sdw,snd_soc_wsa883x,snd_soc_qcom_sdw,audioreach_driver,soundwire_qcom
snd_pcm               163840  6 q6apm_dai,q6apm_lpass_dais,snd_soc_hdmi_codec,snd_compress,audioreach_driver,snd_soc_core
snd                   106496  7 snd_soc_qcom_common,snd_soc_hdmi_codec,snd_timer,snd_compress,audioreach_driver,snd_soc_core,snd_pcm

Remaining

  • check package naming is OK.
  • check out-of-tree module path prefix updates is OK.

@obbardc
Christopher Obbard (obbardc) marked this pull request as ready for review September 18, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant