#!/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. /packages/-/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=/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 # /{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" </config (USM_CONFIGDIR) and /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" < "$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