Forráskód Böngészése

Document DCBA repositories and add an end-to-end verification script

README gains a DCBA section next to the source-repository material: the
.dcba format (the build.tar.xz a package cache holds, renamed), the
repository layout (.dcbar descriptor in the .usmr shape, keys/, public/
with the artifacts and the signed ARTIFACTS.usml listing), one platform
per repository, every usm dcba verb with examples, and the client
behaviour (artifact fetched alongside the source, sha512-verified,
written straight into the cache as build.tar.xz, any failure a warning
that falls back to a source build). The /var/usm layout documents the
build.tar.xz.digest sidecar and the conditional re-archive that keeps an
unchanged archive byte-identical, and the origin-info example gains the
dcba provenance field.

SPECIFICATION.md specifies the DCBA repository formats: the .dcba
artifact, the .dcbar descriptor and the ARTIFACTS.usml entry and
signatures schema field by field, in the PACKAGES.usml style.

PACKAGING_GUIDE.md tells maintainers how to produce and publish
artifacts today (build, copy the cache's build.tar.xz to
<name>-<version>.dcba, dcba repository init/add/publish) and notes that
usm dcba build may come later.

scripts/dcba-e2e.sh builds USM and drives two isolated USM roots
through the full story - package, source repo, install, artifact
production, DCBA repo, then a from-scratch consumer install - asserting
the artifact lands as build.tar.xz without a local build, the dcba
origin provenance, usm clean builds, re-download after cache removal
and the tamper fallback to a source build. 16 assertions, network-free,
exits non-zero on any failure.
saddle-Clanker-Saddle 1 hónapja
szülő
commit
dbaca9df8b
4 módosított fájl, 470 hozzáadás és 1 törlés
  1. 27 0
      PACKAGING_GUIDE.md
  2. 85 1
      README.md
  3. 32 0
      SPECIFICATION.md
  4. 326 0
      scripts/dcba-e2e.sh

+ 27 - 0
PACKAGING_GUIDE.md

