Skip to content
25 changes: 4 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Packages follow the standard Debian/Ubuntu kernel naming convention:
| `linux-image-<KVER>` | Kernel image, modules, DTBs | `linux-image-7.2.0-qcom-next-20260826` |
| `linux-headers-<KVER>` | Headers for out-of-tree modules | `linux-headers-7.2.0-qcom-next-20260826` |
| `linux-image-<KVER>-dbg` | Debug symbols | `linux-image-7.2.0-qcom-next-20260826-dbg` |
| `linux-headers-<KVER>-dbgsym` | Headers debug symbols, generated automatically by debhelper | `linux-headers-7.2.0-qcom-next-20260826-dbgsym` |
| `<BINPKG>` | Image metapackage tracking the newest kernel image | `linux-image-qcom-next` |
| `<HDRPKG>` | Headers metapackage tracking the newest headers | `linux-headers-qcom-next` |

Expand All @@ -28,9 +27,10 @@ build matrix (`binpkg` / derived headers name). They stay constant across
snapshots and depend on the newest versioned package, so installing
`linux-image-qcom-next` follows the latest build of that variant.

`debian/control.in` declares five of these; the `-dbgsym` package is produced by
debhelper rather than declared, so a build publishes six binary packages per
variant.
`debian/control.in` declares all five, so a build publishes five binary packages
per variant. `dh_strip` is run with `--no-automatic-dbgsym`, so debhelper
generates no additional `-dbgsym` package: debug symbols are shipped only by the
declared `linux-image-<KVER>-dbg`.

**`-qcom`** is a static flavour suffix appended by the packaging, identifying
Qualcomm-packaged kernels independently of the branch name.
Expand Down Expand Up @@ -217,9 +217,6 @@ clone → prepare → build. Run it from the repo root.
# Build with explicit LOCALVERSION
./build-kernel.sh --tag qcom-next-7.2-rc7-20260826 --localversion qcom-next-20260826

# Build debug variant
./build-kernel.sh --latest-tag --localversion debug --profiles debug

# Use local kernel source (skip clone)
./build-kernel.sh --local-source /path/to/kernel-source --localversion qcom-next-20260826

Expand Down Expand Up @@ -442,7 +439,6 @@ this fixed order:
| 2 | `debian/rules` | Disable `CONFIG_LOCALVERSION_AUTO` (prevents git hash in `uname -r`) |
| 3 | Unified fragment pipeline | `arch/arm64/configs/prune.config`, then `arch/arm64/configs/qcom.config` (both if present), then `debian/config/*.config` (sorted) |
| 4 | `debian/rules` | Re-check `CONFIG_LOCALVERSION_AUTO` (merges may re-enable it) |
| 5 | `arch/arm64/configs/debug.config` | Debug options — only when `DEB_BUILD_PROFILES=debug` |

### Unified config fragment pipeline (step 3)

Expand Down Expand Up @@ -562,18 +558,6 @@ EOF
Commit it to `config-available/`. It will not affect any build until explicitly
activated via `--enable-configs my-feature` or a manual copy to `config/`.

### Debug build (step 5)

```bash
# Via build-kernel.sh
./build-kernel.sh --latest-tag --profiles debug

# Via dpkg-buildpackage directly
DEB_BUILD_PROFILES=debug dpkg-buildpackage -us -uc -b
```

Merges `arch/arm64/configs/debug.config` from the kernel source if present.

---

## Configuration knobs
Expand All @@ -585,7 +569,6 @@ Merges `arch/arm64/configs/debug.config` from the kernel source if present.
| Always-on config | Add `*.config` to `debian/config/` | Applied on every build |
| Optional config | `--enable-configs name` or copy from `debian/config-available/` | Activated per build |
| Out-of-tree build dir | `O=<dir>` or `KBUILD_OUTPUT=<dir>` | Reads artifacts from objdir |
| Debug build | `DEB_BUILD_PROFILES=debug` or `--profiles debug` | Merges `arch/arm64/configs/debug.config` |
| Parallel jobs | `DEB_BUILD_OPTIONS=parallel=N` | Controls `make -jN` |

---
Expand Down
3 changes: 1 addition & 2 deletions build-kernel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ OPTIONS:
(default: $DEFAULT_DISTRO)
--build-mode MODE docker|native|sbuild (default: $DEFAULT_BUILD_MODE)
--docker-build PATH Path to docker_deb_build.py (docker mode)
--profiles PROFILES DEB_BUILD_PROFILES (e.g. debug)
--profiles PROFILES DEB_BUILD_PROFILES (default: none)
--kernel-config LIST Extra config fragments, beyond debian/config-available/
which is always applied in full. An "intree:" prefix
names a path relative to the kernel source root
Expand All @@ -76,7 +76,6 @@ EXAMPLES:
$0 --local-source /path/to/kernel --build-mode native
$0 --local-source /path/to/kernel --kver-extra -mybuild
$0 --latest-tag --kernel-config docker,systemd-boot
$0 --latest-tag --profiles debug

DISTRIBUTIONS:
noble Ubuntu 24.04 LTS
Expand Down
22 changes: 10 additions & 12 deletions debian/rules
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ override_dh_auto_configure:
# invocation (later fragments win, overrides are reported), followed by
# make olddefconfig to resolve the resulting dependencies.
# 4. Re-check CONFIG_LOCALVERSION_AUTO (merges may re-enable it)
# 5. arch/arm64/configs/debug.config — only when DEB_BUILD_PROFILES=debug
# ---------------------------------------------------------------------------

# Step 1: Base defconfig
Expand Down Expand Up @@ -243,16 +242,6 @@ override_dh_auto_configure:
echo "# CONFIG_LOCALVERSION_AUTO is not set" >> "$$CONFIG_FILE"; \
fi

# Step 5: Merge debug.config if debug build profile is active.
@if echo "$(DEB_BUILD_PROFILES)" | grep -qw "debug"; then \
if [ -f arch/arm64/configs/debug.config ]; then \
echo "Debug profile active: merging arch/arm64/configs/debug.config..."; \
$(MAKE) $(KBUILD_O_ARG) ARCH=$(ARCH) debug.config; \
else \
echo "Warning: debug profile requested but arch/arm64/configs/debug.config not found"; \
fi; \
fi

override_dh_auto_build:
# Read LOCALVERSION from debian/localversion (written by 'prepare' target).
# This avoids the fragile back-derivation from the source package name that
Expand Down Expand Up @@ -386,7 +375,7 @@ override_dh_auto_install:
src_mod="$$mod"; \
[ -n "$$OBJ_DIR" ] && src_mod="$$OBJ_DIR/$$mod"; \
[ -f "$$src_mod" ] || continue; \
dbg="$$DBG_PKG/usr/lib/debug/lib/modules/$$BASE/$$mod"; \
dbg="$$DBG_PKG/usr/lib/debug/lib/modules/$$BASE/kernel/$$mod"; \
mkdir -p "$${dbg%/*}"; \
$(OBJCOPY) --only-keep-debug "$$src_mod" "$$dbg" 2>/dev/null || \
cp -a "$$src_mod" "$$dbg"; \
Expand Down Expand Up @@ -423,6 +412,15 @@ override_dh_auto_install:
--objcopy "$(OBJCOPY)" \
--modules-manifest "$(CURDIR)/debian/dkms-modules"

# Ignore files shipped in debug package.
override_dh_dwz:
dh_dwz -X/usr/lib/debug/

# Ignore files shipped in debug package.
# Don't build "-dbgsym" packages.
override_dh_strip:
dh_strip -X/usr/lib/debug/ --no-automatic-dbgsym

