Wren Linux

wren-linux / tools / build-deb.sh

#!/bin/bash
# build-deb.sh: builds the Wren Linux package (.deb) from a git commit, never from
# files that aren't committed. It installs Wren Linux root-owned and read-only in
# /usr/share/wren, the Wren Linux command in /usr/bin, its man page and Tab
# completion; nothing else. It has no install scripts: installing it changes
# no setting and runs nothing as root. The parts and extras still run as you,
# from /usr/share/wren, when you ask for them (wrenctl part NAME, Wren Linux
# install NAME). The same commit always gives the same file, byte for byte
# (every date is the commit's), so anyone can check a published package
# against the source: build it again and compare the SHA-256.
#   bash tools/build-deb.sh [--commit REV] [--version VERSION] [--out FOLDER]
# Defaults: HEAD; a version from the commit's time and id (0~gitYYYYMMDDHHMMSS.ID),
# so a later commit always sorts higher;
# the current folder. Needs dpkg-deb and git only; lintian checks the result if
# it is installed. Changes nothing outside FOLDER and a temp folder.
set -euo pipefail
HERE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)

die() { printf 'build-deb: %s\n' "$*" >&2; exit 1; }

rev=HEAD version='' out=$PWD
while [ $# -gt 0 ]; do
    case $1 in
        --commit) rev=${2:?}; shift 2 ;;
        --version) version=${2:?}; shift 2 ;;
        --out) out=${2:?}; shift 2 ;;
        *) die "usage: build-deb.sh [--commit REV] [--version VERSION] [--out FOLDER]" ;;
    esac
done
command -v dpkg-deb >/dev/null || die "dpkg-deb is missing (package dpkg)."
g() { git -C "$HERE" "$@"; }
commit=$(g rev-parse --verify --quiet "$rev^{commit}") || die "no commit $rev here."
# every date in the package is the commit's: the same commit, the same file
SOURCE_DATE_EPOCH=$(g log -1 --format=%ct "$commit")
export SOURCE_DATE_EPOCH
# and in one language, whoever builds it: sort's order depends on it (en_US put bin/wrenctl before
# bin/wren-key, C after), and with it the package's list of files
export LC_ALL=C
if [ -z "$version" ]; then
    version="0~git$(date -u -d "@$SOURCE_DATE_EPOCH" +%Y%m%d%H%M%S).$(g rev-parse --short=10 "$commit")"
fi
[[ $version =~ ^[0-9][A-Za-z0-9.+~-]*$ ]] || die "a Debian version starts with a digit and has only letters, digits and . + ~ -: $version"
maint=$(g log -1 --format='%an <%ae>' "$commit")

T=$(mktemp -d)
trap 'rm -rf "$T"' EXIT
R=$T/root
SHARE=$R/usr/share/wren
mkdir -p "$SHARE"
# What is committed, nothing else; not the project's own CI and notes, and not what
# only someone working on Wren Linux uses, which wren-dev's git copy has: the tests
# and build tools, the plans, and the signing key's tools (with their sudo rule and
# user), and the website (site/). data/wren-signing-key.asc stays: every computer
# checks updates with it.
g archive --format=tar "$commit" | tar -C "$SHARE" -xf -
rm -rf "$SHARE/.github" "$SHARE/.gitignore" "$SHARE/CLAUDE.md" "$SHARE/tools" \
    "$SHARE/docs/maintaining.md" "$SHARE/docs/distro-plan.md" "$SHARE/docs/vps.md" \
    "$SHARE/extras/signing-key.sh" "$SHARE/docs/extras/signing-key.md" \
    "$SHARE/bin/wren-key" "$SHARE/bin/wren-key-gpg" "$SHARE/bin/wren-signer" \
    "$SHARE/data/wren-key.1" "$SHARE/data/signer-firewall.nft" "$SHARE/site"
sed -i '/(signing-key\.md)/d' "$SHARE/docs/extras/README.md"

# The command, its man page and its Tab completion where Debian looks for them
install -D -m 0755 "$SHARE/bin/wrenctl" "$R/usr/bin/wrenctl"
mkdir -p "$R/usr/share/man/man1"
gzip -9n <"$SHARE/data/wrenctl.1" >"$R/usr/share/man/man1/wrenctl.1.gz"
install -D -m 0644 "$SHARE/data/wrenctl-completion.bash" "$R/usr/share/bash-completion/completions/wrenctl"

