浏览代码

execs.bump end-to-end verification script and documentation

scripts/bump-e2e.sh drives scratch fixture packages through the whole bump-hook story: a version bump runs the hook as <path> <old> <new> and rewrites the manifest plus a version line in a fixture meson.build; a failing hook aborts with exit 251 leaving the tree untouched; same-version, flags-only and bare --git bumps run no hook; --git stages the hook-edited file into the release commit and tags; a data-package manifest may declare and run the hook. 28 assertions, all PASS.

Documents the hook in SPECIFICATION.md (execs property + Executable Scripts), PACKAGING_GUIDE.md (Version bumping), slopdocs/structure.usm.manifest.md and slopdocs/structure.usm.manifest.executable-scripts.md.
saddle-ClankerSaddle 12 小时之前
父节点
当前提交
73c2d99c90
共有 5 个文件被更改,包括 365 次插入 和 2 次删除
  1. 22 0
      PACKAGING_GUIDE.md
  2. 3 1
      SPECIFICATION.md
  3. 312 0
      scripts/bump-e2e.sh
  4. 25 0
      slopdocs/structure.usm.manifest.executable-scripts.md
  5. 3 1
      slopdocs/structure.usm.manifest.md

+ 22 - 0
PACKAGING_GUIDE.md

@@ -316,6 +316,28 @@ usm manifest bump patch --no-rebuild-dependants
 
 Only set `rebuildDependants` when the update includes C API changes that break downstream compilation. Don't set it for bug fixes or internal changes.
 
+### The bump hook
+
+When your package's version lives in more places than MANIFEST.usm — a `meson.build` `project(version:)` line, a version string in source code — declare a `bump` exec so `usm manifest bump` updates them for you:
+
+```json
+"execs": {
+  "build": "usm-scripts/build.sh",
+  "bump": "usm-scripts/bump.sh"
+}
+```
+
+```bash
+#!/bin/bash
+# usm-scripts/bump.sh — receives <old-version> <new-version>
+set -e
+sed -i "s/version: '$1'/version: '$2'/" meson.build
+```
+
+The hook runs from the source directory (the one holding MANIFEST.usm), invoked as `<path> <old-version> <new-version>`, and only when the version actually changes — a bump to the version the manifest already carries, a flags-only bump (`--rebuild-dependants`), or a bare `--git` release runs nothing. It runs *before* MANIFEST.usm is rewritten: a non-zero exit (or a missing script) aborts the whole bump with exit code 251 and leaves the tree untouched, so a broken hook cannot half-update your tree.
+
+With `--git`, the release commit stages the whole working tree rather than just MANIFEST.usm — what the hook edited cannot be known ahead of time — so its changes land in the `Version <x.y.z>` commit and the tag.
+
 ---
 
 ## Testing your package

+ 3 - 1
SPECIFICATION.md

@@ -72,6 +72,7 @@ The USM manifest file is contains a JSON object with the following properties:
     - `remove` (string, optional): Path to the removal script relative to the package root.
     - `postInstall` (string, optional): Path to the post-install script relative to the package root.
     - `acquire` (string, optional): Path to the acquire script relative to the package root.
+    - `bump` (string, optional): Path to the version-bump script relative to the package root.
 - `md` (string, optional): Path to a markdown file containing a detailed description of the package, relative to the package root.
 - `url` (string, optional): URL to the project's website or repository.
 - `screenshots` (string array, optional): Array of paths to screenshot files, relative to the package root.
@@ -189,4 +190,5 @@ All executable scripts except for `remove` and `postInstall` are run from the so
 - `install` when specified will install the files into the install directory, in the same structure that it would if that directory were the filesystem root. The build directory is passed to this exec as the first argument, the install directory is passed as the second argument, and the install type ("fresh", "upgrade", "downgrade") is passed as the third argument.
 - `remove` is called just before USM deletes the resources of a package on uninstallation. The remove type ("final", "upgrade", "downgrade") is passed as the first argument.
 - `postInstall` is called after USM has installed the packages resources to the system. The build directory is passed to this exec as the first argument, and the install type ("fresh", "upgrade", "downgrade") is passed as the second argument.
