| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- #!/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
|