commit 1d543c5286cc8832946571c6f7490f16833c4cc4
parent b2cd3e535234df4d9e7642509ad843223a5b0aeb
Author: Christian Grothoff <christian@grothoff.org>
Date: Mon, 27 Jul 2026 16:38:19 +0200
packaging/ng: add "upgrade" subcommand, fix tag to debian version mapping
Diffstat:
3 files changed, 232 insertions(+), 6 deletions(-)
diff --git a/packaging/ng/README.md b/packaging/ng/README.md
@@ -53,11 +53,76 @@ apt-get install qemu-user-static binfmt-support
Also contains a `Packages.gz` metadata file generated by `dpkg-scanpackages`
so that this folder can be directly consumed as a trusted package source.
+* `buildconfig/*`: Per-component configuration, see "Versions" below.
* `buildscripts/*`: Build scripts used during the package build steps.
* `distros/*`: Files for building for a specific distro.
* `server-side/*.sh`: Scripts that must be installed in taler-packaing@taler.net:$HOME
+## Versions
+
+Each component is configured by these files in `buildconfig/`:
+
+* `$component.giturl`: the upstream repository.
+* `$component.tag`: the git tag that is built. It determines the version of the
+resulting debian package.
+* `$component.debpath`: the directory within the repository that contains the
+`debian/` folder, relative to the repository root. Optional, defaults to the
+repository root itself.
+
+`debpath` exists so that one repository can provide several packages that are
+built independently. Their tag files are separate, but the tags all come from
+the same repository, so `upgrade` moves all of them to the same tag. This
+affects every component built from `taler-typescript-core`
+(`taler-wallet-cli`, `taler-harness` and the `*-webui` packages), each of which
+has a `packages/$component` debpath.
+
+Supported tag syntax (see `buildscripts/generic`):
+
+* `v$maj.$min.$patch`, e.g. `v1.2.3`: release version.
+* `v$maj.$min.$patch-dev.$n`, e.g. `v1.2.3-dev.4`: dev version.
+* `deb-v$maj.$min.$patch-$revision`, e.g. `deb-v1.2.3-1`: release version with a
+debian revision, for rebuilds that only change `debian/`.
+
+Debian revisions of dev versions are *not* supported. Other tag formats occur
+in some of the repositories (`debian-1.2.3`, `v1.2.3-debian-1`, `v1.2.3a`, ...)
+and are ignored by `taler-pkg`.
+
+Tags are ordered by major, minor and patch first. Within one release, dev
+versions sort before the release and debian revisions after it:
+
+```
+v1.2.3-dev.1 < v1.2.3-dev.2 < v1.2.3 < deb-v1.2.3-1 < v1.2.4-dev.1
+```
+
+### From git tag to debian version
+
+`buildscripts/generic` derives the debian version of a package from the tag in
+two steps. `get_tag_debver` strips the `v` / `deb-v` prefix and rewrites
+`-dev.$n` into `~dev$n`, which sorts before the corresponding release.
+`make_codename_version` then appends the distro codename. If the version has no
+debian revision at that point, `-0` is inserted first: a plain `1.2.3+trixie`
+would be *newer* than `1.2.3`, and `1.2.3~trixie` would be older than any real
+`1.2.3-$revision`, so both would break version requirements in dependencies.
+
+Examples, for codename `trixie`:
+
+```
+v1.2.3 => 1.2.3-0+trixie
+v1.2.3-dev.4 => 1.2.3~dev4-0+trixie
+v1.2.3-dev.10 => 1.2.3~dev10-0+trixie
+deb-v1.2.3 => 1.2.3-0+trixie
+deb-v1.2.3-1 => 1.2.3-1+trixie
+```
+
+The resulting versions sort the same way as the tags they come from, so bumping
+a tag file always produces a package that apt considers newer:
+
+```
+1.2.3~dev1-0+trixie < 1.2.3~dev10-0+trixie < 1.2.3-0+trixie < 1.2.3-1+trixie
+```
+
+
## Recipes
### Building for a distribution
@@ -66,16 +131,42 @@ so that this folder can be directly consumed as a trusted package source.
./taler-pkg build $DISTRO
```
-### Updating component versions
+### Bumping component versions
```
-# will output current vs latest version
+# Show current vs. latest version
./taler-pkg show-latest
-# Update component version
+# Show which tag files would change
+./taler-pkg upgrade --dry
+
+# Bump all components to the latest release tag
+./taler-pkg upgrade
+
+# Bump only some components
+./taler-pkg upgrade gnunet taler-exchange
+
+# Also consider dev tags
+./taler-pkg upgrade --dev
+
+# Set a version manually
echo $desired_version > buildconfig/$component.tag
```
+`upgrade` looks at every `buildconfig/*.tag` file, queries the corresponding
+repository for its tags and writes back the newest one. Without `--dev`, only
+release and `deb-v` tags are considered, with `--dev` also dev tags.
+
+Components are only bumped forwards. A tag file that already points to a
+version newer than the newest candidate is left alone, which is what happens to
+components pinned to a dev tag when running without `--dev`. Tag files with
+unsupported syntax and components without a `.giturl` are also left alone.
+
+Components that share a repository, i.e. those distinguished by a `debpath`,
+always end up on the same tag.
+
+`upgrade` only changes tag files; run `./taler-pkg build $DISTRO` afterwards.
+
### Forcing a rebuild
```
diff --git a/packaging/ng/buildscripts/generic b/packaging/ng/buildscripts/generic
@@ -40,14 +40,14 @@ def get_tag_debver(tag):
if d < 0:
return tag[1:]
else:
- return tag[1:d] + "~dev" + tag[d + len(devsuff)]
+ return tag[1:d] + "~dev" + tag[d + len(devsuff):]
if tag.startswith("deb-v"):
tag = tag[5:]
if "-" in tag:
a, b = tag.split("-")
- return a + ":" + b
+ return a + "-" + b
return tag
- raise Error("unexpected tag format")
+ raise ValueError(f"unexpected tag format: {tag}")
diff --git a/packaging/ng/taler-pkg b/packaging/ng/taler-pkg
@@ -8,6 +8,7 @@ import argparse
import subprocess
import platform
import os
+import re
import sys
from pathlib import Path
@@ -520,6 +521,119 @@ def print_latest(cfg):
check_version(name, giturl)
+# Tag syntax variants supported by buildscripts/generic:
+# v$maj.$min.$patch => release version
+# v$maj.$min.$patch-dev.$n => dev version
+# deb-v$maj.$min.$patch-$revision => release version with debian revision
+# Debian revisions of dev versions are *not* supported
+tag_re_release = re.compile(r"v(\d+)\.(\d+)\.(\d+)")
+tag_re_dev = re.compile(r"v(\d+)\.(\d+)\.(\d+)-dev\.(\d+)")
+tag_re_deb = re.compile(r"deb-v(\d+)\.(\d+)\.(\d+)(?:-(\d+))?")
+
+
+def tag_sortkey(tag):
+ """Get a sort key for a tag, or None if the tag syntax isn't supported.
+
+ Dev versions sort before the corresponding release version, debian
+ revisions sort after it.
+ """
+ m = tag_re_release.fullmatch(tag)
+ if m:
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 1, 0)
+ m = tag_re_dev.fullmatch(tag)
+ if m:
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 0, int(m.group(4)))
+ m = tag_re_deb.fullmatch(tag)
+ if m:
+ rev = m.group(4)
+ return (int(m.group(1)), int(m.group(2)), int(m.group(3)), 1, int(rev or 0))
+ return None
+
+
+def list_remote_tags(url):
+ """Get all tags from the git repo"""
+ cmd = ["git", "ls-remote", "--exit-code", "--refs", "--tags", url]
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
+ tags = []
+ for line in result.stdout.strip().split("\n"):
+ parts = line.split()
+ if len(parts) < 2:
+ continue
+ # refs/tags/v1.0.0 -> v1.0.0
+ tags.append(parts[1].split("/")[-1])
+ return tags
+
+
+def latest_tag(tags, dev):
+ """Find the newest supported tag, only considering dev tags if dev is set"""
+ best = None
+ bestkey = None
+ # Iterate in sorted order, so that the result doesn't depend on
+ # the order in which the remote lists its refs.
+ for tag in sorted(tags):
+ if not dev and tag_re_dev.fullmatch(tag):
+ continue
+ key = tag_sortkey(tag)
+ if key is None:
+ continue
+ if bestkey is None or key > bestkey:
+ best = tag
+ bestkey = key
+ return best
+
+
+def upgrade(cfg):
+ """Upgrade tag files in buildconfig to the latest upstream tag"""
+ names = cfg.components
+ if not names:
+ names = sorted(p.stem for p in Path("buildconfig").glob("*.tag"))
+ # Multiple components can share a repo, only ask each remote once.
+ remote_tags = {}
+ upgraded = []
+ for name in names:
+ tag_file = Path("buildconfig") / f"{name}.tag"
+ url_file = Path("buildconfig") / f"{name}.giturl"
+ if not url_file.exists():
+ print(f"[?] {name} has no giturl, skipping", file=sys.stderr)
+ continue
+ giturl = url_file.read_text().strip()
+ if giturl not in remote_tags:
+ remote_tags[giturl] = list_remote_tags(giturl)
+ latest = latest_tag(remote_tags[giturl], cfg.dev)
+ if latest is None:
+ print(f"[?] {name} has no usable tag in {giturl}, skipping", file=sys.stderr)
+ continue
+ curr = None
+ if tag_file.exists():
+ curr = tag_file.read_text().strip()
+ currkey = tag_sortkey(curr)
+ if currkey is None:
+ print(
+ f"[?] {name} tag {curr} has unsupported syntax, skipping",
+ file=sys.stderr,
+ )
+ continue
+ latestkey = tag_sortkey(latest)
+ if currkey > latestkey:
+ # Happens when the tag file pins a dev version but only
+ # production tags are considered.
+ print(f" {name} {curr} (newer than latest {latest})")
+ continue
+ if currkey == latestkey:
+ print(f" {name} {curr} (up to date)")
+ continue
+ print(f"[!] {name} {curr or '(none)'} -> {latest}")
+ upgraded.append(name)
+ if not cfg.dry:
+ tag_file.write_text(latest + "\n")
+ if not upgraded:
+ print("nothing to upgrade")
+ elif cfg.dry:
+ print("would upgrade:", " ".join(upgraded))
+ else:
+ print("upgraded:", " ".join(upgraded))
+
+
def main():
parser = argparse.ArgumentParser(
prog="taler-pkg", description="Taler Packaging Helper"
@@ -576,6 +690,27 @@ def main():
)
parser_show_latest.set_defaults(func=print_latest)
+ # subcommand upgrade
+
+ parser_upgrade = subparsers.add_parser(
+ "upgrade", help="Upgrade component tags to the latest upstream version."
+ )
+ parser_upgrade.set_defaults(func=upgrade)
+ parser_upgrade.add_argument(
+ "components",
+ nargs="*",
+ help="Components to upgrade (default: all components in buildconfig)",
+ )
+ parser_upgrade.add_argument(
+ "--dev",
+ help="Also consider dev tags, not just production tags",
+ action="store_true",
+ default=False,
+ )
+ parser_upgrade.add_argument(
+ "--dry", help="Dry run", action="store_true", default=False
+ )
+
# subcommand show-order
parser_show_order = subparsers.add_parser("show-order", help="Show build order.")