# Skip automatic dh_installmodules — called manually in override_dh_installdeb.
override_dh_installmodules:
@echo "Skipping automatic dh_installmodules (called manually in override_dh_installdeb)"
Expand Down
118 changes: 106 additions & 12 deletions debian/scripts/bundle-dkms-modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ set -euo pipefail
# 2. Reads PACKAGE_NAME / PACKAGE_VERSION from the package's dkms.conf.
# 3. Builds the module with `dkms build` against the staged kernel headers,
# using a private --dkmstree (mktemp) to avoid writing to /var/lib/dkms/.
# 4. Judges the outcome by artifact presence, not dkms exit code.
# On failure: prints make.log tail (build failure) or BUILD_EXCLUSIVE gate
# analysis (skip), then hard-fails — a manifest entry is a presence contract.
# 5. For each produced .ko:
# 4. Prints the dkms make.log for every module, built or not: the private
# dkms tree is deleted on exit, so this is the only record left in CI.
# 5. Judges the outcome by artifact presence, not dkms exit code.
# On failure: adds BUILD_EXCLUSIVE gate analysis when dkms attempted no
# build, then hard-fails — a manifest entry is a presence contract.
# 6. For each produced .ko:
# - Collision-checks against already-bundled modules and in-tree modules.
# - Installs to <image-pkg-dir>/lib/modules/<kver>/extra/<name>.ko
# - Extracts debug symbols to <dbg-pkg-dir>/usr/lib/debug/lib/modules/<kver>/extra/<name>.ko
Expand Down Expand Up @@ -279,6 +281,38 @@ log_info " objcopy: $OBJCOPY"
log_info " modules: $DKMS_MODULES"
echo

