|
|
@@ -0,0 +1,326 @@
|
|
|
+#!/bin/bash
|
|
|
+#
|
|
|
+# DCBA end-to-end verification — no network, file:// repositories only.
|
|
|
+#
|
|
|
+# Builds USM, then drives two fully isolated USM roots (producer A and
|
|
|
+# consumer B) through the whole DCBA story: a tiny fixture package is
|
|
|
+# packaged, published in a file:// source repository and installed; the
|
|
|
+# resulting build cache is turned into a .dcba artifact published in a
|
|
|
+# file:// DCBA repository; a second root installs from scratch and must
|
|
|
+# receive the pre-built artifact as its build.tar.xz build cache.
|
|
|
+#
|
|
|
+# Assertions (each printed PASS/FAIL, exit non-zero on any failure):
|
|
|
+# a. <state>/packages/<name>-<version>/build.tar.xz exists right after
|
|
|
+# the download phase, and no local build ran (the build script's
|
|
|
+# one-time fingerprint is absent from the artifact)
|
|
|
+# b. origin-info records the DCBA repository name in "dcba"
|
|
|
+# c. the package installs successfully
|
|
|
+# d. usm clean builds removes build.tar.xz and build.tar.xz.digest
|
|
|
+# e. a second install after removing the cache re-downloads the
|
|
|
+# artifact (the "Retrieved pre-built artifact" line reappears)
|
|
|
+# f. a tampered artifact in the repository (content no longer matching
|
|
|
+# the signed listing checksum) is rejected with a warning and the
|
|
|
+# install still succeeds via a source build
|
|
|
+#
|
|
|
+# Environment quirks this script works around (keep in mind when
|
|
|
+# running it elsewhere):
|
|
|
+# * The freshly built usm binary links libusm-0.1.so by SONAME, and the
|
|
|
+# host carries an older /opt/usm/lib/libusm-0.1.so without the DCBA
|
|
|
+# symbols — every invocation needs LD_LIBRARY_PATH=<build>/lib or it
|
|
|
+# silently talks to the stale library.
|
|
|
+# * meson only finds the Invercargill libraries through
|
|
|
+# PKG_CONFIG_PATH=/opt/usm/lib/pkgconfig.
|
|
|
+# * USM never creates the state root itself: each root's
|
|
|
+# <state>/{lists,packages,installed} must exist before the first
|
|
|
+# repository refresh/install.
|
|
|
+# * usm.config must exist per root with is_managed=true and a
|
|
|
+# "managed"."state_path" pointing at that root's state directory;
|
|
|
+# USM_CONFIGDIR/USM_DESTDIR (see Paths.override_from_environment)
|
|
|
+# keep the roots apart.
|
|
|
+#
|
|
|
+# Usage: scripts/dcba-e2e.sh [build-dir]
|
|
|
+# build-dir defaults to a fresh temp directory; pass an existing meson
|
|
|
+# build directory to skip the (slow) compile.
|
|
|
+
|
|
|
+set -u
|
|
|
+set -o pipefail
|
|
|
+
|
|
|
+SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
|
|
|
+SOURCE_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
|
|
|
+
|
|
|
+WORK=$(mktemp -d /tmp/usm-dcba-e2e.XXXXXX)
|
|
|
+trap 'rm -rf "$WORK"' EXIT
|
|
|
+
|
|
|
+PASS_COUNT=0
|
|
|
+FAIL_COUNT=0
|
|
|
+
|
|
|
+report() {
|
|
|
+ if [ "$1" = 0 ]; then
|
|
|
+ printf 'PASS %s\n' "$2"
|
|
|
+ PASS_COUNT=$((PASS_COUNT + 1))
|
|
|
+ else
|
|
|
+ printf 'FAIL %s\n' "$2"
|
|
|
+ FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
|
+ fi
|
|
|
+}
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 0. Build
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+BUILD_DIR=${1:-}
|
|
|
+
|
|
|
+if [ -z "$BUILD_DIR" ]; then
|
|
|
+ BUILD_DIR="$WORK/usm-build"
|
|
|
+ printf '== Building USM into %s\n' "$BUILD_DIR"
|
|
|
+ meson setup "$BUILD_DIR" "$SOURCE_DIR/src" >/dev/null 2>&1 \
|
|
|
+ || { echo "meson setup failed"; exit 1; }
|
|
|
+ (cd "$BUILD_DIR" && PKG_CONFIG_PATH=/opt/usm/lib/pkgconfig ninja >/dev/null 2>&1) \
|
|
|
+ || { echo "ninja failed"; exit 1; }
|
|
|
+fi
|
|
|
+
|
|
|
+USM_BIN="$BUILD_DIR/cli/usm"
|
|
|
+[ -x "$USM_BIN" ] || { echo "no usm binary at $USM_BIN"; exit 1; }
|
|
|
+
|
|
|
+# The stale-host-library workaround described in the header
|
|
|
+export LD_LIBRARY_PATH="$BUILD_DIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 1. Two isolated roots
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+write_config() {
|
|
|
+ mkdir -p "$1"
|
|
|
+ cat > "$1/usm.config" <<EOF
|
|
|
+{
|
|
|
+ "is_managed": true,
|
|
|
+ "managed": { "state_path": "$2" },
|
|
|
+ "download_timeout": 300,
|
|
|
+ "paths": {
|
|
|
+ "prefix": "/usr", "bin": "bin", "sbin": "sbin", "lib": "lib64", "canonlib": "lib",
|
|
|
+ "libexec": "libexec", "include": "include", "data": "share", "info": "share/info",
|
|
|
+ "man": "share/man", "locale": "share/locale", "localState": "var", "sharedState": "com",
|
|
|
+ "sysConfig": "etc", "tags": "share/usm-tags"
|
|
|
+ }
|
|
|
+}
|
|
|
+EOF
|
|
|
+}
|
|
|
+
|
|
|
+make_root() {
|
|
|
+ # <root>/config (USM_CONFIGDIR) and <root>/state, with the
|
|
|
+ # subdirectories USM expects to already exist
|
|
|
+ rm -rf "$1" "$2"
|
|
|
+ mkdir -p "$1" "$2/lists" "$2/packages" "$2/installed"
|
|
|
+ write_config "$1" "$2"
|
|
|
+}
|
|
|
+
|
|
|
+ROOT_A_CONFIG="$WORK/rootA/config"
|
|
|
+ROOT_A_STATE="$WORK/rootA/state"
|
|
|
+ROOT_B_CONFIG="$WORK/rootB/config"
|
|
|
+ROOT_B_STATE="$WORK/rootB/state"
|
|
|
+
|
|
|
+make_root "$ROOT_A_CONFIG" "$ROOT_A_STATE"
|
|
|
+make_root "$ROOT_B_CONFIG" "$ROOT_B_STATE"
|
|
|
+
|
|
|
+# usm_a/usm_b/usm_pkg run the built binary against one isolated root
|
|
|
+usm_a() {
|
|
|
+ (cd "$WORK" \
|
|
|
+ && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" "$USM_BIN" "$@")
|
|
|
+}
|
|
|
+usm_b() {
|
|
|
+ # Root B builds (only in the tamper fallback) carry B's fingerprint
|
|
|
+ (cd "$WORK" \
|
|
|
+ && DCBA_E2E_FINGERPRINT="$FINGERPRINT_B" \
|
|
|
+ USM_CONFIGDIR="$ROOT_B_CONFIG" USM_DESTDIR="$WORK/rootB/dest" "$USM_BIN" "$@")
|
|
|
+}
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 2. Root A: package a fixture, publish it, install it
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+PKG="$WORK/pkg"
|
|
|
+mkdir -p "$PKG/usm-scripts"
|
|
|
+
|
|
|
+# Two fingerprints, one per root: the build script embeds the one its
|
|
|
+# environment passes through DCBA_E2E_FINGERPRINT, so root A's artifact
|
|
|
+# and root B's local build are distinguishable — root B reusing the
|
|
|
+# downloaded artifact must contain A's fingerprint and not B's
|
|
|
+# (assertion a), while the tamper fallback build (assertion f) must
|
|
|
+# contain B's.
|
|
|
+FINGERPRINT_A="dcba-e2e-a-$(date +%s)-$RANDOM"
|
|
|
+FINGERPRINT_B="dcba-e2e-b-$(date +%s)-$RANDOM"
|
|
|
+
|
|
|
+cat > "$PKG/MANIFEST.usm" <<EOF
|
|
|
+{
|
|
|
+ "name": "dcba-e2e-demo",
|
|
|
+ "version": "1.0.0",
|
|
|
+ "summary": "tiny dcba end-to-end fixture",
|
|
|
+ "licences": [{ "name": "MIT", "text": "LICENCE", "category": "libre" }],
|
|
|
+ "flags": [],
|
|
|
+ "provides": { "res:dcba-e2e-demo/hello.txt": "build:hello.txt" },
|
|
|
+ "depends": { "runtime": [], "build": [], "manage": [] },
|
|
|
+ "execs": { "build": "usm-scripts/build.sh" }
|
|
|
+}
|
|
|
+EOF
|
|
|
+
|
|
|
+printf 'MIT\n' > "$PKG/LICENCE"
|
|
|
+# The fingerprint rides in through the environment so the very same
|
|
|
+# .usmc (whose bytes must not differ between the roots) builds with a
|
|
|
+# per-root marker. The script is incremental like any real build system:
|
|
|
+# an up-to-date output is left untouched, so a restored build cache means
|
|
|
+# the compile step does nothing — which is exactly what assertion (a)
|
|
|
+# observes (USM always re-runs the build exec; reuse is the build
|
|
|
+# directory being restored beforehand, not the exec being skipped).
|
|
|
+cat > "$PKG/usm-scripts/build.sh" <<'EOF'
|
|
|
+#!/bin/bash
|
|
|
+set -e
|
|
|
+if [ ! -f "$1/hello.txt" ]; then
|
|
|
+ echo "built locally ${DCBA_E2E_FINGERPRINT:-unset}" > "$1/hello.txt"
|
|
|
+fi
|
|
|
+EOF
|
|
|
+chmod +x "$PKG/usm-scripts/build.sh"
|
|
|
+
|
|
|
+printf '== Root A: packaging the fixture\n'
|
|
|
+(cd "$PKG" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
|
|
|
+ DCBA_E2E_FINGERPRINT="$FINGERPRINT_A" \
|
|
|
+ "$USM_BIN" manifest package >/dev/null 2>&1) || { echo "usm manifest package failed"; exit 1; }
|
|
|
+USMC="$WORK/dcba-e2e-demo-1.0.0.usmc"
|
|
|
+[ -f "$USMC" ] || { echo "no .usmc produced"; exit 1; }
|
|
|
+
|
|
|
+SRC_REPO="$WORK/source-repo"
|
|
|
+mkdir -p "$SRC_REPO"
|
|
|
+printf '== Root A: source repository\n'
|
|
|
+(cd "$SRC_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
|
|
|
+ "$USM_BIN" repository init demo >/dev/null 2>&1) || { echo "usm repository init failed"; exit 1; }
|
|
|
+(cd "$SRC_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
|
|
|
+ "$USM_BIN" repository add "$USMC" >/dev/null 2>&1) || { echo "usm repository add failed"; exit 1; }
|
|
|
+
|
|
|
+usm_a add-repo -y "$SRC_REPO/demo.usmr" >/dev/null 2>&1 || { echo "usm add-repo failed"; exit 1; }
|
|
|
+DCBA_E2E_FINGERPRINT="$FINGERPRINT_A" \
|
|
|
+ usm_a install dcba-e2e-demo -y >"$WORK/rootA-install.log" 2>&1 \
|
|
|
+ || { echo "root A install failed"; cat "$WORK/rootA-install.log"; exit 1; }
|
|
|
+
|
|
|
+CACHE_A="$ROOT_A_STATE/packages/dcba-e2e-demo-1.0.0"
|
|
|
+[ -f "$CACHE_A/build.tar.xz" ] || { echo "root A produced no build.tar.xz"; exit 1; }
|
|
|
+[ -f "$CACHE_A/build.tar.xz.digest" ] || { echo "root A produced no build.tar.xz.digest"; exit 1; }
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 3. Root A: produce the .dcba and run the DCBA repository tooling
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+DCBA_REPO="$WORK/dcba-repo"
|
|
|
+mkdir -p "$DCBA_REPO"
|
|
|
+printf '== Producer: DCBA repository\n'
|
|
|
+(cd "$DCBA_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
|
|
|
+ "$USM_BIN" dcba repository init demoart >/dev/null 2>&1) || { echo "usm dcba repository init failed"; exit 1; }
|
|
|
+
|
|
|
+# The manual step: the cache's build.tar.xz IS the artifact, renamed
|
|
|
+cp "$CACHE_A/build.tar.xz" "$DCBA_REPO/dcba-e2e-demo-1.0.0.dcba"
|
|
|
+(cd "$DCBA_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
|
|
|
+ "$USM_BIN" dcba repository add dcba-e2e-demo-1.0.0.dcba >/dev/null 2>&1) \
|
|
|
+ || { echo "usm dcba repository add failed"; exit 1; }
|
|
|
+[ -f "$DCBA_REPO/public/ARTIFACTS.usml" ] || { echo "no ARTIFACTS.usml built"; exit 1; }
|
|
|
+
|
|
|
+# The listing and the artifacts it references must verify
|
|
|
+(cd "$DCBA_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
|
|
|
+ "$USM_BIN" dcba repository verify demoart.dcbar >/dev/null 2>&1)
|
|
|
+report $? "dcba repository verify (signature + checksums)"
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 4. Root B: consume both repositories
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+printf '== Root B: install from scratch with the DCBA repository configured\n'
|
|
|
+usm_b add-repo -y "$SRC_REPO/demo.usmr" >/dev/null 2>&1 || { echo "root B add-repo failed"; exit 1; }
|
|
|
+usm_b dcba add-repo -y "$DCBA_REPO/demoart.dcbar" >"$WORK/rootB-addrepo.log" 2>&1 \
|
|
|
+ || { echo "root B dcba add-repo failed"; cat "$WORK/rootB-addrepo.log"; exit 1; }
|
|
|
+
|
|
|
+usm_b install dcba-e2e-demo -y >"$WORK/rootB-install.log" 2>&1
|
|
|
+INSTALL_EXIT=$?
|
|
|
+report $INSTALL_EXIT "(c) usm install succeeds with a DCBA repository configured"
|
|
|
+
|
|
|
+CACHE_B="$ROOT_B_STATE/packages/dcba-e2e-demo-1.0.0"
|
|
|
+INSTALLED_B="$WORK/rootB/dest/usr/share/dcba-e2e-demo/hello.txt"
|
|
|
+
|
|
|
+# (a) the artifact landed as the build cache, without a local build
|
|
|
+[ -f "$CACHE_B/build.tar.xz" ]
|
|
|
+report $? "(a) build.tar.xz exists in the cache directory after the download"
|
|
|
+
|
|
|
+if tar -xJOf "$CACHE_B/build.tar.xz" ./hello.txt 2>/dev/null | grep -q "$FINGERPRINT_A"; then
|
|
|
+ report 0 "(a) artifact is the producer's (root A fingerprint present)"
|
|
|
+else
|
|
|
+ report 1 "(a) artifact is the producer's (root A fingerprint present)"
|
|
|
+fi
|
|
|
+if tar -xJOf "$CACHE_B/build.tar.xz" ./hello.txt 2>/dev/null | grep -q "$FINGERPRINT_B"; then
|
|
|
+ report 1 "(a) artifact was reused, not rebuilt locally (root B fingerprint absent)"
|
|
|
+else
|
|
|
+ report 0 "(a) artifact was reused, not rebuilt locally (root B fingerprint absent)"
|
|
|
+fi
|
|
|
+
|
|
|
+# (b) provenance
|
|
|
+grep -q '"dcba"[[:space:]]*:[[:space:]]*"demoart"' "$CACHE_B/origin-info"
|
|
|
+report $? "(b) origin-info records the DCBA repository in \"dcba\""
|
|
|
+
|
|
|
+# (c) the install actually installed the resource
|
|
|
+[ -f "$INSTALLED_B" ]
|
|
|
+report $? "(c) the package's resource was installed"
|
|
|
+
|
|
|
+# The download log must show the artifact retrieval
|
|
|
+tr '\r' '\n' < "$WORK/rootB-install.log" | grep -q "Retrieved pre-built artifact"
|
|
|
+report $? "(evidence) install log reports the retrieved pre-built artifact"
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 5. (d) usm clean builds clears the artifact cache
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+usm_b clean builds >/dev/null 2>&1
|
|
|
+CLEAN_OK=$?
|
|
|
+[ ! -e "$CACHE_B/build.tar.xz" ] && [ ! -e "$CACHE_B/build.tar.xz.digest" ]
|
|
|
+NO_CACHE=$?
|
|
|
+report $CLEAN_OK "(d) usm clean builds succeeds"
|
|
|
+report $NO_CACHE "(d) usm clean builds removed build.tar.xz and build.tar.xz.digest"
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 6. (e) a clean install re-downloads the artifact
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+# Remove the package entirely so the .usmc transfers again — the DCBA
|
|
|
+# fetch only rides along a package download
|
|
|
+rm -rf "$CACHE_B" "$ROOT_B_STATE/installed/dcba-e2e-demo-1.0.0" "$INSTALLED_B"
|
|
|
+usm_b install dcba-e2e-demo -y >"$WORK/rootB-reinstall.log" 2>&1
|
|
|
+REINSTALL_EXIT=$?
|
|
|
+report $REINSTALL_EXIT "(e) reinstall after cache removal succeeds"
|
|
|
+tr '\r' '\n' < "$WORK/rootB-reinstall.log" | grep -q "Retrieved pre-built artifact"
|
|
|
+report $? "(e) reinstall re-downloaded the pre-built artifact"
|
|
|
+[ -f "$CACHE_B/build.tar.xz" ]
|
|
|
+report $? "(e) build.tar.xz is back in the cache directory"
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 7. (f) a tampered artifact falls back to a source build
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+# Corrupt the published artifact WITHOUT rebuilding the listing, so the
|
|
|
+# signed listing still carries the honest checksum and the client must
|
|
|
+# reject the download by checksum mismatch — the non-fatal path
|
|
|
+printf 'tampered, not an archive' > "$DCBA_REPO/public/dcba-e2e-demo-1.0.0.dcba"
|
|
|
+rm -rf "$ROOT_B_STATE/lists/demoart"
|
|
|
+
|
|
|
+rm -rf "$CACHE_B" "$ROOT_B_STATE/installed/dcba-e2e-demo-1.0.0" "$INSTALLED_B"
|
|
|
+usm_b install dcba-e2e-demo -y >"$WORK/rootB-tamper.log" 2>&1
|
|
|
+TAMPER_EXIT=$?
|
|
|
+report $TAMPER_EXIT "(f) install with a tampered artifact still succeeds"
|
|
|
+
|
|
|
+tr '\r' '\n' < "$WORK/rootB-tamper.log" \
|
|
|
+ | grep -q 'Warning: DCBA artifact.*could not be used.*building from source instead'
|
|
|
+report $? "(f) a warning explains the rejected artifact"
|
|
|
+
|
|
|
+[ -f "$INSTALLED_B" ] && grep -q "$FINGERPRINT_B" "$INSTALLED_B"
|
|
|
+report $? "(f) the package was built from source (root B fingerprint present)"
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+printf '\n%d passed, %d failed\n' "$PASS_COUNT" "$FAIL_COUNT"
|
|
|
+if [ "$FAIL_COUNT" -gt 0 ]; then
|
|
|
+ exit 1
|
|
|
+fi
|
|
|
+exit 0
|