diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..f7c6b3d4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,78 @@ +name: Test + +on: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: shellcheck + run: shellcheck -x scripts/build-deb.sh tests/run-tests.sh tests/lib.sh tests/container-prep.sh tests/build-legacy-deb.sh tests/scenarios/*.sh + + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: "1.26" + - run: go vet ./... + - run: go test ./... + + build-packages: + name: Build all packages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: "1.26" + - name: Build stub binaries + run: | + for arch in amd64 arm64; do + CGO_ENABLED=0 GOOS=linux GOARCH="$arch" go build -trimpath -ldflags '-s -w' -o "dist/stub-$arch" ./tests/stub + done + - name: Build every package definition + run: | + for env in packages/*/package.env; do + project=$(basename "$(dirname "$env")") + for arch in amd64 arm64; do + ./scripts/build-deb.sh --project "$project" --version 0.0.1 --arch "$arch" --binary "dist/stub-$arch" --output dist + done + done + ls -l dist/*.deb + - name: Assert package contents are owned by root + run: | + # the runner is not root, so this catches a dropped + # --root-owner-group in build-deb.sh that other gates miss because + # they build debs as root inside the test containers + for deb in dist/*.deb; do + if dpkg-deb -c "$deb" | awk '{print $2}' | grep -vqx 'root/root'; then + echo "non root owner in $deb" >&2 + dpkg-deb -c "$deb" >&2 + exit 1 + fi + done + + test-packages: + name: Test packages + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # oldest and newest of each distro bracket the systemd and dpkg + # version spread + image: + - debian:bullseye + - debian:trixie + - ubuntu:jammy + - ubuntu:questing + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: "1.26" + - name: Run packaging tests + run: tests/run-tests.sh --image ${{ matrix.image }} --project hostd diff --git a/.gitignore b/.gitignore index 496ee2ca..64bdf679 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.DS_Store \ No newline at end of file +.DS_Store +dist/ +*.deb diff --git a/packages/hostd/package.env b/packages/hostd/package.env new file mode 100644 index 00000000..64c1feae --- /dev/null +++ b/packages/hostd/package.env @@ -0,0 +1,5 @@ +# package settings for hostd, read by scripts/build-deb.sh +PKG_NAME=hostd +PKG_DESCRIPTION="host daemon for the Sia network" +PKG_LONG_DESCRIPTION="hostd is the next generation Sia host daemon. It connects storage to the Sia network and manages contracts, pricing, and sector storage." +PKG_HOMEPAGE=https://github.com/SiaFoundation/hostd diff --git a/packages/renterd/package.env b/packages/renterd/package.env new file mode 100644 index 00000000..d0447353 --- /dev/null +++ b/packages/renterd/package.env @@ -0,0 +1,5 @@ +# package settings for renterd, read by scripts/build-deb.sh +PKG_NAME=renterd +PKG_DESCRIPTION="renter daemon for the Sia network" +PKG_LONG_DESCRIPTION="renterd is the Sia renter daemon. It forms contracts with hosts on the Sia network and stores data with erasure coded redundancy." +PKG_HOMEPAGE=https://github.com/SiaFoundation/renterd diff --git a/packages/s3d/package.env b/packages/s3d/package.env new file mode 100644 index 00000000..6a7b3bc4 --- /dev/null +++ b/packages/s3d/package.env @@ -0,0 +1,7 @@ +# package settings for s3d, read by scripts/build-deb.sh +PKG_NAME=s3d +# debian package name. binary, service, and data dir still use PKG_NAME +PKG_DEBIAN_NAME=sia-s3d +PKG_DESCRIPTION="S3-compatible gateway for the Sia network" +PKG_LONG_DESCRIPTION="s3d translates the AWS S3 API to the Sia storage network. Applications that speak S3 can store data on Sia without modification." +PKG_HOMEPAGE=https://github.com/SiaFoundation/s3d diff --git a/packages/walletd/package.env b/packages/walletd/package.env new file mode 100644 index 00000000..27f29bf6 --- /dev/null +++ b/packages/walletd/package.env @@ -0,0 +1,5 @@ +# package settings for walletd, read by scripts/build-deb.sh +PKG_NAME=walletd +PKG_DESCRIPTION="wallet daemon for the Sia network" +PKG_LONG_DESCRIPTION="walletd is the Sia wallet daemon. It manages addresses and transactions on the Sia network and serves a wallet API." +PKG_HOMEPAGE=https://github.com/SiaFoundation/walletd diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh new file mode 100755 index 00000000..9b8e8fa2 --- /dev/null +++ b/scripts/build-deb.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# builds a .deb for one Sia daemon from an already-built linux binary. +# package settings come from packages//package.env. +# the workflow and tests both use this, so they build the same thing. +set -euo pipefail + +usage() { + echo "usage: $0 --project --version --arch --binary --output " >&2 + exit 1 +} + +PROJECT='' VERSION='' ARCH='' BINARY='' OUTPUT='' +while [ $# -gt 0 ]; do + case "$1" in + --project) PROJECT=$2; shift 2 ;; + --version) VERSION=$2; shift 2 ;; + --arch) ARCH=$2; shift 2 ;; + --binary) BINARY=$2; shift 2 ;; + --output) OUTPUT=$2; shift 2 ;; + *) usage ;; + esac +done +if [ -z "$PROJECT" ] || [ -z "$VERSION" ] || [ -z "$ARCH" ] || [ -z "$BINARY" ] || [ -z "$OUTPUT" ]; then + usage +fi + +case "$ARCH" in +amd64|arm64) ;; +*) echo "unsupported architecture: $ARCH" >&2; exit 1 ;; +esac +[ -f "$BINARY" ] || { echo "binary not found: $BINARY" >&2; exit 1; } + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +PKG_DIR=$ROOT/packages/$PROJECT +TEMPLATES=$ROOT/scripts/templates + +[ -f "$PKG_DIR/package.env" ] || { echo "missing $PKG_DIR/package.env, add a package definition first" >&2; exit 1; } +# shellcheck source=/dev/null +. "$PKG_DIR/package.env" +: "${PKG_NAME:?package.env must set PKG_NAME}" +: "${PKG_DESCRIPTION:?package.env must set PKG_DESCRIPTION}" +: "${PKG_LONG_DESCRIPTION:?package.env must set PKG_LONG_DESCRIPTION}" +: "${PKG_HOMEPAGE:?package.env must set PKG_HOMEPAGE}" +[ "$PKG_NAME" = "$PROJECT" ] || { echo "PKG_NAME '$PKG_NAME' does not match project '$PROJECT'" >&2; exit 1; } +# apt package name. +# the installed binary, service, and data dir still use PKG_NAME. +# defaults to PKG_NAME unless package.env overrides it. +: "${PKG_DEBIAN_NAME:=$PKG_NAME}" + +# preinst needs hashes for old stock units. +# ignore Description because the old workflow used free text there. +# the unit text lives in one shared file so tests and packages use the same bytes. +# shellcheck source=scripts/legacy-unit.sh +. "$ROOT/scripts/legacy-unit.sh" +LEGACY_MD5_A=$(legacy_unit "$PKG_NAME" "" a | md5sum | cut -d' ' -f1) +LEGACY_MD5_B=$(legacy_unit "$PKG_NAME" "" b | md5sum | cut -d' ' -f1) +PKG_LEGACY_MD5S="\"$LEGACY_MD5_A\"|\"$LEGACY_MD5_B\"" + +# control files need wrapped long descriptions. +# continuation lines start with a space; empty lines become ".". +PKG_LONG_DESCRIPTION=$(printf '%s\n' "$PKG_LONG_DESCRIPTION" | fold -s -w 76 | sed -e 's/[[:space:]]*$//' -e 's/^$/./' -e 's/^/ /') + +# quote replacements so bash treats &, backslashes, and other chars as plain text. +render() { + local content + content=$(cat "$1") + content=${content//@PKG_NAME@/"$PKG_NAME"} + content=${content//@PKG_DEBIAN_NAME@/"$PKG_DEBIAN_NAME"} + content=${content//@PKG_VERSION@/"$VERSION"} + content=${content//@PKG_ARCH@/"$ARCH"} + content=${content//@PKG_DESCRIPTION@/"$PKG_DESCRIPTION"} + content=${content//@PKG_LONG_DESCRIPTION@/"$PKG_LONG_DESCRIPTION"} + content=${content//@PKG_HOMEPAGE@/"$PKG_HOMEPAGE"} + content=${content//@PKG_INSTALLED_SIZE@/"${PKG_INSTALLED_SIZE:-0}"} + content=${content//@PKG_LEGACY_MD5S@/"$PKG_LEGACY_MD5S"} + printf '%s\n' "$content" +} + +STAGE=$(mktemp -d) +trap 'rm -rf "$STAGE"' EXIT +chmod 755 "$STAGE" + +# payload +install -Dm755 "$BINARY" "$STAGE/usr/bin/$PKG_NAME" + +# a package can override this by adding packages/

/

.service. +install -dm755 "$STAGE/usr/lib/systemd/system" +if [ -f "$PKG_DIR/$PKG_NAME.service" ]; then + install -m644 "$PKG_DIR/$PKG_NAME.service" "$STAGE/usr/lib/systemd/system/$PKG_NAME.service" +else + render "$TEMPLATES/service.tmpl" > "$STAGE/usr/lib/systemd/system/$PKG_NAME.service" + chmod 644 "$STAGE/usr/lib/systemd/system/$PKG_NAME.service" +fi + +# the daemon owns config via " config". +# the package ships no config, so dpkg never overwrites it. +install -dm755 "$STAGE/var/lib/$PKG_NAME" + +install -dm755 "$STAGE/usr/share/doc/$PKG_DEBIAN_NAME" +render "$TEMPLATES/copyright.tmpl" > "$STAGE/usr/share/doc/$PKG_DEBIAN_NAME/copyright" +chmod 644 "$STAGE/usr/share/doc/$PKG_DEBIAN_NAME/copyright" +# dpkg treats a version containing a hyphen as non native, which must name its +# changelog changelog.Debian.gz; versions without one use changelog.gz. +case "$VERSION" in +*-*) CHANGELOG=changelog.Debian.gz ;; +*) CHANGELOG=changelog.gz ;; +esac +# honor SOURCE_DATE_EPOCH so identical inputs produce a byte identical package +CHANGELOG_DATE=$(date -R -u -d "@${SOURCE_DATE_EPOCH:-$(date +%s)}") +{ + printf '%s (%s) stable; urgency=medium\n\n' "$PKG_DEBIAN_NAME" "$VERSION" + printf ' * Packaged upstream release %s.\n\n' "$VERSION" + printf ' -- The Sia Foundation %s\n' "$CHANGELOG_DATE" +} | gzip -9n > "$STAGE/usr/share/doc/$PKG_DEBIAN_NAME/$CHANGELOG" +chmod 644 "$STAGE/usr/share/doc/$PKG_DEBIAN_NAME/$CHANGELOG" + +# control +PKG_INSTALLED_SIZE=$(du -sk "$STAGE" | cut -f1) +install -dm755 "$STAGE/DEBIAN" +render "$TEMPLATES/control.tmpl" > "$STAGE/DEBIAN/control" +for script in preinst postinst prerm postrm; do + render "$TEMPLATES/$script.tmpl" > "$STAGE/DEBIAN/$script" + chmod 755 "$STAGE/DEBIAN/$script" +done +(cd "$STAGE" && find . -type f -not -path './DEBIAN/*' | LC_ALL=C sort | sed 's|^\./||' | xargs md5sum > DEBIAN/md5sums) +chmod 644 "$STAGE/DEBIAN/md5sums" "$STAGE/DEBIAN/control" + +# use xz so bullseye can install the package. +# newer dpkg may default to zstd, which bullseye cannot unpack. +mkdir -p "$OUTPUT" +dpkg-deb --root-owner-group -Zxz --build "$STAGE" "$OUTPUT/${PKG_DEBIAN_NAME}_${VERSION}_${ARCH}.deb" > /dev/null +echo "built $OUTPUT/${PKG_DEBIAN_NAME}_${VERSION}_${ARCH}.deb" diff --git a/scripts/legacy-unit.sh b/scripts/legacy-unit.sh new file mode 100644 index 00000000..26428de6 --- /dev/null +++ b/scripts/legacy-unit.sh @@ -0,0 +1,28 @@ +# shellcheck shell=bash +# one copy of the old systemd unit. +# build-deb.sh hashes it for migrations. +# tests/build-legacy-deb.sh writes it into the fake old package. +# keeping one copy means the hash and test package cannot disagree. +# +# empty desc means no Description line. +# preinst ignores Description because the old workflow got it from free text. +# variant a is older; variant b adds TimeoutStopSec. +legacy_unit() { + local name=$1 desc=$2 variant=$3 + printf '%s\n' '[Unit]' + [ -n "$desc" ] && printf 'Description=%s\n' "$desc" + printf '%s\n' \ + '' \ + 'After=network.target' \ + '[Service]' \ + "ExecStart=/usr/bin/$name" \ + "WorkingDirectory=/var/lib/$name" \ + 'Restart=always' \ + 'RestartSec=15' + [ "$variant" = b ] && printf 'TimeoutStopSec=120\n' + printf '%s\n' \ + '' \ + '[Install]' \ + 'WantedBy=multi-user.target' \ + "Alias=$name.service" +} diff --git a/scripts/templates/control.tmpl b/scripts/templates/control.tmpl new file mode 100644 index 00000000..6570e104 --- /dev/null +++ b/scripts/templates/control.tmpl @@ -0,0 +1,11 @@ +Package: @PKG_DEBIAN_NAME@ +Version: @PKG_VERSION@ +Architecture: @PKG_ARCH@ +Maintainer: The Sia Foundation +Installed-Size: @PKG_INSTALLED_SIZE@ +Depends: ca-certificates +Section: net +Priority: optional +Homepage: @PKG_HOMEPAGE@ +Description: @PKG_DESCRIPTION@ +@PKG_LONG_DESCRIPTION@ diff --git a/scripts/templates/copyright.tmpl b/scripts/templates/copyright.tmpl new file mode 100644 index 00000000..833bd2cf --- /dev/null +++ b/scripts/templates/copyright.tmpl @@ -0,0 +1,26 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: @PKG_NAME@ +Source: @PKG_HOMEPAGE@ + +Files: * +Copyright: 2024-2026 The Sia Foundation +License: MIT + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/scripts/templates/postinst.tmpl b/scripts/templates/postinst.tmpl new file mode 100644 index 00000000..8b5b6e18 --- /dev/null +++ b/scripts/templates/postinst.tmpl @@ -0,0 +1,66 @@ +#!/bin/sh +set -e + +PKG=@PKG_NAME@ +UNIT=@PKG_NAME@.service +LEGACY_UNIT=/etc/systemd/system/$UNIT +MARKER=/run/sia-linux/$PKG.legacy-upgrade + +# config exists if the daemon wrote it in either supported place. +config_present() { + [ -f "/var/lib/$PKG/$PKG.yml" ] || [ -f "/etc/$PKG/$PKG.yml" ] +} + +case "$1" in +configure) + # put back a user-edited old unit. + # keeping it in /etc makes it override our packaged unit. + if [ -e "$LEGACY_UNIT.dpkg-bak" ] && [ ! -e "$LEGACY_UNIT" ]; then + mv "$LEGACY_UNIT.dpkg-bak" "$LEGACY_UNIT" + echo "$PKG: preserved your modified $LEGACY_UNIT" >&2 + echo "$PKG: it overrides the packaged unit in /usr/lib/systemd/system" >&2 + echo "$PKG: consider moving your changes to a drop-in via 'systemctl edit $PKG'" >&2 + echo "$PKG: and removing $LEGACY_UNIT to use the packaged unit" >&2 + fi + + if [ -d /run/systemd/system ]; then + systemctl daemon-reload || true + + if [ -e "$MARKER" ]; then + # first upgrade from the old layout. + # we cannot know if it used to run, so only start it when config exists. + if config_present; then + systemctl enable "$UNIT" 2>/dev/null || true + # old systemd cannot enable this preserved unit. + # create the wants link ourselves. + if ! systemctl is-enabled --quiet "$UNIT" 2>/dev/null && [ -e "$LEGACY_UNIT" ]; then + mkdir -p /etc/systemd/system/multi-user.target.wants || true + ln -sf "$LEGACY_UNIT" "/etc/systemd/system/multi-user.target.wants/$UNIT" || true + systemctl daemon-reload || true + fi + systemctl restart "$UNIT" || true + else + echo "$PKG: service was not restarted because no config was found; run 'systemctl enable --now $PKG' if it should be running" >&2 + fi + elif [ -n "$2" ]; then + # normal upgrade or reinstall: restart if running, do not change enablement. + systemctl try-restart "$UNIT" || true + fi + # fresh installs stay off until the admin creates config. + fi + rm -f "$MARKER" + + # no config on a fresh install: show the setup hint. + if [ -z "${2:-}" ] && ! config_present; then + echo "$PKG: run '$PKG config' to configure it, then 'systemctl enable --now $PKG'" >&2 + fi + + if [ -f "/var/lib/$PKG/$PKG.yml" ]; then + echo "$PKG: note: /var/lib/$PKG/$PKG.yml exists and takes precedence over /etc/$PKG/$PKG.yml" >&2 + fi + ;; +abort-upgrade|abort-remove|abort-deconfigure) + ;; +esac + +exit 0 diff --git a/scripts/templates/postrm.tmpl b/scripts/templates/postrm.tmpl new file mode 100644 index 00000000..9f2ce65a --- /dev/null +++ b/scripts/templates/postrm.tmpl @@ -0,0 +1,45 @@ +#!/bin/sh +set -e + +PKG=@PKG_NAME@ +LEGACY_UNIT=/etc/systemd/system/$PKG.service + +reload_systemd() { + if [ -d /run/systemd/system ]; then + systemctl daemon-reload || true + fi +} + +case "$1" in +remove) + rm -f "/run/sia-linux/$PKG.legacy-upgrade" + reload_systemd + ;; +upgrade) + # also covers downgrade to old packages, which cannot reload systemd themselves. + reload_systemd + ;; +purge) + rm -f "/run/sia-linux/$PKG.legacy-upgrade" + # cleanup in case prerm missed a service enable symlink. + rm -f "/etc/systemd/system/"*.wants/"$PKG.service" + # old preserved units point at a binary that is gone now. + rm -f "$LEGACY_UNIT" "$LEGACY_UNIT.dpkg-bak" + # purge wipes config and data. + rm -rf "/etc/$PKG" "/var/lib/$PKG" + reload_systemd + ;; +abort-upgrade) + # failed upgrade rollback: put the hidden unit back for the old package. + if [ -e "$LEGACY_UNIT.dpkg-bak" ] && [ ! -e "$LEGACY_UNIT" ]; then + mv "$LEGACY_UNIT.dpkg-bak" "$LEGACY_UNIT" + fi + rm -f "/run/sia-linux/$PKG.legacy-upgrade" + # old packages have no postinst, so reload systemd here. + reload_systemd + ;; +abort-install|failed-upgrade|disappear) + ;; +esac + +exit 0 diff --git a/scripts/templates/preinst.tmpl b/scripts/templates/preinst.tmpl new file mode 100644 index 00000000..a77874cb --- /dev/null +++ b/scripts/templates/preinst.tmpl @@ -0,0 +1,32 @@ +#!/bin/sh +set -e + +LEGACY_UNIT=/etc/systemd/system/@PKG_NAME@.service +MARKER_DIR=/run/sia-linux + +case "$1" in +upgrade) + # no vendor unit means we are upgrading from the old /etc layout. + # the old prerm already stopped and disabled the service, so postinst decides what to do. + if [ ! -e "/usr/lib/systemd/system/@PKG_NAME@.service" ] && [ -f "$LEGACY_UNIT" ] && [ ! -L "$LEGACY_UNIT" ]; then + mkdir -p "$MARKER_DIR" + : > "$MARKER_DIR/@PKG_NAME@.legacy-upgrade" + + # hash the old unit, ignoring Description because it came from free text. + sum=$(grep -v '^Description=' "$LEGACY_UNIT" | md5sum | cut -d' ' -f1) + case "$sum" in + @PKG_LEGACY_MD5S@) + # stock unit: let dpkg remove it so the new vendor unit takes over. + ;; + *) + # changed unit: hide it during unpack, then postinst puts it back. + mv "$LEGACY_UNIT" "$LEGACY_UNIT.dpkg-bak" + ;; + esac + fi + ;; +install|abort-upgrade) + ;; +esac + +exit 0 diff --git a/scripts/templates/prerm.tmpl b/scripts/templates/prerm.tmpl new file mode 100644 index 00000000..a71b4af1 --- /dev/null +++ b/scripts/templates/prerm.tmpl @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +case "$1" in +remove) + if [ -d /run/systemd/system ]; then + systemctl disable --now @PKG_NAME@.service || true + fi + ;; +upgrade|deconfigure) + # nothing on upgrade. + # postinst restarts it after the new version is unpacked. + ;; +failed-upgrade) + # the old prerm ran systemctl without checking for systemd. + # exit cleanly here so dpkg can keep going. + ;; +esac + +exit 0 diff --git a/scripts/templates/service.tmpl b/scripts/templates/service.tmpl new file mode 100644 index 00000000..b9be0731 --- /dev/null +++ b/scripts/templates/service.tmpl @@ -0,0 +1,19 @@ +[Unit] +Description=@PKG_NAME@: @PKG_DESCRIPTION@ +Documentation=@PKG_HOMEPAGE@ +Wants=network-online.target +After=network-online.target + +[Service] +Type=exec +ExecStart=/usr/bin/@PKG_NAME@ +WorkingDirectory=/var/lib/@PKG_NAME@ +StateDirectory=@PKG_NAME@ +ConfigurationDirectory=@PKG_NAME@ +ConfigurationDirectoryMode=0700 +Restart=always +RestartSec=15 +TimeoutStopSec=120 + +[Install] +WantedBy=multi-user.target diff --git a/tests/Dockerfile.systemd b/tests/Dockerfile.systemd new file mode 100644 index 00000000..0d5ca9db --- /dev/null +++ b/tests/Dockerfile.systemd @@ -0,0 +1,13 @@ +# test image with systemd. +# plain debian and ubuntu images do not boot an init system. +ARG BASE_IMAGE=debian:bookworm +FROM ${BASE_IMAGE} +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install -y --no-install-recommends systemd systemd-sysv logrotate ca-certificates \ + && rm -rf /var/lib/apt/lists/* +# older systemd can wait forever for serial consoles, especially hvc0 on macOS. +RUN systemctl mask getty.target console-getty.service \ + && ln -s /dev/null /etc/systemd/system/serial-getty@.service +STOPSIGNAL SIGRTMIN+3 +CMD ["/sbin/init"] diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..62534635 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,38 @@ +# Packaging tests + +The suite verifies the .deb packages end to end: install layout, config +preservation, upgrades, migration from the legacy packages, removal, and +lintian cleanliness. Each scenario runs inside a fresh systemd enabled +container, and the packages under test are built inside the container by the +same `scripts/build-deb.sh` that builds release packages, only the daemon +binary is replaced with a stub. + +## Requirements + +Docker and Go. The host architecture decides the package architecture, so +Apple Silicon machines test the arm64 packages against arm64 images. + +## Running + +```bash +# full suite against one distro +tests/run-tests.sh --image debian:bookworm + +# a single scenario while iterating +tests/run-tests.sh --image ubuntu:jammy --scenario legacy + +# keep the container of a failing scenario for inspection +tests/run-tests.sh --scenario fresh --keep +``` + +If `systemctl is-system-running` never settles inside the container, retry +with `CGROUP_MODE=private tests/run-tests.sh ...`, which starts the container +with a private cgroup namespace instead of the host one. + +## Notes + +- The lintian scenario only runs on `debian:trixie`; older lintian versions + lack the tags and flags it uses. +- `debian:bullseye` reaches end of LTS on 2026-08-31. After that date the + image build needs archive.debian.org sources before `apt-get update` works + again. diff --git a/tests/build-legacy-deb.sh b/tests/build-legacy-deb.sh new file mode 100755 index 00000000..8cc8420e --- /dev/null +++ b/tests/build-legacy-deb.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# builds a fake old .deb for migration tests. +# it keeps the old quirks: unit in /etc, unsafe prerm, no conffiles, no md5sums. +# the unit text matches the old script byte for byte. +# shellcheck disable=SC2129 +set -euo pipefail + +usage() { + echo "usage: $0 --project --version --arch --binary --variant --output

" >&2 + exit 1 +} + +PROJECT='' VERSION='' ARCH='' BINARY='' VARIANT='' OUTPUT='' +while [ $# -gt 0 ]; do + case "$1" in + --project) PROJECT=$2; shift 2 ;; + --version) VERSION=$2; shift 2 ;; + --arch) ARCH=$2; shift 2 ;; + --binary) BINARY=$2; shift 2 ;; + --variant) VARIANT=$2; shift 2 ;; + --output) OUTPUT=$2; shift 2 ;; + *) usage ;; + esac +done +if [ -z "$PROJECT" ] || [ -z "$VERSION" ] || [ -z "$ARCH" ] || [ -z "$BINARY" ] || [ -z "$VARIANT" ] || [ -z "$OUTPUT" ]; then + usage +fi + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +# shellcheck source=/dev/null +. "$ROOT/packages/$PROJECT/package.env" +DESCRIPTION=$PKG_DESCRIPTION +# shellcheck source=scripts/legacy-unit.sh +. "$ROOT/scripts/legacy-unit.sh" + +STAGE=$(mktemp -d) +trap 'rm -rf "$STAGE"' EXIT +chmod 755 "$STAGE" + +mkdir -p "$STAGE/DEBIAN" +mkdir -p "$STAGE/usr/bin" +mkdir -p "$STAGE/var/lib/$PROJECT" +mkdir -p "$STAGE/etc/systemd/system" + +cp "$BINARY" "$STAGE/usr/bin/$PROJECT" +chmod 755 "$STAGE/usr/bin/$PROJECT" + +# control file, mirrors the historical echo block line by line on purpose +# shellcheck disable=SC2129 +echo "Package: $PROJECT" > "$STAGE/DEBIAN/control" +echo "Version: $VERSION" >> "$STAGE/DEBIAN/control" +echo "Architecture: $ARCH" >> "$STAGE/DEBIAN/control" +echo "Maintainer: The Sia Foundation " >> "$STAGE/DEBIAN/control" +echo "Priority: optional" >> "$STAGE/DEBIAN/control" +echo "Section: net" >> "$STAGE/DEBIAN/control" +echo "Description: $DESCRIPTION" >> "$STAGE/DEBIAN/control" +echo "Homepage: https://github.com/SiaFoundation/$PROJECT" >> "$STAGE/DEBIAN/control" + +# use the shared old unit text so the test package and hash stay in sync. +UNIT=$STAGE/etc/systemd/system/$PROJECT.service +legacy_unit "$PROJECT" "$DESCRIPTION" "$VARIANT" > "$UNIT" + +# same unsafe prerm the old packages had. +echo "#!/bin/sh" > "$STAGE/DEBIAN/prerm" +echo "systemctl stop $PROJECT.service" >> "$STAGE/DEBIAN/prerm" +echo "systemctl disable $PROJECT.service" >> "$STAGE/DEBIAN/prerm" +chmod +x "$STAGE/DEBIAN/prerm" + +mkdir -p "$OUTPUT" +dpkg-deb --root-owner-group -Zxz --build "$STAGE" "$OUTPUT/${PROJECT}_${VERSION}_${ARCH}.deb" > /dev/null +echo "built $OUTPUT/${PROJECT}_${VERSION}_${ARCH}.deb" diff --git a/tests/container-prep.sh b/tests/container-prep.sh new file mode 100755 index 00000000..6df99ad9 --- /dev/null +++ b/tests/container-prep.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# runs inside the test container. +# copies the read-only repo to /work and builds the .debs used by scenarios. +set -euo pipefail + +PKG=$1 +ARCH=$2 + +mkdir -p /work +cp -r /src/scripts /src/packages /src/tests /work/ +cp "/src/dist/stub-linux-$ARCH" /work/stub + +for v in 1.0.0 2.0.0 3.0.0; do + /work/scripts/build-deb.sh --project "$PKG" --version "$v" --arch "$ARCH" --binary /work/stub --output /work/dist +done + +/work/tests/build-legacy-deb.sh --project "$PKG" --version 0.9.0 --arch "$ARCH" --binary /work/stub --variant a --output /work/dist +/work/tests/build-legacy-deb.sh --project "$PKG" --version 0.9.1 --arch "$ARCH" --binary /work/stub --variant b --output /work/dist diff --git a/tests/lib.sh b/tests/lib.sh new file mode 100755 index 00000000..ec963b11 --- /dev/null +++ b/tests/lib.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# container helpers for the package tests, loaded by run-tests.sh + +start_container() { + local cgroup_args=(--cgroupns=host -v /sys/fs/cgroup:/sys/fs/cgroup:rw) + if [ "${CGROUP_MODE:-}" = private ]; then + cgroup_args=(--cgroupns=private) + fi + + docker run -d --rm \ + --privileged \ + "${cgroup_args[@]}" \ + --tmpfs /run \ + --tmpfs /tmp \ + -v "$ROOT":/src:ro \ + "$TEST_IMAGE" /sbin/init +} + +wait_for_systemd() { + local cid=$1 + local state + for _ in $(seq 1 90); do + state=$(docker exec "$cid" systemctl is-system-running 2>/dev/null || true) + case "$state" in + running|degraded) return 0 ;; + esac + sleep 1 + done + echo "systemd did not become ready in container $cid, last state: ${state:-unknown}" >&2 + echo "try CGROUP_MODE=private if this persists" >&2 + return 1 +} + +stop_container() { + local cid=$1 + docker rm -f "$cid" > /dev/null 2>&1 || true +} diff --git a/tests/lintian-suppressions.txt b/tests/lintian-suppressions.txt new file mode 100644 index 00000000..e0da939c --- /dev/null +++ b/tests/lintian-suppressions.txt @@ -0,0 +1,9 @@ +# statically linked go binaries are intentional, the daemons embed their +# dependencies +statically-linked-binary +# the daemons ship no man pages yet +no-manual-page +# the packages are built with raw dpkg-deb and have no debhelper tooling, +# maintainer scripts call systemctl directly behind a /run/systemd/system +# guard +maintainer-script-calls-systemctl diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100755 index 00000000..f177af92 --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# package test runner. +# builds the stub daemon, boots one systemd container per scenario, +# builds the debs inside it, then runs the scenario checks. +# +# usage: tests/run-tests.sh [--image debian:bookworm] [--project hostd] +# [--scenario ] [--keep] +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +# shellcheck source=tests/lib.sh +. "$ROOT/tests/lib.sh" + +IMAGE=debian:bookworm +PROJECT=hostd +FILTER='' +KEEP=0 +while [ $# -gt 0 ]; do + case "$1" in + --image) IMAGE=$2; shift 2 ;; + --project) PROJECT=$2; shift 2 ;; + --scenario) FILTER=$2; shift 2 ;; + --keep) KEEP=1; shift ;; + *) echo "unknown argument: $1" >&2; exit 1 ;; + esac +done + +case "$(uname -m)" in +x86_64) ARCH=amd64 ;; +arm64|aarch64) ARCH=arm64 ;; +*) echo "unsupported host architecture: $(uname -m)" >&2; exit 1 ;; +esac + +# the scenarios derive the apt package name and doc dir from the project name, +# so they cannot drive a package whose debian name differs (s3d ships as sia-s3d). +ENV_FILE="$ROOT/packages/$PROJECT/package.env" +if [ -f "$ENV_FILE" ]; then + # shellcheck source=/dev/null + DEBIAN_NAME=$(. "$ENV_FILE"; echo "${PKG_DEBIAN_NAME:-$PKG_NAME}") + if [ "$DEBIAN_NAME" != "$PROJECT" ]; then + echo "scenarios do not support split package names yet ($DEBIAN_NAME != $PROJECT)" >&2 + exit 1 + fi +fi + +echo "== building stub daemon for linux/$ARCH" +mkdir -p "$ROOT/dist" +# stripped like the release binaries so lintian sees the same shape +(cd "$ROOT/tests/stub" && CGO_ENABLED=0 GOOS=linux GOARCH=$ARCH go build -trimpath -ldflags '-s -w' -o "$ROOT/dist/stub-linux-$ARCH" .) + +TEST_IMAGE=sia-linux-test:$(echo "$IMAGE" | tr ':/' '--') +echo "== building test image $TEST_IMAGE from $IMAGE" +docker build -q -t "$TEST_IMAGE" --build-arg "BASE_IMAGE=$IMAGE" -f "$ROOT/tests/Dockerfile.systemd" "$ROOT/tests" > /dev/null + +declare -a failed=() +ran=0 +for scenario in "$ROOT"/tests/scenarios/[0-9]*.sh; do + name=$(basename "$scenario") + if [ -n "$FILTER" ] && [[ "$name" != *"$FILTER"* ]]; then + continue + fi + ran=$((ran + 1)) + + echo "== $name ($IMAGE, $PROJECT, $ARCH)" + cid=$(start_container) + if ! wait_for_systemd "$cid"; then + stop_container "$cid" + failed+=("$name (systemd not ready)") + continue + fi + + if docker exec "$cid" bash /src/tests/container-prep.sh "$PROJECT" "$ARCH" \ + && docker exec -e "PKG=$PROJECT" -e "ARCH=$ARCH" "$cid" bash "/work/tests/scenarios/$name"; then + echo "== PASS $name" + else + echo "== FAIL $name" >&2 + failed+=("$name") + if [ "$KEEP" = 1 ]; then + echo "== container kept for debugging: $cid" >&2 + continue + fi + fi + stop_container "$cid" +done + +if [ "$ran" -eq 0 ]; then + echo "no scenario matched filter '$FILTER'" >&2 + exit 1 +fi +if [ "${#failed[@]}" -gt 0 ]; then + echo "failed scenarios: ${failed[*]}" >&2 + exit 1 +fi +echo "all $ran scenarios passed" diff --git a/tests/scenarios/01-fresh-install.sh b/tests/scenarios/01-fresh-install.sh new file mode 100755 index 00000000..fe0e1666 --- /dev/null +++ b/tests/scenarios/01-fresh-install.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# fresh install: correct layout, no shipped config or conffiles, service stays +# off until the admin enables it. +# shellcheck source=tests/scenarios/helpers.sh +. /work/tests/scenarios/helpers.sh + +install_deb "$(deb 1.0.0)" + +# layout +[ -x "/usr/bin/$PKG" ] || fail "binary missing or not executable" +assert_file "/usr/lib/systemd/system/$PKG.service" +assert_no_path "/etc/systemd/system/$PKG.service" +assert_dir "/var/lib/$PKG" +assert_file "/usr/share/doc/$PKG/copyright" +assert_file "/usr/share/doc/$PKG/changelog.gz" + +# the package ships no config and no logrotate rule; the daemon writes its config. +assert_no_path "/etc/$PKG/$PKG.yml" +assert_no_path "/etc/logrotate.d/$PKG" + +# dpkg registers no conffiles. +conffiles=$(dpkg-query -W -f='${Conffiles}' "$PKG" | tr -d '[:space:]') +[ -z "$conffiles" ] || fail "package should register no conffiles, got: $conffiles" + +# a pristine install verifies cleanly. +dpkg -V "$PKG" || fail "dpkg -V reported differences on a pristine install" + +# service stays off by default. +assert_eq "$(unit_state)" disabled "fresh install enablement" +assert_eq "$(active_state)" inactive "fresh install activity" + +# manual enable/start works. +systemctl enable --now "$PKG.service" +sleep 1 +assert_eq "$(unit_state)" enabled +assert_eq "$(active_state)" active diff --git a/tests/scenarios/02-upgrade.sh b/tests/scenarios/02-upgrade.sh new file mode 100755 index 00000000..b0ce95df --- /dev/null +++ b/tests/scenarios/02-upgrade.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# upgrade restarts a running service but keeps its enabled/disabled state. +# shellcheck source=tests/scenarios/helpers.sh +. /work/tests/scenarios/helpers.sh + +install_deb "$(deb 1.0.0)" +systemctl enable --now "$PKG.service" +sleep 1 +pid_before=$(main_pid) + +install_deb "$(deb 2.0.0)" +sleep 1 +assert_eq "$(active_state)" active "service must be running after upgrade" +pid_after=$(main_pid) +[ "$pid_before" != "$pid_after" ] || fail "service was not restarted on upgrade" +assert_eq "$(unit_state)" enabled "upgrade must keep the service enabled" + +# disabled service stays disabled and stopped. +systemctl disable --now "$PKG.service" +install_deb "$(deb 3.0.0)" +assert_eq "$(unit_state)" disabled "upgrade must not enable a disabled service" +assert_eq "$(active_state)" inactive "upgrade must not start a stopped service" diff --git a/tests/scenarios/03-config-preservation.sh b/tests/scenarios/03-config-preservation.sh new file mode 100755 index 00000000..d3ea1810 --- /dev/null +++ b/tests/scenarios/03-config-preservation.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# admin config and systemd drop-ins survive upgrades byte for byte. +# the package ships no config, so dpkg should not touch them. +# shellcheck source=tests/scenarios/helpers.sh +. /work/tests/scenarios/helpers.sh + +install_deb "$(deb 1.0.0)" + +# create the files "$PKG config" and "systemctl edit" would write. +mkdir -p "/etc/$PKG" +printf 'directory: /var/lib/%s\n# local change\n' "$PKG" > "/etc/$PKG/$PKG.yml" +mkdir -p "/etc/systemd/system/$PKG.service.d" +printf '[Service]\nEnvironment=SIA_TEST_OVERRIDE=1\n' > "/etc/systemd/system/$PKG.service.d/override.conf" +systemctl daemon-reload + +sum_config=$(sha256sum "/etc/$PKG/$PKG.yml" | cut -d' ' -f1) +sum_dropin=$(sha256sum "/etc/systemd/system/$PKG.service.d/override.conf" | cut -d' ' -f1) + +systemctl enable --now "$PKG.service" +sleep 1 + +install_deb "$(deb 2.0.0)" +sleep 1 + +assert_eq "$(sha256sum "/etc/$PKG/$PKG.yml" | cut -d' ' -f1)" "$sum_config" "admin config changed during upgrade" +assert_eq "$(sha256sum "/etc/systemd/system/$PKG.service.d/override.conf" | cut -d' ' -f1)" "$sum_dropin" "drop-in changed during upgrade" +systemctl show "$PKG.service" -p Environment | grep -q SIA_TEST_OVERRIDE || fail "drop-in no longer effective after upgrade" +assert_eq "$(active_state)" active diff --git a/tests/scenarios/04-legacy-migration.sh b/tests/scenarios/04-legacy-migration.sh new file mode 100755 index 00000000..1bf42947 --- /dev/null +++ b/tests/scenarios/04-legacy-migration.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# migration from the old packages, which shipped the unit in +# /etc/systemd/system and had an unsafe prerm. +# shellcheck source=tests/scenarios/helpers.sh +. /work/tests/scenarios/helpers.sh + +# stock unit, configured and enabled: the vendor unit takes over and the +# service comes back. +install_deb "$(deb 0.9.1)" +assert_file "/etc/systemd/system/$PKG.service" +write_config +legacy_enable_now +sleep 1 + +install_deb "$(deb 1.0.0)" +sleep 1 +assert_no_path "/etc/systemd/system/$PKG.service" +assert_no_path "/etc/systemd/system/$PKG.service.dpkg-bak" +assert_vendor_unit_active +assert_eq "$(unit_state)" enabled "migration must enable a configured install" +assert_eq "$(active_state)" active "migration must restart a configured install" +cleanup_package + +# older stock unit, from before TimeoutStopSec was added. +install_deb "$(deb 0.9.0)" +write_config +legacy_enable_now +install_deb "$(deb 1.0.0)" +sleep 1 +assert_no_path "/etc/systemd/system/$PKG.service" +assert_vendor_unit_active +assert_eq "$(active_state)" active +cleanup_package + +# a description-only edit still counts as stock; the old workflow set it from +# free text. +install_deb "$(deb 0.9.1)" +sed -i 's/^Description=.*/Description=my own description/' "/etc/systemd/system/$PKG.service" +systemctl daemon-reload +write_config +legacy_enable_now +install_deb "$(deb 1.0.0)" +sleep 1 +assert_no_path "/etc/systemd/system/$PKG.service" +assert_vendor_unit_active +cleanup_package + +# a real unit edit is kept in /etc and overrides the vendor unit. +install_deb "$(deb 0.9.1)" +sed -i 's/^RestartSec=15$/RestartSec=30/' "/etc/systemd/system/$PKG.service" +systemctl daemon-reload +write_config +legacy_enable_now +sleep 1 + +install_deb "$(deb 1.0.0)" +sleep 1 +assert_file "/etc/systemd/system/$PKG.service" +grep -q '^RestartSec=30$' "/etc/systemd/system/$PKG.service" || fail "modified unit content lost" +assert_no_path "/etc/systemd/system/$PKG.service.dpkg-bak" +assert_eq "$(fragment_path)" "/etc/systemd/system/$PKG.service" "preserved unit must override the vendor unit" +assert_eq "$(unit_state)" enabled +assert_eq "$(active_state)" active + +# a later normal upgrade does not migrate again or change enablement, even with +# a unit still in /etc. +systemctl disable --now "$PKG.service" +install_deb "$(deb 2.0.0)" +assert_no_path "/run/sia-linux/$PKG.legacy-upgrade" +assert_file "/etc/systemd/system/$PKG.service" +grep -q '^RestartSec=30$' "/etc/systemd/system/$PKG.service" || fail "preserved unit touched by regular upgrade" +assert_no_path "/etc/systemd/system/$PKG.service.dpkg-bak" +assert_eq "$(unit_state)" disabled "regular upgrade must not enable" +assert_eq "$(active_state)" inactive +cleanup_package +rm -f "/etc/systemd/system/$PKG.service" +systemctl daemon-reload + +# an unconfigured old install stays off after migration. +install_deb "$(deb 0.9.1)" +install_deb "$(deb 1.0.0)" +assert_file "/usr/lib/systemd/system/$PKG.service" +assert_eq "$(unit_state)" disabled "unconfigured migration must not enable" +assert_eq "$(active_state)" inactive diff --git a/tests/scenarios/05-remove-purge.sh b/tests/scenarios/05-remove-purge.sh new file mode 100755 index 00000000..7efebd32 --- /dev/null +++ b/tests/scenarios/05-remove-purge.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# remove leaves admin config and data; purge deletes all of it. +# shellcheck source=tests/scenarios/helpers.sh +. /work/tests/scenarios/helpers.sh + +install_deb "$(deb 1.0.0)" +# create admin config and data the package never shipped. +mkdir -p "/etc/$PKG" +printf 'directory: /var/lib/%s\n# local change\n' "$PKG" > "/etc/$PKG/$PKG.yml" +echo "wallet data" > "/var/lib/$PKG/keep.dat" +systemctl enable --now "$PKG.service" +sleep 1 + +DEBIAN_FRONTEND=noninteractive apt-get remove -y "$PKG" > /dev/null + +# remove deletes package files but keeps config and data. +assert_no_path "/usr/bin/$PKG" +assert_no_path "/usr/lib/systemd/system/$PKG.service" +[ "$(active_state)" != active ] || fail "service still running after remove" +assert_file "/etc/$PKG/$PKG.yml" +assert_file "/var/lib/$PKG/keep.dat" + +# reinstall after remove keeps config and stays disabled. +install_deb "$(deb 1.0.0)" +assert_eq "$(unit_state)" disabled "reinstall must not auto enable" +grep -q '# local change' "/etc/$PKG/$PKG.yml" || fail "config lost across remove and reinstall" + +DEBIAN_FRONTEND=noninteractive apt-get purge -y "$PKG" > /dev/null + +# purge deletes everything. +assert_no_path "/etc/$PKG" +assert_no_path "/var/lib/$PKG" diff --git a/tests/scenarios/06-lintian.sh b/tests/scenarios/06-lintian.sh new file mode 100755 index 00000000..674d19d3 --- /dev/null +++ b/tests/scenarios/06-lintian.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# lintian check. +# only run it on trixie because older lintian lacks needed tags and flags. +# shellcheck source=tests/scenarios/helpers.sh +. /work/tests/scenarios/helpers.sh + +. /etc/os-release +if [ "${VERSION_CODENAME:-}" != trixie ]; then + echo "skipping lintian, only runs on debian trixie" + exit 0 +fi + +apt-get update > /dev/null +DEBIAN_FRONTEND=noninteractive apt-get install -y lintian > /dev/null + +# build the suppression tag list from the file. +tags=$(grep -vE '^(#|$)' /work/tests/lintian-suppressions.txt | paste -sd,) + +lintian --fail-on error,warning ${tags:+--suppress-tags "$tags"} "$(deb 1.0.0)" diff --git a/tests/scenarios/helpers.sh b/tests/scenarios/helpers.sh new file mode 100755 index 00000000..509c8a97 --- /dev/null +++ b/tests/scenarios/helpers.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# helpers loaded by every scenario. +# they run as root in the test container with PKG and ARCH set. +set -euo pipefail + +: "${PKG:?PKG must be set}" +: "${ARCH:?ARCH must be set}" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +deb() { + echo "/work/dist/${PKG}_$1_${ARCH}.deb" +} + +install_deb() { + local out + out=$(DEBIAN_FRONTEND=noninteractive apt-get install -y "$1" 2>&1) \ + || { echo "$out" >&2; fail "failed to install $1"; } +} + +assert_file() { + [ -f "$1" ] || fail "expected file $1" +} + +assert_dir() { + [ -d "$1" ] || fail "expected directory $1" +} + +assert_no_path() { + [ ! -e "$1" ] || fail "expected $1 to be absent" +} + +assert_eq() { + [ "$1" = "$2" ] || fail "${3:-values differ}: got '$1', want '$2'" +} + +unit_state() { + systemctl is-enabled "$PKG.service" 2>/dev/null || true +} + +active_state() { + systemctl is-active "$PKG.service" 2>/dev/null || true +} + +main_pid() { + systemctl show -p MainPID --value "$PKG.service" +} + +fragment_path() { + systemctl show -p FragmentPath --value "$PKG.service" +} + +# merged usr can show the vendor unit under /lib or /usr/lib. +assert_vendor_unit_active() { + case "$(fragment_path)" in + "/usr/lib/systemd/system/$PKG.service"|"/lib/systemd/system/$PKG.service") ;; + *) fail "vendor unit must be the active fragment, got '$(fragment_path)'" ;; + esac +} + +write_config() { + mkdir -p "/var/lib/$PKG" + echo "# test config" > "/var/lib/$PKG/$PKG.yml" +} + +# systemd before 252 cannot enable the old alias. +# do what admins did: create the wants link and start it. +legacy_enable_now() { + if ! systemctl enable --now "$PKG.service" 2>/dev/null; then + mkdir -p "/etc/systemd/system/multi-user.target.wants" + ln -sf "/etc/systemd/system/$PKG.service" "/etc/systemd/system/multi-user.target.wants/$PKG.service" + systemctl daemon-reload + systemctl start "$PKG.service" + fi +} + +# reset package state between subcases. +cleanup_package() { + DEBIAN_FRONTEND=noninteractive apt-get purge -y "$PKG" > /dev/null 2>&1 || true + rm -rf "/var/lib/$PKG" "/run/sia-linux" + systemctl daemon-reload +} diff --git a/tests/stub/main.go b/tests/stub/main.go new file mode 100644 index 00000000..9ff4601a --- /dev/null +++ b/tests/stub/main.go @@ -0,0 +1,21 @@ +// Package main implements a stub daemon used by the packaging tests. It +// stands in for the real Sia daemons so the test suite can exercise package +// install, upgrade, and removal without network access. +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" +) + +func main() { + fmt.Println("stub daemon started") + + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + <-ch + + fmt.Println("stub daemon stopped") +}