From c7ebdf7a76cd3ca566b12458597bd36ba568a495 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 1/9] Join LOCALVERSION with '+' so release candidates sort below finals systemd-boot sorts boot entries on the BLS 'version' key, which kernel-install writes verbatim from uname -r. Under systemd's version comparison: 7.2.0-rc7-qcom-next-20260821 > 7.2.0-qcom-next-20260826 so every release candidate outranked the final release that followed it, and an rc daily stayed the default boot entry after the release was installed. The deciding comparison is a plain strcmp of "rc" against "qcom", where 'r' > 'q'; there is no notion of a pre-release involved. debian/rules already rewrites -rcN to ~rcN, but only for the Debian version, because a package name may not contain '~' and the binary packages are named linux-image-. So dpkg ordered these correctly all along and only the boot menu was wrong. Joining the suffix with '+' fixes it: systemd compares the separator before the chunk behind it, and '-' < '+'. This is not a local invention -- it is why Debian's own kernels sort correctly (linux-image-7.1.10+deb14-amd64). Upstream bindeb-pkg has the identical ~rc/-rc split and gets away with it for a different reason: it puts a numeric commit count in that slot, and systemd ranks digits above letters. qcom-next was the only scheme putting a lowercase word there. The Debian version is unchanged, byte for byte: DATE_PART reads the date off the end of LOCALVERSION, which still matches. Only uname -r and the binary package names change. Note this renames every binary package. Already-installed kernels under the old names are not upgraded or removed by it -- they keep their existing sort position until purged, so the fix applies to newly installed kernels only. Signed-off-by: Christopher Obbard --- ci/scripts/derive-localversion.sh | 28 +++++++++++++++++++++------- debian/rules | 8 ++++---- prepare-source.sh | 12 ++++++------ 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/ci/scripts/derive-localversion.sh b/ci/scripts/derive-localversion.sh index d36e9b0b..90108396 100755 --- a/ci/scripts/derive-localversion.sh +++ b/ci/scripts/derive-localversion.sh @@ -6,14 +6,28 @@ set -euo pipefail # Derive the LOCALVERSION suffix from a kernel variant and resolved ref. # # For dated tag builds (ref ends in -YYYYMMDD): -# Produces --. -# Example: qcom-next-7.2-rc3-20260722 -> -qcom-next-20260722 +# Produces +-. +# Example: qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 # # For branch-tip builds (ref does not end in a date): # Uses the kernel variant and a short SHA for uniqueness. -# Example: qcom-next @ 07f50dc44edd -> -qcom-next-g07f50dc44edd +# Example: qcom-next @ 07f50dc44edd -> +qcom-next-g07f50dc44edd # --sha is required for branch-tip builds. # +# Why the leading '+' and not '-': +# The suffix ends up in KERNELRELEASE (uname -r), which is the 'version' field +# systemd-boot sorts BLS entries on. systemd compares the separator before the +# chunk behind it, and '-' < '+', so joining with '+' puts every -rcN release +# candidate BELOW the final release that follows it: +# +# 7.2.0-rc7+qcom-next-20260821 < 7.2.0+qcom-next-20260826 +# +# Joining with '-' instead falls through to a plain strcmp of "rc" against +# "qcom", where 'r' > 'q', and every rc outranks its own final release in the +# boot menu. This is the same trick Debian's own kernels rely on +# (linux-image-7.1.10+deb14-amd64). It does not affect the Debian version, +# which spells the release candidate ~rcN and orders correctly either way. +# # Usage: # ci/scripts/derive-localversion.sh --variant qcom-next --ref qcom-next-7.2-rc3-20260722 # ci/scripts/derive-localversion.sh --variant arduino --ref main --sha 07f50dc44edd @@ -24,8 +38,8 @@ set -euo pipefail # --sha SHA Short commit SHA (required for branch-tip builds). # # Output: -# LOCALVERSION suffix printed to stdout (e.g. -qcom-next-20260722). -# Always starts with a dash. +# LOCALVERSION suffix printed to stdout (e.g. +qcom-next-20260722). +# Always starts with a plus. # # Exit codes: # 0 Success. @@ -60,7 +74,7 @@ done # the variant supplies the stable package identity used in LOCALVERSION. if [[ "$REF" =~ -([0-9]{8})$ ]]; then DATE="${BASH_REMATCH[1]}" - echo "-${VARIANT}-${DATE}" + echo "+${VARIANT}-${DATE}" else # Branch-tip build: need SHA for uniqueness. [[ -n "$SHA" ]] || { @@ -69,5 +83,5 @@ else } # Use first 12 chars of SHA for a compact but unambiguous suffix. SHORT_SHA="${SHA:0:12}" - echo "-${VARIANT}-g${SHORT_SHA}" + echo "+${VARIANT}-g${SHORT_SHA}" fi diff --git a/debian/rules b/debian/rules index 1de36c97..3cdb4378 100755 --- a/debian/rules +++ b/debian/rules @@ -47,7 +47,7 @@ KREL_FILE := debian/kernel.release # debian/control before invoking any debian/rules targets. # # Inputs (make variables): -# LOCALVERSION Suffix appended to the base kernel version, e.g. -qcom-next-20260722. +# LOCALVERSION Suffix appended to the base kernel version, e.g. +qcom-next-20260722. # Required unless KVER is given explicitly. # KVER Full kernelrelease string (uname -r). Overrides LOCALVERSION. # KVER_EXTRA Optional extra suffix appended after LOCALVERSION, e.g. -ci42. @@ -70,7 +70,7 @@ KREL_FILE := debian/kernel.release # so an empty DKMS_MODULES is a genuine "bundle nothing" # rather than the missing-manifest misconfiguration that # the bundler rejects. -# debian/localversion Plain file containing the LOCALVERSION suffix (e.g. -qcom-next-20260722). +# debian/localversion Plain file containing the LOCALVERSION suffix (e.g. +qcom-next-20260722). # Read by override_dh_auto_build to pass LOCALVERSION= to make. # debian/pkgversion Plain file containing the full Debian package version # (e.g. 7.2.0-rc3+20260722-0qcom1). @@ -93,8 +93,8 @@ prepare: else \ [ -n "$(LOCALVERSION)" ] || { \ echo "ERROR: LOCALVERSION is required when KVER is not set."; \ - echo " Pass LOCALVERSION= (e.g. LOCALVERSION=-qcom-next-20260722)"; \ - echo " or KVER= (e.g. KVER=7.2.0-rc3-qcom-next-20260722)"; \ + echo " Pass LOCALVERSION= (e.g. LOCALVERSION=+qcom-next-20260722)"; \ + echo " or KVER= (e.g. KVER=7.2.0-rc3+qcom-next-20260722)"; \ exit 1; \ }; \ echo "Reading base kernel version from kernel Makefile..."; \ diff --git a/prepare-source.sh b/prepare-source.sh index f0cbbc72..b5db6efe 100755 --- a/prepare-source.sh +++ b/prepare-source.sh @@ -52,7 +52,7 @@ OPTIONS: -d, --distro DISTRO Target suite: trixie|forky|sid|noble|questing|resolute (default: $DEFAULT_DISTRO) --localversion SUFFIX LOCALVERSION suffix appended to the base kernel - version (e.g. -qcom-next-20260722). + version (e.g. +qcom-next-20260722). Auto-detected from git tag if not specified. --kver-extra SUFFIX Extra suffix appended to the final KVER (e.g. -ci42). @@ -101,7 +101,7 @@ EXAMPLES: # Full CI invocation with all options $0 --source-dir /path/to/kernel \\ --distro trixie \\ - --localversion -qcom-next-20260722 \\ + --localversion +qcom-next-20260722 \\ --srcpkg linux-qcom-next \\ --binpkg linux-image-qcom-next \\ --debian-revision 0qcom1 \\ @@ -153,13 +153,13 @@ VALID_DISTROS=(noble questing resolute trixie forky sid unstable) [[ -d "$DEBIAN_DIR" ]] || { log_error "Debian dir not found: $DEBIAN_DIR"; exit 1; } # ── Helper: derive LOCALVERSION from a tag name ────────────────────────────── -# qcom-next-7.2-rc3-20260722 -> -qcom-next-20260722 +# qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 _auto_localversion() { local tag="$1" if [[ "$tag" =~ ^([a-z-]+)-[0-9]+\.[0-9]+.*-([0-9]+)$ ]]; then - echo "-${BASH_REMATCH[1]}-${BASH_REMATCH[2]}" + echo "+${BASH_REMATCH[1]}-${BASH_REMATCH[2]}" else - echo "-$tag" + echo "+$tag" fi } @@ -172,7 +172,7 @@ if [[ -z "$LOCALVERSION" ]]; then else log_warn "LOCALVERSION not set and no exact git tag found." log_warn "Package will be named linux-image- (no branch/date suffix)." - log_warn "Use --localversion to specify, e.g.: --localversion -qcom-next-20260722" + log_warn "Use --localversion to specify, e.g.: --localversion +qcom-next-20260722" fi fi From aeccb2d853db0d7d248389e207f610a9e79182df Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 2/9] Give the Debian revision stub a packaging revision digit The stub was a bare '0qli', which left a packaging-only rebuild -- same kernel snapshot, changed debian/ -- with no field to bump. Neither of the other two components can absorb it: suite_suffix_mapping is a per-suite constant shared by every row, and the delivery suffix is the Daily/Release marker. Bumping either to force a rebuild through would corrupt what they mean. Make the trailing digit part of the stub, so a rebuild is 0qli1 -> 0qli2 and lands uniformly across every suite: 0qli1~bpo13+1~ < 0qli1~bpo13+1 < 0qli2~bpo13+1 derive-debian-revision.sh now rejects a stub that does not end in a digit, alongside the existing "must not end in ~" check. Both guard the same thing: a stub that quietly loses a field it is supposed to carry. Signed-off-by: Christopher Obbard --- .github/workflows/build-kernel-deb.yml | 6 +++--- README.md | 12 ++++++------ ci/build-matrix.json | 8 ++++---- ci/scripts/derive-debian-revision.sh | 17 ++++++++++++++--- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-kernel-deb.yml b/.github/workflows/build-kernel-deb.yml index 9da42580..aca93182 100644 --- a/.github/workflows/build-kernel-deb.yml +++ b/.github/workflows/build-kernel-deb.yml @@ -53,7 +53,7 @@ on: debian-version-stub: description: 'Advanced: Debian version stub; the selected suite''s mapped suffix and a Daily-style trailing ~ are applied automatically' required: false - default: '0qli' + default: '0qli1' localversion: description: 'Advanced: LOCALVERSION override (auto-derived from the resolved ref if empty)' required: false @@ -124,7 +124,7 @@ on: debian-revision: description: 'Debian revision component of the package version' type: string - default: '0qli~' + default: '0qli1~' localversion: description: 'Override LOCALVERSION suffix (auto-derived from the resolved ref if empty)' type: string @@ -301,7 +301,7 @@ jobs: # non-promoting. env: DEBIAN_REVISION_INPUT: ${{ inputs.debian-revision }} - DEBIAN_VERSION_STUB_INPUT: ${{ inputs.debian-version-stub || '0qli' }} + DEBIAN_VERSION_STUB_INPUT: ${{ inputs.debian-version-stub || '0qli1' }} run: | set -euo pipefail INPUT_REVISION="$DEBIAN_REVISION_INPUT" diff --git a/README.md b/README.md index 1c60c355..ebf73381 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The final Production matrix is conceptually: "binpkg": "linux-image-qcom-next", "kernel_config": [], "dkms": ["kgsl", "camx", "iris-vpu"], - "debian_version_stub": "0qli", + "debian_version_stub": "0qli1", "debian_version_suffix": "~" }, { @@ -76,7 +76,7 @@ The final Production matrix is conceptually: "binpkg": "linux-image-qcom-next", "kernel_config": [], "dkms": ["kgsl", "camx", "iris-vpu"], - "debian_version_stub": "0qli", + "debian_version_stub": "0qli1", "debian_version_suffix": "", "target_workspace": "qli" } @@ -93,9 +93,9 @@ above: | Suite | Daily | Release | | --- | --- | --- | -| Trixie | `0qli~bpo13+1~` | `0qli~bpo13+1` | -| Forky | `0qli~` | `0qli` | -| Resolute | `0qli~26.04.1~` | (not a configured Release suite) | +| Trixie | `0qli1~bpo13+1~` | `0qli1~bpo13+1` | +| Forky | `0qli1~` | `0qli1` | +| Resolute | `0qli1~26.04.1~` | (not a configured Release suite) | `~` always sorts below the same prefix without it in Debian version ordering, so Daily always sorts below Release for the same suite and stub. @@ -439,7 +439,7 @@ The available inputs are: | `srcpkg` | `linux-qcom-next` | Advanced source package identity override. | | `binpkg` | `linux-image-qcom-next` | Advanced image metapackage identity override. | | `kernel-config` | Empty | Advanced extra fragments applied on top of all of `debian/config-available/`, e.g. `intree:arch/arm64/configs/qcom_debug.config`. | -| `debian-version-stub` | `0qli` | Advanced Debian version stub. The selected suite's mapped suffix and a Daily-style trailing `~` are applied automatically; direct builds always use Daily semantics since they are build-only and non-promoting. | +| `debian-version-stub` | `0qli1` | Advanced Debian version stub. The selected suite's mapped suffix and a Daily-style trailing `~` are applied automatically; direct builds always use Daily semantics since they are build-only and non-promoting. | | `localversion` | Auto-derived | Advanced explicit `LOCALVERSION` override. | | `kver-extra` | Empty | Advanced kernel-release suffix. | | `debug-build` | `false` | Advanced debug configuration toggle. | diff --git a/ci/build-matrix.json b/ci/build-matrix.json index 655aefcb..a28175d4 100644 --- a/ci/build-matrix.json +++ b/ci/build-matrix.json @@ -26,7 +26,7 @@ "iris-vpu", "audioreach" ], - "debian_version_stub": "0qli", + "debian_version_stub": "0qli1", "debian_version_suffix": "~" }, { @@ -49,7 +49,7 @@ "iris-vpu", "audioreach" ], - "debian_version_stub": "0qli", + "debian_version_stub": "0qli1", "debian_version_suffix": "" }, { @@ -75,7 +75,7 @@ "iris-vpu", "audioreach" ], - "debian_version_stub": "0qli", + "debian_version_stub": "0qli1", "debian_version_suffix": "~" }, { @@ -101,7 +101,7 @@ "iris-vpu", "audioreach" ], - "debian_version_stub": "0qli", + "debian_version_stub": "0qli1", "debian_version_suffix": "" } ] diff --git a/ci/scripts/derive-debian-revision.sh b/ci/scripts/derive-debian-revision.sh index 71b3af8a..008e993e 100755 --- a/ci/scripts/derive-debian-revision.sh +++ b/ci/scripts/derive-debian-revision.sh @@ -15,11 +15,13 @@ set -euo pipefail # context), so the derivation and its validation live in exactly one place. # # Usage: -# ci/scripts/derive-debian-revision.sh --stub 0qli --suite trixie --delivery-type Daily -# ci/scripts/derive-debian-revision.sh --stub 0qli --suite forky --delivery-type Release --matrix-file ci/build-matrix.json +# ci/scripts/derive-debian-revision.sh --stub 0qli1 --suite trixie --delivery-type Daily +# ci/scripts/derive-debian-revision.sh --stub 0qli1 --suite forky --delivery-type Release --matrix-file ci/build-matrix.json # # Options: -# --stub STUB Debian version stub. Must be non-empty and must not +# --stub STUB Debian version stub, e.g. 0qli1. Must end in a digit: +# that digit is the packaging revision, bumped for a +# rebuild of an unchanged kernel snapshot. Must not # end in ~ (the delivery suffix supplies any # trailing ~). Required. # --suite SUITE Target suite; must have an entry in @@ -62,6 +64,15 @@ done [[ -n "$SUITE" ]] || { echo "ERROR: --suite is required" >&2; exit 1; } [[ -n "$DELIVERY_TYPE" ]] || { echo "ERROR: --delivery-type is required" >&2; exit 1; } [[ "$STUB" != *"~" ]] || { echo "ERROR: --stub must not end in ~ (got '$STUB')" >&2; exit 1; } +# The trailing digit is the packaging revision: it is the only field left to +# bump when the kernel snapshot is unchanged but the packaging is rebuilt. +# suite_suffix_mapping is a per-suite constant and cannot carry it, and the +# delivery suffix is the Daily/Release marker, so a stub without a digit leaves +# a rebuild with nowhere to go. +[[ "$STUB" =~ [0-9]$ ]] || { + echo "ERROR: --stub must end in a digit, the packaging revision (got '$STUB'; use '${STUB}1')" >&2 + exit 1 +} [[ -f "$MATRIX_FILE" ]] || { echo "ERROR: Matrix file not found: $MATRIX_FILE" >&2; exit 1; } jq empty "$MATRIX_FILE" 2>/dev/null \ From 2c4c49f035c5e1d1e5c55ede001917a63af28616 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 3/9] Compose the Debian snapshot forward instead of parsing LOCALVERSION debian/rules recovered the snapshot date by matching the tail of LOCALVERSION, a string it did not build and cannot safely parse. The variant name in the middle is caller-supplied and unbounded, and a branch-tip suffix ends in a 12-hex SHA that can be eight digits: $ echo '+qcom-next-g123456789012' | grep -oE '[0-9]{8}$' 56789012 Such a build was versioned 7.2.0~rc7+56789012-0qli1, naming a snapshot date that never existed and sorting it against real ones. Roughly one branch-tip SHA in ten ends that way, so this is a live bug, not a theoretical one. Anchoring the match would have fixed those refs while leaving the guess in place. Carry the field instead. derive-localversion.sh already knows the snapshot -- it matched the tag to build LOCALVERSION in the first place -- so it now emits both: LOCALVERSION=+qcom-next-20260722 SNAPSHOT=20260722 KEY=VALUE because the workflow appends them straight to GITHUB_ENV. prepare-source.sh gains --snapshot and forwards it, and its tag auto-detect derives both fields in one match. debian/rules takes SNAPSHOT as an input and validates its shape rather than discovering it. Neither caller reconstructs the field when it is absent. An explicit --localversion replaces the derived suffix precisely because the ref no longer describes the build, so parsing it back would reinstate the guess this commit removes. Both paths warn that the Debian version will carry no dated component, which is visible and correctable, unlike a date invented from a SHA. Signed-off-by: Christopher Obbard --- .github/workflows/build-kernel-deb.yml | 15 ++++++++--- README.md | 2 +- ci/scripts/derive-localversion.sh | 29 ++++++++++++++++----- debian/rules | 27 +++++++++++++------- prepare-source.sh | 35 ++++++++++++++++++++------ 5 files changed, 80 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-kernel-deb.yml b/.github/workflows/build-kernel-deb.yml index aca93182..c947b8cf 100644 --- a/.github/workflows/build-kernel-deb.yml +++ b/.github/workflows/build-kernel-deb.yml @@ -273,7 +273,7 @@ jobs: echo "KERNEL_SHA=$KERNEL_SHA" >> "$GITHUB_ENV" echo "Kernel HEAD SHA: $KERNEL_SHA" - - name: Derive LOCALVERSION + - name: Derive version fields env: LOCALVERSION_INPUT: ${{ inputs.localversion }} KERNEL_VARIANT_INPUT: ${{ inputs.kernel-variant || 'qcom-next' }} @@ -281,15 +281,21 @@ jobs: set -euo pipefail OVERRIDE_LV="$LOCALVERSION_INPUT" if [[ -n "$OVERRIDE_LV" ]]; then + # An override supplies no snapshot, and the ref it was chosen to + # replace cannot be trusted to describe it. The Debian version then + # carries no dated component; prepare-source.sh warns about the same. echo "Using explicit LOCALVERSION override: $OVERRIDE_LV" echo "LOCALVERSION=$OVERRIDE_LV" >> "$GITHUB_ENV" + echo "SNAPSHOT=" >> "$GITHUB_ENV" else - LOCALVERSION=$(ci/scripts/derive-localversion.sh \ + # Emits LOCALVERSION= and SNAPSHOT= lines, already in GITHUB_ENV form. + FIELDS=$(ci/scripts/derive-localversion.sh \ --variant "$KERNEL_VARIANT_INPUT" \ --ref "$KERNEL_REF" \ --sha "$(echo "$KERNEL_SHA" | cut -c1-12)") - echo "LOCALVERSION=$LOCALVERSION" >> "$GITHUB_ENV" - echo "Derived LOCALVERSION: $LOCALVERSION" + echo "$FIELDS" >> "$GITHUB_ENV" + echo "Derived version fields:" + echo "$FIELDS" fi - name: Resolve Debian revision @@ -402,6 +408,7 @@ jobs: ) [[ -n "$LOCALVERSION" ]] && ARGS+=(--localversion "$LOCALVERSION") + [[ -n "$SNAPSHOT" ]] && ARGS+=(--snapshot "$SNAPSHOT") [[ -n "$KVER_EXTRA_INPUT" ]] && ARGS+=(--kver-extra "$KVER_EXTRA_INPUT") [[ -n "$KERNEL_CONFIG_INPUT" ]] && ARGS+=(--kernel-config "$KERNEL_CONFIG_INPUT") # Always passed, unlike the optional inputs above: an empty list is a diff --git a/README.md b/README.md index ebf73381..bcb15a68 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ Supporting scripts keep workflow YAML small and testable: | --- | --- | | `ci/scripts/resolve-matrix.sh` | Validates and flattens matrix rows. | | `ci/scripts/resolve-kernel-ref.sh` | Resolves a matrix-selected dated tag or validates a direct ref. | -| `ci/scripts/derive-localversion.sh` | Derives `LOCALVERSION` from the variant and resolved kernel ref. | +| `ci/scripts/derive-localversion.sh` | Derives the version fields from the variant and resolved kernel ref, printing `LOCALVERSION=` and `SNAPSHOT=` lines. `SNAPSHOT` is the dated component of the Debian version and is empty for branch-tip builds. | | `ci/scripts/derive-debian-revision.sh` | Derives the final suite-specific `debian_revision` from `debian_version_stub`, `suite_suffix_mapping`, and delivery type. | ## Architecture diff --git a/ci/scripts/derive-localversion.sh b/ci/scripts/derive-localversion.sh index 90108396..dea72cd7 100755 --- a/ci/scripts/derive-localversion.sh +++ b/ci/scripts/derive-localversion.sh @@ -3,7 +3,14 @@ # SPDX-License-Identifier: BSD-3-Clause-Clear set -euo pipefail -# Derive the LOCALVERSION suffix from a kernel variant and resolved ref. +# Derive the version fields for a build from a kernel variant and resolved ref. +# +# Emits LOCALVERSION (the kernel release suffix) and SNAPSHOT (the dated +# component of the Debian version), both derived from the ref in one place. +# SNAPSHOT is emitted alongside rather than recovered from LOCALVERSION later: +# reading it back out means guessing where the date ends in a string that also +# carries a variant name and, for branch-tip builds, a hex SHA that can end in +# eight digits of its own. # # For dated tag builds (ref ends in -YYYYMMDD): # Produces +-. @@ -38,8 +45,14 @@ set -euo pipefail # --sha SHA Short commit SHA (required for branch-tip builds). # # Output: -# LOCALVERSION suffix printed to stdout (e.g. +qcom-next-20260722). -# Always starts with a plus. +# Two KEY=VALUE lines on stdout, in GITHUB_ENV / 'set -a' form: +# +# LOCALVERSION=+qcom-next-20260722 +# SNAPSHOT=20260722 +# +# LOCALVERSION always starts with a plus. SNAPSHOT is empty for branch-tip +# builds, which have no date; the Debian version then carries no snapshot +# component at all. # # Exit codes: # 0 Success. @@ -73,8 +86,8 @@ done # Dated tags use a trailing YYYYMMDD snapshot. The matrix selects the tag set; # the variant supplies the stable package identity used in LOCALVERSION. if [[ "$REF" =~ -([0-9]{8})$ ]]; then - DATE="${BASH_REMATCH[1]}" - echo "+${VARIANT}-${DATE}" + SNAPSHOT="${BASH_REMATCH[1]}" + LOCALVERSION="+${VARIANT}-${SNAPSHOT}" else # Branch-tip build: need SHA for uniqueness. [[ -n "$SHA" ]] || { @@ -83,5 +96,9 @@ else } # Use first 12 chars of SHA for a compact but unambiguous suffix. SHORT_SHA="${SHA:0:12}" - echo "+${VARIANT}-g${SHORT_SHA}" + LOCALVERSION="+${VARIANT}-g${SHORT_SHA}" + SNAPSHOT="" fi + +echo "LOCALVERSION=${LOCALVERSION}" +echo "SNAPSHOT=${SNAPSHOT}" diff --git a/debian/rules b/debian/rules index 3cdb4378..cc20c1b9 100755 --- a/debian/rules +++ b/debian/rules @@ -51,6 +51,8 @@ KREL_FILE := debian/kernel.release # Required unless KVER is given explicitly. # KVER Full kernelrelease string (uname -r). Overrides LOCALVERSION. # KVER_EXTRA Optional extra suffix appended after LOCALVERSION, e.g. -ci42. +# SNAPSHOT Dated component of the Debian version, e.g. 20260722. +# Empty for branch-tip builds, which have no snapshot date. # DISTRO Target Debian/Ubuntu suite (default: unstable). # SRCPKG Source package name (default: linux-qcom-next). # BINPKG Binary metapackage name (default: linux-image-qcom-next). @@ -77,6 +79,7 @@ KREL_FILE := debian/kernel.release # Read by override_dh_gencontrol. # --------------------------------------------------------------------------- KVER_EXTRA ?= +SNAPSHOT ?= DISTRO ?= unstable SRCPKG ?= linux-qcom-next BINPKG ?= linux-image-qcom-next @@ -112,11 +115,11 @@ prepare: echo "Appended KVER_EXTRA=$(KVER_EXTRA) -> KVER=$$KVER_RESOLVED"; \ fi; \ \ - # ── Derive package version from KVER ───────────────────────────────────── - # Package version format: +- - # where base_kver = kernel version without the LOCALVERSION suffix, - # and date = the 8-digit date component of LOCALVERSION (if present). - # For branch-tip builds (no date in LOCALVERSION), version = -. + # ── Derive package version ─────────────────────────────────────────────── + # Package version format: +- + # where base_kver = kernel version without the LOCALVERSION suffix, and + # snapshot = the SNAPSHOT input. + # For branch-tip builds, which have no snapshot, version = -. # # Extract base_kver: read directly from kernel Makefile (same as above but # without LOCALVERSION appended), so it is always the pure kernel version. @@ -131,10 +134,16 @@ prepare: EXTRA_DEB=$$(echo "$$EXTRA" | sed 's/^-rc/~rc/'); \ BASE_KVER_CLEAN="$${VER}.$${PATCH}.$${SUB}$${EXTRA_DEB}$${TREE_LOCALVER}"; \ \ - # Extract date from LOCALVERSION: last dash-delimited component if 8 digits. - DATE_PART=$$(echo "$(LOCALVERSION)" | grep -oE '[0-9]{8}$$' || true); \ - if [ -n "$$DATE_PART" ]; then \ - UPSTREAM_VER="$${BASE_KVER_CLEAN}+$${DATE_PART}"; \ + # SNAPSHOT arrives as its own input rather than being recovered from the end + # of LOCALVERSION, because that string is not safely parseable: it carries a + # variant name whose shape is caller-supplied, and for branch-tip builds a + # 12-hex SHA that can itself end in eight digits. + if [ -n "$(SNAPSHOT)" ]; then \ + echo "$(SNAPSHOT)" | grep -qE '^[0-9]{8}$$' || { \ + echo "ERROR: SNAPSHOT must be YYYYMMDD (got '$(SNAPSHOT)')"; \ + exit 1; \ + }; \ + UPSTREAM_VER="$${BASE_KVER_CLEAN}+$(SNAPSHOT)"; \ else \ UPSTREAM_VER="$${BASE_KVER_CLEAN}"; \ fi; \ diff --git a/prepare-source.sh b/prepare-source.sh index b5db6efe..01cfc838 100755 --- a/prepare-source.sh +++ b/prepare-source.sh @@ -54,6 +54,10 @@ OPTIONS: --localversion SUFFIX LOCALVERSION suffix appended to the base kernel version (e.g. +qcom-next-20260722). Auto-detected from git tag if not specified. + --snapshot SNAPSHOT Dated component of the Debian version, e.g. + 20260722. Auto-detected from git tag alongside + --localversion; pass it explicitly whenever + --localversion is passed explicitly. --kver-extra SUFFIX Extra suffix appended to the final KVER (e.g. -ci42). @@ -115,6 +119,7 @@ EOF SOURCE_DIR="" DISTRO="$DEFAULT_DISTRO" LOCALVERSION="" +SNAPSHOT="" KVER_EXTRA="" SRCPKG="$DEFAULT_SRCPKG" BINPKG="$DEFAULT_BINPKG" @@ -127,6 +132,7 @@ while [[ $# -gt 0 ]]; do -s|--source-dir) SOURCE_DIR="$2"; shift 2 ;; -d|--distro) DISTRO="$2"; shift 2 ;; --localversion) LOCALVERSION="$2"; shift 2 ;; + --snapshot) SNAPSHOT="$2"; shift 2 ;; --kver-extra) KVER_EXTRA="$2"; shift 2 ;; --srcpkg) SRCPKG="$2"; shift 2 ;; --binpkg) BINPKG="$2"; shift 2 ;; @@ -152,28 +158,39 @@ VALID_DISTROS=(noble questing resolute trixie forky sid unstable) [[ -d "$DEBIAN_DIR" ]] || { log_error "Debian dir not found: $DEBIAN_DIR"; exit 1; } -# ── Helper: derive LOCALVERSION from a tag name ────────────────────────────── -# qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 -_auto_localversion() { +# ── Helper: derive LOCALVERSION and SNAPSHOT from a tag name ───────────────── +# qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 / 20260722 +# +# Both fields come out of the tag together. Recovering the snapshot from +# LOCALVERSION afterwards would mean parsing a string that also holds a variant +# name and, for a branch tip, a hex SHA that can end in eight digits. +_auto_version_fields() { local tag="$1" if [[ "$tag" =~ ^([a-z-]+)-[0-9]+\.[0-9]+.*-([0-9]+)$ ]]; then - echo "+${BASH_REMATCH[1]}-${BASH_REMATCH[2]}" + LOCALVERSION="+${BASH_REMATCH[1]}-${BASH_REMATCH[2]}" + SNAPSHOT="${BASH_REMATCH[2]}" else - echo "+$tag" + LOCALVERSION="+$tag" + SNAPSHOT="" fi } -# ── Auto-detect LOCALVERSION from git tag (if not provided) ────────────────── +# ── Auto-detect LOCALVERSION and SNAPSHOT from git tag (if not provided) ───── if [[ -z "$LOCALVERSION" ]]; then GIT_TAG=$(git -C "$SOURCE_DIR" describe --tags --exact-match 2>/dev/null || true) if [[ -n "$GIT_TAG" ]]; then - LOCALVERSION="$(_auto_localversion "$GIT_TAG")" - log_info "Auto-detected LOCALVERSION='$LOCALVERSION' from tag '$GIT_TAG'" + _auto_version_fields "$GIT_TAG" + log_info "Auto-detected LOCALVERSION='$LOCALVERSION' SNAPSHOT='$SNAPSHOT' from tag '$GIT_TAG'" else log_warn "LOCALVERSION not set and no exact git tag found." log_warn "Package will be named linux-image- (no branch/date suffix)." log_warn "Use --localversion to specify, e.g.: --localversion +qcom-next-20260722" fi +elif [[ -z "$SNAPSHOT" ]]; then + # An explicit --localversion is not parsed for a snapshot; say so rather + # than silently dropping the dated component from the Debian version. + log_warn "--localversion given without --snapshot: the Debian version will" + log_warn "carry no dated component. Pass --snapshot to supply one." fi log_step "Configuration:" @@ -183,6 +200,7 @@ log_info " Source package: $SRCPKG" log_info " Binary metapkg: $BINPKG" log_info " Debian revision: $DEBIAN_REVISION" [[ -n "$LOCALVERSION" ]] && log_info " LOCALVERSION: $LOCALVERSION" +[[ -n "$SNAPSHOT" ]] && log_info " SNAPSHOT: $SNAPSHOT" [[ -n "$KVER_EXTRA" ]] && log_info " KVER_EXTRA: $KVER_EXTRA" [[ -n "$KERNEL_CONFIG" ]] && log_info " Kernel config: $KERNEL_CONFIG" [[ -n "$DKMS_MODULES" ]] && log_info " DKMS modules: $DKMS_MODULES" @@ -296,6 +314,7 @@ fi log_step "Running debian/rules prepare..." PREPARE_ARGS="DISTRO=$DISTRO SRCPKG=$SRCPKG BINPKG=$BINPKG DEBIAN_REVISION=$DEBIAN_REVISION" [[ -n "$LOCALVERSION" ]] && PREPARE_ARGS="$PREPARE_ARGS LOCALVERSION=$LOCALVERSION" +[[ -n "$SNAPSHOT" ]] && PREPARE_ARGS="$PREPARE_ARGS SNAPSHOT=$SNAPSHOT" [[ -n "$KVER_EXTRA" ]] && PREPARE_ARGS="$PREPARE_ARGS KVER_EXTRA=$KVER_EXTRA" # Spaces are stripped so a list written as "kgsl, camx" stays a single make # argument; debian/rules validates the names it is given. From bf718e0091726ce9eef50ebeff361e020367bbb5 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 4/9] Carry a tag's respin ordinal through the version fields A second tag cut on the same day (qcom-next-7.2-20260826.1) collapsed onto the same version as the first, because every derivation matched only a bare YYYYMMDD. derive-localversion.sh dropped the ordinal; prepare-source.sh matched trailing digits with -([0-9]+)$, which a ".1" suffix cannot satisfy at all, so the tag fell through to the + fallback. Match the date width explicitly with an optional ordinal after it, so the component stays whole: qcom-next-7.2-20260826 -> +qcom-next-20260826 / 20260826 qcom-next-7.2-20260826.1 -> +qcom-next-20260826.1 / 20260826.1 The first tag of a day deliberately keeps the plain form rather than normalising to ".0". Both comparators read an absent ordinal as lower than a present one, so the respin already sorts above it and every other build is spared a ".0" it never needed. Signed-off-by: Christopher Obbard --- ci/scripts/derive-localversion.sh | 24 ++++++++++++++++-------- debian/rules | 7 ++++--- prepare-source.sh | 18 ++++++++++++------ 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/ci/scripts/derive-localversion.sh b/ci/scripts/derive-localversion.sh index dea72cd7..a5063fe5 100755 --- a/ci/scripts/derive-localversion.sh +++ b/ci/scripts/derive-localversion.sh @@ -12,9 +12,16 @@ set -euo pipefail # carries a variant name and, for branch-tip builds, a hex SHA that can end in # eight digits of its own. # -# For dated tag builds (ref ends in -YYYYMMDD): -# Produces +-. -# Example: qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 +# For dated tag builds (ref ends in -YYYYMMDD, optionally .): +# Produces +-[.]. +# Example: qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 +# qcom-next-7.2-rc3-20260722.1 -> +qcom-next-20260722.1 +# +# The respin ordinal distinguishes a second tag cut on the same day. It is +# carried verbatim rather than normalised, so the first tag of a day stays +# plain +-: systemd compares the separator before the chunk +# behind it, so an absent ordinal already sorts below a present one and no +# build has to spell a ".0". # # For branch-tip builds (ref does not end in a date): # Uses the kernel variant and a short SHA for uniqueness. @@ -47,8 +54,8 @@ set -euo pipefail # Output: # Two KEY=VALUE lines on stdout, in GITHUB_ENV / 'set -a' form: # -# LOCALVERSION=+qcom-next-20260722 -# SNAPSHOT=20260722 +# LOCALVERSION=+qcom-next-20260722.1 +# SNAPSHOT=20260722.1 # # LOCALVERSION always starts with a plus. SNAPSHOT is empty for branch-tip # builds, which have no date; the Debian version then carries no snapshot @@ -83,9 +90,10 @@ done exit 1 } -# Dated tags use a trailing YYYYMMDD snapshot. The matrix selects the tag set; -# the variant supplies the stable package identity used in LOCALVERSION. -if [[ "$REF" =~ -([0-9]{8})$ ]]; then +# Dated tags use a trailing YYYYMMDD snapshot, optionally followed by a respin +# ordinal. The matrix selects the tag set; the variant supplies the stable +# package identity used in LOCALVERSION. +if [[ "$REF" =~ -([0-9]{8}(\.[0-9]+)?)$ ]]; then SNAPSHOT="${BASH_REMATCH[1]}" LOCALVERSION="+${VARIANT}-${SNAPSHOT}" else diff --git a/debian/rules b/debian/rules index cc20c1b9..ba864470 100755 --- a/debian/rules +++ b/debian/rules @@ -51,7 +51,8 @@ KREL_FILE := debian/kernel.release # Required unless KVER is given explicitly. # KVER Full kernelrelease string (uname -r). Overrides LOCALVERSION. # KVER_EXTRA Optional extra suffix appended after LOCALVERSION, e.g. -ci42. -# SNAPSHOT Dated component of the Debian version, e.g. 20260722. +# SNAPSHOT Dated component of the Debian version: YYYYMMDD with an +# optional . ordinal, e.g. 20260722 or 20260722.1. # Empty for branch-tip builds, which have no snapshot date. # DISTRO Target Debian/Ubuntu suite (default: unstable). # SRCPKG Source package name (default: linux-qcom-next). @@ -139,8 +140,8 @@ prepare: # variant name whose shape is caller-supplied, and for branch-tip builds a # 12-hex SHA that can itself end in eight digits. if [ -n "$(SNAPSHOT)" ]; then \ - echo "$(SNAPSHOT)" | grep -qE '^[0-9]{8}$$' || { \ - echo "ERROR: SNAPSHOT must be YYYYMMDD (got '$(SNAPSHOT)')"; \ + echo "$(SNAPSHOT)" | grep -qE '^[0-9]{8}(\.[0-9]+)?$$' || { \ + echo "ERROR: SNAPSHOT must be YYYYMMDD with an optional . (got '$(SNAPSHOT)')"; \ exit 1; \ }; \ UPSTREAM_VER="$${BASE_KVER_CLEAN}+$(SNAPSHOT)"; \ diff --git a/prepare-source.sh b/prepare-source.sh index 01cfc838..bda24b34 100755 --- a/prepare-source.sh +++ b/prepare-source.sh @@ -54,10 +54,11 @@ OPTIONS: --localversion SUFFIX LOCALVERSION suffix appended to the base kernel version (e.g. +qcom-next-20260722). Auto-detected from git tag if not specified. - --snapshot SNAPSHOT Dated component of the Debian version, e.g. - 20260722. Auto-detected from git tag alongside - --localversion; pass it explicitly whenever - --localversion is passed explicitly. + --snapshot SNAPSHOT Dated component of the Debian version: YYYYMMDD + with an optional . ordinal (e.g. + 20260722 or 20260722.1). Auto-detected from git + tag alongside --localversion; pass it explicitly + whenever --localversion is passed explicitly. --kver-extra SUFFIX Extra suffix appended to the final KVER (e.g. -ci42). @@ -159,14 +160,19 @@ VALID_DISTROS=(noble questing resolute trixie forky sid unstable) [[ -d "$DEBIAN_DIR" ]] || { log_error "Debian dir not found: $DEBIAN_DIR"; exit 1; } # ── Helper: derive LOCALVERSION and SNAPSHOT from a tag name ───────────────── -# qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 / 20260722 +# qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 / 20260722 +# qcom-next-7.2-rc3-20260722.1 -> +qcom-next-20260722.1 / 20260722.1 +# +# The trailing component is a YYYYMMDD snapshot with an optional respin ordinal +# for a second tag cut on the same day. Matching the date width explicitly (and +# not just "trailing digits") keeps the ordinal attached to it. # # Both fields come out of the tag together. Recovering the snapshot from # LOCALVERSION afterwards would mean parsing a string that also holds a variant # name and, for a branch tip, a hex SHA that can end in eight digits. _auto_version_fields() { local tag="$1" - if [[ "$tag" =~ ^([a-z-]+)-[0-9]+\.[0-9]+.*-([0-9]+)$ ]]; then + if [[ "$tag" =~ ^([a-z-]+)-[0-9]+\.[0-9]+.*-([0-9]{8}(\.[0-9]+)?)$ ]]; then LOCALVERSION="+${BASH_REMATCH[1]}-${BASH_REMATCH[2]}" SNAPSHOT="${BASH_REMATCH[2]}" else From d37b5a12b9b763832f588c6baa2eda0c22b71e3b Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 5/9] Spell the Debian snapshot in Debian's own +git idiom The dated component was appended bare, as +20260722, which reads as part of the upstream version rather than as a snapshot marker. Debian spells a VCS snapshot +git, so use that: 7.2.0~rc3+git20260722-0qcom1 7.2.0~rc3+git20260722.1-0qcom1 Ordering is unchanged -- the 'git' is a constant prefix on both sides of every comparison, and the date behind it still decides. Signed-off-by: Christopher Obbard --- debian/rules | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/debian/rules b/debian/rules index ba864470..98b12a8e 100755 --- a/debian/rules +++ b/debian/rules @@ -76,7 +76,7 @@ KREL_FILE := debian/kernel.release # debian/localversion Plain file containing the LOCALVERSION suffix (e.g. +qcom-next-20260722). # Read by override_dh_auto_build to pass LOCALVERSION= to make. # debian/pkgversion Plain file containing the full Debian package version -# (e.g. 7.2.0-rc3+20260722-0qcom1). +# (e.g. 7.2.0~rc3+git20260722-0qcom1). # Read by override_dh_gencontrol. # --------------------------------------------------------------------------- KVER_EXTRA ?= @@ -117,9 +117,9 @@ prepare: fi; \ \ # ── Derive package version ─────────────────────────────────────────────── - # Package version format: +- + # Package version format: +git- # where base_kver = kernel version without the LOCALVERSION suffix, and - # snapshot = the SNAPSHOT input. + # snapshot = the SNAPSHOT input (YYYYMMDD with an optional respin ordinal). # For branch-tip builds, which have no snapshot, version = -. # # Extract base_kver: read directly from kernel Makefile (same as above but @@ -144,7 +144,7 @@ prepare: echo "ERROR: SNAPSHOT must be YYYYMMDD with an optional . (got '$(SNAPSHOT)')"; \ exit 1; \ }; \ - UPSTREAM_VER="$${BASE_KVER_CLEAN}+$(SNAPSHOT)"; \ + UPSTREAM_VER="$${BASE_KVER_CLEAN}+git$(SNAPSHOT)"; \ else \ UPSTREAM_VER="$${BASE_KVER_CLEAN}"; \ fi; \ From 9d1a30d6ba7d51da921d0d20fb8ed87df2990f3f Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 6/9] Name the commit in both version strings The snapshot date identifies the tag, not the commit the tag pointed at when the build was cut. A moved tag then produces two different kernels under one version, indistinguishable in the archive and on a running system. Append the commit to both strings: uname -r 7.2.0-rc7+qcom-next-20260826.1-gabcdef123456 Debian version 7.2.0~rc7+git20260826.1~gabcdef123456-0qli1~bpo13+1 The separators differ, and the difference is load-bearing rather than sloppy. systemd weighs the separator before the chunk behind it, so '-g' leaves an absent respin ordinal sorting below a present one. dpkg instead alternates digit and non-digit runs and reads an exhausted run as lower than a letter, so the same '.' or '-' join would decide before reaching the ordinal: 7.2.0+git20260826.g3f2f3ca1a81e > 7.2.0+git20260826.1.gabcdef123456 putting a respin below the build it respins. '~' sorts below everything including the empty string, so the ordinal is always compared first and the SHA only ever breaks a tie between builds sharing a snapshot. The alternative -- spelling ".0" on every non-respin build -- buys the same ordering at the cost of a redundant ordinal in every version forever. What '~' costs is one reading: the version sorts below the same snapshot without a SHA, as though it preceded it. Nothing occupies that slot, because every snapshot build carries a SHA, so the claim is never tested against a real version. The SHA cannot order anything -- two SHAs have no relation -- so a moved tag still needs its respin ordinal or the packaging revision bumped before users can upgrade onto it. What this buys is that the two builds are never confused for each other. --sha becomes required for every build rather than just branch tips, and is validated as hex, since it is now part of every kernel release rather than a fallback identity for refs that had nothing else. Signed-off-by: Christopher Obbard --- .github/workflows/build-kernel-deb.yml | 17 +++++--- README.md | 23 ++++++++++- ci/scripts/derive-localversion.sh | 57 +++++++++++++++----------- debian/rules | 26 +++++++++++- prepare-source.sh | 28 +++++++++---- 5 files changed, 110 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build-kernel-deb.yml b/.github/workflows/build-kernel-deb.yml index c947b8cf..53c38c1d 100644 --- a/.github/workflows/build-kernel-deb.yml +++ b/.github/workflows/build-kernel-deb.yml @@ -59,7 +59,7 @@ on: required: false default: '' kver-extra: - description: 'Advanced: extra package-version suffix (e.g. -ci42)' + description: 'Advanced: extra KVER suffix (e.g. -ci42), appended to the kernel release verbatim' required: false default: '' debug-build: @@ -130,7 +130,7 @@ on: type: string default: '' kver-extra: - description: 'Extra suffix appended to the package version' + description: 'Extra KVER suffix, appended to the kernel release verbatim' type: string default: '' debusine-parent-workspace: @@ -283,16 +283,18 @@ jobs: if [[ -n "$OVERRIDE_LV" ]]; then # An override supplies no snapshot, and the ref it was chosen to # replace cannot be trusted to describe it. The Debian version then - # carries no dated component; prepare-source.sh warns about the same. + # carries no +git; prepare-source.sh warns about the same. echo "Using explicit LOCALVERSION override: $OVERRIDE_LV" echo "LOCALVERSION=$OVERRIDE_LV" >> "$GITHUB_ENV" echo "SNAPSHOT=" >> "$GITHUB_ENV" + echo "GITSHA=" >> "$GITHUB_ENV" else - # Emits LOCALVERSION= and SNAPSHOT= lines, already in GITHUB_ENV form. + # Emits LOCALVERSION=, SNAPSHOT= and GITSHA= lines, already in + # GITHUB_ENV form. FIELDS=$(ci/scripts/derive-localversion.sh \ --variant "$KERNEL_VARIANT_INPUT" \ --ref "$KERNEL_REF" \ - --sha "$(echo "$KERNEL_SHA" | cut -c1-12)") + --sha "$KERNEL_SHA") echo "$FIELDS" >> "$GITHUB_ENV" echo "Derived version fields:" echo "$FIELDS" @@ -407,8 +409,13 @@ jobs: --debian-revision "$DEBIAN_REVISION" ) + # All three version fields come from derive-localversion.sh, which + # composed them from the ref and HEAD together. Nothing here appends + # to them: the SHA is already inside LOCALVERSION, and GITSHA carries + # it separately for the Debian version, which joins it differently. [[ -n "$LOCALVERSION" ]] && ARGS+=(--localversion "$LOCALVERSION") [[ -n "$SNAPSHOT" ]] && ARGS+=(--snapshot "$SNAPSHOT") + [[ -n "$GITSHA" ]] && ARGS+=(--git-sha "$GITSHA") [[ -n "$KVER_EXTRA_INPUT" ]] && ARGS+=(--kver-extra "$KVER_EXTRA_INPUT") [[ -n "$KERNEL_CONFIG_INPUT" ]] && ARGS+=(--kernel-config "$KERNEL_CONFIG_INPUT") # Always passed, unlike the optional inputs above: an empty list is a diff --git a/README.md b/README.md index bcb15a68..bc7fd8d5 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ Supporting scripts keep workflow YAML small and testable: | --- | --- | | `ci/scripts/resolve-matrix.sh` | Validates and flattens matrix rows. | | `ci/scripts/resolve-kernel-ref.sh` | Resolves a matrix-selected dated tag or validates a direct ref. | -| `ci/scripts/derive-localversion.sh` | Derives the version fields from the variant and resolved kernel ref, printing `LOCALVERSION=` and `SNAPSHOT=` lines. `SNAPSHOT` is the dated component of the Debian version and is empty for branch-tip builds. | +| `ci/scripts/derive-localversion.sh` | Derives the version fields from the variant, resolved kernel ref and HEAD, printing `LOCALVERSION=`, `SNAPSHOT=` and `GITSHA=` lines. `SNAPSHOT` is the dated component of the Debian version and is empty for branch-tip builds. | | `ci/scripts/derive-debian-revision.sh` | Derives the final suite-specific `debian_revision` from `debian_version_stub`, `suite_suffix_mapping`, and delivery type. | ## Architecture @@ -399,6 +399,27 @@ For the current matrix, package generation produces: names. Only the Debian version field converts it to `~rcN`, so a release candidate correctly sorts before the corresponding final kernel release. +Every build names both its snapshot and the commit it was cut from: + +| | Format | Example | +| --- | --- | --- | +| Kernel release (`uname -r`) | `+-[.]-g` | `7.2.0-rc7+qcom-next-20260826.1-g011a82096bee` | +| Debian version | `+git[.]~g-` | `7.2.0~rc7+git20260826.1~g011a82096bee-0qli1~bpo13+1` | + +The date orders builds, the respin ordinal separates two tags cut on the same +day, and the SHA is only a final discriminator so a moved tag cannot produce two +different kernels under one version. Branch-tip builds have no date and are +identified by the SHA alone (`7.2.0-rc7+qcom-next-g011a82096bee`). + +The two strings join the SHA differently because their comparators differ. +`uname -r` is compared by systemd, which weighs the separator before the chunk +behind it, so `-g` leaves an absent respin ordinal sorting below a present +one. dpkg instead reads an exhausted run as lower than a letter, which would put +a respin *below* the build it respins, so the Debian version joins with `~` — +lower than everything, including the empty string. The visible cost is that a +Debian version reads as preceding the same snapshot without a SHA; nothing ever +occupies that slot, since every snapshot build carries one. + `KVER_EXTRA` is supported for explicit suffixes such as `-ci42` or `-local`. The packaging rules verify that the declared versioned image package matches the resolved kernel release and fail instead of creating inconsistent metadata. diff --git a/ci/scripts/derive-localversion.sh b/ci/scripts/derive-localversion.sh index a5063fe5..03b45411 100755 --- a/ci/scripts/derive-localversion.sh +++ b/ci/scripts/derive-localversion.sh @@ -5,17 +5,17 @@ set -euo pipefail # Derive the version fields for a build from a kernel variant and resolved ref. # -# Emits LOCALVERSION (the kernel release suffix) and SNAPSHOT (the dated -# component of the Debian version), both derived from the ref in one place. -# SNAPSHOT is emitted alongside rather than recovered from LOCALVERSION later: -# reading it back out means guessing where the date ends in a string that also -# carries a variant name and, for branch-tip builds, a hex SHA that can end in -# eight digits of its own. +# Emits LOCALVERSION (the kernel release suffix), SNAPSHOT (the dated component +# of the Debian version) and GITSHA, all derived from the ref in one place. +# SNAPSHOT and GITSHA are emitted alongside rather than recovered from +# LOCALVERSION later: reading them back out means guessing where each field ends +# in a string that also carries a variant name, and a hex SHA can end in eight +# digits of its own. # # For dated tag builds (ref ends in -YYYYMMDD, optionally .): -# Produces +-[.]. -# Example: qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 -# qcom-next-7.2-rc3-20260722.1 -> +qcom-next-20260722.1 +# Produces +-[.]-g<12 hex>. +# Example: qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722-g07f50dc44edd +# qcom-next-7.2-rc3-20260722.1 -> +qcom-next-20260722.1-g07f50dc44edd # # The respin ordinal distinguishes a second tag cut on the same day. It is # carried verbatim rather than normalised, so the first tag of a day stays @@ -23,10 +23,12 @@ set -euo pipefail # behind it, so an absent ordinal already sorts below a present one and no # build has to spell a ".0". # +# The SHA names the commit the tag pointed at when the build was cut, so a +# moved tag cannot silently produce two different kernels under one release. +# # For branch-tip builds (ref does not end in a date): -# Uses the kernel variant and a short SHA for uniqueness. +# Uses the kernel variant and the SHA alone; there is no date to order by. # Example: qcom-next @ 07f50dc44edd -> +qcom-next-g07f50dc44edd -# --sha is required for branch-tip builds. # # Why the leading '+' and not '-': # The suffix ends up in KERNELRELEASE (uname -r), which is the 'version' field @@ -43,19 +45,20 @@ set -euo pipefail # which spells the release candidate ~rcN and orders correctly either way. # # Usage: -# ci/scripts/derive-localversion.sh --variant qcom-next --ref qcom-next-7.2-rc3-20260722 +# ci/scripts/derive-localversion.sh --variant qcom-next --ref qcom-next-7.2-rc3-20260722 --sha 07f50dc44edd # ci/scripts/derive-localversion.sh --variant arduino --ref main --sha 07f50dc44edd # # Options: # --variant VARIANT Kernel variant identifier. Defaults to qcom-next. # --ref REF Kernel ref (tag name or branch name). Required. -# --sha SHA Short commit SHA (required for branch-tip builds). +# --sha SHA Commit SHA, truncated to 12 hex characters. Required. # # Output: -# Two KEY=VALUE lines on stdout, in GITHUB_ENV / 'set -a' form: +# Three KEY=VALUE lines on stdout, in GITHUB_ENV / 'set -a' form: # -# LOCALVERSION=+qcom-next-20260722.1 +# LOCALVERSION=+qcom-next-20260722.1-g07f50dc44edd # SNAPSHOT=20260722.1 +# GITSHA=07f50dc44edd # # LOCALVERSION always starts with a plus. SNAPSHOT is empty for branch-tip # builds, which have no date; the Debian version then carries no snapshot @@ -63,7 +66,7 @@ set -euo pipefail # # Exit codes: # 0 Success. -# 1 Error (invalid args, branch-tip without --sha). +# 1 Error (invalid args, missing or malformed --sha). VARIANT="qcom-next" REF="" @@ -89,24 +92,28 @@ done echo "ERROR: --variant must use lowercase letters, digits, and internal hyphens" >&2 exit 1 } +# Every build identifies its commit, so --sha is required for all of them, not +# just the branch tips that cannot be identified any other way. +[[ "$SHA" =~ ^[0-9a-f]{12,40}$ ]] || { + echo "ERROR: --sha is required and must be at least 12 lowercase hex characters (got '$SHA')" >&2 + exit 1 +} +# 12 chars is upstream's own abbreviation width in -g, and short enough +# to keep the kernel release readable in a boot menu. +GITSHA="${SHA:0:12}" # Dated tags use a trailing YYYYMMDD snapshot, optionally followed by a respin # ordinal. The matrix selects the tag set; the variant supplies the stable # package identity used in LOCALVERSION. if [[ "$REF" =~ -([0-9]{8}(\.[0-9]+)?)$ ]]; then SNAPSHOT="${BASH_REMATCH[1]}" - LOCALVERSION="+${VARIANT}-${SNAPSHOT}" + LOCALVERSION="+${VARIANT}-${SNAPSHOT}-g${GITSHA}" else - # Branch-tip build: need SHA for uniqueness. - [[ -n "$SHA" ]] || { - echo "ERROR: --sha is required for branch-tip builds (ref '$REF' is not a dated tag)" >&2 - exit 1 - } - # Use first 12 chars of SHA for a compact but unambiguous suffix. - SHORT_SHA="${SHA:0:12}" - LOCALVERSION="+${VARIANT}-g${SHORT_SHA}" + # Branch-tip build: no date, so the SHA is the whole identity. + LOCALVERSION="+${VARIANT}-g${GITSHA}" SNAPSHOT="" fi echo "LOCALVERSION=${LOCALVERSION}" echo "SNAPSHOT=${SNAPSHOT}" +echo "GITSHA=${GITSHA}" diff --git a/debian/rules b/debian/rules index 98b12a8e..f5ea1914 100755 --- a/debian/rules +++ b/debian/rules @@ -54,6 +54,9 @@ KREL_FILE := debian/kernel.release # SNAPSHOT Dated component of the Debian version: YYYYMMDD with an # optional . ordinal, e.g. 20260722 or 20260722.1. # Empty for branch-tip builds, which have no snapshot date. +# GITSHA Commit the build was cut from, 12 hex characters. Appended to +# the snapshot as a final discriminator. Ignored without a +# SNAPSHOT to discriminate. # DISTRO Target Debian/Ubuntu suite (default: unstable). # SRCPKG Source package name (default: linux-qcom-next). # BINPKG Binary metapackage name (default: linux-image-qcom-next). @@ -81,6 +84,7 @@ KREL_FILE := debian/kernel.release # --------------------------------------------------------------------------- KVER_EXTRA ?= SNAPSHOT ?= +GITSHA ?= DISTRO ?= unstable SRCPKG ?= linux-qcom-next BINPKG ?= linux-image-qcom-next @@ -117,11 +121,24 @@ prepare: fi; \ \ # ── Derive package version ─────────────────────────────────────────────── - # Package version format: +git- + # Package version format: +git~g- # where base_kver = kernel version without the LOCALVERSION suffix, and # snapshot = the SNAPSHOT input (YYYYMMDD with an optional respin ordinal). # For branch-tip builds, which have no snapshot, version = -. # + # The snapshot orders these; the SHA only discriminates two builds that + # share one. It is joined with '~' rather than '.' because dpkg alternates + # digit and non-digit runs and reads an exhausted run as lower than a + # letter, so a '.' would put a respin BELOW the build it respins: + # + # 7.2.0+git20260826.g3f2f3ca1a81e > 7.2.0+git20260826.1.gabcdef123456 + # + # '~' sorts below everything, including the empty string, so the ordinal is + # always compared before the SHA is reached and no build has to spell a + # ".0" to hold the position. The cost is that a version reads as preceding + # the same snapshot without a SHA -- a slot nothing ever occupies, since + # every snapshot build carries one. + # # Extract base_kver: read directly from kernel Makefile (same as above but # without LOCALVERSION appended), so it is always the pure kernel version. VER=$$(grep '^VERSION = ' Makefile | awk '{print $$3}'); \ @@ -145,6 +162,13 @@ prepare: exit 1; \ }; \ UPSTREAM_VER="$${BASE_KVER_CLEAN}+git$(SNAPSHOT)"; \ + if [ -n "$(GITSHA)" ]; then \ + echo "$(GITSHA)" | grep -qE '^[0-9a-f]{12}$$' || { \ + echo "ERROR: GITSHA must be 12 lowercase hex characters (got '$(GITSHA)')"; \ + exit 1; \ + }; \ + UPSTREAM_VER="$${UPSTREAM_VER}~g$(GITSHA)"; \ + fi; \ else \ UPSTREAM_VER="$${BASE_KVER_CLEAN}"; \ fi; \ diff --git a/prepare-source.sh b/prepare-source.sh index bda24b34..3cecaa99 100755 --- a/prepare-source.sh +++ b/prepare-source.sh @@ -59,6 +59,10 @@ OPTIONS: 20260722 or 20260722.1). Auto-detected from git tag alongside --localversion; pass it explicitly whenever --localversion is passed explicitly. + --git-sha SHA Commit the build was cut from, truncated to 12 + hex characters. Discriminates two builds of one + snapshot (a moved tag). Auto-detected from HEAD + alongside --localversion. --kver-extra SUFFIX Extra suffix appended to the final KVER (e.g. -ci42). @@ -121,6 +125,7 @@ SOURCE_DIR="" DISTRO="$DEFAULT_DISTRO" LOCALVERSION="" SNAPSHOT="" +GITSHA="" KVER_EXTRA="" SRCPKG="$DEFAULT_SRCPKG" BINPKG="$DEFAULT_BINPKG" @@ -134,6 +139,7 @@ while [[ $# -gt 0 ]]; do -d|--distro) DISTRO="$2"; shift 2 ;; --localversion) LOCALVERSION="$2"; shift 2 ;; --snapshot) SNAPSHOT="$2"; shift 2 ;; + --git-sha) GITSHA="$2"; shift 2 ;; --kver-extra) KVER_EXTRA="$2"; shift 2 ;; --srcpkg) SRCPKG="$2"; shift 2 ;; --binpkg) BINPKG="$2"; shift 2 ;; @@ -159,34 +165,36 @@ VALID_DISTROS=(noble questing resolute trixie forky sid unstable) [[ -d "$DEBIAN_DIR" ]] || { log_error "Debian dir not found: $DEBIAN_DIR"; exit 1; } -# ── Helper: derive LOCALVERSION and SNAPSHOT from a tag name ───────────────── -# qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722 / 20260722 -# qcom-next-7.2-rc3-20260722.1 -> +qcom-next-20260722.1 / 20260722.1 +# ── Helper: derive LOCALVERSION, SNAPSHOT and GITSHA from a tag name ───────── +# qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722-g / 20260722 +# qcom-next-7.2-rc3-20260722.1 -> +qcom-next-20260722.1-g / 20260722.1 # # The trailing component is a YYYYMMDD snapshot with an optional respin ordinal # for a second tag cut on the same day. Matching the date width explicitly (and # not just "trailing digits") keeps the ordinal attached to it. # -# Both fields come out of the tag together. Recovering the snapshot from +# All three fields come out of the tag and HEAD together. Recovering them from # LOCALVERSION afterwards would mean parsing a string that also holds a variant -# name and, for a branch tip, a hex SHA that can end in eight digits. +# name and a hex SHA that can end in eight digits. _auto_version_fields() { local tag="$1" if [[ "$tag" =~ ^([a-z-]+)-[0-9]+\.[0-9]+.*-([0-9]{8}(\.[0-9]+)?)$ ]]; then - LOCALVERSION="+${BASH_REMATCH[1]}-${BASH_REMATCH[2]}" SNAPSHOT="${BASH_REMATCH[2]}" + LOCALVERSION="+${BASH_REMATCH[1]}-${SNAPSHOT}-g${GITSHA}" else LOCALVERSION="+$tag" SNAPSHOT="" fi } -# ── Auto-detect LOCALVERSION and SNAPSHOT from git tag (if not provided) ───── +# ── Auto-detect LOCALVERSION, SNAPSHOT and GITSHA from git (if not provided) ── if [[ -z "$LOCALVERSION" ]]; then GIT_TAG=$(git -C "$SOURCE_DIR" describe --tags --exact-match 2>/dev/null || true) if [[ -n "$GIT_TAG" ]]; then + [[ -n "$GITSHA" ]] || \ + GITSHA=$(git -C "$SOURCE_DIR" rev-parse --short=12 HEAD 2>/dev/null || true) _auto_version_fields "$GIT_TAG" - log_info "Auto-detected LOCALVERSION='$LOCALVERSION' SNAPSHOT='$SNAPSHOT' from tag '$GIT_TAG'" + log_info "Auto-detected LOCALVERSION='$LOCALVERSION' SNAPSHOT='$SNAPSHOT' GITSHA='$GITSHA' from tag '$GIT_TAG'" else log_warn "LOCALVERSION not set and no exact git tag found." log_warn "Package will be named linux-image- (no branch/date suffix)." @@ -196,7 +204,7 @@ elif [[ -z "$SNAPSHOT" ]]; then # An explicit --localversion is not parsed for a snapshot; say so rather # than silently dropping the dated component from the Debian version. log_warn "--localversion given without --snapshot: the Debian version will" - log_warn "carry no dated component. Pass --snapshot to supply one." + log_warn "carry no +git component. Pass --snapshot to supply one." fi log_step "Configuration:" @@ -207,6 +215,7 @@ log_info " Binary metapkg: $BINPKG" log_info " Debian revision: $DEBIAN_REVISION" [[ -n "$LOCALVERSION" ]] && log_info " LOCALVERSION: $LOCALVERSION" [[ -n "$SNAPSHOT" ]] && log_info " SNAPSHOT: $SNAPSHOT" +[[ -n "$GITSHA" ]] && log_info " GITSHA: $GITSHA" [[ -n "$KVER_EXTRA" ]] && log_info " KVER_EXTRA: $KVER_EXTRA" [[ -n "$KERNEL_CONFIG" ]] && log_info " Kernel config: $KERNEL_CONFIG" [[ -n "$DKMS_MODULES" ]] && log_info " DKMS modules: $DKMS_MODULES" @@ -321,6 +330,7 @@ log_step "Running debian/rules prepare..." PREPARE_ARGS="DISTRO=$DISTRO SRCPKG=$SRCPKG BINPKG=$BINPKG DEBIAN_REVISION=$DEBIAN_REVISION" [[ -n "$LOCALVERSION" ]] && PREPARE_ARGS="$PREPARE_ARGS LOCALVERSION=$LOCALVERSION" [[ -n "$SNAPSHOT" ]] && PREPARE_ARGS="$PREPARE_ARGS SNAPSHOT=$SNAPSHOT" +[[ -n "$GITSHA" ]] && PREPARE_ARGS="$PREPARE_ARGS GITSHA=$GITSHA" [[ -n "$KVER_EXTRA" ]] && PREPARE_ARGS="$PREPARE_ARGS KVER_EXTRA=$KVER_EXTRA" # Spaces are stripped so a list written as "kgsl, camx" stays a single make # argument; debian/rules validates the names it is given. From 35190975147b911ca346f984b4a3f7323049d095 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 7/9] Date branch-tip builds by their HEAD commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit d6c478d, reworked) A branch-tip build had no snapshot, so debian/rules emitted a version with no +git at all -- 7.2.0~rc7-0qli1~bpo13+1 -- which sorts below every dated build rather than among them. Take the date from the HEAD commit instead, so these builds land in the same sequence: 7.2.0~rc7+git20260821~g07f50dc44edd-… tag build 7.2.0~rc7+git20260904~g07f50dc44edd-… branch tip, dated by commit The date is the committer date of HEAD, not the build date: - rebuilding a commit reproduces its version instead of inventing a higher one on each run - the date describes the source rather than when CI happened to run - it lands in the same space as upstream's tag dates, which track the commit each tag is cut from Committer date rather than author date, because an author date can be months old on a backported patch. Normalised to UTC, since --date=short would otherwise render in each committer's timezone and make one commit yield different dates. The trade-off is that a commit date need not advance: rewinding the branch to an older commit produces a lower version that apt will not offer as an upgrade. Documented in derive-localversion.sh rather than left to be found. The original replaced --sha with --date, because at the time the SHA was not in the version and dating a build meant giving up identifying it. Both are kept here: --sha is required for every build and --date only supplies what a branch tip's ref cannot, so a branch-tip release now has the same shape as a tag build rather than a second shape of its own. Signed-off-by: Christopher Obbard --- .github/workflows/build-kernel-deb.yml | 26 ++++++++++++-- ci/scripts/derive-localversion.sh | 50 ++++++++++++++++++++------ 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-kernel-deb.yml b/.github/workflows/build-kernel-deb.yml index 53c38c1d..d31dd3ba 100644 --- a/.github/workflows/build-kernel-deb.yml +++ b/.github/workflows/build-kernel-deb.yml @@ -273,6 +273,18 @@ jobs: echo "KERNEL_SHA=$KERNEL_SHA" >> "$GITHUB_ENV" echo "Kernel HEAD SHA: $KERNEL_SHA" + # Committer date of HEAD, normalised to UTC. Committer rather than + # author date: an author date can be months old on a backported + # patch, which would date the build by when someone first wrote the + # code rather than by when this snapshot came to exist. + KERNEL_DATE=$(TZ=UTC git -C "$KERNEL_DIR" log -1 --format=%cd --date=format-local:%Y%m%d) + [[ "$KERNEL_DATE" =~ ^[0-9]{8}$ ]] || { + echo "::error::could not read a YYYYMMDD commit date from $KERNEL_REF (got '$KERNEL_DATE')" + exit 1 + } + echo "KERNEL_DATE=$KERNEL_DATE" >> "$GITHUB_ENV" + echo "Kernel HEAD commit date (UTC): $KERNEL_DATE" + - name: Derive version fields env: LOCALVERSION_INPUT: ${{ inputs.localversion }} @@ -291,10 +303,17 @@ jobs: else # Emits LOCALVERSION=, SNAPSHOT= and GITSHA= lines, already in # GITHUB_ENV form. + # + # --date is only consulted for branch-tip builds, where the ref + # carries no snapshot of its own. It comes from the commit rather + # than the clock, so rebuilding a commit reproduces its version + # instead of inventing a higher one, and it lands in the same space + # as upstream's tag dates. FIELDS=$(ci/scripts/derive-localversion.sh \ --variant "$KERNEL_VARIANT_INPUT" \ --ref "$KERNEL_REF" \ - --sha "$KERNEL_SHA") + --sha "$KERNEL_SHA" \ + --date "$KERNEL_DATE") echo "$FIELDS" >> "$GITHUB_ENV" echo "Derived version fields:" echo "$FIELDS" @@ -415,7 +434,10 @@ jobs: # it separately for the Debian version, which joins it differently. [[ -n "$LOCALVERSION" ]] && ARGS+=(--localversion "$LOCALVERSION") [[ -n "$SNAPSHOT" ]] && ARGS+=(--snapshot "$SNAPSHOT") - [[ -n "$GITSHA" ]] && ARGS+=(--git-sha "$GITSHA") + # Full SHA: prepare-source.sh takes the first 12 for the version + # strings and records the whole thing in the changelog, alongside the + # repository and ref that the version strings do not name. + ARGS+=(--git-sha "$KERNEL_SHA" --git-clone "$KERNEL_URL" --git-ref "$KERNEL_REF") [[ -n "$KVER_EXTRA_INPUT" ]] && ARGS+=(--kver-extra "$KVER_EXTRA_INPUT") [[ -n "$KERNEL_CONFIG_INPUT" ]] && ARGS+=(--kernel-config "$KERNEL_CONFIG_INPUT") # Always passed, unlike the optional inputs above: an empty list is a diff --git a/ci/scripts/derive-localversion.sh b/ci/scripts/derive-localversion.sh index 03b45411..eca3c989 100755 --- a/ci/scripts/derive-localversion.sh +++ b/ci/scripts/derive-localversion.sh @@ -27,8 +27,24 @@ set -euo pipefail # moved tag cannot silently produce two different kernels under one release. # # For branch-tip builds (ref does not end in a date): -# Uses the kernel variant and the SHA alone; there is no date to order by. -# Example: qcom-next @ 07f50dc44edd -> +qcom-next-g07f50dc44edd +# Takes the date from the HEAD commit instead of the tag, so the result has +# the same shape as a tag build and orders in the same sequence. +# Example: qcom-next @ 07f50dc44edd, committed 2026-09-04 +# -> +qcom-next-20260904-g07f50dc44edd +# --date is required for these; pass YYYYMMDD.N to separate two branch-tip +# builds sharing a commit date. +# +# The caller supplies the COMMIT date rather than the build date, so that +# rebuilding a commit reproduces its version instead of inventing a higher +# one, and so that the date describes the source rather than when CI ran. It +# also lands in the same space as upstream's tag dates, which track the +# commit each tag is cut from. +# +# The trade-off: a build date always advances, a commit date need not. If the +# branch is ever rewound to an older commit, the next build's version goes +# DOWN and apt will not offer it as an upgrade. That is arguably honest +# -- older source, older version -- but it is the one case where dating by +# the clock would behave differently. # # Why the leading '+' and not '-': # The suffix ends up in KERNELRELEASE (uname -r), which is the 'version' field @@ -46,12 +62,15 @@ set -euo pipefail # # Usage: # ci/scripts/derive-localversion.sh --variant qcom-next --ref qcom-next-7.2-rc3-20260722 --sha 07f50dc44edd -# ci/scripts/derive-localversion.sh --variant arduino --ref main --sha 07f50dc44edd +# ci/scripts/derive-localversion.sh --variant arduino --ref main --sha 07f50dc44edd --date 20260904 # # Options: # --variant VARIANT Kernel variant identifier. Defaults to qcom-next. # --ref REF Kernel ref (tag name or branch name). Required. # --sha SHA Commit SHA, truncated to 12 hex characters. Required. +# --date DATE HEAD commit date as YYYYMMDD or YYYYMMDD.N. Required for +# branch-tip builds; ignored for dated tags, which carry +# their own date. # # Output: # Three KEY=VALUE lines on stdout, in GITHUB_ENV / 'set -a' form: @@ -60,17 +79,17 @@ set -euo pipefail # SNAPSHOT=20260722.1 # GITSHA=07f50dc44edd # -# LOCALVERSION always starts with a plus. SNAPSHOT is empty for branch-tip -# builds, which have no date; the Debian version then carries no snapshot -# component at all. +# LOCALVERSION always starts with a plus. Every build carries a snapshot, +# whether it came from the tag or from the HEAD commit. # # Exit codes: # 0 Success. -# 1 Error (invalid args, missing or malformed --sha). +# 1 Error (invalid args, malformed --sha, branch tip without --date). VARIANT="qcom-next" REF="" SHA="" +DATE="" usage() { sed -n '/^# Usage:/,/^$/p' "$0" | sed 's/^# \?//' @@ -82,6 +101,7 @@ while [[ $# -gt 0 ]]; do --variant) VARIANT="$2"; shift 2 ;; --ref) REF="$2"; shift 2 ;; --sha) SHA="$2"; shift 2 ;; + --date) DATE="$2"; shift 2 ;; -h|--help) usage ;; *) echo "ERROR: Unknown option: $1" >&2; usage ;; esac @@ -109,9 +129,19 @@ if [[ "$REF" =~ -([0-9]{8}(\.[0-9]+)?)$ ]]; then SNAPSHOT="${BASH_REMATCH[1]}" LOCALVERSION="+${VARIANT}-${SNAPSHOT}-g${GITSHA}" else - # Branch-tip build: no date, so the SHA is the whole identity. - LOCALVERSION="+${VARIANT}-g${GITSHA}" - SNAPSHOT="" + # Branch-tip build: the ref carries no date, so the commit date supplies + # one. Without it these builds had no snapshot at all, which put their + # Debian version below every dated build rather than among them. + [[ -n "$DATE" ]] || { + echo "ERROR: --date is required for branch-tip builds (ref '$REF' is not a dated tag)" >&2 + exit 1 + } + [[ "$DATE" =~ ^[0-9]{8}(\.[0-9]+)?$ ]] || { + echo "ERROR: --date must be YYYYMMDD or YYYYMMDD.N (got '$DATE')" >&2 + exit 1 + } + SNAPSHOT="$DATE" + LOCALVERSION="+${VARIANT}-${SNAPSHOT}-g${GITSHA}" fi echo "LOCALVERSION=${LOCALVERSION}" From 639551ff45a2f32c0ea79584bf5e3d0305b11c61 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 8/9] Record the source repository, ref and commit in the changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit d6c478d, reworked) @GITCLONE@ and @BRANCH@ were substituted with empty strings, so every changelog carried a bare "* Source:" naming nothing. Thread the clone URL, ref and full SHA through prepare-source.sh so the entry says where the build came from: * Source: https://github.com/qualcomm-linux/kernel qcom-next * Commit: 07f50dc44edd… The original justified this as the only place the exact source was pinned, since LOCALVERSION identified a build by date alone. That is no longer true -- the version strings carry the commit -- but the repository and ref are still nowhere else, and a 12-character abbreviation is not the commit itself. The comments asserting otherwise are corrected rather than carried over. prepare-source.sh gains one --git-sha taking the full SHA, not two. The original added a second option under that name for the changelog, alongside the existing one for the version field; bash takes the first matching case arm, so the changelog copy would have been silently unset and every entry would have read "Commit: unknown". They are the same commit at two widths, so the short form is derived from the long one in a single place. Signed-off-by: Christopher Obbard --- debian/changelog.in | 1 + debian/rules | 16 ++++++++++++++-- prepare-source.sh | 35 +++++++++++++++++++++++++++-------- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/debian/changelog.in b/debian/changelog.in index 95b41aeb..802a600a 100644 --- a/debian/changelog.in +++ b/debian/changelog.in @@ -3,6 +3,7 @@ * Qualcomm ARM64 Linux kernel package * Kernel version: @KVER@ * Source: @GITCLONE@ @BRANCH@ + * Commit: @GITSHA@ * Includes kernel image, modules, DTBs, headers, and debug symbols -- Bjordis Collaku Fri, 14 Nov 2025 10:00:00 -0800 diff --git a/debian/rules b/debian/rules index f5ea1914..d888c1c0 100755 --- a/debian/rules +++ b/debian/rules @@ -67,6 +67,11 @@ KREL_FILE := debian/kernel.release # nothing. Each name must have a -dkms package available # in the build environment; the Build-Depends entry for it is # generated from this list, so nothing else declares it. +# GIT_CLONE Kernel repository URL, recorded in debian/changelog. +# GIT_REF Resolved kernel ref (tag or branch), recorded in the changelog. +# GIT_SHA Full kernel HEAD commit SHA, recorded in the changelog. +# GITSHA above is the same commit at 12 characters, for the +# version strings; this is the unabbreviated value. # # Outputs (generated files, all listed in debian/clean): # debian/control Substituted from debian/control.in @@ -90,6 +95,12 @@ SRCPKG ?= linux-qcom-next BINPKG ?= linux-image-qcom-next DEBIAN_REVISION ?= 0qcom1 DKMS_MODULES ?= +# Source provenance recorded in debian/changelog. The version strings name the +# commit but not the repository it came from, so without these a build cannot be +# traced back to its source tree. +GIT_CLONE ?= unknown +GIT_REF ?= unknown +GIT_SHA ?= unknown .PHONY: prepare prepare: @@ -227,8 +238,9 @@ prepare: -e "s|@PKGVER@|$$PKG_VERSION|g" \ -e "s|@DISTRO@|$(DISTRO)|g" \ -e "s|@KVER@|$$KVER_RESOLVED|g" \ - -e "s|@GITCLONE@||g" \ - -e "s|@BRANCH@||g" \ + -e "s|@GITCLONE@|$(GIT_CLONE)|g" \ + -e "s|@BRANCH@|$(GIT_REF)|g" \ + -e "s|@GITSHA@|$(GIT_SHA)|g" \ debian/changelog.in > debian/changelog; \ \ # ── Write runtime state files ───────────────────────────────────────────── diff --git a/prepare-source.sh b/prepare-source.sh index 3cecaa99..b6219431 100755 --- a/prepare-source.sh +++ b/prepare-source.sh @@ -59,12 +59,16 @@ OPTIONS: 20260722 or 20260722.1). Auto-detected from git tag alongside --localversion; pass it explicitly whenever --localversion is passed explicitly. - --git-sha SHA Commit the build was cut from, truncated to 12 - hex characters. Discriminates two builds of one - snapshot (a moved tag). Auto-detected from HEAD - alongside --localversion. + --git-sha SHA Full commit SHA the build was cut from. Its + first 12 characters discriminate two builds of + one snapshot (a moved tag) in the version + strings; the full value is recorded in the + changelog. Auto-detected from HEAD. --kver-extra SUFFIX Extra suffix appended to the final KVER (e.g. -ci42). + --git-clone URL Kernel repository URL, recorded in the changelog. + --git-ref REF Resolved kernel ref (tag or branch), recorded in + the changelog. Package naming: --srcpkg NAME Source package name (default: $DEFAULT_SRCPKG) @@ -125,13 +129,15 @@ SOURCE_DIR="" DISTRO="$DEFAULT_DISTRO" LOCALVERSION="" SNAPSHOT="" -GITSHA="" KVER_EXTRA="" SRCPKG="$DEFAULT_SRCPKG" BINPKG="$DEFAULT_BINPKG" DEBIAN_REVISION="$DEFAULT_DEBIAN_REVISION" KERNEL_CONFIG="" DKMS_MODULES="" +GIT_CLONE="" +GIT_REF="" +GIT_SHA="" while [[ $# -gt 0 ]]; do case $1 in @@ -139,8 +145,10 @@ while [[ $# -gt 0 ]]; do -d|--distro) DISTRO="$2"; shift 2 ;; --localversion) LOCALVERSION="$2"; shift 2 ;; --snapshot) SNAPSHOT="$2"; shift 2 ;; - --git-sha) GITSHA="$2"; shift 2 ;; + --git-sha) GIT_SHA="$2"; shift 2 ;; --kver-extra) KVER_EXTRA="$2"; shift 2 ;; + --git-clone) GIT_CLONE="$2"; shift 2 ;; + --git-ref) GIT_REF="$2"; shift 2 ;; --srcpkg) SRCPKG="$2"; shift 2 ;; --binpkg) BINPKG="$2"; shift 2 ;; --debian-revision) DEBIAN_REVISION="$2"; shift 2 ;; @@ -165,6 +173,13 @@ VALID_DISTROS=(noble questing resolute trixie forky sid unstable) [[ -d "$DEBIAN_DIR" ]] || { log_error "Debian dir not found: $DEBIAN_DIR"; exit 1; } +# ── Resolve the commit, once ───────────────────────────────────────────────── +# One SHA, used at two widths: the first 12 characters go in the version strings +# (short enough to keep a boot menu readable), the full value goes in the +# changelog. Deriving one from the other is what keeps them the same commit. +[[ -n "$GIT_SHA" ]] || GIT_SHA=$(git -C "$SOURCE_DIR" rev-parse HEAD 2>/dev/null || true) +GITSHA="${GIT_SHA:0:12}" + # ── Helper: derive LOCALVERSION, SNAPSHOT and GITSHA from a tag name ───────── # qcom-next-7.2-rc3-20260722 -> +qcom-next-20260722-g / 20260722 # qcom-next-7.2-rc3-20260722.1 -> +qcom-next-20260722.1-g / 20260722.1 @@ -191,8 +206,6 @@ _auto_version_fields() { if [[ -z "$LOCALVERSION" ]]; then GIT_TAG=$(git -C "$SOURCE_DIR" describe --tags --exact-match 2>/dev/null || true) if [[ -n "$GIT_TAG" ]]; then - [[ -n "$GITSHA" ]] || \ - GITSHA=$(git -C "$SOURCE_DIR" rev-parse --short=12 HEAD 2>/dev/null || true) _auto_version_fields "$GIT_TAG" log_info "Auto-detected LOCALVERSION='$LOCALVERSION' SNAPSHOT='$SNAPSHOT' GITSHA='$GITSHA' from tag '$GIT_TAG'" else @@ -332,6 +345,12 @@ PREPARE_ARGS="DISTRO=$DISTRO SRCPKG=$SRCPKG BINPKG=$BINPKG DEBIAN_REVISION=$DEBI [[ -n "$SNAPSHOT" ]] && PREPARE_ARGS="$PREPARE_ARGS SNAPSHOT=$SNAPSHOT" [[ -n "$GITSHA" ]] && PREPARE_ARGS="$PREPARE_ARGS GITSHA=$GITSHA" [[ -n "$KVER_EXTRA" ]] && PREPARE_ARGS="$PREPARE_ARGS KVER_EXTRA=$KVER_EXTRA" +# Source provenance for debian/changelog. LOCALVERSION identifies a build by +# date, not by commit, so the SHA recorded here is what makes a build traceable +# back to exact source -- particularly for branch-tip builds. +[[ -n "$GIT_CLONE" ]] && PREPARE_ARGS="$PREPARE_ARGS GIT_CLONE=$GIT_CLONE" +[[ -n "$GIT_REF" ]] && PREPARE_ARGS="$PREPARE_ARGS GIT_REF=$GIT_REF" +[[ -n "$GIT_SHA" ]] && PREPARE_ARGS="$PREPARE_ARGS GIT_SHA=$GIT_SHA" # Spaces are stripped so a list written as "kgsl, camx" stays a single make # argument; debian/rules validates the names it is given. [[ -n "$DKMS_MODULES" ]] && PREPARE_ARGS="$PREPARE_ARGS DKMS_MODULES=$(tr -d ' ' <<< "$DKMS_MODULES")" From 9e9b9d99281288b6bc3ef5653d8010ad488f65c5 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Fri, 4 Sep 2026 17:23:08 +0100 Subject: [PATCH 9/9] docs: describe the version scheme Records why the kernel release and the Debian version are two strings rather than one, and why each spells the same four fields differently: the '+' join that keeps a release candidate below its final release under systemd, and the '~' before the SHA that keeps a respin above the build it respins under dpkg. Both separators look arbitrary and both are load-bearing, so the reasoning is written down where someone tempted to normalise them will find it. Also covers what the version deliberately does not carry -- the repository and ref live in the changelog -- and the one case where a branch-tip version can go backwards, when the branch is rewound to an older commit. README.md loses its copy of the same reasoning, which had grown into a second account that would drift, and keeps the format table plus a pointer here. Scoped to qcom-next; the derivation is not variant-agnostic. Signed-off-by: Christopher Obbard --- README.md | 23 ++----- docs/version.md | 179 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 16 deletions(-) create mode 100644 docs/version.md diff --git a/README.md b/README.md index bc7fd8d5..294e6f48 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,9 @@ isolated `kernel_variant + suite` build leg. Both build the same kernel ref. `derive-localversion.sh` folds the variant name into LOCALVERSION, so each produces a distinct kernel release -(`-qcom-next-` and `-qcom-next-debug-`) and therefore a distinct -versioned image package that can be installed alongside the other. +(`+qcom-next--g` and `+qcom-next-debug--g`) and therefore a +distinct versioned image package that can be installed alongside the other. See +[docs/version.md](docs/version.md) for how the version strings are composed. `ci/build-matrix.json` is the source of truth; this table is a summary. @@ -238,7 +239,7 @@ Supporting scripts keep workflow YAML small and testable: | --- | --- | | `ci/scripts/resolve-matrix.sh` | Validates and flattens matrix rows. | | `ci/scripts/resolve-kernel-ref.sh` | Resolves a matrix-selected dated tag or validates a direct ref. | -| `ci/scripts/derive-localversion.sh` | Derives the version fields from the variant, resolved kernel ref and HEAD, printing `LOCALVERSION=`, `SNAPSHOT=` and `GITSHA=` lines. `SNAPSHOT` is the dated component of the Debian version and is empty for branch-tip builds. | +| `ci/scripts/derive-localversion.sh` | Derives the version fields from the variant, resolved kernel ref and HEAD, printing `LOCALVERSION=`, `SNAPSHOT=` and `GITSHA=` lines. `SNAPSHOT` is the dated component of the Debian version: the tag's date, or the HEAD commit date for a branch-tip build. Scheme and rationale: [docs/version.md](docs/version.md). | | `ci/scripts/derive-debian-revision.sh` | Derives the final suite-specific `debian_revision` from `debian_version_stub`, `suite_suffix_mapping`, and delivery type. | ## Architecture @@ -406,19 +407,9 @@ Every build names both its snapshot and the commit it was cut from: | Kernel release (`uname -r`) | `+-[.]-g` | `7.2.0-rc7+qcom-next-20260826.1-g011a82096bee` | | Debian version | `+git[.]~g-` | `7.2.0~rc7+git20260826.1~g011a82096bee-0qli1~bpo13+1` | -The date orders builds, the respin ordinal separates two tags cut on the same -day, and the SHA is only a final discriminator so a moved tag cannot produce two -different kernels under one version. Branch-tip builds have no date and are -identified by the SHA alone (`7.2.0-rc7+qcom-next-g011a82096bee`). - -The two strings join the SHA differently because their comparators differ. -`uname -r` is compared by systemd, which weighs the separator before the chunk -behind it, so `-g` leaves an absent respin ordinal sorting below a present -one. dpkg instead reads an exhausted run as lower than a letter, which would put -a respin *below* the build it respins, so the Debian version joins with `~` — -lower than everything, including the empty string. The visible cost is that a -Debian version reads as preceding the same snapshot without a SHA; nothing ever -occupies that slot, since every snapshot build carries one. +The two strings spell the same fields differently because they are compared by +different rules — `+` and `~` are both load-bearing, not stylistic. See +[docs/version.md](docs/version.md) before changing either. `KVER_EXTRA` is supported for explicit suffixes such as `-ci42` or `-local`. The packaging rules verify that the declared versioned image package matches the diff --git a/docs/version.md b/docs/version.md new file mode 100644 index 00000000..f396079d --- /dev/null +++ b/docs/version.md @@ -0,0 +1,179 @@ +# Version scheme + +This describes how a build's two version strings are composed. The examples are +`qcom-next` specific: another variant substitutes its own name and Debian +revision stub, and a variant built from a differently-shaped tag would need its +own derivation. + +A build produces two version strings, and they are deliberately not the same +string: + +```text +uname -r 7.2.0-rc7+qcom-next-20260821-gabcdef123456 +Debian version 7.2.0~rc7+git20260821~gabcdef123456-0qli1~bpo13+1 +``` + +They carry the same four facts — upstream kernel version, snapshot date, +same-day respin, commit — but they are read by two different comparators with +two different sets of rules, and each string is spelled for its own. + +## What the fields mean + +Both strings are derived from the tag and HEAD together, by +`ci/scripts/derive-localversion.sh`: + +```text +qcom-next-7.2-rc7-20260821 @ abcdef123456… + │ │ + │ └── snapshot: 20260821, optionally . + └────────── upstream kernel version: 7.2.0-rc7 +``` + +The **snapshot date** orders builds. The **respin ordinal** separates two tags +cut on the same day. The **commit** orders nothing — two SHAs have no relation — +and exists only so that a moved tag cannot produce two different kernels under +one version. It therefore comes last in both strings, after everything that does +carry ordering. + +There is no `.0` on the first tag of a day. Both comparators read an absent +ordinal as lower than a present one, so the respin already sorts above it, and +the version stays closer to the tag it came from. + +## Kernel release + +```text ++qcom-next-[.]-g<12 hex> +``` + +The full upstream version survives here, `-rc7` included: `uname -r` is the +first thing in a bug report, and it is what says whether the reporter is on a +release candidate or a stable sublevel. + +The suffix joins with `+`, not `-`. systemd compares the separator before the +chunk behind it, and `-` sorts below `+`, so `+` puts every release candidate +below the final release that follows it. Joining with `-` instead falls through +to a plain comparison of `rc` against `qcom`, where `r` > `q`, and every rc +outranks its own final release in the boot menu. This is the same trick Debian's +own kernels use (`linux-image-7.1.10+deb14-amd64`). + +The variant name is part of the string, so a flavour is a distinct kernel that +installs alongside the others rather than replacing them: + +```text +7.2.0-rc7+qcom-next-debug-20260821-gabcdef123456 +``` + +This string is also the versioned binary package name +(`linux-image-`), so a new commit means a new package name. That +is intended: it is what lets several builds coexist, and what makes the commit +recoverable from an archive listing. + +## Debian version + +```text ++git[.]~g<12 hex>- +``` + +`-rcN` becomes `~rcN`, because dpkg reads `~` as "sorts below", giving +`7.2.0~rc7 < 7.2.0`. Spelled `-rc7` it would sort *above* the release it +precedes. + +The snapshot is spelled `+git` in Debian's usual idiom for a VCS snapshot. + +The commit joins with `~`, and this is the part most likely to look like a typo. +dpkg alternates digit and non-digit runs and reads an exhausted run as lower +than a letter, so joining with `.` decides the comparison before ever reaching +the respin ordinal: + +```text +7.2.0+git20260826.g3f2f3ca1a81e > 7.2.0+git20260826.1.gabcdef123456 +``` + +— the respin sorting *below* the build it respins. `~` sorts below everything, +including the empty string, so the ordinal is always compared first and the SHA +only ever breaks a tie between builds that share a snapshot. The alternative, +spelling `.0` on every non-respin build, buys the same ordering at the cost of a +redundant ordinal in every version forever. + +The cost of `~` is one misleading reading: the version sorts below the same +snapshot without a SHA, as though it preceded it. Nothing occupies that slot, +because every snapshot build carries a SHA. + +The revision (`0qli1~bpo13+1`) is derived separately, from the version stub, the +suite, and whether the build is a Daily or a Release. See the matrix +documentation in the top-level [README](../README.md#matrix-model); the trailing +digit on the stub is the packaging revision, bumped when the packaging changes +but the kernel snapshot does not. + +## Ordering + +The full chain for one suite, in the order dpkg sorts it: + +```text +7.2.0~rc7+git20260820.1~g011a82096bee-0qli1~bpo13+1 first tag of the 20th +7.2.0~rc7+git20260820.2~g3f2f3ca1a81e-0qli1~bpo13+1 respin, same day +7.2.0~rc7+git20260821~gabcdef123456-0qli1~bpo13+1~ Daily, next snapshot +7.2.0~rc7+git20260821~gabcdef123456-0qli1~bpo13+1 Release of the same +7.2.0~rc7+git20260821~gabcdef123456-0qli2~bpo13+1 packaging rebuild +7.2.0+git20260902~g123456789abc-0qli1~bpo13+1 7.2 final +``` + +A Daily sorts below the Release of the same snapshot because its revision ends +in a trailing `~`. + +## Moved tags + +If an upstream tag is re-cut against a different commit, the SHA changes and so +does the version, so the two builds cannot be mistaken for each other. But +nothing guarantees the replacement sorts *above* the original — two hashes have +no order — so apt may not offer it as an upgrade: + +```text +7.2.0~rc7+git20260821~g011a82096bee-0qli1~bpo13+1 original +7.2.0~rc7+git20260821~gabcdef123456-0qli1~bpo13+1 retagged; happens to sort above +7.2.0~rc7+git20260821~g0009f3c1d2e4-0qli1~bpo13+1 retagged; sorts BELOW, no upgrade +``` + +Whether it lands above or below is down to the hex, so treat a moved tag as +needing a version bump of its own — the respin ordinal, or the packaging +revision: + +```text +7.2.0~rc7+git20260821.1~g0009f3c1d2e4-0qli1~bpo13+1 supersedes it either way +``` + +## Branch-tip builds + +A build from a branch rather than a tag has no tag date, so the date comes from +the HEAD commit instead. The result has the same shape as a tag build and orders +in the same sequence: + +```text +uname -r 7.2.0-rc7+qcom-next-20260904-g07f50dc44edd +Debian version 7.2.0~rc7+git20260904~g07f50dc44edd-0qli1~bpo13+1 +``` + +It is the *committer* date of the commit, not the time the build ran. That means +rebuilding a commit reproduces its version instead of inventing a higher one, +and the date describes the source rather than when CI happened to start. Author +dates are not used, because a backported patch can carry one months old. + +The catch is that a build clock only ever moves forwards, and a commit date does +not. If the branch is rewound to an older commit, the next build's version goes +*down*, and apt will not offer it as an upgrade. That is arguably the honest +answer — older source, older version — but it is the one case where dating by +the clock would behave differently. + +## What is not in the version + +The version strings name the commit, but not the tree it came from. The +repository, the resolved ref and the full 40-character SHA are recorded in the +package changelog instead: + +```text + * Kernel version: 7.2.0-rc7+qcom-next-20260904-g07f50dc44edd + * Source: https://github.com/qualcomm-linux/kernel qcom-next + * Commit: 07f50dc44edd… +``` + +so `apt changelog` on an installed image is enough to find the exact source.