# Debian's documentation files: the licenses, and a changelog
DOC=$R/usr/share/doc/wren-linux
mkdir -p "$DOC"
{
    echo "Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/"
    echo "Upstream-Name: Wren Linux"
    echo "Source: https://wrenlinux.org/git/wren-linux.git"
    echo
    echo "Files: *"
    echo "Copyright: the Wren Linux authors"
    echo "License: MIT"
    echo
    echo "Files: usr/share/wren/vendor/mockvps-kvm/*"
    echo "Copyright: the mockvps-kvm authors"
    echo "License: MIT"
    echo
    echo "License: MIT"
    sed 's/^$/./; s/^/ /' "$SHARE/LICENSE"
} >"$DOC/copyright"
{
    echo "wren-linux ($version) unstable; urgency=medium"
    echo
    echo "  * Built from commit $commit."
    echo
    echo " -- $maint  $(date -u -R -d "@$SOURCE_DATE_EPOCH")"
} | gzip -9n >"$DOC/changelog.gz"

# Root's files, read-only to everyone else: directories 0755, files 0644, and
# 0755 only for what git records as executable (a USB copy can lose the bit,
# git can't). Nothing setuid, nothing writable by others.
find "$R" -type d -exec chmod 0755 {} +
find "$R" -type f -exec chmod 0644 {} +
chmod 0755 "$R/usr/bin/wrenctl"
while IFS= read -r f; do
    # (what was left out above has no marks to fix)
    [ ! -e "$SHARE/$f" ] || chmod 0755 "$SHARE/$f"
done < <(g ls-tree -r "$commit" | awk '$1 == "100755" { sub(/^[^\t]*\t/, ""); print }' | grep -vx -e '.github/.*' -e CLAUDE.md || true)

mkdir -p "$R/DEBIAN"
installed_kb=$(du -sk --exclude=DEBIAN "$R" | cut -f1)
cat >"$R/DEBIAN/control" <<EOF
Package: wren-linux
Version: $version
Architecture: all
Maintainer: $maint
Installed-Size: $installed_kb
Depends: sudo
Recommends: gpg, git, wget, python3, acl, nftables, bash-completion
Section: admin
Priority: optional
Homepage: https://wrenlinux.org/
Description: small, privacy-minded sway desktop for Debian, as plain scripts
 Wren Linux turns a bare Debian 13 install into a small sway desktop with careful
 defaults: a firewall, kernel hardening, automatic security updates, encrypted
 DNS, and sandboxes for the browser, coding assistants and media. It is shell
 scripts you can read; this package puts them, root-owned, in
 /usr/share/wren, with the wrenctl command. Installing it changes nothing
 else: run "wrenctl" to set up the desktop and its optional programs.
EOF
(cd "$R" && find . -path ./DEBIAN -prune -o -type f -print0 | sort -z | xargs -0 md5sum | sed 's|  \./|  |') >"$R/DEBIAN/md5sums"
chmod 0755 "$R/DEBIAN"
chmod 0644 "$R/DEBIAN/control" "$R/DEBIAN/md5sums"
# the same date on every file, so the archive is the same each time
find "$R" -exec touch -h -d "@$SOURCE_DATE_EPOCH" {} +

mkdir -p "$out"
deb=$out/wren-linux_${version}_all.deb
dpkg-deb --root-owner-group -Zxz --build "$R" "$deb" >/dev/null
echo "$deb"
sha256sum "$deb" | cut -d' ' -f1 | sed 's/^/SHA-256: /'
# Remarks are one thing; lintian itself failing, as it would on an option a new
# version no longer takes, is another: then nothing was checked. lintian's exit
# code for remarks changed between versions (1 before, 2 since --fail-on, and 1
# is now its own failure), so --fail-on none: its exit then only says whether it
# ran, and whether it had remarks is whether it printed any
lintian_check() {
    local rc=0 out
    out=$(lintian --fail-on none --tag-display-limit 0 "$1") || rc=$?
    [ -z "$out" ] || printf '%s\n' "$out"
    if [ "$rc" -ne 0 ]; then
        echo "!! lintian itself failed (exit $rc), so this package was NOT checked by it. Has one of its options changed? See tools/build-deb.sh" >&2
    elif [ -n "$out" ]; then
        echo "(lintian has remarks: see above)"
    fi
}
if command -v lintian >/dev/null; then
    lintian_check "$deb"
else
    echo "(lintian isn't installed, so the package wasn't checked by it: wrenctl install wren-dev)"
fi