-- `acquire` is used to download the authoritative sources. It is usually used by package maintainers when a project does not natively have `MANIFEST.usm` support to enable putting many USM manifests into a repository, and using tooling to download and extract the sources to make `.usmc` (USM Complete) packages. This script usually consists of either a `git clone` command, or a `wget` call followed by an archive extraction command. No parameters are passed to this exec.
+- `acquire` is used to download the authoritative sources. It is usually used by package maintainers when a project does not natively have `MANIFEST.usm` support to enable putting many USM manifests into a repository, and using tooling to download and extract the sources to make `.usmc` (USM Complete) packages. This script usually consists of either a `git clone` command, or a `wget` call followed by an archive extraction command. No parameters are passed to this exec.
+- `bump` is a maintainer-side hook `usm manifest bump` runs when the version actually changes — not on flags-only bumps, bare `--git` releases, or a bump to the version the manifest already carries. It runs from the source directory (the directory holding MANIFEST.usm), invoked as `<path> <old-version> <new-version>`, before MANIFEST.usm is rewritten, and is meant for updating version numbers that live outside the manifest (source code, `meson.build`, …). A non-zero exit (or a failure to spawn) aborts the whole bump, leaving the tree untouched. With `--git`, whatever the hook edited is staged into the release commit alongside the manifest.

+ 312 - 0
scripts/bump-e2e.sh