# ---------------------------------------------------------------------------
# Report any dkms configuration present on the build host.
#
# dkms hardcodes the paths it reads configuration from: /etc/dkms/framework.conf
# and /etc/dkms/framework.conf.d/*.conf in read_framework_conf, and the
# /etc/dkms/<module>*.conf overrides in read_conf. None of them can be
# redirected by an option or an environment variable, so a build cannot opt out
# of whatever the host happens to ship; it can only be explicit about it.
#
# Most of the exposure is already closed: framework.conf accepts a fixed
# variable list, it is sourced before the command line is parsed so --dkmstree
# and --kernelsourcedir are higher priority and the --directive passed to dkms
# build is applied after every conf file. What remains is tmp_location,
# parallel_jobs, the compress_*_opts, and post_transaction, the last being an
# arbitrary command dkms will run.
#
# Print whatever is there so a surprising host setting shows up in the build
# log rather than acting silently. This is deliberately not fatal: a build host
# is allowed to have dkms configured.
# ---------------------------------------------------------------------------
_dkms_host_conf=0
for _conf in /etc/dkms/framework.conf /etc/dkms/framework.conf.d/*.conf; do
[[ -e "$_conf" ]] || continue
_dkms_host_conf=1
log_warn "Host dkms configuration in effect: $_conf"
grep -vE '^[[:space:]]*(#|$)' "$_conf" | sed 's/^/ | /' || true
done
if [[ "$_dkms_host_conf" -eq 0 ]]; then
log_info "No host dkms framework configuration found."
fi
echo

# ---------------------------------------------------------------------------
# Private DKMS tree — redirects artifacts away from /var/lib/dkms/ (root-owned,
# not writable under fakeroot / non-root dpkg-buildpackage).
Expand Down Expand Up @@ -346,33 +380,60 @@ for name in $DKMS_MODULES; do
log_info " kernelsourcedir: $HEADERS_DIR"
log_info " dkmstree: $DKMS_TREE"

# ── Report per-module dkms.conf overrides on the build host ──────────────
# read_conf sources these after the vendor dkms.conf, so they can change how
# this module is built. The --directive below still wins over them, but
# anything they set that we do not pin takes effect silently otherwise.
for _conf in "/etc/dkms/$PKG_NAME.conf" \
"/etc/dkms/$PKG_NAME-$PKG_VER.conf" \
"/etc/dkms/$PKG_NAME-$PKG_VER-$KVER.conf" \
"/etc/dkms/$PKG_NAME--$KVER.conf"; do
[[ -e "$_conf" ]] || continue
log_warn "Host override for $PKG_NAME in effect: $_conf"
grep -vE '^[[:space:]]*(#|$)' "$_conf" | sed 's/^/ | /' || true
done

# ── Run dkms build ────────────────────────────────────────────────────────
# Capture exit code separately: dkms exit-code conventions vary across
# versions (a BUILD_EXCLUSIVE skip can exit 0). Outcome is judged by
# artifact presence, not exit code.
#
# --directive STRIP=no stops dkms from stripping the module.
dkms_rc=0
dkms build "$PKG_NAME/$PKG_VER" \
--kernelsourcedir "$HEADERS_DIR" \
--dkmstree "$DKMS_TREE" \
-k "$KVER" \
--arch "$DKMS_ARCH" \
--directive "STRIP=no" \
|| dkms_rc=$?

# ── Report the build log ──────────────────────────────────────────────────
# dkms writes make.log inside the private dkms tree, which is deleted on
# EXIT, so this is the only surviving record of the build in CI.
#
# Print it whether or not the build succeeded. A module that builds can
# still be compiled with the wrong flags, and the compiler command lines
# are the only place that is visible — debian/rules exports KBUILD_VERBOSE,
# so they are all here.
mklog="$(find "$DKMS_TREE/$PKG_NAME/$PKG_VER" -name make.log 2>/dev/null \
| head -1 || true)"
if [[ -n "$mklog" ]]; then
log_step "dkms build log for $PKG_NAME/$PKG_VER ($mklog):"
sed 's/^/ | /' "$mklog"
echo
fi

# ── Judge outcome by artifacts ────────────────────────────────────────────
# A .ko under <tree>/<name>/<ver>/<kver>/ means success.
# dkms's make.log separates the two failure modes:
# - make.log present → build was attempted and failed
# - no make.log → dkms attempted no build (BUILD_EXCLUSIVE gate)
# The log is printed inline because the private dkms tree is deleted on
# EXIT, so it is the only surviving record in CI.
kos="$(find "$DKMS_TREE/$PKG_NAME/$PKG_VER/$KVER" -name '*.ko' 2>/dev/null || true)"

if [[ "$dkms_rc" -ne 0 || -z "$kos" ]]; then
mklog="$(find "$DKMS_TREE/$PKG_NAME/$PKG_VER" -name make.log 2>/dev/null \
| head -1 || true)"
if [[ -n "$mklog" ]]; then
log_error "dkms build failed for $PKG_NAME/$PKG_VER on kernel $KVER (dkms exit $dkms_rc); make.log tail:"
tail -n 300 "$mklog" | sed 's/^/ | /' >&2
log_error "dkms build failed for $PKG_NAME/$PKG_VER on kernel $KVER (dkms exit $dkms_rc); see the build log above."
else
log_error "$PKG_NAME/$PKG_VER produced no module for kernel $KVER; dkms attempted no build (dkms exit $dkms_rc)."
gates="$(grep -E '^[[:space:]]*BUILD_EXCLUSIVE' "$conf" 2>/dev/null || true)"
Expand Down Expand Up @@ -444,9 +505,42 @@ for name in $DKMS_MODULES; do
install -D -m 644 "$ko" "$dest"

# Stage 2: extract debug symbols before stripping (non-destructive read)
# Falling back to a full copy keeps the -dbg package usable when objcopy
# cannot extract, but that is a degradation, not a normal outcome:
# report what objcopy said instead of discarding it.
mkdir -p "${dbg%/*}"
"$OBJCOPY" --only-keep-debug "$dest" "$dbg" 2>/dev/null \
|| cp -a "$dest" "$dbg"
if ! objcopy_err="$("$OBJCOPY" --only-keep-debug "$dest" "$dbg" 2>&1)"; then
log_warn "objcopy --only-keep-debug failed for $b; copying the module instead"
printf '%s\n' "$objcopy_err" | sed 's/^/ | /' >&2
cp -a "$dest" "$dbg"
fi

# Stage 2b: the extraction above succeeds even when there is nothing to
# extract, so assert the result actually carries DWARF.
#
# Capture readelf's output rather than piping it into `grep -q`: grep -q
# exits at the first match, readelf dies of SIGPIPE on its next write,
# and under `set -o pipefail` the pipeline reports that failure — firing
# this assertion on a file that does carry DWARF. Whether readelf gets
# far enough to be killed depends on stdio flush timing, so the pipeline
# form fails only sometimes, which is worse than failing always.
if ! dbg_sections="$(readelf -SW "$dbg" 2>&1)"; then
log_error "readelf failed on the extracted debug file for $b"
log_error " debug: $dbg"
printf '%s\n' "$dbg_sections" | sed 's/^/ | /' >&2
exit 1
fi
if ! grep -q '\.debug_info' <<< "$dbg_sections"; then
log_error "No DWARF in the extracted debug file for $b"
log_error " module: $ko"
log_error " debug: $dbg"
log_error "The -dbg package would ship a debug file with no symbols."
log_error "Check that the module was compiled with debug information"
log_error "and that nothing stripped it before this script ran."
log_error "Sections in the extracted debug file:"
printf '%s\n' "$dbg_sections" | sed 's/^/ | /' >&2
exit 1
fi

# Stage 3: strip the shipped copy in place
strip --strip-debug "$dest"
Expand Down
20 changes: 0 additions & 20 deletions prepare-source.sh
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,6 @@ OPTIONS:
redundant, since that fragment is already applied.
Entries are processed in LC_ALL=C sorted order.

Debug:
--debug Enable debug build: copies arch/arm64/configs/debug.config
from the kernel source into debian/config/ so it is
applied as a config fragment during the build.

Paths:
--debian-dir DIR Path to the debian/ packaging directory
(default: $DEBIAN_DIR)
Expand Down Expand Up @@ -113,7 +108,6 @@ SRCPKG="$DEFAULT_SRCPKG"
BINPKG="$DEFAULT_BINPKG"
DEBIAN_REVISION="$DEFAULT_DEBIAN_REVISION"
KERNEL_CONFIG=""
DEBUG=false

while [[ $# -gt 0 ]]; do
case $1 in
Expand All @@ -126,7 +120,6 @@ while [[ $# -gt 0 ]]; do
--debian-revision) DEBIAN_REVISION="$2"; shift 2 ;;
--kernel-config) KERNEL_CONFIG="$2"; shift 2 ;;
--debian-dir) DEBIAN_DIR="$2"; shift 2 ;;
--debug) DEBUG=true; shift ;;
-h|--help) usage ;;
*) log_error "Unknown option: $1"; exit 1 ;;
esac
Expand Down Expand Up @@ -178,7 +171,6 @@ log_info " Debian revision: $DEBIAN_REVISION"
[[ -n "$LOCALVERSION" ]] && log_info " LOCALVERSION: $LOCALVERSION"
[[ -n "$KVER_EXTRA" ]] && log_info " KVER_EXTRA: $KVER_EXTRA"
[[ -n "$KERNEL_CONFIG" ]] && log_info " Kernel config: $KERNEL_CONFIG"
[[ "$DEBUG" == true ]] && log_info " Debug build: yes"
echo

# ── Inject debian/ ───────────────────────────────────────────────────────────
Expand Down Expand Up @@ -285,18 +277,6 @@ if [[ -n "$KERNEL_CONFIG" ]]; then
done < <(printf '%s\n' "${CFG_LIST[@]}" | tr -d ' ' | LC_ALL=C sort)
fi

# ── Debug config fragment ─────────────────────────────────────────────────────
if [[ "$DEBUG" == true ]]; then
DEBUG_CONFIG="$SOURCE_DIR/kernel/configs/debug.config"
if [[ -f "$DEBUG_CONFIG" ]]; then
mkdir -p "$SOURCE_DIR/debian/config"
cp "$DEBUG_CONFIG" "$SOURCE_DIR/debian/config/debug.config"
log_info "Copied kernel/configs/debug.config into debian/config/"
else
log_warn "kernel/configs/debug.config not found — debug config will not be applied"
fi
fi

# ── Prepare: generate control, changelog, localversion, pkgversion ───────────
log_step "Running debian/rules prepare..."
PREPARE_ARGS="DISTRO=$DISTRO SRCPKG=$SRCPKG BINPKG=$BINPKG DEBIAN_REVISION=$DEBIAN_REVISION"
Expand Down
Loading