Compare commits

...

5 Commits

Author SHA1 Message Date
Xavier Roche
33ddb27243 mk-sbuild-chroot: suggest a concrete usermod for the subuid range
Compute a start past every range already in /etc/subuid+subgid and print the
canonical sudo usermod --add-subuids/--add-subgids command, instead of a raw
file append the user has to adjust by hand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-06-20 14:13:06 +02:00
Xavier Roche
4606dfbf66 mk-sbuild-chroot: require a subuid/subgid range up front
The unshare backend maps a whole UID range, not just the caller's, because the
base install creates system users. Without an /etc/subuid+subgid entry the
install crashes (dpkg SIGSEGV) instead of failing cleanly. Check for the range
before bootstrapping and point at the one-line fix; skip the check for root,
which uses mode=root.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-06-20 14:07:10 +02:00
Xavier Roche
a6f1b9a3dd mk-sbuild-chroot: only treat an active $chroot_mode line as configured
The idempotency guard matched chroot_mode.*unshare anywhere in ~/.sbuildrc,
including a commented-out line, so --write-sbuildrc would silently skip the
append and leave the unshare backend unconfigured. Anchor the match to an
active assignment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-06-20 14:02:42 +02:00
Xavier Roche
fb35d6a0f1 tools: add mk-sbuild-chroot.sh to set up the --sbuild gate
The --sbuild gate needs an sbuild chroot, which was only documented as loose
commands. This adds a companion script that bootstraps one with the rootless
unshare backend (mmdebstrap into ~/.cache/sbuild/<dist>-<arch>.tar.zst, where
sbuild finds it by name), idempotent unless --force, optionally writing the
unshare mode into ~/.sbuildrc. mkdeb.sh's --sbuild help now points at it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-06-20 13:43:34 +02:00
Xavier Roche
8a270fec03 mkdeb: add an optional --sbuild clean-room build gate
With source-only uploads the archive's buildds are the first place the package
is built in a clean environment, so an undeclared Build-Depends or any FTBFS
only shows up after the upload. --sbuild rebuilds the freshly produced .dsc in a
minimal chroot holding only the declared Build-Depends, reproducing the buildd
environment; a failure aborts the release before the upload. It runs after the
source package is built and before the upstream-tarball release artifacts are
signed. Logs and the clean-built debs land in <outdir>/sbuild.

The distribution comes from the changelog (UNRELEASED falls back to unstable),
and the flag fails fast if sbuild isn't installed. Off by default; needs an
sbuild chroot for the target suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-06-20 13:37:20 +02:00
2 changed files with 191 additions and 0 deletions

