dcba-e2e.sh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. #!/bin/bash
  2. #
  3. # DCBA end-to-end verification — no network, file:// repositories only.
  4. #
  5. # Builds USM, then drives two fully isolated USM roots (producer A and
  6. # consumer B) through the whole DCBA story: a tiny fixture package is
  7. # packaged, published in a file:// source repository and installed; the
  8. # resulting build cache is turned into a .dcba artifact published in a
  9. # file:// DCBA repository; a second root installs from scratch and must
  10. # receive the pre-built artifact as its build.tar.xz build cache.
  11. #
  12. # Assertions (each printed PASS/FAIL, exit non-zero on any failure):
  13. # a. <state>/packages/<name>-<version>/build.tar.xz exists right after
  14. # the download phase, and no local build ran (the build script's
  15. # one-time fingerprint is absent from the artifact)
  16. # b. origin-info records the DCBA repository name in "dcba"
  17. # c. the package installs successfully
  18. # d. usm clean builds removes build.tar.xz and build.tar.xz.digest
  19. # e. a second install after removing the cache re-downloads the
  20. # artifact (the "Retrieved pre-built artifact" line reappears)
  21. # f. a tampered artifact in the repository (content no longer matching
  22. # the signed listing checksum) is rejected with a warning and the
  23. # install still succeeds via a source build
  24. #
  25. # Environment quirks this script works around (keep in mind when
  26. # running it elsewhere):
  27. # * The freshly built usm binary links libusm-0.1.so by SONAME, and the
  28. # host carries an older /opt/usm/lib/libusm-0.1.so without the DCBA
  29. # symbols — every invocation needs LD_LIBRARY_PATH=<build>/lib or it
  30. # silently talks to the stale library.
  31. # * meson only finds the Invercargill libraries through
  32. # PKG_CONFIG_PATH=/opt/usm/lib/pkgconfig.
  33. # * USM never creates the state root itself: each root's
  34. # <state>/{lists,packages,installed} must exist before the first
  35. # repository refresh/install.
  36. # * usm.config must exist per root with is_managed=true and a
  37. # "managed"."state_path" pointing at that root's state directory;
  38. # USM_CONFIGDIR/USM_DESTDIR (see Paths.override_from_environment)
  39. # keep the roots apart.
  40. #
  41. # Usage: scripts/dcba-e2e.sh [build-dir]
  42. # build-dir defaults to a fresh temp directory; pass an existing meson
  43. # build directory to skip the (slow) compile.
  44. set -u
  45. set -o pipefail
  46. SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
  47. SOURCE_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
  48. WORK=$(mktemp -d /tmp/usm-dcba-e2e.XXXXXX)
  49. trap 'rm -rf "$WORK"' EXIT
  50. PASS_COUNT=0
  51. FAIL_COUNT=0
  52. report() {
  53. if [ "$1" = 0 ]; then
  54. printf 'PASS %s\n' "$2"
  55. PASS_COUNT=$((PASS_COUNT + 1))
  56. else
  57. printf 'FAIL %s\n' "$2"
  58. FAIL_COUNT=$((FAIL_COUNT + 1))
  59. fi
  60. }
  61. # ---------------------------------------------------------------------------
  62. # 0. Build
  63. # ---------------------------------------------------------------------------
  64. BUILD_DIR=${1:-}
  65. if [ -z "$BUILD_DIR" ]; then
  66. BUILD_DIR="$WORK/usm-build"
  67. printf '== Building USM into %s\n' "$BUILD_DIR"
  68. meson setup "$BUILD_DIR" "$SOURCE_DIR/src" >/dev/null 2>&1 \
  69. || { echo "meson setup failed"; exit 1; }
  70. (cd "$BUILD_DIR" && PKG_CONFIG_PATH=/opt/usm/lib/pkgconfig ninja >/dev/null 2>&1) \
  71. || { echo "ninja failed"; exit 1; }
  72. fi
  73. USM_BIN="$BUILD_DIR/cli/usm"
  74. [ -x "$USM_BIN" ] || { echo "no usm binary at $USM_BIN"; exit 1; }
  75. # The stale-host-library workaround described in the header
  76. export LD_LIBRARY_PATH="$BUILD_DIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
  77. # ---------------------------------------------------------------------------
  78. # 1. Two isolated roots
  79. # ---------------------------------------------------------------------------
  80. write_config() {
  81. mkdir -p "$1"
  82. cat > "$1/usm.config" <<EOF
  83. {
  84. "is_managed": true,
  85. "managed": { "state_path": "$2" },
  86. "download_timeout": 300,
  87. "paths": {
  88. "prefix": "/usr", "bin": "bin", "sbin": "sbin", "lib": "lib64", "canonlib": "lib",
  89. "libexec": "libexec", "include": "include", "data": "share", "info": "share/info",
  90. "man": "share/man", "locale": "share/locale", "localState": "var", "sharedState": "com",
  91. "sysConfig": "etc", "tags": "share/usm-tags"
  92. }
  93. }
  94. EOF
  95. }
  96. make_root() {
  97. # <root>/config (USM_CONFIGDIR) and <root>/state, with the
  98. # subdirectories USM expects to already exist
  99. rm -rf "$1" "$2"
  100. mkdir -p "$1" "$2/lists" "$2/packages" "$2/installed"
  101. write_config "$1" "$2"
  102. }
  103. ROOT_A_CONFIG="$WORK/rootA/config"
  104. ROOT_A_STATE="$WORK/rootA/state"
  105. ROOT_B_CONFIG="$WORK/rootB/config"
  106. ROOT_B_STATE="$WORK/rootB/state"
  107. make_root "$ROOT_A_CONFIG" "$ROOT_A_STATE"
  108. make_root "$ROOT_B_CONFIG" "$ROOT_B_STATE"
  109. # usm_a/usm_b/usm_pkg run the built binary against one isolated root
  110. usm_a() {
  111. (cd "$WORK" \
  112. && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" "$USM_BIN" "$@")
  113. }
  114. usm_b() {
  115. # Root B builds (only in the tamper fallback) carry B's fingerprint
  116. (cd "$WORK" \
  117. && DCBA_E2E_FINGERPRINT="$FINGERPRINT_B" \
  118. USM_CONFIGDIR="$ROOT_B_CONFIG" USM_DESTDIR="$WORK/rootB/dest" "$USM_BIN" "$@")
  119. }
  120. # ---------------------------------------------------------------------------
  121. # 2. Root A: package a fixture, publish it, install it
  122. # ---------------------------------------------------------------------------
  123. PKG="$WORK/pkg"
  124. mkdir -p "$PKG/usm-scripts"
  125. # Two fingerprints, one per root: the build script embeds the one its
  126. # environment passes through DCBA_E2E_FINGERPRINT, so root A's artifact
  127. # and root B's local build are distinguishable — root B reusing the
  128. # downloaded artifact must contain A's fingerprint and not B's
  129. # (assertion a), while the tamper fallback build (assertion f) must
  130. # contain B's.
  131. FINGERPRINT_A="dcba-e2e-a-$(date +%s)-$RANDOM"
  132. FINGERPRINT_B="dcba-e2e-b-$(date +%s)-$RANDOM"
  133. cat > "$PKG/MANIFEST.usm" <<EOF
  134. {
  135. "name": "dcba-e2e-demo",
  136. "version": "1.0.0",
  137. "summary": "tiny dcba end-to-end fixture",
  138. "licences": [{ "name": "MIT", "text": "LICENCE", "category": "libre" }],
  139. "flags": [],
  140. "provides": { "res:dcba-e2e-demo/hello.txt": "build:hello.txt" },
  141. "depends": { "runtime": [], "build": [], "manage": [] },
  142. "execs": { "build": "usm-scripts/build.sh" }
  143. }
  144. EOF
  145. printf 'MIT\n' > "$PKG/LICENCE"
  146. # The fingerprint rides in through the environment so the very same
  147. # .usmc (whose bytes must not differ between the roots) builds with a
  148. # per-root marker. The script is incremental like any real build system:
  149. # an up-to-date output is left untouched, so a restored build cache means
  150. # the compile step does nothing — which is exactly what assertion (a)
  151. # observes (USM always re-runs the build exec; reuse is the build
  152. # directory being restored beforehand, not the exec being skipped).
  153. cat > "$PKG/usm-scripts/build.sh" <<'EOF'
  154. #!/bin/bash
  155. set -e
  156. if [ ! -f "$1/hello.txt" ]; then
  157. echo "built locally ${DCBA_E2E_FINGERPRINT:-unset}" > "$1/hello.txt"
  158. fi
  159. EOF
  160. chmod +x "$PKG/usm-scripts/build.sh"
  161. printf '== Root A: packaging the fixture\n'
  162. (cd "$PKG" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
  163. DCBA_E2E_FINGERPRINT="$FINGERPRINT_A" \
  164. "$USM_BIN" manifest package >/dev/null 2>&1) || { echo "usm manifest package failed"; exit 1; }
  165. USMC="$WORK/dcba-e2e-demo-1.0.0.usmc"
  166. [ -f "$USMC" ] || { echo "no .usmc produced"; exit 1; }
  167. SRC_REPO="$WORK/source-repo"
  168. mkdir -p "$SRC_REPO"
  169. printf '== Root A: source repository\n'
  170. (cd "$SRC_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
  171. "$USM_BIN" repository init demo >/dev/null 2>&1) || { echo "usm repository init failed"; exit 1; }
  172. (cd "$SRC_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
  173. "$USM_BIN" repository add "$USMC" >/dev/null 2>&1) || { echo "usm repository add failed"; exit 1; }
  174. usm_a add-repo -y "$SRC_REPO/demo.usmr" >/dev/null 2>&1 || { echo "usm add-repo failed"; exit 1; }
  175. DCBA_E2E_FINGERPRINT="$FINGERPRINT_A" \
  176. usm_a install dcba-e2e-demo -y >"$WORK/rootA-install.log" 2>&1 \
  177. || { echo "root A install failed"; cat "$WORK/rootA-install.log"; exit 1; }
  178. CACHE_A="$ROOT_A_STATE/packages/dcba-e2e-demo-1.0.0"
  179. [ -f "$CACHE_A/build.tar.xz" ] || { echo "root A produced no build.tar.xz"; exit 1; }
  180. [ -f "$CACHE_A/build.tar.xz.digest" ] || { echo "root A produced no build.tar.xz.digest"; exit 1; }
  181. # ---------------------------------------------------------------------------
  182. # 3. Root A: produce the .dcba and run the DCBA repository tooling
  183. # ---------------------------------------------------------------------------
  184. DCBA_REPO="$WORK/dcba-repo"
  185. mkdir -p "$DCBA_REPO"
  186. printf '== Producer: DCBA repository\n'
  187. (cd "$DCBA_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
  188. "$USM_BIN" dcba repository init demoart >/dev/null 2>&1) || { echo "usm dcba repository init failed"; exit 1; }
  189. # The manual step: the cache's build.tar.xz IS the artifact, renamed
  190. cp "$CACHE_A/build.tar.xz" "$DCBA_REPO/dcba-e2e-demo-1.0.0.dcba"
  191. (cd "$DCBA_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
  192. "$USM_BIN" dcba repository add dcba-e2e-demo-1.0.0.dcba >/dev/null 2>&1) \
  193. || { echo "usm dcba repository add failed"; exit 1; }
  194. [ -f "$DCBA_REPO/public/ARTIFACTS.usml" ] || { echo "no ARTIFACTS.usml built"; exit 1; }
  195. # The listing and the artifacts it references must verify
  196. (cd "$DCBA_REPO" && USM_CONFIGDIR="$ROOT_A_CONFIG" USM_DESTDIR="$WORK/rootA/dest" \
  197. "$USM_BIN" dcba repository verify demoart.dcbar >/dev/null 2>&1)
  198. report $? "dcba repository verify (signature + checksums)"
  199. # ---------------------------------------------------------------------------
  200. # 4. Root B: consume both repositories
  201. # ---------------------------------------------------------------------------
  202. printf '== Root B: install from scratch with the DCBA repository configured\n'
  203. usm_b add-repo -y "$SRC_REPO/demo.usmr" >/dev/null 2>&1 || { echo "root B add-repo failed"; exit 1; }
  204. usm_b dcba add-repo -y "$DCBA_REPO/demoart.dcbar" >"$WORK/rootB-addrepo.log" 2>&1 \
  205. || { echo "root B dcba add-repo failed"; cat "$WORK/rootB-addrepo.log"; exit 1; }
  206. usm_b install dcba-e2e-demo -y >"$WORK/rootB-install.log" 2>&1
  207. INSTALL_EXIT=$?
  208. report $INSTALL_EXIT "(c) usm install succeeds with a DCBA repository configured"
  209. CACHE_B="$ROOT_B_STATE/packages/dcba-e2e-demo-1.0.0"
  210. INSTALLED_B="$WORK/rootB/dest/usr/share/dcba-e2e-demo/hello.txt"
  211. # (a) the artifact landed as the build cache, without a local build
  212. [ -f "$CACHE_B/build.tar.xz" ]
  213. report $? "(a) build.tar.xz exists in the cache directory after the download"
  214. if tar -xJOf "$CACHE_B/build.tar.xz" ./hello.txt 2>/dev/null | grep -q "$FINGERPRINT_A"; then
  215. report 0 "(a) artifact is the producer's (root A fingerprint present)"
  216. else
  217. report 1 "(a) artifact is the producer's (root A fingerprint present)"
  218. fi
  219. if tar -xJOf "$CACHE_B/build.tar.xz" ./hello.txt 2>/dev/null | grep -q "$FINGERPRINT_B"; then
  220. report 1 "(a) artifact was reused, not rebuilt locally (root B fingerprint absent)"
  221. else
  222. report 0 "(a) artifact was reused, not rebuilt locally (root B fingerprint absent)"
  223. fi
  224. # (b) provenance
  225. grep -q '"dcba"[[:space:]]*:[[:space:]]*"demoart"' "$CACHE_B/origin-info"
  226. report $? "(b) origin-info records the DCBA repository in \"dcba\""
  227. # (c) the install actually installed the resource
  228. [ -f "$INSTALLED_B" ]
  229. report $? "(c) the package's resource was installed"
  230. # The download log must show the artifact retrieval
  231. tr '\r' '\n' < "$WORK/rootB-install.log" | grep -q "Retrieved pre-built artifact"
  232. report $? "(evidence) install log reports the retrieved pre-built artifact"
  233. # ---------------------------------------------------------------------------
  234. # 5. (d) usm clean builds clears the artifact cache
  235. # ---------------------------------------------------------------------------
  236. usm_b clean builds >/dev/null 2>&1
  237. CLEAN_OK=$?
  238. [ ! -e "$CACHE_B/build.tar.xz" ] && [ ! -e "$CACHE_B/build.tar.xz.digest" ]
  239. NO_CACHE=$?
  240. report $CLEAN_OK "(d) usm clean builds succeeds"
  241. report $NO_CACHE "(d) usm clean builds removed build.tar.xz and build.tar.xz.digest"
  242. # ---------------------------------------------------------------------------
  243. # 6. (e) a clean install re-downloads the artifact
  244. # ---------------------------------------------------------------------------
  245. # Remove the package entirely so the .usmc transfers again — the DCBA
  246. # fetch only rides along a package download
  247. rm -rf "$CACHE_B" "$ROOT_B_STATE/installed/dcba-e2e-demo-1.0.0" "$INSTALLED_B"
  248. usm_b install dcba-e2e-demo -y >"$WORK/rootB-reinstall.log" 2>&1
  249. REINSTALL_EXIT=$?
  250. report $REINSTALL_EXIT "(e) reinstall after cache removal succeeds"
  251. tr '\r' '\n' < "$WORK/rootB-reinstall.log" | grep -q "Retrieved pre-built artifact"
  252. report $? "(e) reinstall re-downloaded the pre-built artifact"
  253. [ -f "$CACHE_B/build.tar.xz" ]
  254. report $? "(e) build.tar.xz is back in the cache directory"
  255. # ---------------------------------------------------------------------------
  256. # 7. (f) a tampered artifact falls back to a source build
  257. # ---------------------------------------------------------------------------
  258. # Corrupt the published artifact WITHOUT rebuilding the listing, so the
  259. # signed listing still carries the honest checksum and the client must
  260. # reject the download by checksum mismatch — the non-fatal path
  261. printf 'tampered, not an archive' > "$DCBA_REPO/public/dcba-e2e-demo-1.0.0.dcba"
  262. rm -rf "$ROOT_B_STATE/lists/demoart"
  263. rm -rf "$CACHE_B" "$ROOT_B_STATE/installed/dcba-e2e-demo-1.0.0" "$INSTALLED_B"
  264. usm_b install dcba-e2e-demo -y >"$WORK/rootB-tamper.log" 2>&1
  265. TAMPER_EXIT=$?
  266. report $TAMPER_EXIT "(f) install with a tampered artifact still succeeds"
  267. tr '\r' '\n' < "$WORK/rootB-tamper.log" \
  268. | grep -q 'Warning: DCBA artifact.*could not be used.*building from source instead'
  269. report $? "(f) a warning explains the rejected artifact"
  270. [ -f "$INSTALLED_B" ] && grep -q "$FINGERPRINT_B" "$INSTALLED_B"
  271. report $? "(f) the package was built from source (root B fingerprint present)"
  272. # ---------------------------------------------------------------------------
  273. printf '\n%d passed, %d failed\n' "$PASS_COUNT" "$FAIL_COUNT"
  274. if [ "$FAIL_COUNT" -gt 0 ]; then
  275. exit 1
  276. fi
  277. exit 0