Skip to content

packaging: ship out-of-tree DKMS modules as their own packages - #114

Draft
Christopher Obbard (obbardc) wants to merge 59 commits into
mainfrom
wip/obbardc/dkms-module-packages
Draft

Christopher Obbard (obbardc) wants to merge 59 commits into
mainfrom
wip/obbardc/dkms-module-packages

Conversation

@obbardc

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

Copy link
Copy Markdown
Contributor

Out-of-tree DKMS modules were built against the kernel and installed into
linux-image-<kernelrelease>, which meant the image package's contents depended
on which modules happened to be listed and gave no way to install the kernel
without them.

Each module now produces its own pair of packages,
<name>-modules-<kernelrelease> and its -dbg, installing under
/lib/modules/<kernelrelease>/updates/qli/ and carrying the modprobe.d, udev and
initramfs files the -dkms source ships. They conflict with the corresponding
-dkms package, and record it in Built-Using. The bundler gains --stage-root
so it can stage into a per-module package directory.

Also here: the flavour moves to the end of the kernel release string, as Debian
does, and System.map is installed into the image package.

Depends on both #110 and #107

@obbardc
Christopher Obbard (obbardc) marked this pull request as ready for review September 15, 2026 04:27
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 --
verified both ways against installed packages, where dh-dkms correctly reports
dkms (= 3.2.2-1) rather than its own name.

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.

Verified against dpkg-gencontrol directly, for all three states of the
substvar: set yields "Built-Using: kgsl (= 1.0.6-0qli1)"; present-but-empty
drops the field silently, which is the union-dependency behaviour; and
undefined warns. The bundler always writes the key or exits, so only the first
state occurs in a real build.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
A -dkms package ships more than module source. kgsl-dkms also installs a
modprobe.d snippet, a udev rule and an initramfs hook; audioreach-dkms a udev
rule and a modprobe blacklist; iris-vpu-dkms a modprobe.d snippet. None of it
reached the built module, so the module we ship has been arriving without the
configuration that makes it work, and the only alternatives were to restate
every package's files in the kernel packaging or to let each -dkms package
grow a second delivery path.

Copy them out of the -dkms package instead, at their original paths -- a udev
rule or modprobe.d snippet is found by its location, so rewriting it would
break it. The file list comes from dpkg -L, so a package that grows a udev
rule needs no change here.

Classification is by path prefix rather than by filename, with the longest
matching prefix across the include and exclude lists deciding. That is what
lets the narrow /usr/share/initramfs-tools/ include sit inside the broad
/usr/share/ exclude without either list having to be ordered, and what lets a
--include-prefix given on the command line override a built-in default without
removing it.

A path matching neither list is fatal. Skipping it with a warning would drop
runtime integration the module may need, and that failure surfaces as a module
that loads but misbehaves on a target -- far more expensive to diagnose than a
failed build. It is also how the rest of this script already treats a
surprise: a missing manifest, a module that produces no .ko and a debug file
with no DWARF are all fatal today.

Excluded: /usr/src and /var/lib/dkms, which are DKMS's own machinery; any
/lib/modules content, since the modules we ship are built here; and the
documentation tree. The generated package therefore needs no dependency on
dkms and carries none.

The copyright file is the exception to "copy at the original path": it goes to
debian/<pkg>.copyright and is left to dh_installdocs, which places it under
the generated package's own name rather than the -dkms package's.
dh_installdocs prefers that file over debian/copyright, and dh_compress leaves
a file named copyright alone.

An initramfs hook that is not executable is skipped by update-initramfs
without saying so, and dh_fixperms has no rule that would correct one, so the
mode is asserted rather than assumed.

The package adds and removes kernel modules, so the initramfs may need
rebuilding after it is installed. dh_installinitramfs declares that trigger
itself when it finds a copied hook, so declare it by hand only when it will
not -- otherwise the trigger would be declared twice, and dpkg rejects the
same trigger declared with differing await semantics.

Verified by running the classifier over the real file lists: all 1055 paths in
camx-dkms_1.0.3-0qli2~_arm64.deb classify as exclude, which is correct since
it ships only source and documentation; every runtime file kgsl, iris-vpu and
audioreach install classifies as include, including the initramfs hook under
the excluded /usr/share/; and an unexpected /usr/bin or /opt path classifies
as neither and so fails the build.

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
two 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 how the runtime-file classification
decides -- longest matching prefix across the include and exclude lists, which
is what lets /usr/share/initramfs-tools/ be carried while /usr/share/ is not.

Also documented: dh_installsystemd will auto-enable a copied .service unit and
dh_installtmpfiles picks up a copied tmpfiles.d snippet. Neither is written
anywhere in this packaging -- they follow from staging the file at all -- so
the only place a reader can learn it is here.

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 in the top-level README, prepare-source.sh's --dkms help, and the
DKMS_MODULES header comment in debian/rules, which is what a reader running
prepare by hand sees first.

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.yaml 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 --
mainline and linux-next in the current matrix -- 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>
@obbardc
Christopher Obbard (obbardc) marked this pull request as draft 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