summaryrefslogtreecommitdiffstats
path: root/deb_packaging
diff options
context:
space:
mode:
authorAnton <anton@picapica.im>2026-09-15 03:56:29 +0400
committerAnton <anton@picapica.im>2026-09-15 03:56:29 +0400
commit30ee9355a97ea3939514b6fcb08ea7da5c6df8f7 (patch)
treec4900c04f1fa2700f542eac81e5f4d0c67272e1b /deb_packaging
parent4f9ebf0ebf61d95d9e3495b6596c954568e68c9d (diff)
downloadpica-pica-30ee9355a97ea3939514b6fcb08ea7da5c6df8f7.tar.gz
pica-pica-30ee9355a97ea3939514b6fcb08ea7da5c6df8f7.zip
build scripts for building *.deb packages and repositories for supported distros
Diffstat (limited to 'deb_packaging')
-rw-r--r--deb_packaging/README.md138
-rwxr-xr-xdeb_packaging/build-pica-packages.sh391
-rwxr-xr-xdeb_packaging/build-webrtc-audio-processing.sh342
-rwxr-xr-xdeb_packaging/container-build.sh136
4 files changed, 1007 insertions, 0 deletions
diff --git a/deb_packaging/README.md b/deb_packaging/README.md
new file mode 100644
index 0000000..c87fd15
--- /dev/null
+++ b/deb_packaging/README.md
@@ -0,0 +1,138 @@
+# Debian and Ubuntu packaging
+
+Builds the pica-pica packages for every distribution we support, each in its
+own podman container, and assembles the results into a signed apt repository.
+
+## Scripts
+
+| | |
+|---|---|
+| `build-pica-packages.sh` | The one to run. Installs podman, builds every target, assembles and signs the repository. |
+| `build-webrtc-audio-processing.sh` | Backports Debian's webrtc-audio-processing 1.3 to a distribution that has no 1.x of its own. Usable on its own; the orchestrator calls it per container. |
+| `container-build.sh` | The in-container worker. Not run by hand. |
+
+## Usage
+
+Make a source tarball the usual way, then hand it over:
+
+```sh
+cd /path/to/pica-pica && ./configure && make dist
+cd deb_packaging
+./build-pica-packages.sh --gpg-key packaging@picapica.im \
+ ../pica-pica-0.9.0dev.tar.gz
+```
+
+That leaves a repository in `repo/` and the raw `.deb` files in `out/`.
+
+Useful while iterating:
+
+```sh
+./build-pica-packages.sh --only 'noble/*' --no-sign ../pica-pica-*.tar.gz
+./build-pica-packages.sh --skip-build -k KEYID # re-sign what is in out/
+```
+
+A `make dist` tarball is self-contained: it already carries the generated
+`configure` and `Makefile.in`, so the containers have nothing to bootstrap.
+They will fall back to `autoreconf -i -f` if handed a tarball made some other
+way, and `autoconf`/`automake` are installed regardless — if the tarball's
+timestamps leave `Makefile.am` looking newer than `Makefile.in`, automake's
+own rebuild rules fire during the build and want them.
+
+Whatever the tarball is called and however it is compressed, it is bind
+mounted into each container under one fixed name; `tar` works the compression
+out from the content.
+
+## Build matrix
+
+| Distribution | Codename | amd64 | i386 | webrtc-audio-processing |
+|---|---|---|---|---|
+| Ubuntu 24.04 LTS | noble | yes | **no** | backported by us (archive has 0.3.1) |
+| Ubuntu 26.04 LTS | resolute | yes | **no** | from the archive (1.3-3build2) |
+| Debian 13 | trixie | yes | yes | from the archive (1.3-3) |
+
+### Why there is no Ubuntu i386
+
+Ubuntu dropped i386 as an architecture after 18.04. What is left is a small
+multiarch compatibility subset with no installable base system: there is no
+i386 Ubuntu container image (`docker.io/i386/ubuntu` stops at `18.04`) and no
+i386 Qt to build against. Debian still carries i386 as a full architecture,
+including `qtbase5-dev`, so the 32 bit build is done there.
+
+## The webrtc-audio-processing backport
+
+pica-client needs AEC3, which arrived in webrtc-audio-processing 1.x. Ubuntu
+24.04 ships 0.3.1, which predates it and has an entirely different API, so
+noble needs a backport of Debian's 1.3-3. Nothing else does — the check is
+made inside each container, and the script exits without doing anything where
+the archive is already good enough.
+
+Debian's packaging is taken unmodified except for two things:
+
+1. **The `-dev` package is renamed** to `libwebrtc-audio-processing-1-dev`,
+ which is the name Debian itself used for 1.3-1. Debian renamed it to the
+ unversioned `libwebrtc-audio-processing-dev` in 1.3-2, after dropping 0.3
+ from the archive — but noble still ships 0.3.1 under exactly that name, and
+ taking it over would replace a package other things build against.
+
+ Nothing else collides. Upstream already versions everything that matters:
+
+ | | 0.3.1 (noble) | 1.3 (ours) |
+ |---|---|---|
+ | headers | `/usr/include/webrtc_audio_processing/` | `/usr/include/webrtc-audio-processing-1/` |
+ | library | `libwebrtc_audio_processing.so.1` | `libwebrtc-audio-processing-1.so.3` |
+ | pkg-config | `webrtc-audio-processing` | `webrtc-audio-processing-1` |
+ | runtime deb | `libwebrtc-audio-processing1` | `libwebrtc-audio-processing-1-3` |
+
+ So the four packages we produce — `libwebrtc-audio-processing-1-dev`,
+ `libwebrtc-audio-processing-1-3`, `libwebrtc-audio-coding-dev`,
+ `libwebrtc-audio-coding-1-3` — are all collision-free on noble.
+
+2. **Debian's rename-era `Breaks`/`Replaces` are dropped.** With the old name
+ back they say nothing, and the pair on `libwebrtc-audio-coding-dev` would
+ otherwise declare `Replaces: libwebrtc-audio-processing-dev (<< 1.3-2~)`,
+ which on noble matches the distribution's own 0.3 package — the exact thing
+ this is trying not to disturb. The unrelated relationship between the two
+ runtime packages is kept.
+
+The version is `1.3-3~noble1`. The `~` sorts it *below* plain `1.3-3`, so if
+Ubuntu ever ships 1.3 itself, apt upgrades away from the backport rather than
+being held back by it.
+
+## Signing
+
+The GPG key never enters a container. Containers produce `.deb` files;
+the repository is assembled and signed on the host. Both `InRelease`
+(what modern apt fetches) and `Release.gpg` are produced, and the public key
+is exported to `repo/pica-pica-archive-keyring.asc`.
+
+Packages themselves are not signed with `debsigs` — apt does not check that;
+repository trust comes from the signed `Release`.
+
+## Repository layout
+
+One pool per codename rather than a shared one. Indices are per suite, and a
+shared pool would let noble's `Packages` list trixie's binaries — apt would
+offer them and they would not install.
+
+```
+repo/
+├── dists/<codename>/
+│ ├── Release, Release.gpg, InRelease
+│ └── main/binary-<arch>/{Packages,Packages.gz,Release}
+├── pool/<codename>/main/<letter>/<source>/*.deb
+└── pica-pica-archive-keyring.asc
+```
+
+## Client setup
+
+```sh
+sudo install -d /usr/share/keyrings
+sudo curl -fsSL https://YOUR.HOST/pica-pica-archive-keyring.asc \
+ -o /usr/share/keyrings/pica-pica-archive-keyring.asc
+
+echo "deb [signed-by=/usr/share/keyrings/pica-pica-archive-keyring.asc] \
+https://YOUR.HOST $(lsb_release -cs) main" \
+ | sudo tee /etc/apt/sources.list.d/pica-pica.list
+
+sudo apt update && sudo apt install pica-client
+```
diff --git a/deb_packaging/build-pica-packages.sh b/deb_packaging/build-pica-packages.sh
new file mode 100755
index 0000000..4371c2e
--- /dev/null
+++ b/deb_packaging/build-pica-packages.sh
@@ -0,0 +1,391 @@
+#!/bin/bash
+#
+# Builds the pica-pica Debian packages for every distribution we support, each
+# in its own podman container, and assembles the results into a signed apt
+# repository.
+#
+# Takes a source tarball as produced by "make dist" - a self contained
+# autotools distribution, which already carries the generated configure, so
+# the containers have nothing to bootstrap.
+#
+# Meant to be run on an Ubuntu 24.04 host, but the only thing that is host
+# specific is installing podman; give it podman by hand and it will run
+# anywhere.
+#
+# The build matrix is at the top of the script. Note what is NOT in it:
+# Ubuntu i386. Ubuntu has had no i386 architecture since 19.10 - what remains
+# is a small multiarch compatibility subset with no installable base system,
+# so there is no i386 Ubuntu container image to build in (docker.io/i386/ubuntu
+# stops at 18.04) and no i386 Qt to build against. Debian still has i386 as a
+# full architecture, so the 32 bit build happens there.
+#
+# The GPG key never enters a container. Containers produce .deb files; the
+# repository is assembled and signed here on the host.
+#
+# (c) Copyright 2012 - 2026 Anton Sviridenko
+# https://picapica.im
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3.
+
+set -euo pipefail
+
+# codename | container image | dpkg arch | podman platform
+TARGETS=(
+ "noble|docker.io/library/ubuntu:24.04|amd64|linux/amd64"
+ "resolute|docker.io/library/ubuntu:26.04|amd64|linux/amd64"
+ "trixie|docker.io/library/debian:trixie|amd64|linux/amd64"
+ "trixie|docker.io/i386/debian:trixie|i386|linux/386"
+)
+
+SCRIPTDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+TARBALL=""
+OUTDIR="$SCRIPTDIR/out"
+REPODIR="$SCRIPTDIR/repo"
+GPGKEY=""
+SIGN=1
+ONLY=""
+SKIP_BUILD=0
+
+ORIGIN="Pica Pica"
+LABEL="Pica Pica"
+
+progname=$(basename "$0")
+
+usage()
+{
+ cat <<EOF
+Usage: $progname [OPTIONS] TARBALL
+
+Build the pica-pica packages in containers and assemble a signed apt
+repository from the results.
+
+TARBALL is a source tarball as produced by "make dist" in the pica-pica tree,
+e.g. pica-pica-0.9.0dev.tar.gz. Not needed with --skip-build.
+
+Options:
+ -k, --gpg-key KEYID Key to sign the repository's Release files with. Any
+ form gpg accepts - key id, fingerprint, or the email
+ address on the uid. Required unless --no-sign.
+ --no-sign Build the repository but leave it unsigned. apt will
+ refuse it without extra configuration; for testing.
+ -o, --output DIR Where the containers leave their .deb files.
+ Default: $OUTDIR
+ -r, --repo DIR Where to assemble the apt repository.
+ Default: $REPODIR
+ --only PATTERN Only build targets whose "codename/arch" matches this
+ shell pattern, e.g. --only 'noble/*' or --only '*/i386'.
+ --skip-build Do not build anything; just assemble and sign the
+ repository from whatever is already in the output
+ directory.
+ -h, --help This message.
+
+Targets:
+EOF
+
+ local t cn img arch plat
+ for t in "${TARGETS[@]}"; do
+ IFS='|' read -r cn img arch plat <<<"$t"
+ printf ' %-16s %-34s %s\n' "$cn/$arch" "$img" "$plat"
+ done
+
+ cat <<'EOF'
+
+There is no Ubuntu i386 target and there cannot be: Ubuntu dropped i386 as an
+architecture after 18.04, so there is no i386 Ubuntu base image and no i386 Qt
+to build against. The 32 bit build is done on Debian, which still carries i386
+as a full architecture.
+EOF
+}
+
+die()
+{
+ echo "$progname: error: $*" >&2
+ exit 1
+}
+
+note()
+{
+ echo
+ echo "=== $*"
+}
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -k|--gpg-key) GPGKEY="${2:?--gpg-key needs a value}"; shift 2 ;;
+ --no-sign) SIGN=0; shift ;;
+ -o|--output) OUTDIR="${2:?--output needs a value}"; shift 2 ;;
+ -r|--repo) REPODIR="${2:?--repo needs a value}"; shift 2 ;;
+ --only) ONLY="${2:?--only needs a value}"; shift 2 ;;
+ --skip-build) SKIP_BUILD=1; shift ;;
+ -h|--help) usage; exit 0 ;;
+ -*) die "unknown option $1 (try --help)" ;;
+ *) break ;;
+ esac
+done
+
+if [ $# -gt 1 ]; then
+ die "expected at most one tarball, got: $*"
+elif [ $# -eq 1 ]; then
+ TARBALL=$(readlink -f "$1")
+fi
+
+if [ "$SIGN" -eq 1 ] && [ -z "$GPGKEY" ]; then
+ die "no signing key given. Pass --gpg-key KEYID, or --no-sign to skip signing"
+fi
+
+if [ "$SKIP_BUILD" -eq 0 ]; then
+ [ -n "$TARBALL" ] ||
+ die "no source tarball given. Run \"make dist\" in the pica-pica tree and pass the result (try --help)"
+ [ -f "$TARBALL" ] ||
+ die "$TARBALL does not exist"
+ tar -tf "$TARBALL" >/dev/null 2>&1 ||
+ die "$TARBALL is not a readable tar archive"
+fi
+
+if [ "$(id -u)" -eq 0 ]; then
+ SUDO=""
+else
+ SUDO="sudo"
+ command -v sudo >/dev/null ||
+ die "not root and no sudo - install podman yourself and re-run"
+fi
+
+# --- podman ---------------------------------------------------------------
+
+if ! command -v podman >/dev/null; then
+ note "installing podman"
+ $SUDO apt-get update
+ $SUDO apt-get install -y podman
+else
+ note "podman is already installed: $(podman --version)"
+fi
+
+# apt-ftparchive assembles the repository indices; gpg signs them.
+for tool in apt-ftparchive gpg; do
+ command -v "$tool" >/dev/null && continue
+ note "installing $tool"
+ case "$tool" in
+ apt-ftparchive) $SUDO apt-get install -y apt-utils ;;
+ gpg) $SUDO apt-get install -y gnupg ;;
+ esac
+done
+
+if [ "$SIGN" -eq 1 ]; then
+ gpg --list-keys "$GPGKEY" >/dev/null 2>&1 ||
+ die "gpg does not know a key called '$GPGKEY'"
+ gpg --list-secret-keys "$GPGKEY" >/dev/null 2>&1 ||
+ die "no SECRET key for '$GPGKEY' - the repository cannot be signed with it"
+fi
+
+# --- work area ------------------------------------------------------------
+
+WORKDIR=$(mktemp -d /tmp/pica-packaging.XXXXXX)
+trap 'rm -rf "$WORKDIR"' EXIT
+
+if [ "$SKIP_BUILD" -eq 0 ]; then
+ note "source tarball"
+ echo " $TARBALL"
+ echo " $(du -h "$TARBALL" | cut -f1), top level: $(tar -tf "$TARBALL" | head -1)"
+fi
+
+# --- build ----------------------------------------------------------------
+
+mkdir -p "$OUTDIR"
+
+BUILT=()
+FAILED=()
+
+if [ "$SKIP_BUILD" -eq 0 ]; then
+ for target in "${TARGETS[@]}"; do
+ IFS='|' read -r codename image arch platform <<<"$target"
+
+ if [ -n "$ONLY" ]; then
+ # shellcheck disable=SC2254
+ case "$codename/$arch" in
+ $ONLY) ;;
+ *) echo " skipping $codename/$arch (does not match --only '$ONLY')"; continue ;;
+ esac
+ fi
+
+ note "$codename/$arch ($image)"
+
+ if ! podman pull --platform "$platform" "$image"; then
+ FAILED+=("$codename/$arch: could not pull $image")
+ continue
+ fi
+
+ # --userns=keep-id is deliberately not used: the build wants to be
+ # root inside the container to install packages, and in rootless
+ # podman that still maps back to this user on the way out.
+ # The tarball is bind mounted as a single file under a fixed name, so
+ # container-build.sh does not have to be told what it is called. tar
+ # works out the compression from the content, so the missing .gz or
+ # .xz on that name costs nothing.
+ if podman run --rm \
+ --platform "$platform" \
+ -v "$TARBALL:/src/pica-source.tar:ro" \
+ -v "$SCRIPTDIR:/scripts:ro" \
+ -v "$OUTDIR:/out" \
+ -e PICA_DISTRO="$codename" \
+ -e PICA_ARCH="$arch" \
+ -w / \
+ "$image" \
+ /bin/bash /scripts/container-build.sh
+ then
+ BUILT+=("$codename/$arch")
+ else
+ FAILED+=("$codename/$arch: build failed")
+ fi
+ done
+
+ note "build summary"
+ for b in "${BUILT[@]:-}"; do [ -n "$b" ] && echo " ok $b"; done
+ for f in "${FAILED[@]:-}"; do [ -n "$f" ] && echo " FAILED $f"; done
+
+ [ ${#BUILT[@]} -gt 0 ] || die "nothing built - not assembling a repository"
+fi
+
+# --- repository -----------------------------------------------------------
+#
+# Laid out with a pool per codename rather than one shared pool. The indices
+# are per suite, and a shared pool would let noble's Packages list trixie's
+# binaries - apt would happily offer them and they would not install.
+
+note "assembling the repository in $REPODIR"
+
+rm -rf "$REPODIR"
+mkdir -p "$REPODIR"
+
+CODENAMES=()
+
+for distdir in "$OUTDIR"/*/; do
+ [ -d "$distdir" ] || continue
+ codename=$(basename "$distdir")
+
+ arches=()
+
+ for archdir in "$distdir"*/; do
+ [ -d "$archdir" ] || continue
+ arch=$(basename "$archdir")
+
+ compgen -G "$archdir*.deb" >/dev/null || continue
+ arches+=("$arch")
+
+ for deb in "$archdir"*.deb; do
+ # Pool path by source package, the way Debian lays it out, so
+ # that the binaries of one source sit together.
+ src=$(dpkg-deb -f "$deb" Source 2>/dev/null | awk '{print $1}')
+ [ -n "$src" ] || src=$(dpkg-deb -f "$deb" Package)
+ case "$src" in
+ lib*) letter="${src:0:4}" ;;
+ *) letter="${src:0:1}" ;;
+ esac
+
+ pool="$REPODIR/pool/$codename/main/$letter/$src"
+ mkdir -p "$pool"
+ cp "$deb" "$pool/"
+ done
+ done
+
+ [ ${#arches[@]} -gt 0 ] || continue
+ CODENAMES+=("$codename")
+
+ # Deduplicate: a codename built for two architectures appears twice above.
+ mapfile -t arches < <(printf '%s\n' "${arches[@]}" | sort -u)
+
+ echo " $codename: ${arches[*]}"
+
+ for arch in "${arches[@]}"; do
+ bindir="$REPODIR/dists/$codename/main/binary-$arch"
+ mkdir -p "$bindir"
+
+ # Run from the repository root so that the Filename fields come out
+ # relative to it, which is what apt expects.
+ ( cd "$REPODIR" &&
+ apt-ftparchive --arch "$arch" packages "pool/$codename" \
+ > "dists/$codename/main/binary-$arch/Packages" )
+
+ gzip -9 -k -f "$bindir/Packages"
+
+ cat > "$bindir/Release" <<EOF
+Archive: $codename
+Suite: $codename
+Component: main
+Origin: $ORIGIN
+Label: $LABEL
+Architecture: $arch
+EOF
+ done
+
+ # The suite Release, carrying the checksums of everything above. Written
+ # to a temporary file first - apt-ftparchive walks the directory it is
+ # indexing, and would otherwise hash a half-written Release.
+ ( cd "$REPODIR" &&
+ apt-ftparchive \
+ -o APT::FTPArchive::Release::Origin="$ORIGIN" \
+ -o APT::FTPArchive::Release::Label="$LABEL" \
+ -o APT::FTPArchive::Release::Suite="$codename" \
+ -o APT::FTPArchive::Release::Codename="$codename" \
+ -o APT::FTPArchive::Release::Components="main" \
+ -o APT::FTPArchive::Release::Architectures="${arches[*]}" \
+ -o APT::FTPArchive::Release::Description="Pica Pica Messenger packages" \
+ release "dists/$codename" > "$WORKDIR/Release.$codename" )
+
+ mv "$WORKDIR/Release.$codename" "$REPODIR/dists/$codename/Release"
+
+ if [ "$SIGN" -eq 1 ]; then
+ echo " signing $codename with $GPGKEY"
+ rm -f "$REPODIR/dists/$codename/Release.gpg" \
+ "$REPODIR/dists/$codename/InRelease"
+
+ # Both forms: InRelease is what modern apt fetches, Release.gpg is
+ # the detached signature older clients look for.
+ gpg --batch --yes --default-key "$GPGKEY" \
+ --armor --detach-sign \
+ -o "$REPODIR/dists/$codename/Release.gpg" \
+ "$REPODIR/dists/$codename/Release"
+
+ gpg --batch --yes --default-key "$GPGKEY" \
+ --clearsign \
+ -o "$REPODIR/dists/$codename/InRelease" \
+ "$REPODIR/dists/$codename/Release"
+ fi
+done
+
+[ ${#CODENAMES[@]} -gt 0 ] || die "no packages found under $OUTDIR"
+
+if [ "$SIGN" -eq 1 ]; then
+ gpg --armor --export "$GPGKEY" > "$REPODIR/pica-pica-archive-keyring.asc"
+fi
+
+# --- how to use it --------------------------------------------------------
+
+note "done"
+cat <<EOF
+
+Repository: $REPODIR
+Suites: ${CODENAMES[*]}
+
+Serve that directory over HTTP, then on a client:
+
+ sudo install -d /usr/share/keyrings
+ sudo curl -fsSL https://YOUR.HOST/pica-pica-archive-keyring.asc \\
+ -o /usr/share/keyrings/pica-pica-archive-keyring.asc
+
+ echo "deb [signed-by=/usr/share/keyrings/pica-pica-archive-keyring.asc] \\
+https://YOUR.HOST \$(lsb_release -cs) main" \\
+ | sudo tee /etc/apt/sources.list.d/pica-pica.list
+
+ sudo apt update && sudo apt install pica-client
+EOF
+
+if [ "$SIGN" -eq 0 ]; then
+ cat <<'EOF'
+
+The repository is NOT signed. apt will reject it unless clients add
+[trusted=yes] to the sources.list entry, which disables authentication -
+do that for local testing only.
+EOF
+fi
diff --git a/deb_packaging/build-webrtc-audio-processing.sh b/deb_packaging/build-webrtc-audio-processing.sh
new file mode 100755
index 0000000..6108e04
--- /dev/null
+++ b/deb_packaging/build-webrtc-audio-processing.sh
@@ -0,0 +1,342 @@
+#!/bin/bash
+#
+# Build a backported webrtc-audio-processing 1.3 for a distribution whose
+# archive does not carry it.
+#
+# pica-client needs AEC3, which arrived in the 1.x series. Ubuntu 24.04 still
+# ships 0.3.1, which predates it entirely and has a different API. Debian 13
+# and Ubuntu 25.10 or newer already have 1.3, so on those this script does
+# nothing unless forced.
+#
+# Debian's own packaging is taken as-is and given a two-part delta:
+#
+# - The -dev binary package is renamed back to the name Debian itself used
+# for 1.3-1, libwebrtc-audio-processing-1-dev. Debian renamed it to the
+# unversioned libwebrtc-audio-processing-dev in 1.3-2, after dropping 0.3
+# from the archive - but on a distribution that still ships 0.3 under
+# that unversioned name, taking it over would replace a package other
+# things build against. Nothing else collides: the runtime package
+# (libwebrtc-audio-processing-1-3), the SONAME, the include directory and
+# the pkg-config name are all versioned by upstream already.
+#
+# - The Breaks/Replaces relationships that record Debian's own rename are
+# dropped. They are meaningless once the package carries the old name
+# again, and on the audio-coding package they would otherwise declare a
+# Replaces on the distribution's 0.3 -dev package, which is the exact
+# thing this is trying not to disturb.
+#
+# Run this inside the target distribution - a container from
+# build-pica-packages.sh, an sbuild chroot, or a machine running it. It uses
+# apt, so it has to be somewhere that apt describes the target archive.
+#
+# (c) Copyright 2012 - 2026 Anton Sviridenko
+# https://picapica.im
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3.
+
+set -euo pipefail
+
+UPSTREAM_VERSION=1.3
+DEBIAN_REVISION=3
+DEBIAN_MIRROR=https://deb.debian.org/debian
+POOL="$DEBIAN_MIRROR/pool/main/w/webrtc-audio-processing"
+
+# The version below which the distribution's own package is no good to us.
+# 0.3 has no AEC3; see the note at the top.
+MINIMUM_USABLE_VERSION=1.3
+
+ARCH=""
+OUTDIR="$PWD/out"
+FORCE=0
+RENAME_DEV=1
+KEEP_BUILD=0
+
+progname=$(basename "$0")
+
+usage()
+{
+ cat <<EOF
+Usage: $progname [OPTIONS] DISTRO
+
+Build webrtc-audio-processing $UPSTREAM_VERSION-$DEBIAN_REVISION as a backport for DISTRO.
+
+DISTRO is the target distribution codename - noble, resolute, trixie. It is
+used in the package version (${UPSTREAM_VERSION}-${DEBIAN_REVISION}~DISTRO1) and the changelog entry, so that
+the distribution's own package supersedes this one if it ever gains a newer
+version.
+
+Options:
+ -a, --arch ARCH Architecture to build for. Accepts either dpkg names
+ (amd64, arm64, i386) or uname names (x86_64, aarch64,
+ i686), which are normalised. Defaults to the
+ architecture this is running on. Anything else is a
+ cross build and needs crossbuild-essential-ARCH, which
+ this will install.
+ -o, --output DIR Where to put the resulting .deb files.
+ Default: $OUTDIR
+ -f, --force Build even when the distribution already has a good
+ enough webrtc-audio-processing.
+ --no-rename-dev Keep Debian's unversioned libwebrtc-audio-processing-dev
+ name. Only correct where the distribution has no 0.3 of
+ its own to collide with.
+ --keep-build Leave the build tree behind for inspection.
+ -h, --help This message.
+
+Exit status:
+ 0 packages built, or the distribution already has a good enough one
+ 1 something went wrong
+EOF
+}
+
+die()
+{
+ echo "$progname: error: $*" >&2
+ exit 1
+}
+
+note()
+{
+ echo "==> $*"
+}
+
+# dpkg and uname disagree about architecture names, and people reach for
+# whichever they saw last. Take both.
+normalise_arch()
+{
+ case "$1" in
+ x86_64|amd64) echo amd64 ;;
+ aarch64|arm64) echo arm64 ;;
+ i386|i486|i586|i686) echo i386 ;;
+ armv7l|armhf) echo armhf ;;
+ ppc64le|ppc64el) echo ppc64el ;;
+ *) echo "$1" ;;
+ esac
+}
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ -a|--arch) ARCH=$(normalise_arch "${2:?--arch needs a value}"); shift 2 ;;
+ -o|--output) OUTDIR="${2:?--output needs a value}"; shift 2 ;;
+ -f|--force) FORCE=1; shift ;;
+ --no-rename-dev) RENAME_DEV=0; shift ;;
+ --keep-build) KEEP_BUILD=1; shift ;;
+ -h|--help) usage; exit 0 ;;
+ -*) die "unknown option $1 (try --help)" ;;
+ *) break ;;
+ esac
+done
+
+[ $# -eq 1 ] || { usage >&2; exit 1; }
+DISTRO="$1"
+
+[ "$(id -u)" -eq 0 ] || die "this installs build dependencies, so it needs to run as root"
+
+command -v apt-get >/dev/null || die "no apt-get - this has to run inside the target distribution"
+
+HOST_ARCH=$(dpkg --print-architecture)
+BUILD_ARCH="${ARCH:-$HOST_ARCH}"
+CROSS=0
+[ "$BUILD_ARCH" = "$HOST_ARCH" ] || CROSS=1
+
+VERSION="${UPSTREAM_VERSION}-${DEBIAN_REVISION}~${DISTRO}1"
+
+export DEBIAN_FRONTEND=noninteractive
+
+# Is it needed at all?
+#
+# apt-cache policy reports the candidate the target archive would install.
+# Anything from 1.x on is fine and there is no reason to ship our own.
+if [ "$FORCE" -eq 0 ]; then
+ apt-get update -qq
+
+ candidate=$(apt-cache policy libwebrtc-audio-processing-dev 2>/dev/null |
+ sed -n 's/^ Candidate: //p')
+
+ if [ -n "$candidate" ] && [ "$candidate" != "(none)" ] &&
+ dpkg --compare-versions "$candidate" ge "$MINIMUM_USABLE_VERSION"; then
+ note "$DISTRO already has libwebrtc-audio-processing-dev $candidate - nothing to build"
+ note "(pass --force to build the backport anyway)"
+ exit 0
+ fi
+
+ if [ -n "$candidate" ] && [ "$candidate" != "(none)" ]; then
+ note "$DISTRO has libwebrtc-audio-processing-dev $candidate, which predates AEC3 - backporting $VERSION"
+ else
+ note "$DISTRO has no libwebrtc-audio-processing-dev at all - backporting $VERSION"
+ fi
+fi
+
+note "building webrtc-audio-processing $VERSION for $BUILD_ARCH"
+
+# Just enough to fetch and unpack a Debian source package. The build
+# dependencies proper come from the extracted debian/control further down,
+# via apt-get build-dep, so that this does not have to be kept in step with
+# Debian's packaging by hand.
+#
+# devscripts is deliberately not installed: the changelog entry below is
+# written directly, so this works without DEBEMAIL or DEBFULLNAME being set.
+note "installing the tools needed to fetch the source"
+apt-get install -qq -y --no-install-recommends \
+ build-essential \
+ ca-certificates \
+ curl \
+ dpkg-dev \
+ xz-utils
+
+if [ "$CROSS" -eq 1 ]; then
+ note "cross building for $BUILD_ARCH from $HOST_ARCH"
+ dpkg --add-architecture "$BUILD_ARCH"
+ apt-get update -qq
+ apt-get install -qq -y --no-install-recommends \
+ "crossbuild-essential-$BUILD_ARCH"
+fi
+
+BUILDROOT=$(mktemp -d /tmp/webrtc-backport.XXXXXX)
+cleanup()
+{
+ [ "$KEEP_BUILD" -eq 1 ] && { note "build tree left at $BUILDROOT"; return; }
+ rm -rf "$BUILDROOT"
+}
+trap cleanup EXIT
+
+cd "$BUILDROOT"
+
+DSC="webrtc-audio-processing_${UPSTREAM_VERSION}-${DEBIAN_REVISION}.dsc"
+
+note "fetching Debian's source package"
+for f in "$DSC" \
+ "webrtc-audio-processing_${UPSTREAM_VERSION}.orig.tar.gz" \
+ "webrtc-audio-processing_${UPSTREAM_VERSION}-${DEBIAN_REVISION}.debian.tar.xz"; do
+ curl -fsSL --retry 3 -o "$f" "$POOL/$f" || die "could not fetch $f from $POOL"
+done
+
+# dpkg-source checks the tarballs against the checksums in the .dsc as it
+# extracts, so a corrupted or substituted tarball fails here. It does not
+# check the .dsc's own OpenPGP signature; dscverify from devscripts does that
+# and needs the Debian keyring, so it is done only when both are present.
+if command -v dscverify >/dev/null && [ -d /usr/share/keyrings ]; then
+ if dscverify --keyring /usr/share/keyrings/debian-keyring.gpg "$DSC" >/dev/null 2>&1; then
+ note "the .dsc signature verifies against the Debian keyring"
+ else
+ note "WARNING: could not verify the .dsc signature (keyring missing or untrusted signer)"
+ fi
+fi
+
+note "extracting"
+dpkg-source -x "$DSC" src >/dev/null
+cd src
+
+# Straight out of the extracted debian/control, so Debian's own list is the
+# source of truth rather than a copy of it kept here.
+note "installing Debian's build dependencies for it"
+if [ "$CROSS" -eq 1 ]; then
+ apt-get build-dep -qq -y -a "$BUILD_ARCH" ./ ||
+ die "could not satisfy the build dependencies for $BUILD_ARCH"
+else
+ apt-get build-dep -qq -y ./ ||
+ die "could not satisfy the build dependencies from debian/control"
+fi
+
+# --- the delta ------------------------------------------------------------
+
+if [ "$RENAME_DEV" -eq 1 ]; then
+ note "renaming libwebrtc-audio-processing-dev to libwebrtc-audio-processing-1-dev"
+
+ # Only the -dev package is renamed. The runtime package keeps Debian's
+ # name (libwebrtc-audio-processing-1-3), which does not collide with
+ # 0.3's libwebrtc-audio-processing1 and is what pica-client will depend
+ # on through shlibs.
+ sed -i 's/^Package: libwebrtc-audio-processing-dev$/Package: libwebrtc-audio-processing-1-dev/' \
+ debian/control
+
+ grep -q '^Package: libwebrtc-audio-processing-1-dev$' debian/control ||
+ die "the -dev package rename did not apply - has Debian's control changed?"
+fi
+
+# Drop the Breaks/Replaces that record Debian's 1.3-1 -> 1.3-2 rename.
+#
+# With the -dev package carrying its old name again these say nothing useful,
+# and the pair on libwebrtc-audio-coding-dev is actively wrong here: it
+# declares Breaks/Replaces on libwebrtc-audio-processing-dev (<< 1.3-2~),
+# which on a distribution still shipping 0.3 under that name matches the
+# distribution's own package.
+note "dropping Debian's rename-era Breaks/Replaces"
+# perl rather than python3: a minimal Debian or Ubuntu container has no
+# python3, but debhelper depends on perl so it is always here.
+#
+# A deb822 field runs from its name to the next line starting in column zero,
+# continuation lines being those that start with whitespace - hence the
+# (?:\n[ \t][^\n]*)* . Only the relationships naming the -dev packages are
+# dropped; anything else Debian declares is left alone.
+perl -0777 -i -pe '
+ s{^((?:Breaks|Replaces):[^\n]*(?:\n[ \t][^\n]*)*\n)}{
+ $1 =~ /libwebrtc-audio-(?:processing|coding)(?:-1)?-dev/ ? q{} : $1
+ }gme;
+' debian/control
+
+# A changelog entry written directly rather than through dch, so that this
+# does not depend on DEBEMAIL/DEBFULLNAME being set in the container.
+#
+# The ~DISTRO1 suffix sorts BELOW the plain Debian version, so if the
+# distribution ever ships 1.3-3 itself, apt upgrades away from this backport
+# rather than being held back by it.
+note "adding the changelog entry for $VERSION"
+{
+ cat <<EOF
+webrtc-audio-processing ($VERSION) $DISTRO; urgency=medium
+
+ * Rebuild of Debian's ${UPSTREAM_VERSION}-${DEBIAN_REVISION} for $DISTRO, which has no
+ webrtc-audio-processing 1.x of its own. pica-client needs it for AEC3.
+EOF
+
+ if [ "$RENAME_DEV" -eq 1 ]; then
+ cat <<'EOF'
+ * Keep the versioned libwebrtc-audio-processing-1-dev name that Debian used
+ for 1.3-1, so that this coexists with the 0.3 that the distribution ships
+ as libwebrtc-audio-processing-dev instead of replacing it
+ * Drop the Breaks/Replaces recording Debian's rename of that package; with
+ the old name back they say nothing, and on libwebrtc-audio-coding-dev the
+ pair would match the distribution's own 0.3 -dev package
+EOF
+ fi
+
+ cat <<EOF
+
+ -- Pica Pica packaging <anton@picapica.im> $(date -R)
+
+EOF
+ cat debian/changelog
+} > debian/changelog.new
+mv debian/changelog.new debian/changelog
+
+note "the delta applied to Debian's packaging:"
+sed -n '1,20p' debian/changelog | sed 's/^/ /'
+
+# --- build ----------------------------------------------------------------
+
+note "building"
+if [ "$CROSS" -eq 1 ]; then
+ dpkg-buildpackage --host-arch "$BUILD_ARCH" -b -us -uc
+else
+ dpkg-buildpackage -b -us -uc
+fi
+
+cd "$BUILDROOT"
+
+mkdir -p "$OUTDIR"
+found=0
+for deb in *.deb; do
+ [ -e "$deb" ] || continue
+ cp -v "$deb" "$OUTDIR/"
+ found=1
+done
+for changes in *.changes; do
+ [ -e "$changes" ] || continue
+ cp "$changes" "$OUTDIR/"
+done
+
+[ "$found" -eq 1 ] || die "the build produced no .deb files"
+
+note "done - packages are in $OUTDIR"
diff --git a/deb_packaging/container-build.sh b/deb_packaging/container-build.sh
new file mode 100755
index 0000000..f8ef801
--- /dev/null
+++ b/deb_packaging/container-build.sh
@@ -0,0 +1,136 @@
+#!/bin/bash
+#
+# Builds the pica-pica packages inside one container. Not meant to be run by
+# hand - build-pica-packages.sh starts a container per target and runs this
+# as its entry point.
+#
+# What it expects to find, all put there by the orchestrator:
+#
+# /src/pica-source.tar the pica-pica source tarball, as produced by
+# "make dist" - bind mounted under that fixed name
+# whatever it is really called, and whatever it is
+# compressed with
+# /scripts/ this directory, so that the backport script is
+# reachable
+# /out/ writable; artifacts are left in
+# /out/<codename>/<arch>/
+#
+# and in the environment: PICA_DISTRO (codename) and PICA_ARCH (dpkg name).
+#
+# (c) Copyright 2012 - 2026 Anton Sviridenko
+# https://picapica.im
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3.
+
+set -euo pipefail
+
+DISTRO="${PICA_DISTRO:?PICA_DISTRO is not set}"
+ARCH="${PICA_ARCH:-$(dpkg --print-architecture)}"
+
+export DEBIAN_FRONTEND=noninteractive
+
+note()
+{
+ echo "==> [$DISTRO/$ARCH] $*"
+}
+
+die()
+{
+ echo "[$DISTRO/$ARCH] error: $*" >&2
+ exit 1
+}
+
+note "starting, $(. /etc/os-release && echo "$PRETTY_NAME"), dpkg arch $(dpkg --print-architecture)"
+
+note "refreshing the package lists"
+apt-get update -qq
+
+# autoconf/automake because the pica-pica tree does not carry a generated
+# configure - it is not in git, so the tree has to be bootstrapped here.
+note "installing the base build tooling"
+apt-get install -qq -y --no-install-recommends \
+ build-essential \
+ ca-certificates \
+ curl \
+ dpkg-dev \
+ debhelper \
+ autoconf \
+ automake \
+ pkgconf
+
+# webrtc-audio-processing, if this distribution has nothing new enough.
+# The script decides that for itself and exits without doing anything when
+# the archive already has 1.x, so it is safe to call unconditionally.
+note "checking whether webrtc-audio-processing needs backporting"
+WEBRTC_OUT=/tmp/webrtc-backport-out
+mkdir -p "$WEBRTC_OUT"
+
+bash /scripts/build-webrtc-audio-processing.sh --output "$WEBRTC_OUT" "$DISTRO"
+
+if compgen -G "$WEBRTC_OUT/*.deb" >/dev/null; then
+ note "installing the backported webrtc-audio-processing"
+ apt-get install -qq -y --no-install-recommends "$WEBRTC_OUT"/*.deb
+
+ # These go into the repository too - without them the pica-client
+ # package would be uninstallable on this distribution.
+ mkdir -p "/out/$DISTRO/$ARCH"
+ cp -v "$WEBRTC_OUT"/*.deb "/out/$DISTRO/$ARCH/"
+else
+ note "the archive's own webrtc-audio-processing is good enough"
+fi
+
+# --- pica-pica ------------------------------------------------------------
+
+note "unpacking the source"
+rm -rf /build
+mkdir -p /build
+tar -xf /src/pica-source.tar -C /build
+
+# "make dist" produces exactly one top level directory, pica-pica-<version>.
+SRCDIR=$(find /build -maxdepth 1 -mindepth 1 -type d | head -1)
+[ -n "$SRCDIR" ] || die "the source tarball did not contain a directory"
+cd "$SRCDIR"
+note "source is $(basename "$SRCDIR")"
+
+[ -f debian/control ] ||
+ die "no debian/control in the tarball - was it made by \"make dist\" in a tree that has debian/?"
+
+note "installing pica-pica's build dependencies"
+# Reading them straight out of debian/control means the alternative
+# "libwebrtc-audio-processing-dev (>= 1.3) | libwebrtc-audio-processing-1-dev"
+# is resolved by apt rather than second-guessed here.
+apt-get build-dep -qq -y ./ ||
+ die "could not satisfy the build dependencies from debian/control"
+
+# A "make dist" tarball already carries the generated configure, so there is
+# normally nothing to do here. The fallback is for a tarball made some other
+# way; autoconf and automake are installed above either way, because if the
+# tarball's timestamps make Makefile.am look newer than Makefile.in, automake's
+# own rebuild rules fire during the build and want them.
+if [ -x configure ]; then
+ note "the tarball carries a generated configure - not bootstrapping"
+else
+ note "no configure in the tarball - bootstrapping the build system"
+ autoreconf -i -f
+fi
+
+note "building the packages"
+# Binary-only: dpkg-source is not involved, so this does not need an .orig
+# tarball alongside a 3.0 (quilt) source format.
+dpkg-buildpackage -b -us -uc
+
+note "collecting artifacts"
+mkdir -p "/out/$DISTRO/$ARCH"
+cd /build
+found=0
+for f in *.deb *.changes *.buildinfo; do
+ [ -e "$f" ] || continue
+ cp -v "$f" "/out/$DISTRO/$ARCH/"
+ case "$f" in *.deb) found=1 ;; esac
+done
+
+[ "$found" -eq 1 ] || die "the build produced no .deb files"
+
+note "done"