diff --git a/.github/workflows/build-kernel-deb.yml b/.github/workflows/build-kernel-deb.yml index 9da4258..d31dd3b 100644 --- a/.github/workflows/build-kernel-deb.yml +++ b/.github/workflows/build-kernel-deb.yml @@ -53,13 +53,13 @@ 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 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: @@ -124,13 +124,13 @@ 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 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: @@ -273,7 +273,19 @@ jobs: echo "KERNEL_SHA=$KERNEL_SHA" >> "$GITHUB_ENV" echo "Kernel HEAD SHA: $KERNEL_SHA" - - name: Derive LOCALVERSION + # 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 }} KERNEL_VARIANT_INPUT: ${{ inputs.kernel-variant || 'qcom-next' }} @@ -281,15 +293,30 @@ 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 +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 - LOCALVERSION=$(ci/scripts/derive-localversion.sh \ + # 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 "$(echo "$KERNEL_SHA" | cut -c1-12)") - echo "LOCALVERSION=$LOCALVERSION" >> "$GITHUB_ENV" - echo "Derived LOCALVERSION: $LOCALVERSION" + --sha "$KERNEL_SHA" \ + --date "$KERNEL_DATE") + echo "$FIELDS" >> "$GITHUB_ENV" + echo "Derived version fields:" + echo "$FIELDS" fi - name: Resolve Debian revision @@ -301,7 +328,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" @@ -401,7 +428,16 @@ 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") + # 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/README.md b/README.md index 1c60c35..294e6f4 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. @@ -62,7 +63,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 +77,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 +94,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. @@ -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 `LOCALVERSION` from the variant and resolved kernel 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: 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 @@ -399,6 +400,17 @@ 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 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 resolved kernel release and fail instead of creating inconsistent metadata. @@ -439,7 +451,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 655aefc..a28175d 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 71b3af8..008e993 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 \ diff --git a/ci/scripts/derive-localversion.sh b/ci/scripts/derive-localversion.sh index d36e9b0..eca3c98 100755 --- a/ci/scripts/derive-localversion.sh +++ b/ci/scripts/derive-localversion.sh @@ -3,37 +3,93 @@ # 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. # -# For dated tag builds (ref ends in -YYYYMMDD): -# Produces --. -# Example: qcom-next-7.2-rc3-20260722 -> -qcom-next-20260722 +# 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 +-[.]-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 +# 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". +# +# 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. -# Example: qcom-next @ 07f50dc44edd -> -qcom-next-g07f50dc44edd -# --sha is required for branch-tip builds. +# 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 +# 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 +# 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 --date 20260904 # # 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. +# --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: -# LOCALVERSION suffix printed to stdout (e.g. -qcom-next-20260722). -# Always starts with a dash. +# Three KEY=VALUE lines on stdout, in GITHUB_ENV / 'set -a' form: +# +# LOCALVERSION=+qcom-next-20260722.1-g07f50dc44edd +# SNAPSHOT=20260722.1 +# GITSHA=07f50dc44edd +# +# 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, branch-tip without --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/^# \?//' @@ -45,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 @@ -55,19 +112,38 @@ 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. 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}" +# 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}-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 + # 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 } - # Use first 12 chars of SHA for a compact but unambiguous suffix. - SHORT_SHA="${SHA:0:12}" - echo "-${VARIANT}-g${SHORT_SHA}" + [[ "$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}" +echo "SNAPSHOT=${SNAPSHOT}" +echo "GITSHA=${GITSHA}" diff --git a/debian/changelog.in b/debian/changelog.in index 95b41ae..802a600 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 1de36c9..d888c1c 100755 --- a/debian/rules +++ b/debian/rules @@ -47,10 +47,16 @@ 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. +# 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). @@ -61,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 @@ -70,18 +81,26 @@ 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). +# (e.g. 7.2.0~rc3+git20260722-0qcom1). # Read by override_dh_gencontrol. # --------------------------------------------------------------------------- KVER_EXTRA ?= +SNAPSHOT ?= +GITSHA ?= DISTRO ?= unstable 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: @@ -93,8 +112,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..."; \ @@ -112,11 +131,24 @@ 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: +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. @@ -131,10 +163,23 @@ 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}(\.[0-9]+)?$$' || { \ + echo "ERROR: SNAPSHOT must be YYYYMMDD with an optional . (got '$(SNAPSHOT)')"; \ + 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; \ @@ -193,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/docs/version.md b/docs/version.md new file mode 100644 index 0000000..f396079 --- /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. diff --git a/prepare-source.sh b/prepare-source.sh index f0cbbc7..b621943 100755 --- a/prepare-source.sh +++ b/prepare-source.sh @@ -52,10 +52,23 @@ 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. + --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. + --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) @@ -101,7 +114,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 \\ @@ -115,19 +128,27 @@ EOF SOURCE_DIR="" DISTRO="$DEFAULT_DISTRO" LOCALVERSION="" +SNAPSHOT="" 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 -s|--source-dir) SOURCE_DIR="$2"; shift 2 ;; -d|--distro) DISTRO="$2"; shift 2 ;; --localversion) LOCALVERSION="$2"; shift 2 ;; + --snapshot) SNAPSHOT="$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 ;; @@ -152,28 +173,51 @@ 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() { +# ── 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 +# +# 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. +# +# 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 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]}" + if [[ "$tag" =~ ^([a-z-]+)-[0-9]+\.[0-9]+.*-([0-9]{8}(\.[0-9]+)?)$ ]]; then + SNAPSHOT="${BASH_REMATCH[2]}" + LOCALVERSION="+${BASH_REMATCH[1]}-${SNAPSHOT}-g${GITSHA}" else - echo "-$tag" + LOCALVERSION="+$tag" + SNAPSHOT="" fi } -# ── Auto-detect LOCALVERSION 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 - 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' 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)." - 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 +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 +git component. Pass --snapshot to supply one." fi log_step "Configuration:" @@ -183,6 +227,8 @@ 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 "$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" @@ -296,7 +342,15 @@ 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 "$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")"