@@ -336,3 +336,30 @@ USM_CONFIGDIR=/tmp/kilo/test-config usm install mylib
 ```
 
 Verify that every `provides` entry actually exists at the expected location in the DESTDIR. If it doesn't, either the install script or the manifest is wrong.
+---
+
+## Distributing pre-built artifacts (DCBA)
+
+A DCBA repository lets clients of one platform skip the compile: when a matching pre-built artifact exists, `usm install` downloads it alongside the source and uses it as the package's local build cache. Artifacts are per platform — publish one DCBA repository per architecture you build for.
+
+Producing artifacts today is manual (a `usm dcba build` command may come later):
+
+```bash
+# 1. Build and install the package normally, on the platform the repository serves
+usm install mylib -y
+
+# 2. The cache's build.tar.xz IS the artifact — copy and rename it
+cp /var/usm/packages/mylib-1.0.0/build.tar.xz mylib-1.0.0.dcba
+
+# 3. Publish it in a DCBA repository
+cd /path/to/my-artifacts && usm dcba repository init my-artifacts   # first time only
+usm dcba repository add ../mylib-1.0.0.dcba
+usm dcba repository publish /srv/www/my-artifacts
+```
+
+The filename must be `<name>-<version>.dcba`; `usm dcba repository add` validates that the file is a readable `tar.xz` and that its name carries a parseable version. Adding the same name and version with different content is rejected (identical content is a no-op). `usm dcba repository verify` checks the listing signature and every artifact checksum before you publish.
+
+Clients opt in with `usm dcba add-repo <url-or-path>`; from then on `usm install` uses artifacts automatically whenever one matches the package's name and version exactly.
+
+Only ship artifacts you built and trust: a client verifies the download against the repository's signed listing, then runs the package's normal install from the artifact — same trust model as a source repository, but the client executes no compile step.
+

+ 85 - 1
README.md

@@ -130,6 +130,84 @@ Last line consists of the signatures for the repository
 
 Complete package is simply a tar.xz file containing a MANIFEST.usm file and all the requisite files that would be acquired if the acquire script was run. That is to say, a USMC file could be created by simply running `usm manifest acquire` followed by `tar -cJf ../package.usmc .`.
 
+## DCBA repositories (Distribution Compiled Binary Artifacts)
+
+A DCBA repository is a second repository type next to a USM source repository. Instead of sources it distributes pre-built build artifacts: one `.dcba` file per package and version. A `.dcba` file is a plain `tar.xz` containing only the build artifact USM normally creates per system — exactly what lands in a package cache directory as `build.tar.xz`, renamed.
+
+The repository layout mirrors a source repository:
+
+- `<name>.dcbar` — the repository descriptor, the same JSON shape as `.usmr` (`name`, `summary`, `uris`/`url`, `key` base64 ed25519, `cache_expiration`); a different suffix so the two repository types can never be mistaken for one another
+- `keys/` — the signing keys (`public-key`, `private-key` mode 0600)
+- `public/` — the published `<name>-<version>.dcba` artifacts plus the signed `ARTIFACTS.usml` listing
+
+One repository serves one platform/architecture: a `.dcbar` descriptor states nothing about the platform, the repository is simply built for one system. A different architecture means the user configures a different DCBA repository.
+
+### DCBA artifact listing (ARTIFACTS.usml)
+
+Entries in JSON lines format, one per artifact:
+
+```json
+{
+  "type": "dcba",
+  "name": "my-package",
+  "version": "1.0.5+2",
+  "path": "my-package-1.0.5+2.dcba",
+  "sha512": "0+6QpQ5Wst2bPmAPYKO/RKrFYuq93rcaRs7tqt9lCo9zu8v6jpJsDqd7yPIoCWDt0KpOQ3uLK64Tmn9FYNw3eQ==",
+  "size": 220
+}
+```
+
+The last line is the signatures line, exactly as in `PACKAGES.usml`: the SHA-512 over the concatenated entry lines (as serialised, no newline terminators), ed25519-signed with the repository key.
+
+```json
+{
+  "type": "signatures",
+  "signatures": [
+    {
+      "key": "EDXLsUvOZEne+xcv+huvSaqNBs8TTldCv6hd69GdmYw=",
+      "signature": "0rHQiDGKgumMK2S3B6D9jjU3NLnLLjijYJRvaB5vLbp4zxzjlixfyX8yfvvJUCCd34CC1STzyzawDtb9ZammCTE4zFHFdpnO8eseDS5OY+aKgAyL13oBGizvy/V0zGHVCdv83GQ7QUtgSWje0RKKk1zPGDAIF/prkurlFDgd3Vg="
+    }
+  ]
+}
+```
+
+### Producing artifacts
+
+Producing a `.dcba` file is a manual step for now: build and install the package normally (on the platform the repository serves), then copy the package cache's `build.tar.xz` to `<name>-<version>.dcba`. A `usm dcba build` command may arrive later.
+
+### DCBA Commands
+
+- `usm dcba repository init <name>`: Create the DCBA repository layout in the current directory: `<name>.dcbar`, `keys/` (signing keys, private key mode 0600 and gitignored via `.gitignore`) and an empty `public/` artifacts directory
+- `usm dcba repository add <name-version.dcba>`: Validate the artifact (a readable tar.xz whose filename carries a parseable `<name>-<version>`), copy it into `public/`, then rebuild and re-sign `ARTIFACTS.usml`; the same name and version is rejected unless the content is identical (identical is a no-op)
+- `usm dcba repository remove <name|name-version|filename>`: Delete the matching `.dcba` from `public/` (exact filename, `<name>-<version>`, or a `<name>-` prefix; an ambiguous name lists the candidates and asks for the filename), then rebuild and re-sign
+- `usm dcba repository list [repo.dcbar]`: Print name, version and size of every artifact in the signed listing (from the repository root, or fetched from the given `.dcbar`'s URI)
+- `usm dcba repository verify <repo.dcbar>`: Check the listing signature, then deep-verify every entry's sha512 against the `.dcba` fetched from the repository URI; exits non-zero on any failure
+- `usm dcba repository publish <path>`: Copy the `.dcbar` and `ARTIFACTS.usml` to `<path>` (always overwriting) and each `public/*.dcba` only when the destination copy is missing or older by mtime
+- `usm dcba repository build-list`: Rebuild and re-sign `ARTIFACTS.usml` from whatever `.dcba` files sit in `public/`
+- `usm dcba add-repo <url-or-path>`: Add a DCBA repository (`.dcbar`) to the configuration, refreshing its artifact listing
+
+The `repository` verbs locate the DCBA repository root by searching the current directory and its ancestors for a directory containing a `*.dcbar` file, a `keys/` directory and a `public/` directory — deliberately separate from the source-repository search, so the two can never be confused even when nested in the same tree. Signing keys load from `keys/public-key` and `keys/private-key` in the repository root, falling back to the legacy loose `public-key` and `private-key` files.
+
+```bash
+# on the build machine for the platform the repository serves
+usm install my-package -y
+cp /var/usm/packages/my-package-1.0.5+2/build.tar.xz my-package-1.0.5+2.dcba
+
+usm dcba repository init my-artifacts
+usm dcba repository add my-package-1.0.5+2.dcba
+usm dcba repository publish /srv/www/my-artifacts
+
+# on a client of the same platform
+usm dcba add-repo https://my.software/my-artifacts/my-artifacts.dcbar
+usm install my-package -y
+```
+
+### Client behaviour
+
+During `usm install` (and `usm update`/`usm downgrade`), once a package's `.usmc` has been downloaded and verified, USM looks for an artifact with the same name and version in the configured DCBA repositories — in configuration order — and, on the first match, downloads it, verifies it against the listing's sha512 and writes it directly as `<state>/packages/<name>-<version>/build.tar.xz`. No separate `.dcba` file lingers in the cache. From that point on USM behaves exactly as if a local build cache existed: the transaction's build step restores and reuses it (an incremental build — the build exec still runs, against the restored build tree), install runs from it, and `usm clean builds` cleans it like any build cache. The retrieval is recorded in the package's origin-info (`"dcba"` names the repository) and logged as `Retrieved pre-built artifact ...`.
+
+A package's own local artifacts always win: when the cache directory already holds a build directory or a `build.tar.xz`, nothing is downloaded. Any artifact problem — no DCBA repositories configured, no matching entry, an unreachable repository, a listing that fails signature verification, a checksum mismatch — is a non-fatal warning and the package simply builds from source.
+
 ## .usmignore
 
 `usm manifest package` consults an optional `.usmignore` file at the root of the package (next to `MANIFEST.usm`) to prune files and directories from the produced archive.
@@ -196,6 +274,7 @@ A manifest with the `"dataPackage"` flag ships static files: fonts, icons, share
     - /package.usmc
     - /build-log (when built)
     - /build.tar.xz (when built)
+    - /build.tar.xz.digest (when built)
     - /build (when building)
 - /installed
   - /packagename-0.1.1 (symlink to /var/usm/packages/packagename-0.1.1)
@@ -207,16 +286,21 @@ A manifest with the `"dataPackage"` flag ships static files: fonts, icons, share
   - 2020-03-21T14:28:23.382748
   - 2020-03-23T14:23:43.382748
 
+`build.tar.xz` is the archived build tree a transaction leaves behind, and `build.tar.xz.digest` records a digest of the build tree state that produced it. The two together make re-archiving conditional: in the clean-up stage, when a pre-existing archive is present and the build tree's digest still matches the sidecar, the existing archive is kept byte-identical (mtime untouched) and only the build directory is cleaned — identical bytes are never re-tarred. A missing sidecar (an archive from before digests were recorded), a changed, added or removed file, or changed permissions or symlink targets re-archive as before. A system that restores a downloaded DCBA artifact and builds nothing therefore never churns the archive.
+
 ### Origin info format
 ```json
 {
     "repository": "repositoryname",
     "listfile": "2020-03-20T14:34:42.382748.usml",
     "original_path": "packagename-0.1.1.usmc",
-    "signature_verified": true
+    "signature_verified": true,
+    "dcba": null
 }
 ```
 
+`dcba` names the DCBA repository a pre-built build artifact was downloaded from (`"my-artifacts"` above), or null when the package's `build.tar.xz` was built locally. See [DCBA repositories](#dcba-repositories-distribution-compiled-binary-artifacts).
+
 
 ## Resource Ref Additions
 

+ 32 - 0
SPECIFICATION.md

@@ -140,6 +140,38 @@ In `provides`, only `pc` resource names may carry a constraint, only the `==` op
 
 Version-constrained dependencies are not yet forwarded to system package managers: they resolve from local presence or USM packages only.
 
+## DCBA Repositories
+
+A DCBA (Distribution Compiled Binary Artifact) repository is a second repository type alongside a USM source repository. It distributes pre-built build artifacts — one `.dcba` file per package and version — instead of sources. Its layout mirrors a source repository:
+
+- `<name>.dcbar`: the repository descriptor, the same JSON shape as `.usmr` (`name`, `summary`, `uris`/`url`, `key`, `cache_expiration`). The descriptor states nothing about the platform: one repository serves one platform/architecture, and a different architecture is a different configured repository.
+- `keys/`: the signing keys (`public-key`, `private-key`).
+- `public/`: the published artifacts and the signed listing.
+
+### The .dcba artifact format
+
+A `.dcba` file is a plain `tar.xz` archive containing only the build artifact USM normally creates per system: exactly the contents of a package cache directory's `build.tar.xz`, renamed to `<name>-<version>.dcba`. The filename must carry the package's name and a parseable version, split at the last `-` whose suffix parses as a version (so `lib-base-1.2+3.dcba` is `lib-base` version `1.2+3`).
+
+### DCBA artifact listing (ARTIFACTS.usml)
+
+The listing sits at the repository root as `ARTIFACTS.usml` — JSON lines, one entry per artifact, terminated by a signatures line, the `PACKAGES.usml` counterpart. Each entry has the following properties:
+
+- `type` (string, required): The literal `"dcba"`.
+- `name` (string, required): The name of the package the artifact was built from.
+- `version` (string, required): The version of the package the artifact was built from, exactly as the package's manifest states it. Clients match entries by a plain string comparison of `name` and `version`, so the value never mutates through a version round trip.
+- `path` (string, required): The repository-relative path of the `.dcba` file, e.g. `my-package-1.0.0.dcba`.
+- `sha512` (string, required): The base64-encoded SHA-512 of the artifact file, which clients verify every download against.
+- `size` (integer, optional): The artifact's size in bytes. Listings predating the field carry no `size`; clients must treat a missing `size` as unknown, never as zero bytes of content.
+
+The last line is the signatures line, identical in form to `PACKAGES.usml`:
+
+- `type` (string, required): The literal `"signatures"`.
+- `signatures` (array, required): One object per signing key:
+    - `key` (string, required): The base64-encoded ed25519 public key that made the signature.
+    - `signature` (string, required): The base64-encoded ed25519 signature over the SHA-512 of the concatenated entry lines — each entry's bare serialised JSON, in listing order, with no newline terminators.
+
+A listing that ends without a signatures line is an error. Unknown entry types are ignored, as in `PACKAGES.usml`.
+
 ## Installation Lifecycle Directories
 
 Each of these directories are created by USM at runtime for each package.

+ 326 - 0
scripts/dcba-e2e.sh

@@ -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