152
tools/mk-sbuild-chroot.sh Executable file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env bash
#
# Bootstrap an sbuild chroot for the clean-room build gate (mkdeb.sh --sbuild).
#
# Uses the rootless unshare backend: no root, no schroot daemon. It builds a
# minimal buildd chroot tarball into ~/.cache/sbuild/<dist>-<arch>.tar.zst, where
# sbuild --dist=<dist> finds it automatically in unshare mode.
#
# Usage:
# tools/mk-sbuild-chroot.sh [options]
#
# Options:
# -d, --dist DIST suite to bootstrap (default: unstable)
# -a, --arch ARCH architecture (default: dpkg --print-architecture)
# -m, --mirror URL apt mirror (default: http://deb.debian.org/debian)
# --components LIST comma-separated components (default: main)
# -f, --force rebuild even if the tarball already exists
# --write-sbuildrc add "$chroot_mode = 'unshare';" to ~/.sbuildrc if absent
# -h, --help show this help
#
# One-time setup; refresh later with sbuild-update or by rerunning with --force.
# Requires mmdebstrap and the uidmap tools (newuidmap) for the unshare backend.
set -euo pipefail
readonly PROGNAME=${0##*/}
die() {
printf '%s: error: %s\n' "$PROGNAME" "$*" >&2
exit 1
}
info() {
printf '==> %s\n' "$*" >&2
}
usage() {
sed -n '2,/^set -euo/{/^set -euo/!p}' "$0" | sed 's/^# \{0,1\}//'
}
need() {
local tool
for tool in "$@"; do
command -v "$tool" >/dev/null 2>&1 || die "required tool not found: $tool"
done
}
main() {
local dist=unstable
local arch=""
local mirror=http://deb.debian.org/debian
local components=main
local force=0
local write_sbuildrc=0
while [[ $# -gt 0 ]]; do
case $1 in
-d | --dist)
[[ $# -ge 2 ]] || die "missing argument for $1"
dist=$2
shift 2
;;
-a | --arch)
[[ $# -ge 2 ]] || die "missing argument for $1"
arch=$2
shift 2
;;
-m | --mirror)
[[ $# -ge 2 ]] || die "missing argument for $1"
mirror=$2
shift 2
;;
--components)
[[ $# -ge 2 ]] || die "missing argument for $1"
components=$2
shift 2
;;
-f | --force)
force=1
shift
;;
--write-sbuildrc)
write_sbuildrc=1
shift
;;
-h | --help)
usage
exit 0
;;
*)
die "unknown option: $1 (try --help)"
;;
esac
done
need mmdebstrap dpkg
# Unshare needs the setuid uid/gid mappers; mmdebstrap fails cryptically without.
command -v newuidmap >/dev/null 2>&1 ||
die "newuidmap not found; install the uidmap package for the unshare backend"
# Unshare maps a whole UID range, not just the caller's: the base install
# creates system users, and without an /etc/subuid+subgid range the install
# crashes (dpkg SIGSEGV) instead of erroring cleanly. Root uses mode=root and
# needs no range.
if [[ $(id -u) -ne 0 ]]; then
local me
me=$(id -un)
if ! grep -qs "^$me:" /etc/subuid || ! grep -qs "^$me:" /etc/subgid; then
# Suggest a range starting past every allocation in either file.
local start
start=$(awk -F: '{e = $2 + $3; if (e > m) m = e} END {print (m ? m : 100000)}' \
/etc/subuid /etc/subgid 2>/dev/null)
die "no /etc/subuid+subgid range for $me; the unshare backend needs one:
sudo usermod --add-subuids $start-$((start + 65535)) --add-subgids $start-$((start + 65535)) $me"
fi
fi
: "${arch:=$(dpkg --print-architecture)}"
local cache=$HOME/.cache/sbuild
local tarball=$cache/${dist}-${arch}.tar.zst
if [[ -e $tarball && $force -eq 0 ]]; then
info "chroot already exists: $tarball (use --force to rebuild)"
else
info "bootstrapping $dist/$arch chroot into $tarball"
mkdir -p "$cache"
mmdebstrap --variant=buildd --arch="$arch" --components="$components" \
"$dist" "$tarball" "$mirror"
info "chroot ready: $tarball"
fi
local rc=$HOME/.sbuildrc
local mode_line="\$chroot_mode = 'unshare';"
# shellcheck disable=SC2016 # $chroot_mode is literal regex text, not a shell var.
if grep -qsE '^[[:space:]]*\$chroot_mode[[:space:]]*=.*unshare' "$rc"; then
: # already configured (active, non-commented line)
elif [[ $write_sbuildrc -eq 1 ]]; then
info "enabling the unshare backend in $rc"
printf '%s\n' "$mode_line" >>"$rc"
else
cat >&2 <<EOF
==> To use this chroot without passing --chroot-mode each time, add to $rc:
$mode_line
(or rerun with --write-sbuildrc). Then verify with:
sbuild --dist=$dist path/to/package.dsc
and build the release gate with:
tools/mkdeb.sh --source-only --sbuild
EOF
fi
}
main "$@"

View File

@@ -23,8 +23,17 @@
# -s, --source-only build only the source package
# -u, --unsigned do not sign anything (implies no release sigs)
# --no-release-artifacts skip the orig tarball .asc/.md5/.sha1
# --sbuild additionally build the .dsc in a clean sbuild
# chroot as a from-scratch verification gate
# -h, --help show this help
#
# --sbuild reproduces the buildd environment: it builds the source package in a
# minimal chroot holding only the declared Build-Depends, so an FTBFS or a
# missing dependency fails here instead of on the archive's buildds (which, with
# a source-only upload, are otherwise the first clean build). It needs an sbuild
# chroot for the changelog's distribution; create one once with the companion
# tools/mk-sbuild-chroot.sh (rootless unshare backend).
#
# SOURCE_DATE_EPOCH is honored for reproducible output.
set -euo pipefail
@@ -60,6 +69,7 @@ main() {
local source_only=0
local unsigned=0
local release_artifacts=1
local sbuild=0
while [[ $# -gt 0 ]]; do
case $1 in
@@ -85,6 +95,10 @@ main() {
release_artifacts=0
shift
;;
--sbuild)
sbuild=1
shift
;;
-h | --help)
usage
exit 0
@@ -96,6 +110,7 @@ main() {
done
need git autoreconf debuild dcmd
[[ $sbuild -eq 1 ]] && need sbuild dpkg-parsechangelog
if [[ $unsigned -eq 0 ]]; then
need gpg
[[ -n $key ]] || die "no signing key (pass --key or set DEBSIGN_KEYID, or use --unsigned)"
@@ -179,6 +194,30 @@ main() {
[[ ${#changes[@]} -ge 1 ]] || die "debuild produced no .changes file"
dcmd cp -- "${changes[@]}" "$outdir/"
# Clean-room build gate: rebuild the source package in a minimal chroot that
# holds only the declared Build-Depends, the same way the buildds will. An
# undeclared dependency or any FTBFS aborts the release here instead of
# surfacing after a source-only upload. Logs and clean-built debs land in
# $outdir/sbuild for inspection.
if [[ $sbuild -eq 1 ]]; then
local -a dscs
shopt -s nullglob
dscs=("$scratch"/*.dsc)
shopt -u nullglob
[[ ${#dscs[@]} -ge 1 ]] || die "no .dsc to sbuild"
local dist
dist=$(cd "$scratch/httrack-$ver" && dpkg-parsechangelog -S Distribution)
[[ $dist == UNRELEASED ]] && dist=unstable
info "clean-room build with sbuild (dist $dist)"
local sbdir=$outdir/sbuild
rm -rf -- "$sbdir"
mkdir -p "$sbdir"
(cd "$sbdir" && sbuild --dist="$dist" -- "${dscs[0]}")
info "sbuild clean-room build passed; logs in $sbdir"
fi
# Release artifacts for the upstream tarball (detached sig + checksums).
if [[ $release_artifacts -eq 1 && $unsigned -eq 0 ]]; then
info "signing upstream tarball"