@@ -0,0 +1,312 @@
+#!/bin/bash
+#
+# execs.bump end-to-end verification — the manifest version-bump hook.
+#
+# Builds USM, then drives scratch fixture packages through the whole
+# bump-hook story. Every fixture lives OUTSIDE the checkout: the bump
+# verb rewrites the CWD's MANIFEST.usm and creates git tags, so it must
+# never run inside the USM source tree.
+#
+# Assertions (each printed PASS/FAIL, exit non-zero on any failure):
+#   a. a version bump runs the hook as `<path> <old> <new>` (the hook
+#      records its arguments), MANIFEST.usm's version is rewritten, and
+#      the version line the hook rewrites in a fixture meson.build is
+#      updated
+#   b. a failing hook (non-zero exit) aborts the bump with exit 251,
+#      leaving MANIFEST.usm and the fixture meson.build untouched
+#   c. an explicit bump to the version the manifest already carries
+#      does NOT run the hook
+#   d. a flags-only bump (--rebuild-dependants, version unchanged) does
+#      NOT run the hook, and the flag IS written to the manifest
+#   e. a bare --git release (no version argument, no version change)
+#      does NOT run the hook, and still commits + tags
+#   f. --git with a hook that edits a file: the release commit contains
+#      both MANIFEST.usm and the hook-edited file, and tag v<version>
+#      exists
+#   g. a data-package manifest (flags: ["dataPackage"], acquire + bump
+#      execs) may declare the hook; it runs and the version bumps
+#
+# 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 — 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.
+#   * The hook is spawned through GLib, which PATH-searches a path
+#     with no slash in it: a manifest declaring "bump": "bump.sh"
+#     fails to spawn even with the file executable in the CWD. Real
+#     manifests place scripts under usm-scripts/, whose slash makes
+#     the path CWD-relative — the fixtures do the same.
+#   * usm manifest bump --git commits through plain git: without a
+#     configured identity it dies with "unable to auto-detect email
+#     address". The GIT_AUTHOR_*/GIT_COMMITTER_* exports below give
+#     both usm's internal commits and ours one.
+#
+# Usage: scripts/bump-e2e.sh [build-dir]
+#   build-dir defaults to a fresh temp directory; pass an existing meson
+#   build directory to skip the (slow) compile.
+
+set -e
+set -u
+set -o pipefail
+
+SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
+SOURCE_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd)
+
+WORK=$(mktemp -d /tmp/usm-bump-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
+}
+
+# Asserts <description> against <command...>: the command succeeding is
+# a PASS, failing is a FAIL (set -e must not abort on a failed
+# assertion — the failure is the observation, reported like any other).
+try() {
+    local description=$1
+    shift
+    if "$@" >/dev/null 2>&1; then
+        report 0 "$description"
+    else
+        report 1 "$description"
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# 0. Build
+# ---------------------------------------------------------------------------
+
+BUILD_DIR=${1:-}
+
+if [ -z "$BUILD_DIR" ]; then
+    BUILD_DIR="$WORK/usm-build"
+    printf '== Building USM into %s\n' "$BUILD_DIR"
+    PKG_CONFIG_PATH=/opt/usm/lib/pkgconfig meson setup "$BUILD_DIR" "$SOURCE_DIR/src" >/dev/null 2>&1 \
+        || { echo "meson setup failed"; exit 1; }
+    PKG_CONFIG_PATH=/opt/usm/lib/pkgconfig ninja -C "$BUILD_DIR" >/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}"
+
+# One identity for our git calls and usm's internal release commits
+export GIT_AUTHOR_NAME="USM Bump E2E"
+export GIT_AUTHOR_EMAIL="bump-e2e@example.invalid"
+export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
+export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
+
+# ---------------------------------------------------------------------------
+# 1. Fixtures
+# ---------------------------------------------------------------------------
+
+# Runs the built usm against one fixture directory; the exit code lands
+# in BUMP_EXIT (never aborts the script — a non-zero bump IS the test).
+BUMP_EXIT=0
+bump() {
+    BUMP_EXIT=0
+    (cd "$1" && "$USM_BIN" manifest bump "${@:2}") || BUMP_EXIT=$?
+}
+
+# A code-package fixture: a manifest declaring the bump hook, the hook
+# itself (records "<old> <new>", rewrites the meson.build version line),
+# a meson.build carrying the version, and the provided resource.
+make_fixture() {
+    # make_fixture <dir> <name> <version> <hook>  (hook: record | fail)
+    local dir=$1 name=$2 version=$3 hook=$4
+    rm -rf "$dir"
+    mkdir -p "$dir/usm-scripts"
+    cat > "$dir/MANIFEST.usm" <<EOF
+{
+ "name": "$name",
+ "version": "$version",
+ "summary": "execs.bump end-to-end fixture",
+ "licences": [],
+ "flags": [],
+ "provides": { "res:$name/hello.txt": "source:hello.txt" },
+ "depends": { "runtime": [], "build": [], "manage": ["bin:bash"] },
+ "execs": { "bump": "usm-scripts/bump.sh" }
+}
+EOF
+    printf 'fixture payload\n' > "$dir/hello.txt"
+    printf "project('%s', 'c', version: '%s')\n" "$name" "$version" > "$dir/meson.build"
+    if [ "$hook" = fail ]; then
+        cat > "$dir/usm-scripts/bump.sh" <<'EOF'
+#!/bin/bash
+# Records its arguments, then fails — the bump must abort.
+echo "$1 $2" > hook-args.txt
+echo "deliberate bump-hook failure" >&2
+exit 1
+EOF
+    else
+        cat > "$dir/usm-scripts/bump.sh" <<'EOF'
+#!/bin/bash
+# Records "<old> <new>", then rewrites the meson.build project version.
+echo "$1 $2" > hook-args.txt
+sed -i "s/version: '$1'/version: '$2'/" meson.build
+EOF
+    fi
+    chmod +x "$dir/usm-scripts/bump.sh"
+}
+
+# Wraps a fixture into a throwaway git repository with everything
+# committed, so --git releases have something to tag.
+git_init_fixture() {
+    (cd "$1" && git init -q && git add -A && git commit -qm "fixture base")
+}
+
+FX_A="$WORK/fixture-a"
+FX_B="$WORK/fixture-b"
+FX_C="$WORK/fixture-c"
+FX_E="$WORK/fixture-e"
+FX_F="$WORK/fixture-f"
+FX_G="$WORK/fixture-g"
+
+make_fixture "$FX_A" bump-e2e-demo 1.0.0 record
+make_fixture "$FX_B" bump-e2e-fail 1.0.0 fail
+make_fixture "$FX_C" bump-e2e-same 1.0.1 record
+make_fixture "$FX_E" bump-e2e-release 1.0.1 record
+make_fixture "$FX_F" bump-e2e-git 1.0.1 record
+git_init_fixture "$FX_E"
+git_init_fixture "$FX_F"
+
+# The data-package fixture: the DATA_MANIFEST shape (acquire + bump are
+# the maintainer-side execs a data package may declare) with a VERSION
+# file in place of meson.build for the hook to rewrite.
+mkdir -p "$FX_G/usm-scripts"
+cat > "$FX_G/MANIFEST.usm" <<'EOF'
+{
+ "name": "bump-e2e-fonts",
+ "version": "1.0.0",
+ "summary": "Example fonts",
+ "licences": [],
+ "flags": ["dataPackage"],
+ "provides": { "res:bump-e2e-fonts/Example.ttf": "source:Example.ttf" },
+ "depends": { "runtime": [], "build": [], "manage": ["bin:bash"] },
+ "execs": { "acquire": "usm-scripts/acquire.sh", "bump": "usm-scripts/bump.sh" }
+}
+EOF
+printf 'stub font payload\n' > "$FX_G/Example.ttf"
+printf "data-version: '1.0.0'\n" > "$FX_G/VERSION"
+printf '#!/bin/bash\nexit 0\n' > "$FX_G/usm-scripts/acquire.sh"
+cat > "$FX_G/usm-scripts/bump.sh" <<'EOF'
+#!/bin/bash
+echo "$1 $2" > hook-args.txt
+sed -i "s/data-version: '$1'/data-version: '$2'/" VERSION
+EOF
+chmod +x "$FX_G/usm-scripts/acquire.sh" "$FX_G/usm-scripts/bump.sh"
+
+# ---------------------------------------------------------------------------
+# 2. (a) A version bump runs the hook and rewrites every version line
+# ---------------------------------------------------------------------------
+
+printf '== (a) version bump runs the hook\n'
+bump "$FX_A" 1.0.1
+try "(a) usm manifest bump 1.0.1 exits 0" test "$BUMP_EXIT" = 0
+try '(a) hook received the arguments "1.0.0 1.0.1"' \
+    test "$(cat "$FX_A/hook-args.txt" 2>/dev/null || true)" = "1.0.0 1.0.1"
+try "(a) MANIFEST.usm version rewritten to 1.0.1" \
+    grep -Eq '"version"[[:space:]]*:[[:space:]]*"1\.0\.1"' "$FX_A/MANIFEST.usm"
+try "(a) meson.build version line rewritten to 1.0.1" \
+    grep -q "version: '1.0.1'" "$FX_A/meson.build"
+
+# ---------------------------------------------------------------------------
+# 3. (b) A failing hook aborts the bump, leaving the tree untouched
+# ---------------------------------------------------------------------------
+
+printf '== (b) failing hook aborts the bump\n'
+bump "$FX_B" 1.0.1
+try "(b) bump exits 251 when the hook fails" test "$BUMP_EXIT" = 251
+try '(b) the hook did run (its arguments were recorded)' \
+    test "$(cat "$FX_B/hook-args.txt" 2>/dev/null || true)" = "1.0.0 1.0.1"
+try "(b) MANIFEST.usm untouched (still 1.0.0)" \
+    grep -Eq '"version"[[:space:]]*:[[:space:]]*"1\.0\.0"' "$FX_B/MANIFEST.usm"
+try "(b) meson.build untouched (still 1.0.0)" \
+    grep -q "version: '1.0.0'" "$FX_B/meson.build"
+
+# ---------------------------------------------------------------------------
+# 4. (c)/(d) No version change — no hook
+# ---------------------------------------------------------------------------
+
+printf '== (c) explicit same-version bump\n'
+bump "$FX_C" 1.0.1
+try "(c) bump 1.0.1 on a 1.0.1 manifest exits 0" test "$BUMP_EXIT" = 0
+try "(c) the hook did NOT run" test ! -e "$FX_C/hook-args.txt"
+try "(c) MANIFEST.usm still carries 1.0.1" \
+    grep -Eq '"version"[[:space:]]*:[[:space:]]*"1\.0\.1"' "$FX_C/MANIFEST.usm"
+
+printf '== (d) flags-only bump\n'
+# A flags-only invocation needs the version argument to match the
+# manifest (no version and no --git is a usage error), so this bump
+# changes nothing but the flags array.
+bump "$FX_C" 1.0.1 --rebuild-dependants
+try "(d) bump 1.0.1 --rebuild-dependants exits 0" test "$BUMP_EXIT" = 0
+try "(d) the hook did NOT run" test ! -e "$FX_C/hook-args.txt"
+try "(d) the rebuildDependants flag IS in the manifest" \
+    grep -q '"rebuildDependants"' "$FX_C/MANIFEST.usm"
+
+# ---------------------------------------------------------------------------
+# 5. (e) A bare --git release does not run the hook
+# ---------------------------------------------------------------------------
+
+printf '== (e) bare --git release\n'
+bump "$FX_E" --git
+try "(e) usm manifest bump --git exits 0" test "$BUMP_EXIT" = 0
+try "(e) the hook did NOT run" test ! -e "$FX_E/hook-args.txt"
+try "(e) tag v1.0.1 exists" test -n "$(git -C "$FX_E" tag -l "v1.0.1")"
+try "(e) meson.build untouched (still 1.0.1)" \
+    grep -q "version: '1.0.1'" "$FX_E/meson.build"
+
+# ---------------------------------------------------------------------------
+# 6. (f) --git with a hook: the release commit carries the hook's edits
+# ---------------------------------------------------------------------------
+
+printf '== (f) --git release after a hook run\n'
+bump "$FX_F" patch --git
+try "(f) usm manifest bump patch --git exits 0" test "$BUMP_EXIT" = 0
+try '(f) hook received the arguments "1.0.1 1.0.2"' \
+    test "$(cat "$FX_F/hook-args.txt" 2>/dev/null || true)" = "1.0.1 1.0.2"
+try "(f) tag v1.0.2 exists" test -n "$(git -C "$FX_F" tag -l "v1.0.2")"
+try "(f) the release commit contains MANIFEST.usm" \
+    test -n "$(git -C "$FX_F" show --name-only --format= "v1.0.2" -- MANIFEST.usm)"
+try "(f) the release commit contains the hook-edited meson.build" \
+    test -n "$(git -C "$FX_F" show --name-only --format= "v1.0.2" -- meson.build)"
+try "(f) the tagged meson.build carries the new version" \
+    test -n "$(git -C "$FX_F" show "v1.0.2":meson.build | grep "version: '1.0.2'")"
+
+# ---------------------------------------------------------------------------
+# 7. (g) A data package may declare and run the bump hook
+# ---------------------------------------------------------------------------
+
+printf '== (g) data-package bump hook\n'
+bump "$FX_G" 1.0.1
+try "(g) data-package bump 1.0.1 exits 0" test "$BUMP_EXIT" = 0
+try '(g) hook received the arguments "1.0.0 1.0.1"' \
+    test "$(cat "$FX_G/hook-args.txt" 2>/dev/null || true)" = "1.0.0 1.0.1"
+try "(g) MANIFEST.usm version rewritten to 1.0.1" \
+    grep -Eq '"version"[[:space:]]*:[[:space:]]*"1\.0\.1"' "$FX_G/MANIFEST.usm"
+try "(g) the hook's VERSION file rewritten to 1.0.1" \
+    grep -q "data-version: '1.0.1'" "$FX_G/VERSION"
+
+# ---------------------------------------------------------------------------
+
+printf '\n%d passed, %d failed\n' "$PASS_COUNT" "$FAIL_COUNT"
+if [ "$FAIL_COUNT" -gt 0 ]; then
+    exit 1
+fi
+exit 0

