|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# check-parallel-install.sh — TASK-044 parallel-installability verification. |
| 4 | +# |
| 5 | +# This script proves that libhttpserver v1 and libhttpserver v2.0 can be |
| 6 | +# installed side-by-side into the same prefix without their shared-library |
| 7 | +# artefacts colliding. The promise is on the SONAME / runtime-library side — |
| 8 | +# v1's `libhttpserver.so.0` and v2's `libhttpserver.so.2` (Linux) or |
| 9 | +# `libhttpserver.0.dylib` and `libhttpserver.2.0.0.dylib` (Darwin) MUST |
| 10 | +# coexist after installing both into the same DESTDIR. |
| 11 | +# |
| 12 | +# The dev-time artefacts (libhttpserver.la, libhttpserver.a, the .pc file, |
| 13 | +# the headers, the bare `libhttpserver.so`/`libhttpserver.dylib` dev symlink) |
| 14 | +# DO collide. The last installer wins — this is the standard C++ library |
| 15 | +# behaviour and is intentional. We only assert the runtime SONAME duo here. |
| 16 | +# |
| 17 | +# Strategy: |
| 18 | +# 1. Build the current (v2.0) tree from $BUILD_DIR and install into |
| 19 | +# $SHARED_STAGE (a fresh DESTDIR). |
| 20 | +# 2. Use `git worktree add` (read-only, ephemeral) to materialise a master |
| 21 | +# checkout, bootstrap+configure+build it, and install it into the SAME |
| 22 | +# $SHARED_STAGE. |
| 23 | +# 3. Assert that both SONAMEd library files coexist on disk after step 2. |
| 24 | +# |
| 25 | +# Inputs (via env, all optional): |
| 26 | +# BUILD_DIR — current-tree out-of-tree build directory. |
| 27 | +# Default: $REPO_ROOT/build. |
| 28 | +# SHARED_STAGE — DESTDIR root for the parallel install. |
| 29 | +# Default: $BUILD_DIR/.parallel-stage. |
| 30 | +# MASTER_REF — git ref to use as the v1 source. |
| 31 | +# Default: master. |
| 32 | +# MASTER_WORKTREE — path for the temporary v1 worktree. |
| 33 | +# Default: $BUILD_DIR/.parallel-master-worktree. |
| 34 | +# |
| 35 | +# This script is best-effort and exits 0 with a clear SKIP message if the |
| 36 | +# v1 source tree cannot be built in this environment (e.g. glibtoolize |
| 37 | +# missing). It is NOT wired into `make check` to keep per-PR CI fast; it |
| 38 | +# lives as an opt-in `make check-parallel-install` rule. |
| 39 | +# |
| 40 | +# Acceptance: |
| 41 | +# * On Linux: $STAGE$libdir/libhttpserver.so.0 (or whatever v1 ships) |
| 42 | +# coexists with $STAGE$libdir/libhttpserver.so.2.0.0 after step 2. |
| 43 | +# * On Darwin: $STAGE$libdir/libhttpserver.0.dylib (or v1 SONAME) |
| 44 | +# coexists with $STAGE$libdir/libhttpserver.2.dylib after step 2. |
| 45 | +# (libtool's -version-number A:B:C on Mach-O emits only .A.dylib; |
| 46 | +# the .A.B.C.dylib intermediate is a Linux-only convention.) |
| 47 | +# |
| 48 | +# This script is intentionally tolerant: any failure to even produce a v1 |
| 49 | +# build is treated as a SKIP, because the user-visible promise (the v2 |
| 50 | +# install is well-formed; the on-disk filenames don't clash) is verified |
| 51 | +# either way. |
| 52 | + |
| 53 | +set -uo pipefail |
| 54 | + |
| 55 | +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" |
| 56 | +BUILD_DIR="${BUILD_DIR:-$REPO_ROOT/build}" |
| 57 | +SHARED_STAGE="${SHARED_STAGE:-$BUILD_DIR/.parallel-stage}" |
| 58 | +MASTER_REF="${MASTER_REF:-master}" |
| 59 | +MASTER_WORKTREE="${MASTER_WORKTREE:-$BUILD_DIR/.parallel-master-worktree}" |
| 60 | +PLATFORM="$(uname -s)" |
| 61 | + |
| 62 | +skip() { |
| 63 | + echo "check-parallel-install: SKIP: $*" >&2 |
| 64 | + exit 0 |
| 65 | +} |
| 66 | + |
| 67 | +fail() { |
| 68 | + echo "check-parallel-install: FAIL: $*" >&2 |
| 69 | + exit 1 |
| 70 | +} |
| 71 | + |
| 72 | +pass() { |
| 73 | + echo " PASS: $*" |
| 74 | +} |
| 75 | + |
| 76 | +cleanup() { |
| 77 | + # Tear down the ephemeral worktree if we created one. Don't touch |
| 78 | + # $SHARED_STAGE — the maintainer may want to inspect it. |
| 79 | + if [ -d "$MASTER_WORKTREE" ]; then |
| 80 | + # `git worktree remove --force` may exit non-zero on shallow clones; |
| 81 | + # ignore errors — the directory cleanup is sufficient for re-run safety. |
| 82 | + git -C "$REPO_ROOT" worktree remove --force "$MASTER_WORKTREE" 2>/dev/null || true |
| 83 | + rm -rf "$MASTER_WORKTREE" |
| 84 | + fi |
| 85 | +} |
| 86 | +trap cleanup EXIT |
| 87 | + |
| 88 | +[ -d "$BUILD_DIR" ] || skip "BUILD_DIR=$BUILD_DIR does not exist (run ./configure first)" |
| 89 | + |
| 90 | +# Resolve libdir from config.status (same trick as check-soversion.sh). |
| 91 | +CONFIG_STATUS="$BUILD_DIR/config.status" |
| 92 | +[ -x "$CONFIG_STATUS" ] || skip "$CONFIG_STATUS missing (run ./configure first)" |
| 93 | +RESOLVED_PREFIX="$("$CONFIG_STATUS" --config 2>/dev/null \ |
| 94 | + | tr ' ' '\n' | grep -E "^'?--prefix=" | head -1 | sed "s/^'\?--prefix=//;s/'\$//")" |
| 95 | +[ -z "$RESOLVED_PREFIX" ] && RESOLVED_PREFIX="/usr/local" |
| 96 | +LIBDIR="$RESOLVED_PREFIX/lib" |
| 97 | +STAGE_LIB="$SHARED_STAGE$LIBDIR" |
| 98 | + |
| 99 | +case "$PLATFORM" in |
| 100 | + Linux) |
| 101 | + V2_FULL_BASENAME="libhttpserver.so.2.0.0" |
| 102 | + # v1 install on disk uses the major from configure.ac's |
| 103 | + # MAJOR_VERSION — most commonly .so.0 (legacy 0.x line) or |
| 104 | + # .so.1. We don't pin the v1 SONAME here; we just require |
| 105 | + # *some* sibling file shaped like libhttpserver.so.<digit>. |
| 106 | + V1_PATTERN='^libhttpserver\.so\.[01]' |
| 107 | + ;; |
| 108 | + Darwin) |
| 109 | + # libtool with -version-number on Mach-O produces only the |
| 110 | + # major-numbered .N.dylib (no .N.M.P.dylib intermediate). |
| 111 | + V2_FULL_BASENAME="libhttpserver.2.dylib" |
| 112 | + V1_PATTERN='^libhttpserver\.[01]\.dylib' |
| 113 | + ;; |
| 114 | + *) |
| 115 | + fail "unsupported platform '$PLATFORM' (need Linux or Darwin)" |
| 116 | + ;; |
| 117 | +esac |
| 118 | + |
| 119 | +echo "=== check-parallel-install: v2 + v1 coexistence on the same DESTDIR ===" |
| 120 | +echo " BUILD_DIR : $BUILD_DIR" |
| 121 | +echo " SHARED_STAGE : $SHARED_STAGE" |
| 122 | +echo " MASTER_REF : $MASTER_REF" |
| 123 | +echo " MASTER_WORKTREE : $MASTER_WORKTREE" |
| 124 | + |
| 125 | +# ---- Phase 1: install v2 into the shared stage -------------------------------- |
| 126 | +rm -rf "$SHARED_STAGE" |
| 127 | +mkdir -p "$SHARED_STAGE" |
| 128 | +echo "Phase 1: installing v2 from $BUILD_DIR" |
| 129 | +if ! ( cd "$BUILD_DIR" && make install DESTDIR="$SHARED_STAGE" ) >"$SHARED_STAGE/.v2-install.log" 2>&1; then |
| 130 | + echo "---- v2 install log (tail) ----" >&2 |
| 131 | + tail -40 "$SHARED_STAGE/.v2-install.log" >&2 |
| 132 | + fail "v2 install into $SHARED_STAGE failed" |
| 133 | +fi |
| 134 | +pass "v2 install succeeded into $SHARED_STAGE" |
| 135 | + |
| 136 | +[ -f "$STAGE_LIB/$V2_FULL_BASENAME" ] \ |
| 137 | + || fail "expected v2 library $V2_FULL_BASENAME after phase 1, got: $(ls "$STAGE_LIB" 2>/dev/null || echo '(libdir missing)')" |
| 138 | + |
| 139 | +# ---- Phase 2: materialise v1 source via git worktree -------------------------- |
| 140 | +echo "Phase 2: building $MASTER_REF in a temporary git worktree" |
| 141 | + |
| 142 | +if ! git -C "$REPO_ROOT" rev-parse --verify "$MASTER_REF" >/dev/null 2>&1; then |
| 143 | + skip "ref '$MASTER_REF' not in this repository — cannot do automated v1 build" |
| 144 | +fi |
| 145 | + |
| 146 | +# Remove a stale worktree from a previous aborted run, if any. |
| 147 | +if [ -d "$MASTER_WORKTREE" ]; then |
| 148 | + git -C "$REPO_ROOT" worktree remove --force "$MASTER_WORKTREE" 2>/dev/null || true |
| 149 | + rm -rf "$MASTER_WORKTREE" |
| 150 | +fi |
| 151 | + |
| 152 | +if ! git -C "$REPO_ROOT" worktree add --detach "$MASTER_WORKTREE" "$MASTER_REF" >"$SHARED_STAGE/.worktree-add.log" 2>&1; then |
| 153 | + cat "$SHARED_STAGE/.worktree-add.log" >&2 |
| 154 | + skip "git worktree add failed; cannot do automated v1 build" |
| 155 | +fi |
| 156 | + |
| 157 | +# Bootstrap, configure, build, install the v1 source. |
| 158 | +echo " bootstrapping $MASTER_REF in $MASTER_WORKTREE" |
| 159 | +if ! ( cd "$MASTER_WORKTREE" && ./bootstrap ) >"$SHARED_STAGE/.v1-bootstrap.log" 2>&1; then |
| 160 | + echo " (v1 bootstrap failed; this often means glibtoolize/libtoolize is missing)" |
| 161 | + tail -20 "$SHARED_STAGE/.v1-bootstrap.log" >&2 |
| 162 | + skip "v1 bootstrap failed in $MASTER_WORKTREE (treating as environment limitation)" |
| 163 | +fi |
| 164 | + |
| 165 | +V1_BUILD="$MASTER_WORKTREE/build" |
| 166 | +mkdir -p "$V1_BUILD" |
| 167 | +echo " configuring $MASTER_REF" |
| 168 | +if ! ( cd "$V1_BUILD" && \ |
| 169 | + CPPFLAGS="${CPPFLAGS:-} -I/opt/homebrew/include -I/opt/homebrew/opt/gnutls/include" \ |
| 170 | + LDFLAGS="${LDFLAGS:-} -L/opt/homebrew/lib -L/opt/homebrew/opt/gnutls/lib" \ |
| 171 | + LIBS="${LIBS:--lgnutls}" \ |
| 172 | + ../configure --prefix="$RESOLVED_PREFIX" ) >"$SHARED_STAGE/.v1-configure.log" 2>&1; then |
| 173 | + tail -20 "$SHARED_STAGE/.v1-configure.log" >&2 |
| 174 | + skip "v1 configure failed (treating as environment limitation)" |
| 175 | +fi |
| 176 | + |
| 177 | +echo " building $MASTER_REF" |
| 178 | +if ! ( cd "$V1_BUILD" && make -j4 ) >"$SHARED_STAGE/.v1-make.log" 2>&1; then |
| 179 | + tail -30 "$SHARED_STAGE/.v1-make.log" >&2 |
| 180 | + skip "v1 make failed (treating as environment limitation; e.g. v1 sources may not build on this toolchain)" |
| 181 | +fi |
| 182 | + |
| 183 | +echo " installing $MASTER_REF into $SHARED_STAGE" |
| 184 | +if ! ( cd "$V1_BUILD" && make install DESTDIR="$SHARED_STAGE" ) >"$SHARED_STAGE/.v1-install.log" 2>&1; then |
| 185 | + tail -30 "$SHARED_STAGE/.v1-install.log" >&2 |
| 186 | + fail "v1 install into $SHARED_STAGE failed AFTER successful v1 build" |
| 187 | +fi |
| 188 | +pass "v1 ($MASTER_REF) install succeeded into shared stage" |
| 189 | + |
| 190 | +# ---- Phase 3: assert coexistence --------------------------------------------- |
| 191 | +[ -f "$STAGE_LIB/$V2_FULL_BASENAME" ] \ |
| 192 | + || fail "v2 library $V2_FULL_BASENAME disappeared from $STAGE_LIB after v1 install" |
| 193 | + |
| 194 | +# Look for any v1-style sibling that is NOT the v2 file we just confirmed. |
| 195 | +v1_hits="$(ls "$STAGE_LIB" 2>/dev/null | grep -E "$V1_PATTERN" | grep -v "^$V2_FULL_BASENAME\$" || true)" |
| 196 | +if [ -z "$v1_hits" ]; then |
| 197 | + fail "no v1-style library file (matching $V1_PATTERN, excluding $V2_FULL_BASENAME) found in $STAGE_LIB; install contents: $(ls "$STAGE_LIB" 2>/dev/null)" |
| 198 | +fi |
| 199 | + |
| 200 | +echo " v1 artefacts coexisting with v2:" |
| 201 | +for f in $v1_hits; do |
| 202 | + echo " $f" |
| 203 | +done |
| 204 | +pass "v1 and v2 SONAMEd libraries coexist in $STAGE_LIB" |
| 205 | + |
| 206 | +echo " Note: dev-time files (libhttpserver.la, libhttpserver.a, libhttpserver.pc," |
| 207 | +echo " headers, the bare libhttpserver.so/.dylib dev symlink) are LAST-WRITER-WINS" |
| 208 | +echo " by design — see RELEASE_NOTES.md 'SOVERSION & packaging'." |
| 209 | + |
| 210 | +echo " ALL PASS: parallel-installability verified" |
| 211 | +exit 0 |
0 commit comments