mirror of
https://github.com/logos-co/eth-lez-atomic-swaps.git
synced 2026-08-27 18:01:10 +00:00
The logos.dev fleet migrated Waku cluster 2 -> 3 (2026-08-07/08). Source
verdicts (both traced end-to-end, not README-trusted):
* delivery_module v0.1.1 (bundled logos-delivery 509c8755): the flat
clusterId IS parsed into WakuNodeConf, but applyNetworkConf
unconditionally overwrites it with the preset's cluster 2 at build time
(waku/factory/conf_builder/waku_conf_builder.nim:313-324, warn "Cluster
id was provided alongside a network conf" used=2 discarded=3). The node
subscribes /waku/2/rs/2/<shard>, meshes with 0 fleet peers, 0 offers.
Unsteerable: dropping the preset forfeits the fleet entry nodes.
* delivery_module v0.2.0 (bundled logos-delivery f8b03659): merge order
flipped — explicit fields win, presets fill gaps
(checkSetPresetValueToField, logos_delivery/waku/factory/conf_builder/
waku_conf_builder.nim:355-389). Our existing flat emission
{logLevel,mode,preset,clusterId,portsShift} parses via parseFlatConf
(logos_delivery/api/conf/logos_delivery_conf_json.nim:58-100,148-155)
and lands cluster 3 with the preset's entry nodes and 8 autoshards:
/waku/2/rs/3/0..7. messagingOverrides is NOT the vehicle: it is the
reliability-layer conf (clusterId there is rejected) and the layered
parser rejects mixing it with bare keys like portsShift.
Changes:
- scaffold.toml: pin delivery_module v0.1.1 -> v0.2.0 (harness + CI smoke
read this same pin).
- basecamp-dev.sh: key the delivery .lgx cache by flake ref so the pin
bump invalidates the stale v0.1.1 cache; drop the
logos-delivery/nixpkgs 403 workaround — v0.2.0's logos-delivery locks
nixpkgs 535f3e69 (2026-06-03), past NixOS/nixpkgs#512735 (2026-04-26),
and an untouched lock stays Attic-cache eligible.
- run-basecamp-ui-smoke.sh: same workaround drop for the scaffold-pin
delivery build (swap/swap_ui overrides stay — swap-module/flake.nix
still pins the v0.1.1 delivery input for codegen).
- swap_delivery_adapter.cpp: adapter emission UNCHANGED (it is already
the v0.2.0-correct shape); comments now carry the dual-version source
citations, and both degraded hints state the delivery_module >= 0.2.0
floor.
- test_swap.cpp: assertions unchanged; comment cites the v0.2.0 merge
semantics.
- docs/local-dev-harness.md: >= 0.2.0 requirement + the /waku/2/rs/N log
grep to tell a cluster-2 (dead) node from a cluster-3 (healthy) one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
523 lines
25 KiB
Bash
Executable File
523 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# basecamp-dev.sh — LOCAL working-tree module harness for real Basecamp on macOS.
|
|
#
|
|
# WHAT THIS IS
|
|
# ------------
|
|
# The fastest inner loop for "did my C++/QML change actually load in Basecamp?":
|
|
# it builds swap (core) + swap_ui (ui plugin) from the CURRENT WORKING TREE,
|
|
# installs them plus the pinned delivery_module dependency into an isolated dev
|
|
# profile using scaffold's own pinned `lgpm`, stamps a visibly distinct
|
|
# `-dev.<sha>` version into each staged manifest, and launches Basecamp against
|
|
# that profile. ~3-6 min warm, no CI, no catalogue, no module-manager service,
|
|
# no release.
|
|
#
|
|
# RELATIONSHIP TO SCAFFOLD (`lgs`)
|
|
# --------------------------------
|
|
# This is a THIN composition over what scaffold already provides, filling the
|
|
# two gaps scaffold's `lgs basecamp` verbs structurally cannot cover for a
|
|
# working-tree dev loop:
|
|
#
|
|
# REUSED FROM SCAFFOLD:
|
|
# * scaffold.toml is the single source of truth for the delivery_module,
|
|
# lgpm, and basecamp pins — this script reads them, hardcodes nothing.
|
|
# * The pinned `lgpm cli-portable` (scaffold's install tool) performs the
|
|
# install, so staged manifests round-trip `view` + `hashes` exactly like
|
|
# `make basecamp-ui-smoke` and `lgs basecamp install`.
|
|
# * The pinned basecamp bundle (scaffold's basecamp pin) is the preferred
|
|
# launch target when it is already in the Nix store.
|
|
# * The Makefile's layout-drift guard pattern is reused before launch.
|
|
#
|
|
# GAPS THIS HARNESS FILLS (candidate scaffold feature requests):
|
|
# 1. WORKING-TREE BUILDS. `lgs basecamp build/install` build the *captured*
|
|
# (committed) source set — `git+file:.?dir=…` refs in scaffold.toml. A
|
|
# dev loop needs the LIVE working tree, so this script builds the path
|
|
# flake refs `./swap-module#lgx-portable` / `./swap-ui#lgx-portable`.
|
|
# 2. DEV-VERSION STAMP. Nothing in `lgs` stamps an installed module so the
|
|
# corner badge proves which build is live. This script does.
|
|
# 3. NO-SCRUB LAUNCH. `lgs basecamp launch` scrubs the profile and REPLAYS
|
|
# the captured module set on every invocation — which would wipe the
|
|
# stamp. This script launches Basecamp directly against the already
|
|
# staged, stamped profile.
|
|
#
|
|
# WHY THE `-dev.<sha>` STAMP MATTERS
|
|
# ----------------------------------
|
|
# Three times the owner's Basecamp kept LOADING a stale module after a catalogue
|
|
# update, with no way to tell from the GUI which build was live. The stamped
|
|
# version is the fix: the corner version badge reads the exact staged version,
|
|
# so "which build am I running?" is answered by looking, not guessing.
|
|
#
|
|
# See docs/local-dev-harness.md for when to use this vs the canary channel vs
|
|
# the official catalogue, and for what this harness does NOT exercise.
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/basecamp-dev.sh [options]
|
|
|
|
Options:
|
|
--skip-build Reuse the last-built swap / swap_ui LGX artifacts (no nix build).
|
|
--no-launch Stage + verify, print the exact launch command, and stop.
|
|
--pinned-basecamp Force building/using scaffold's pinned basecamp bundle
|
|
(reproducible; may trigger a long first-time build on macOS).
|
|
--installed-app Force using the installed /Applications Basecamp .app.
|
|
-h, --help Show this help.
|
|
|
|
Environment overrides:
|
|
DEV_ROOT Dev root / isolated user-dir (default: ~/.eth-lez-dev).
|
|
BASECAMP_APP Path to a Basecamp .app to launch (skips app resolution).
|
|
DELIVERY_MODULE_LGX Path to a delivery_module .lgx to stage instead of building it.
|
|
|
|
Two-command owner workflow:
|
|
make basecamp-dev # build working tree + stage + launch
|
|
# ...edit swap-module/ or swap-ui/, then:
|
|
make basecamp-dev # again — badge shows the new -dev.<sha>
|
|
EOF
|
|
}
|
|
|
|
SKIP_BUILD=0
|
|
NO_LAUNCH=0
|
|
FORCE_PINNED=0
|
|
FORCE_INSTALLED=0
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--skip-build) SKIP_BUILD=1 ;;
|
|
--no-launch) NO_LAUNCH=1 ;;
|
|
--pinned-basecamp) FORCE_PINNED=1 ;;
|
|
--installed-app) FORCE_INSTALLED=1 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Repo + tooling.
|
|
# ---------------------------------------------------------------------------
|
|
repo_root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
|
|
cd "$repo_root"
|
|
|
|
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
echo "basecamp-dev.sh targets macOS (found $(uname -s)). On Linux use 'make basecamp-ui-smoke'." >&2
|
|
exit 1
|
|
fi
|
|
for command_name in nix git tar file python3; do
|
|
command -v "$command_name" >/dev/null 2>&1 || { echo "missing required command: $command_name" >&2; exit 1; }
|
|
done
|
|
|
|
DEV_ROOT=${DEV_ROOT:-"$HOME/.eth-lez-dev"}
|
|
# Basecamp derives its whole data tree — modules/, plugins/, module_data/, logs/
|
|
# — from LOGOS_USER_DIR / --user-dir (app/utils/LogosBasecampPaths.h
|
|
# baseDirectory; docs/scaffold-upstream-tracker.md TR-21). So the staged modules
|
|
# MUST live under the user-dir: user-dir == the isolated dev profile. This
|
|
# mirrors the CI smoke's throwaway user-dir and scaffold's per-profile layout.
|
|
user_dir="$DEV_ROOT/profile"
|
|
modules_dir="$user_dir/modules"
|
|
plugins_dir="$user_dir/plugins"
|
|
cache_dir="$DEV_ROOT/cache"
|
|
mkdir -p "$cache_dir"
|
|
|
|
# scaffold.toml is the single source of truth for every pin.
|
|
toml_get() {
|
|
python3 - "$repo_root/scaffold.toml" "$1" "$2" <<'PY'
|
|
import re, sys
|
|
text = open(sys.argv[1]).read()
|
|
section, key = sys.argv[2], sys.argv[3]
|
|
m = re.search(r'\[' + re.escape(section) + r'\](.*?)(?:\n\[|\Z)', text, re.S)
|
|
if not m:
|
|
sys.exit(0)
|
|
km = re.search(r'(?m)^\s*' + re.escape(key) + r'\s*=\s*"([^"]+)"', m.group(1))
|
|
print(km.group(1) if km else "")
|
|
PY
|
|
}
|
|
lgpm_source=$(toml_get repos.lgpm source)
|
|
lgpm_pin=$(toml_get repos.lgpm pin)
|
|
basecamp_source=$(toml_get repos.basecamp source)
|
|
basecamp_pin=$(toml_get repos.basecamp pin)
|
|
# [repos.basecamp].attr is an inline per-system table
|
|
# ({ aarch64-darwin = "bin-macos-app", ... }); pull the arm64-darwin entry.
|
|
basecamp_attr=$(python3 - "$repo_root/scaffold.toml" <<'PY'
|
|
import re, sys
|
|
t = open(sys.argv[1]).read()
|
|
m = re.search(r'\[repos\.basecamp\](.*?)(?:\n\[|\Z)', t, re.S)
|
|
a = re.search(r'aarch64-darwin\s*=\s*"([^"]+)"', m.group(1)) if m else None
|
|
print(a.group(1) if a else "")
|
|
PY
|
|
)
|
|
basecamp_attr=${basecamp_attr:-bin-macos-app}
|
|
delivery_flake=$(toml_get modules.delivery_module flake)
|
|
[[ -n "$lgpm_pin" && -n "$basecamp_pin" ]] || { echo "could not read lgpm/basecamp pins from scaffold.toml" >&2; exit 1; }
|
|
basecamp_ref="github:logos-co/logos-basecamp/$basecamp_pin#$basecamp_attr"
|
|
lgpm_ref="$lgpm_source/$lgpm_pin#cli-portable"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dev version suffix: proves, at a glance, which build is live.
|
|
# ---------------------------------------------------------------------------
|
|
short_sha=$(git rev-parse --short HEAD)
|
|
dirty=""
|
|
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
dirty=".dirty"
|
|
fi
|
|
dev_suffix="dev.${short_sha}${dirty}"
|
|
|
|
echo "==> eth-lez-atomic-swaps local Basecamp dev harness"
|
|
echo " repo root : $repo_root"
|
|
echo " dev root : $DEV_ROOT"
|
|
echo " user-dir : $user_dir"
|
|
echo " dev suffix : -$dev_suffix"
|
|
echo " scaffold pins: lgpm@${lgpm_pin:0:12} basecamp@${basecamp_pin:0:12} ($basecamp_attr)"
|
|
echo
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# nix build helpers.
|
|
# ---------------------------------------------------------------------------
|
|
build_out() {
|
|
# Realize a flake ref; print its single out-path on stdout, progress on stderr.
|
|
# On a TTY the full build log streams live (-L): a cold Rust compile can sit
|
|
# silent for minutes otherwise and read as "stuck". Non-TTY (CI, pipes)
|
|
# keeps the quiet capture-and-dump-on-failure behavior.
|
|
local label=$1 ref=$2; shift 2
|
|
echo "==> building $label ($ref)" >&2
|
|
local start out tmp="/tmp/basecamp-dev.$$.$RANDOM"
|
|
start=$(date +%s)
|
|
if [[ -t 2 ]]; then
|
|
if ! out=$(nix build "$ref" --no-link --print-out-paths -L "$@"); then
|
|
echo "$label build failed (log above)" >&2; return 1
|
|
fi
|
|
out=$(printf '%s\n' "$out" | sed '/^$/d')
|
|
else
|
|
if ! nix build "$ref" --no-link --print-out-paths "$@" >"$tmp" 2>"$tmp.err"; then
|
|
echo "$label build failed:" >&2; cat "$tmp.err" >&2; rm -f "$tmp" "$tmp.err"; return 1
|
|
fi
|
|
out=$(sed '/^$/d' "$tmp"); rm -f "$tmp" "$tmp.err"
|
|
fi
|
|
echo " $label built in $(( $(date +%s) - start ))s -> $out" >&2
|
|
printf '%s\n' "$out"
|
|
}
|
|
find_single_lgx() {
|
|
local label=$1 dir=$2 matches=()
|
|
while IFS= read -r c; do matches+=("$c"); done < <(find "$dir" -maxdepth 1 -type f -name '*.lgx' -print)
|
|
[[ ${#matches[@]} -eq 1 ]] || { echo "$label: expected one .lgx in $dir, found ${#matches[@]}" >&2; return 1; }
|
|
printf '%s\n' "${matches[0]}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Pinned lgpm (scaffold's install tool) — cached across runs.
|
|
# ---------------------------------------------------------------------------
|
|
lgpm_out=$(build_out "lgpm (scaffold pin)" "$lgpm_ref")
|
|
lgpm_bin="$lgpm_out/bin/lgpm"
|
|
[[ -x "$lgpm_bin" ]] || { echo "pinned lgpm binary missing: $lgpm_bin" >&2; exit 1; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Working-tree module builds (GAP #1: path refs = live working tree).
|
|
# ---------------------------------------------------------------------------
|
|
swap_path_file="$cache_dir/last-swap.out"
|
|
swap_ui_path_file="$cache_dir/last-swap_ui.out"
|
|
if [[ "$SKIP_BUILD" == 1 ]]; then
|
|
echo "==> --skip-build: reusing last-built artifacts"
|
|
[[ -s "$swap_path_file" && -s "$swap_ui_path_file" ]] || { echo "no cached build; run once without --skip-build" >&2; exit 1; }
|
|
swap_out=$(cat "$swap_path_file"); swap_ui_out=$(cat "$swap_ui_path_file")
|
|
[[ -d "$swap_out" && -d "$swap_ui_out" ]] || { echo "cached out-paths gone from store; rebuild without --skip-build" >&2; exit 1; }
|
|
else
|
|
build_start=$(date +%s)
|
|
swap_out=$(build_out swap './swap-module#lgx-portable')
|
|
swap_ui_out=$(build_out swap_ui './swap-ui#lgx-portable')
|
|
echo "$swap_out" > "$swap_path_file"; echo "$swap_ui_out" > "$swap_ui_path_file"
|
|
echo "==> working-tree module builds finished in $(( $(date +%s) - build_start ))s"
|
|
fi
|
|
swap_lgx=$(find_single_lgx swap "$swap_out")
|
|
swap_ui_lgx=$(find_single_lgx swap_ui "$swap_ui_out")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. delivery_module dependency: override | cache | build the scaffold pin.
|
|
# The cache file is keyed by the scaffold.toml flake ref, so a pin bump
|
|
# (e.g. v0.1.1 -> v0.2.0) invalidates it instead of silently reusing a
|
|
# stale .lgx of the previous release.
|
|
# ---------------------------------------------------------------------------
|
|
delivery_cache_key=$(printf '%s' "$delivery_flake" | shasum -a 256 | cut -c1-12)
|
|
delivery_cache="$cache_dir/delivery_module-${delivery_cache_key}.lgx"
|
|
if [[ -n "${DELIVERY_MODULE_LGX:-}" ]]; then
|
|
[[ -f "$DELIVERY_MODULE_LGX" ]] || { echo "DELIVERY_MODULE_LGX not found: $DELIVERY_MODULE_LGX" >&2; exit 1; }
|
|
echo "==> delivery_module: using override $DELIVERY_MODULE_LGX"; delivery_lgx="$DELIVERY_MODULE_LGX"
|
|
elif [[ -f "$delivery_cache" ]]; then
|
|
echo "==> delivery_module: reusing cached $delivery_cache"; delivery_lgx="$delivery_cache"
|
|
else
|
|
[[ "$delivery_flake" == *"#lgx" ]] || { echo "bad [modules.delivery_module].flake in scaffold.toml: $delivery_flake" >&2; exit 1; }
|
|
# No nixpkgs override needed since the v0.2.0 pin: its logos-delivery
|
|
# (f8b03659) locks nixpkgs 535f3e69 (2026-06-03), which postdates the
|
|
# crates.io User-Agent fix (NixOS/nixpkgs#512735, 2026-04-26) that the
|
|
# v0.1.1-era `--override-input logos-delivery/nixpkgs` workaround existed
|
|
# for. Building the flake untouched also keeps its lock eligible for the
|
|
# Logos Attic binary cache instead of forcing a cold rebuild.
|
|
delivery_out=$(build_out delivery_module "${delivery_flake%#lgx}#lgx-portable")
|
|
cp "$(find_single_lgx delivery_module "$delivery_out")" "$delivery_cache"
|
|
echo " cached delivery_module -> $delivery_cache"; delivery_lgx="$delivery_cache"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Install via pinned lgpm (REUSED: same install path as the CI smoke and
|
|
# `lgs basecamp install` — auto-classifies core -> modules/, ui_qml ->
|
|
# plugins/, and round-trips view + hashes into each manifest).
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "==> installing into $user_dir via pinned lgpm"
|
|
rm -rf "$modules_dir" "$plugins_dir"
|
|
mkdir -p "$modules_dir" "$plugins_dir"
|
|
for lgx in "$delivery_lgx" "$swap_lgx" "$swap_ui_lgx"; do
|
|
"$lgpm_bin" --modules-dir "$modules_dir" --ui-plugins-dir "$plugins_dir" install --file "$lgx"
|
|
done
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Dev-version stamp (GAP #2). delivery_module is the pinned dependency, not a
|
|
# working-tree build, so it keeps its released version.
|
|
# ---------------------------------------------------------------------------
|
|
echo "==> stamping dev version into working-tree modules"
|
|
stamp_manifest() {
|
|
python3 - "$1" "$dev_suffix" <<'PY'
|
|
import json, sys
|
|
path, suffix = sys.argv[1], sys.argv[2]
|
|
d = json.load(open(path))
|
|
base = d.get("version", "0.0.0").split("-dev.", 1)[0] # idempotent re-stamp
|
|
d["version"] = f"{base}-{suffix}"
|
|
json.dump(d, open(path, "w"), indent=2, ensure_ascii=False); open(path, "a").write("\n")
|
|
print(f" {d['name']}: version -> {d['version']}")
|
|
PY
|
|
}
|
|
stamp_manifest "$modules_dir/swap/manifest.json"
|
|
stamp_manifest "$plugins_dir/swap_ui/manifest.json"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. Self-verification (file-level; GUI stays with the human).
|
|
# ---------------------------------------------------------------------------
|
|
echo
|
|
echo "==> verifying staged dev root"
|
|
fail=0
|
|
note() { echo " [ ok ] $1"; }
|
|
bad() { echo " [FAIL] $1" >&2; fail=1; }
|
|
for path in "$modules_dir/delivery_module/manifest.json" "$modules_dir/swap/manifest.json" "$plugins_dir/swap_ui/manifest.json"; do
|
|
[[ -f "$path" ]] && note "manifest present: ${path#$user_dir/}" || bad "missing manifest: $path"
|
|
done
|
|
python3 - "$modules_dir/swap/manifest.json" "$plugins_dir/swap_ui/manifest.json" "$dev_suffix" <<'PY' || fail=1
|
|
import json, sys
|
|
s = json.load(open(sys.argv[1])); u = json.load(open(sys.argv[2])); suffix = sys.argv[3]
|
|
ok = True
|
|
for label, m in (("swap", s), ("swap_ui", u)):
|
|
if not m.get("version", "").endswith(suffix):
|
|
print(f" [FAIL] {label} version not stamped: {m.get('version')}"); ok = False
|
|
else:
|
|
print(f" [ ok ] {label} stamped: {m['version']}")
|
|
if "delivery_module" not in (s.get("dependencies") or []):
|
|
print(" [FAIL] swap lost its delivery_module dependency"); ok = False
|
|
if u.get("type") != "ui_qml" or not u.get("view"):
|
|
print(f" [FAIL] swap_ui not a ui_qml plugin with a view: type={u.get('type')} view={u.get('view')}"); ok = False
|
|
else:
|
|
print(f" [ ok ] swap_ui view preserved: {u['view']}")
|
|
sys.exit(0 if ok else 1)
|
|
PY
|
|
dylib_count=0
|
|
while IFS= read -r dylib; do
|
|
dylib_count=$((dylib_count + 1))
|
|
if file "$dylib" | grep -q 'arm64'; then note "arm64 dylib: ${dylib#$user_dir/}"
|
|
else bad "non-arm64 dylib: $dylib ($(file -b "$dylib"))"; fi
|
|
done < <(find "$modules_dir/swap" "$plugins_dir/swap_ui" -type f \( -name '*.dylib' -o -name '*.so' \))
|
|
[[ "$dylib_count" -gt 0 ]] || bad "no dylib/.so found under staged swap / swap_ui"
|
|
[[ "$fail" == 0 ]] || { echo "==> staging verification FAILED — not launching" >&2; exit 1; }
|
|
echo "==> staging verification passed"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Resolve the Basecamp bundle to launch.
|
|
# Delivery context creation (delivery_module.createNode) is validated ONLY
|
|
# against the scaffold-pinned Basecamp. On an older host (e.g. 0.2.1) it
|
|
# fails at startup with "createNode failed: Failed to create Delivery
|
|
# context" — see the root-cause note in docs/local-dev-harness.md. So the
|
|
# harness now PROVIDES the validated bundle itself, in strict order:
|
|
# 1. cached pinned bundle — a prior run's gc-rooted nix out-link; instant.
|
|
# 2. build pinned bundle — one-time cold macOS build (~5-15 min), then
|
|
# cached under $cache_dir for instant reuse.
|
|
# 3. installed /Applications app — LAST RESORT. Used directly (no build)
|
|
# only when it is already >= the validated version; otherwise it is
|
|
# launched with a LOUD banner because delivery is known to break there.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# The scaffold basecamp pin ([repos.basecamp].pin) builds this Basecamp release;
|
|
# it is the version delivery_module.createNode is validated against. Bump in
|
|
# lockstep when the pin advances to a new Basecamp release.
|
|
validated_basecamp_version="0.2.3"
|
|
pinned_link="$cache_dir/basecamp-pinned" # nix out-link == gc-rooted cache
|
|
|
|
plist_version() {
|
|
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" \
|
|
"$1/Contents/Info.plist" 2>/dev/null || echo "?"
|
|
}
|
|
# Exit 0 iff $1 >= $2 as dotted-numeric versions; non-zero on "older" OR when
|
|
# either side is unparseable (so an unknown version never masquerades as new).
|
|
version_ge() {
|
|
python3 - "$1" "$2" <<'PY'
|
|
import sys
|
|
def parse(v):
|
|
parts = [int(p) for p in v.split(".") if p.isdigit()]
|
|
return tuple(parts) if parts else None
|
|
a, b = parse(sys.argv[1]), parse(sys.argv[2])
|
|
sys.exit(0 if (a is not None and b is not None and a >= b) else 1)
|
|
PY
|
|
}
|
|
app_in_bundle() { find "$1" -maxdepth 2 -iname '*basecamp*.app' -print 2>/dev/null | head -1; }
|
|
bin_in_app() { local b="$1/Contents/MacOS/LogosBasecamp"; [[ -x "$b" ]] && printf '%s\n' "$b"; }
|
|
find_installed_app() { find /Applications -maxdepth 1 -iname '*basecamp*.app' -print 2>/dev/null | head -1; }
|
|
|
|
resolve_basecamp_bin() {
|
|
local out app bin
|
|
# Explicit override wins, unconditionally.
|
|
if [[ -n "${BASECAMP_APP:-}" ]]; then
|
|
bin=$(bin_in_app "$BASECAMP_APP") \
|
|
|| { echo "BASECAMP_APP has no LogosBasecamp binary: $BASECAMP_APP/Contents/MacOS/LogosBasecamp" >&2; return 1; }
|
|
echo "installed(override):$BASECAMP_APP:$bin"; return 0
|
|
fi
|
|
|
|
if [[ "$FORCE_INSTALLED" == 1 ]]; then
|
|
app=$(find_installed_app)
|
|
[[ -n "$app" ]] || { echo "no Basecamp .app under /Applications; set BASECAMP_APP or drop --installed-app" >&2; return 1; }
|
|
bin=$(bin_in_app "$app") || { echo "installed app has no LogosBasecamp binary: $app" >&2; return 1; }
|
|
echo "installed:$app:$bin"; return 0
|
|
fi
|
|
|
|
# (1) Cached pinned bundle — a prior run's out-link, still valid in the store.
|
|
if [[ "$FORCE_PINNED" == 0 && -L "$pinned_link" ]]; then
|
|
out=$(readlink "$pinned_link" 2>/dev/null || true)
|
|
if [[ -n "$out" && -d "$out" ]]; then
|
|
app=$(app_in_bundle "$out"); bin=$(bin_in_app "$app" 2>/dev/null || true)
|
|
[[ -n "$bin" ]] && { echo "pinned-cached:$app:$bin"; return 0; }
|
|
fi
|
|
fi
|
|
|
|
# An installed app already >= the validated version is a valid host — use it
|
|
# and skip the long build. (Not applicable under --pinned-basecamp.)
|
|
if [[ "$FORCE_PINNED" == 0 ]]; then
|
|
app=$(find_installed_app)
|
|
if [[ -n "$app" ]] && version_ge "$(plist_version "$app")" "$validated_basecamp_version"; then
|
|
bin=$(bin_in_app "$app") && { echo "installed-ok:$app:$bin"; return 0; }
|
|
fi
|
|
fi
|
|
|
|
# (2) Build the pinned bundle, caching it via a gc-rooted out-link so the next
|
|
# run hits tier (1) instantly. nix progress streams to the terminal
|
|
# (stderr); 1>&2 keeps nix's stdout out of the captured result.
|
|
echo "==> Basecamp: building scaffold's pinned bundle v$validated_basecamp_version ($basecamp_attr)." >&2
|
|
echo " ONE-TIME cost (cold: ~5-15 min on macOS; then cached to" >&2
|
|
echo " $pinned_link for instant reuse). Delivery context creation is" >&2
|
|
echo " validated only on this bundle. Skip with --installed-app / BASECAMP_APP." >&2
|
|
local start; start=$(date +%s)
|
|
if nix build "$basecamp_ref" --out-link "$pinned_link" 1>&2; then
|
|
out=$(readlink "$pinned_link" 2>/dev/null || true)
|
|
app=$(app_in_bundle "$out"); bin=$(bin_in_app "$app" 2>/dev/null || true)
|
|
if [[ -n "$bin" ]]; then
|
|
echo " pinned bundle ready in $(( $(date +%s) - start ))s -> $app" >&2
|
|
echo "pinned-built:$app:$bin"; return 0
|
|
fi
|
|
echo "pinned basecamp build produced no LogosBasecamp binary under $out" >&2
|
|
else
|
|
echo "pinned basecamp build FAILED (see nix output above)." >&2
|
|
fi
|
|
|
|
# --pinned-basecamp is an explicit reproducibility request: never silently
|
|
# downgrade to the installed app.
|
|
[[ "$FORCE_PINNED" == 1 ]] && { echo "could not provide the pinned Basecamp bundle (see error above)" >&2; return 1; }
|
|
|
|
# (3) LAST RESORT: installed app (a loud banner fires below if it is stale).
|
|
app=$(find_installed_app)
|
|
[[ -n "$app" ]] || { echo "no pinned bundle and no Basecamp .app under /Applications; set BASECAMP_APP or --pinned-basecamp" >&2; return 1; }
|
|
bin=$(bin_in_app "$app") || { echo "installed app has no LogosBasecamp binary: $app" >&2; return 1; }
|
|
echo "installed-fallback:$app:$bin"; return 0
|
|
}
|
|
|
|
resolved=$(resolve_basecamp_bin) || exit 1
|
|
bc_kind=${resolved%%:*}; rest=${resolved#*:}; bc_app=${rest%%:*}; basecamp_bin=${rest#*:}
|
|
app_version=$(plist_version "$bc_app")
|
|
|
|
# Loud banner when we could only reach an installed host older than the
|
|
# validated version — delivery context creation is known to break there.
|
|
case "$bc_kind" in
|
|
installed|installed-fallback|"installed(override)")
|
|
if ! version_ge "$app_version" "$validated_basecamp_version"; then
|
|
cat >&2 <<BANNER
|
|
|
|
##########################################################################
|
|
## WARNING host Basecamp v$app_version < validated v$validated_basecamp_version
|
|
## This host is KNOWN TO BREAK delivery context creation
|
|
## ("createNode failed: Failed to create Delivery context"): swap offers
|
|
## will NOT load and any delivery-dependent result is UNRELIABLE.
|
|
## Fix: let the harness build the pinned bundle (drop --installed-app /
|
|
## BASECAMP_APP), or install Basecamp >= v$validated_basecamp_version.
|
|
##########################################################################
|
|
BANNER
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. Layout-drift guard (REUSED pattern from Makefile basecamp-launch-%): the
|
|
# dirs we hand Basecamp must be the ones we actually staged into.
|
|
# ---------------------------------------------------------------------------
|
|
[[ -d "$modules_dir/swap" && -d "$plugins_dir/swap_ui" ]] || {
|
|
echo "layout drift: staged swap/swap_ui not found under $user_dir" >&2; exit 1; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 9. Echo exactly what will load, then launch (or print the command).
|
|
# ---------------------------------------------------------------------------
|
|
mver() { python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))["version"])' "$1"; }
|
|
staged_swap_ver=$(mver "$modules_dir/swap/manifest.json")
|
|
staged_ui_ver=$(mver "$plugins_dir/swap_ui/manifest.json")
|
|
staged_delivery_ver=$(mver "$modules_dir/delivery_module/manifest.json")
|
|
|
|
echo
|
|
echo "==> ready"
|
|
echo " Basecamp : $bc_app"
|
|
echo " Basecamp version : v$app_version (validated: v$validated_basecamp_version) [$bc_kind]"
|
|
echo " isolated user-dir : $user_dir"
|
|
echo " modules dir : $modules_dir"
|
|
echo " plugins dir : $plugins_dir"
|
|
echo " loaded modules : swap $staged_swap_ver | swap_ui $staged_ui_ver | delivery_module $staged_delivery_ver"
|
|
echo
|
|
echo " >>> corner version badge should read: $staged_ui_ver <<<"
|
|
echo
|
|
|
|
# Belt-and-suspenders: the load path is --user-dir (Basecamp exports it as
|
|
# LOGOS_USER_DIR and reads modules/ + plugins/ beneath it). The explicit dir
|
|
# exports document intent and cover any pin that consults them directly; they
|
|
# point at the same staged tree.
|
|
launch_cmd=(
|
|
env
|
|
LOGOS_USER_DIR="$user_dir"
|
|
LOGOS_MODULES_DIR="$modules_dir"
|
|
LOGOS_UI_PLUGINS_DIR="$plugins_dir"
|
|
"$basecamp_bin" --user-dir "$user_dir"
|
|
)
|
|
|
|
kill_prior_dev_instance() {
|
|
local pids
|
|
pids=$(pgrep -f "LogosBasecamp.*$user_dir" 2>/dev/null || true)
|
|
if [[ -n "$pids" ]]; then
|
|
echo "==> killing prior dev-profile Basecamp: $pids"
|
|
# shellcheck disable=SC2086
|
|
kill $pids 2>/dev/null || true; sleep 1
|
|
pids=$(pgrep -f "LogosBasecamp.*$user_dir" 2>/dev/null || true)
|
|
# shellcheck disable=SC2086
|
|
[[ -n "$pids" ]] && kill -9 $pids 2>/dev/null || true
|
|
fi
|
|
}
|
|
print_launch_cmd() { echo "==> launch command:"; printf ' '; printf ' %q' "${launch_cmd[@]}"; printf '\n'; }
|
|
|
|
if [[ "$NO_LAUNCH" == 1 ]]; then
|
|
print_launch_cmd
|
|
echo; echo "==> --no-launch: staged and verified; launch skipped."
|
|
exit 0
|
|
fi
|
|
|
|
kill_prior_dev_instance
|
|
|
|
# GUI verification is deliberately the human's job — the entire point of the
|
|
# `-dev.<sha>` stamp is that the corner badge answers "which build is live?"
|
|
# at a glance. The script's own verification stops at the file level above.
|
|
print_launch_cmd
|
|
echo "==> launching Basecamp — check the corner badge reads: $staged_ui_ver"
|
|
exec "${launch_cmd[@]}"
|