+ 25 - 0
slopdocs/structure.usm.manifest.executable-scripts.md

@@ -199,6 +199,31 @@ cd ${build_dir}
 echo "All tests passed"
 ```
 
+### bump
+Optional maintainer-side hook that `usm manifest bump` runs when the version actually changes.
+
+**Arguments**: `[old-version] [new-version]`
+- `old-version`: The manifest's version before the bump
+- `new-version`: The version being bumped to
+
+**Working Directory**: Source directory (the directory holding MANIFEST.usm)
+
+**Runs**: Only when the version actually changes — not on a bump to the version the manifest already carries, a flags-only bump (`--rebuild-dependants`), or a bare `--git` release. It runs before MANIFEST.usm is rewritten; a non-zero exit (or a failure to spawn) aborts the whole bump with exit code 251, leaving the tree untouched. With `--git`, whatever the hook edited is staged into the release commit (the whole working tree) alongside the manifest.
+
+**Use Case**: Updating version numbers that live outside the manifest — a `meson.build` `project(version:)` line, a version string in source code — so the tree stays in concordance with the manifest.
+
+**Example**:
+```bash
+#!/bin/bash
+# usm-scripts/bump.sh
+set -e
+
+old_version=$1
+new_version=$2
+
+sed -i "s/version: '${old_version}'/version: '${new_version}'/" meson.build
+```
+
 ## Script Requirements
 
 ### Permissions

+ 3 - 1
slopdocs/structure.usm.manifest.md

@@ -84,13 +84,15 @@ Executable scripts for different package lifecycle phases.
 - `postInstall` (string, optional): Post-install script path
 - `acquire` (string, optional): Source acquisition script path
 - `test` (string, optional): Test script path
+- `bump` (string, optional): Version-bump hook script path
 
 ```json
 "execs": {
   "build": "usm-scripts/build.sh",
   "install": "usm-scripts/install.sh",
   "acquire": "usm-scripts/acquire.sh",
-  "test": "usm-scripts/test.sh"
+  "test": "usm-scripts/test.sh",
+  "bump": "usm-scripts/bump.sh"
 }
 ```