Skip to content

Package version improvements - #102

Open
Christopher Obbard (obbardc) wants to merge 8 commits into
mainfrom
wip/obbardc/version-improvements
Open

Package version improvements#102
Christopher Obbard (obbardc) wants to merge 8 commits into
mainfrom
wip/obbardc/version-improvements

Conversation

@obbardc

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

Copy link
Copy Markdown
Contributor

This branch reworks how the package's version is derived.

This is required because systemd-boot sorted versions with rc7 higher than non-rc in the systemd-boot menu.

The Kernel release (uname -r) and Debian package versions now both carry:

  • a + (not -) join, so release candidates sort below their final release in the systemd-boot menu
  • the original source tag's snapshot date, or the HEAD commit's date for branch-tip builds, so every build lands in the same dated sequence instead of branch tips sorting below everything
  • ability for original source to package a bumped tag cut on the same day (e.g. <version>.1)
  • the original source commit SHA, so a moved tag can never silently produce two different kernels under one version.

Also:

  • the Debian snapshot is composed by derive-localversion.sh rather than parsed from LOCALVERSION, which was fragile (a branch-tip SHA can itself end in 8 digits and get misread as a date)
  • the changelog now records the source repository, ref and full commit SHA, which were previously templated to empty strings
  • the packaging revision stub gained a digit so a packaging-only rebuild has something to bump
  • docs/version.md documents the scheme and the reasoning behind both separators, since neither is stylistic

See docs/version.md for the full rationale.

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-<KVER>. 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 <chris.obbard@oss.qualcomm.com>
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 <chris.obbard@oss.qualcomm.com>
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.

The composed component is spelled +git<snapshot>, which is Debian's own idiom
for a VCS snapshot, rather than appended bare as +<snapshot> where it reads as
part of the upstream version:

  7.2.0~rc3+git20260722-0qcom1

Ordering is unaffected -- 'git' is a constant prefix on both sides of every
comparison, and the date behind it still decides.

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 <chris.obbard@oss.qualcomm.com>
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 +<whole-tag> 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 <chris.obbard@oss.qualcomm.com>
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<sha>'
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 <chris.obbard@oss.qualcomm.com>
A branch-tip build had no snapshot, so debian/rules emitted a version with no
+git<date> 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.

--date only supplies what a branch tip's ref cannot: --sha is still required
for every build, and a dated tag carries its own date and ignores --date. A
branch-tip release therefore has the same shape as a tag build rather than a
second shape of its own.

prepare-source.sh gets the same treatment, so the fix is not confined to the
paths CI happens to drive. Its auto-detect covers two refs that carry no date
-- a tag that is not dated, and no tag at all -- and both left SNAPSHOT empty,
which reproduced exactly the undated version above for anyone running the
script directly. Both now date the build by HEAD, matching how
derive-localversion.sh treats any ref without a date of its own.

An explicit --localversion still supplies no snapshot and still warns. That
path is an override: the ref was replaced precisely because it no longer
describes the build, so dating it from HEAD would assert something the caller
has just denied.

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.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
@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 version strings already name the commit, so this is not the only record
of it. What they do not carry is the repository and the ref, which are
nowhere else at all, and a 12-character abbreviation is not the commit
itself. The comments claiming the changelog is the sole pin on the exact
source are corrected to say what it actually pins.

prepare-source.sh gains one --git-sha, taking the full SHA. The 12-character
form used in the version strings is derived from it a few lines later rather
than accepted as a second option, so the two widths are provably the same
commit and there is one place to change if that width ever moves.

Signed-off-by: Christopher Obbard <chris.obbard@oss.qualcomm.com>
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 keeps the format table and the note that the separators are
load-bearing, and points here for the reasoning rather than carrying a second
account of it that would drift.

Scoped to qcom-next; the derivation is not variant-agnostic.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran the ordering claims through a dpkg comparator, including the one that matters most for a version rework: the upgrade path from what is already published. 7.2.0+20260826-0qli~bpo13+1 sorts below 7.2.0+git20260826~gd49c...-0qli1~bpo13+1, so nothing in qli gets stranded. rc below final, respin above first tag of the day, Daily below Release and the packaging rebuild all check out too.

~ where the version field allows it and + where a package name cannot carry it is the right split. docs/version.md is a good record of why, particularly the ~g<sha> reasoning.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants