diff --git a/docs/103-runtime-topology.md b/docs/103-runtime-topology.md index 725eed0..d3576ca 100644 --- a/docs/103-runtime-topology.md +++ b/docs/103-runtime-topology.md @@ -166,3 +166,80 @@ ssh john@58.38.22.103 'curl -fsS http://127.0.0.1:8082/health' ssh john@58.38.22.103 'for p in $(seq 18006 18014); do curl -kfsS https://127.0.0.1:$p/status; echo; done' ssh john@58.38.22.103 'grep ^TKMIND_API_TARGETS= /Users/john/Project/Memind/.env' ``` + +## imgproxy + +103 imgproxy runs as a **native vendor runtime** (no brew on 103; Docker `memind-imgproxy` retired). + +| Item | Value | +|------|-------| +| Runtime base | `/Users/john/Project/imgproxy-runtime` | +| Current release (2026-07-31) | `20260731-052342-imgproxy-native` (**native launchd**, imgproxy 4.0.7) | +| Legacy Docker release | `20260715-3819ccd5` (retired) | +| Vendor binary | `releases//vendor/imgproxy-runtime/bin/imgproxy` (+ bundled dylibs) | +| LaunchAgent | `cn.tkmind.imgproxy` → direct binary exec → `127.0.0.1:20082` | +| Compat proxy | `cn.tkmind.imgproxy-compat` → `10.10.0.2:20081` | +| Public entry | `https://img.tkmind.cn` | +| Signing config | Portal `.env` → `IMGPROXY_SIGNING_KEY` / `IMGPROXY_SIGNING_SALT` | +| Storage root | `MINDSPACE_STORAGE_ROOT=/Users/john/MindSpace/data/mindspace` | + +Build locally, release to 103: + +```bash +npm run build:imgproxy-runtime-native +bash scripts/release-imgproxy-native-prod.sh --dry-run # from release/* on synced main +bash scripts/release-imgproxy-native-prod.sh --yes +``` + +Install on 103 reads Portal `.env` for signing keys and storage root; launchd plist **must exec the vendored binary directly** (not `/bin/bash` wrapper). If bootstrap fails with `error 5`, run `launchctl enable gui/$(id -u)/cn.tkmind.imgproxy` before re-bootstrap. + +Verify: + +```bash +ssh john@58.38.22.103 'curl -fsS http://127.0.0.1:20082/health' +ssh john@58.38.22.103 'curl -fsS http://10.10.0.2:20081/health' +curl -fsS https://img.tkmind.cn/health +``` + +Legacy Docker release remains in `scripts/release-imgproxy-runtime-prod.sh` for rollback only. + +Do **not** point launchd at `/Users/john/Project/Memind/vendor/...` without a Portal runtime that actually ships that path; imgproxy is an independent runtime under `imgproxy-runtime/`. + +## image_make + +`image_make` is an independent service. Portal runtime does **not** bundle it. + +| Item | Value | +|------|-------| +| Local source | `/Users/john/Project/image_make` | +| 103 runtime base | `/Users/john/Project/image_make` | +| Current release (2026-07-30) | `20260730-220317-native-ceaff399a073` (**native launchd**) | +| Legacy Docker release | `20260720-standalone-rc1` (retired) | +| Listen | `127.0.0.1:18083` | +| LaunchAgent | `cn.tkmind.image-make` | +| Persistent env | `shared/.env` | +| Persistent data | `shared/data/artifacts` | +| Build (local) | `bash scripts/build-image-make-runtime-native.sh` | +| Release (103) | `bash scripts/release-image-make-runtime-prod.sh --yes` | +| Source fingerprint | `release-metadata.env` → `IMAGE_MAKE_SOURCE_TREE_SHA256` | + +103 runs **darwin/arm64 native** (uv venv in release tar). Docker/Colima is no longer required for image_make. + +**ComfyUI is a separate runtime** (not bundled into image_make tar): + +| Item | Value | +|------|-------| +| Local build tree | `/Users/john/Project/image_make/.runtime/ComfyUI` + `comfyui-venv` | +| 103 runtime base | `/Users/john/Project/comfyui-runtime` | +| Current release (2026-07-30) | `20260730-222051-native-e87e7a81bb76` (**native launchd**) | +| Publish entry | `bash scripts/release-comfyui-runtime-prod.sh --yes` (from local Mac) | +| LaunchAgent | `cn.tkmind.comfyui` → `127.0.0.1:8188` | +| Fingerprint | `config/comfyui/runtime.lock` + SD1.5 SHA256 | + +103 must **not** run `install_comfyui_runtime.sh` or download models directly; the local machine stages +`/Users/john/Project/comfyui-runtime/releases//` with production paths, tars it, and scp installs it. + +Enable `comfyui` in Portal `h5_image_make_admin_config` only after ComfyUI is running. +Current production: default `aliyun_bailian`, `comfyui` enabled as failover backup. + +Memind integration: HTTP client only (`image-make-client.mjs`); config via `IMAGE_MAKE_BASE_URL` + token. Docs: [image-make-integration.md](./image-make-integration.md). diff --git a/package.json b/package.json index ba06f1a..1e163a9 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "test:release-gate:all": "node scripts/run-release-gate.mjs --mode all", "verify:release-gate-report": "node scripts/verify-release-gate-report.mjs", "build:imgproxy-runtime": "bash scripts/build-imgproxy-runtime-image.sh", + "build:imgproxy-runtime-native": "bash scripts/build-imgproxy-runtime-native.sh", "build:mindspace-service-runtime": "node scripts/build-mindspace-service-runtime.mjs", "check:mindspace-public-links": "node scripts/check-mindspace-public-links.mjs --downloads-only", "check:mindspace-public-links:all": "node scripts/check-mindspace-public-links.mjs --all-links", diff --git a/scripts/build-imgproxy-runtime-native.sh b/scripts/build-imgproxy-runtime-native.sh new file mode 100755 index 0000000..11f7e48 --- /dev/null +++ b/scripts/build-imgproxy-runtime-native.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Materialize a darwin/arm64 imgproxy vendor binary into .runtime/imgproxy-native/. +# Prefers the local Homebrew Cellar binary; bundles non-system dylibs for 103. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT_DIR="${IMGPROXY_NATIVE_OUT_DIR:-${ROOT}/.runtime/imgproxy-native}" +VENDOR_DIR="${OUT_DIR}/vendor/imgproxy-runtime" +BIN_DIR="${VENDOR_DIR}/bin" +LIB_DIR="${VENDOR_DIR}/lib" +IMGPROXY_VERSION="${IMGPROXY_VERSION:-4.0.7}" +BREW_PREFIX="${HOMEBREW_PREFIX:-/opt/homebrew}" +DRY_RUN=0 + +# shellcheck source=scripts/lib/bundle-mach-dylibs.sh +source "${ROOT}/scripts/lib/bundle-mach-dylibs.sh" + +case "${1:-}" in + --dry-run) DRY_RUN=1 ;; + --help|-h) + cat <<'EOF' +Usage: bash scripts/build-imgproxy-runtime-native.sh [--dry-run] + +Builds .runtime/imgproxy-native with: + vendor/imgproxy-runtime/bin/imgproxy + vendor/imgproxy-runtime/lib/*.dylib + vendor/imgproxy-runtime/VERSION.txt + vendor/imgproxy-runtime/SHA256SUMS + scripts/install-imgproxy-native-prod.sh + scripts/run-imgproxy-prod.sh + imgproxy-compat-proxy.mjs + RUNBOOK.txt + +Set IMGPROXY_SOURCE_BIN to copy a specific binary, or IMGPROXY_VERSION for Cellar lookup. +EOF + exit 0 + ;; +esac + +resolve_imgproxy_binary() { + if [[ -n "${IMGPROXY_SOURCE_BIN:-}" ]]; then + [[ -x "${IMGPROXY_SOURCE_BIN}" ]] || { + echo "IMGPROXY_SOURCE_BIN is not executable: ${IMGPROXY_SOURCE_BIN}" >&2 + exit 1 + } + printf '%s\n' "${IMGPROXY_SOURCE_BIN}" + return + fi + + local cellar_bin="${BREW_PREFIX}/Cellar/imgproxy/${IMGPROXY_VERSION}/bin/imgproxy" + if [[ -x "${cellar_bin}" ]]; then + printf '%s\n' "${cellar_bin}" + return + fi + + if command -v brew >/dev/null 2>&1; then + local tmp cache_glob candidate + tmp="$(mktemp -d "${TMPDIR:-/tmp}/imgproxy-bottle.XXXXXX")" + brew fetch --force-bottle imgproxy >/dev/null + cache_glob="${HOME}/Library/Caches/Homebrew/downloads/*imgproxy*${IMGPROXY_VERSION}*.bottle.tar.gz" + candidate="$(ls -1 ${cache_glob} 2>/dev/null | head -1 || true)" + [[ -n "${candidate}" ]] || { + echo "imgproxy ${IMGPROXY_VERSION} bottle not found after brew fetch" >&2 + exit 1 + } + tar -xzf "${candidate}" -C "${tmp}" + find "${tmp}" -path "*/bin/imgproxy" -type f | head -1 + return + fi + + echo "imgproxy binary not found; install via brew or set IMGPROXY_SOURCE_BIN" >&2 + exit 1 +} + +if [[ "${DRY_RUN}" == 1 ]]; then + candidate="$(resolve_imgproxy_binary || true)" + printf 'version=%s\nsource=%s\nout_dir=%s\n' "${IMGPROXY_VERSION}" "${candidate:-missing}" "${OUT_DIR}" + exit 0 +fi + +mkdir -p "${BIN_DIR}" "${LIB_DIR}" "${OUT_DIR}/scripts" +source_bin="$(resolve_imgproxy_binary)" +cp "${source_bin}" "${BIN_DIR}/imgproxy" +chmod 755 "${BIN_DIR}/imgproxy" +bundle_mach_dylibs "${BIN_DIR}/imgproxy" "${LIB_DIR}" "${source_bin}" +"${BIN_DIR}/imgproxy" version > "${VENDOR_DIR}/VERSION.txt" +( + cd "${VENDOR_DIR}" + shasum -a 256 bin/imgproxy VERSION.txt lib/* > SHA256SUMS +) + +cp "${ROOT}/scripts/install-imgproxy-native-prod.sh" "${OUT_DIR}/scripts/" +cp "${ROOT}/scripts/run-imgproxy-prod.sh" "${OUT_DIR}/scripts/" +cp "${ROOT}/scripts/imgproxy-compat-proxy.mjs" "${OUT_DIR}/" +chmod 755 "${OUT_DIR}/scripts/install-imgproxy-native-prod.sh" "${OUT_DIR}/scripts/run-imgproxy-prod.sh" + +cat > "${OUT_DIR}/RUNBOOK.txt" < releases// +LaunchAgent: cn.tkmind.imgproxy (127.0.0.1:20082) +Compat proxy: cn.tkmind.imgproxy-compat (10.10.0.2:20081) +Health: http://127.0.0.1:20082/health +Public: https://img.tkmind.cn/health + +Vendor binary: vendor/imgproxy-runtime/bin/imgproxy +Bundled libs: vendor/imgproxy-runtime/lib/ +Built from: ${source_bin} + +This runtime replaces the Docker imgproxy container on 103. +Portal runtime releases stay separate. +EOF + +lib_count="$(find "${LIB_DIR}" -name '*.dylib' | wc -l | tr -d ' ')" +printf 'imgproxy native runtime built at %s (%s, %s dylibs)\n' \ + "${OUT_DIR}" "$(cat "${VENDOR_DIR}/VERSION.txt")" "${lib_count}" diff --git a/scripts/imgproxy-runtime-package.test.mjs b/scripts/imgproxy-runtime-package.test.mjs index ba51e64..b1ee9b8 100644 --- a/scripts/imgproxy-runtime-package.test.mjs +++ b/scripts/imgproxy-runtime-package.test.mjs @@ -38,3 +38,34 @@ test('release verifies checksums before loading the image and backs up launch st assert.match(release, /release\/\*/); assert.match(release, /rev-parse origin\/main/); }); + +test('native builder vendors a pinned darwin arm64 imgproxy binary', () => { + const builder = read('scripts/build-imgproxy-runtime-native.sh'); + assert.match(builder, /vendor\/imgproxy-runtime\/bin\/imgproxy/); + assert.match(builder, /vendor\/imgproxy-runtime\/lib/); + assert.match(builder, /bundle_mach_dylibs/); + assert.match(builder, /shasum -a 256 bin\/imgproxy VERSION\.txt lib\/\*/); + assert.match(builder, /IMGPROXY_SOURCE_BIN/); + assert.match(builder, /run-imgproxy-prod\.sh/); +}); + +test('native installer uses the vendored binary and retires docker imgproxy', () => { + const installer = read('scripts/install-imgproxy-native-prod.sh'); + assert.match(installer, /vendor\/imgproxy-runtime\/bin\/imgproxy/); + assert.match(installer, /ProgramArguments/); + assert.match(installer, /IMGPROXY_KEY/); + assert.match(installer, /127\.0\.0\.1:20082/); + assert.match(installer, /10\.10\.0\.2/); + assert.match(installer, /"\$\{DOCKER_BIN\}" stop/); + assert.match(installer, /"\$\{DOCKER_BIN\}" rm -f/); + assert.match(installer, /cn\.tkmind\.imgproxy/); +}); + +test('native release bundles vendor artifacts and verifies health on 103', () => { + const release = read('scripts/release-imgproxy-native-prod.sh'); + assert.match(release, /build-imgproxy-runtime-native\.sh/); + assert.match(release, /vendor\/imgproxy-runtime\/bin\/imgproxy/); + assert.match(release, /install-imgproxy-native-prod\.sh/); + assert.match(release, /imgproxy-native-runtime-\$RELEASE_ID\.tar\.gz/); + assert.match(release, /img\.tkmind\.cn\/health/); +}); diff --git a/scripts/install-imgproxy-native-prod.sh b/scripts/install-imgproxy-native-prod.sh new file mode 100755 index 0000000..cdaac33 --- /dev/null +++ b/scripts/install-imgproxy-native-prod.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# Runs only on 103 from a verified imgproxy native release directory. +set -euo pipefail + +ROOT="${ROOT:-/Users/john/Project/Memind}" +RUNTIME_BASE="${IMGPROXY_RUNTIME_BASE:-/Users/john/Project/imgproxy-runtime}" +RUNTIME_DIR="${IMGPROXY_RUNTIME_DIR:?IMGPROXY_RUNTIME_DIR is required}" +IMGPROXY_BIN="${RUNTIME_DIR}/vendor/imgproxy-runtime/bin/imgproxy" +RUN_SCRIPT="${RUNTIME_DIR}/scripts/run-imgproxy-prod.sh" +NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}" +DOCKER_BIN="${DOCKER_BIN:-/opt/homebrew/bin/docker}" +CONTAINER="${IMGPROXY_CONTAINER:-memind-imgproxy}" +NATIVE_LABEL="cn.tkmind.imgproxy" +COMPAT_LABEL="cn.tkmind.imgproxy-compat" +NATIVE_PLIST="${HOME}/Library/LaunchAgents/${NATIVE_LABEL}.plist" +COMPAT_PLIST="${HOME}/Library/LaunchAgents/${COMPAT_LABEL}.plist" +LOG_DIR="${HOME}/Library/Logs" +GUI="gui/$(id -u)" + +[[ -x "${IMGPROXY_BIN}" ]] || { + echo "missing vendor imgproxy binary: ${IMGPROXY_BIN}" >&2 + exit 1 +} +[[ -x "${RUN_SCRIPT}" ]] || { + echo "missing imgproxy run script: ${RUN_SCRIPT}" >&2 + exit 1 +} + +set -a +source "${ROOT}/.env" +set +a +: "${IMGPROXY_SIGNING_KEY:?missing IMGPROXY_SIGNING_KEY}" +: "${IMGPROXY_SIGNING_SALT:?missing IMGPROXY_SIGNING_SALT}" +: "${MINDSPACE_STORAGE_ROOT:?missing MINDSPACE_STORAGE_ROOT}" + +cd "${RUNTIME_DIR}/vendor/imgproxy-runtime" +shasum -a 256 -c SHA256SUMS >/dev/null + +mkdir -p "${HOME}/Library/LaunchAgents" "${LOG_DIR}" + +cat > "${NATIVE_PLIST}" < + + + + Label + ${NATIVE_LABEL} + ProgramArguments + + ${IMGPROXY_BIN} + + EnvironmentVariables + + IMGPROXY_BIND + 127.0.0.1:20082 + IMGPROXY_LOCAL_FILESYSTEM_ROOT + ${MINDSPACE_STORAGE_ROOT} + IMGPROXY_KEY + ${IMGPROXY_SIGNING_KEY} + IMGPROXY_SALT + ${IMGPROXY_SIGNING_SALT} + IMGPROXY_USE_ETAG + true + IMGPROXY_ENABLE_WEBP_DETECTION + true + + WorkingDirectory + ${ROOT} + RunAtLoad + + KeepAlive + + ThrottleInterval + 10 + StandardOutPath + ${LOG_DIR}/imgproxy.log + StandardErrorPath + ${LOG_DIR}/imgproxy.log + + +EOF + +xattr -cr "${RUNTIME_DIR}" 2>/dev/null || true + +if [[ -x "${DOCKER_BIN}" ]]; then + "${DOCKER_BIN}" stop "${CONTAINER}" >/dev/null 2>&1 || true + "${DOCKER_BIN}" rm -f "${CONTAINER}" >/dev/null 2>&1 || true + "${DOCKER_BIN}" update --restart=no "${CONTAINER}" >/dev/null 2>&1 || true +fi + +launchctl bootout "${GUI}/${NATIVE_LABEL}" 2>/dev/null || true +plutil -lint "${NATIVE_PLIST}" >/dev/null +launchctl enable "${GUI}/${NATIVE_LABEL}" 2>/dev/null || true +launchctl bootstrap "${GUI}" "${NATIVE_PLIST}" +launchctl kickstart -k "${GUI}/${NATIVE_LABEL}" + +for _ in $(seq 1 30); do + curl -fsS --max-time 2 http://127.0.0.1:20082/health >/dev/null && break + sleep 1 +done +curl -fsS --max-time 3 http://127.0.0.1:20082/health >/dev/null + +cat > "${COMPAT_PLIST}" < + + +Label${COMPAT_LABEL} +ProgramArguments${NODE_BIN}${RUNTIME_DIR}/imgproxy-compat-proxy.mjs +WorkingDirectory${RUNTIME_DIR} +EnvironmentVariablesIMGPROXY_COMPAT_HOST10.10.0.2IMGPROXY_COMPAT_PORT20081IMGPROXY_UPSTREAMhttp://127.0.0.1:20082 +RunAtLoadKeepAliveThrottleInterval10 +StandardOutPath${LOG_DIR}/imgproxy-compat.logStandardErrorPath${LOG_DIR}/imgproxy-compat.log + +EOF + +launchctl bootout "${GUI}/${COMPAT_LABEL}" 2>/dev/null || true +plutil -lint "${COMPAT_PLIST}" >/dev/null +launchctl enable "${GUI}/${COMPAT_LABEL}" 2>/dev/null || true +launchctl bootstrap "${GUI}" "${COMPAT_PLIST}" +launchctl kickstart -k "${GUI}/${COMPAT_LABEL}" + +for _ in $(seq 1 30); do + curl -fsS --max-time 2 http://10.10.0.2:20081/health >/dev/null && break + sleep 1 +done +curl -fsS --max-time 3 http://10.10.0.2:20081/health >/dev/null + +printf 'imgproxy native installed from %s (%s)\n' "${RUNTIME_DIR}" "$("${IMGPROXY_BIN}" version)" diff --git a/scripts/lib/bundle-mach-dylibs.sh b/scripts/lib/bundle-mach-dylibs.sh new file mode 100755 index 0000000..480a3cb --- /dev/null +++ b/scripts/lib/bundle-mach-dylibs.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# Recursively copy non-system dylibs next to a Mach-O binary and rewrite load paths. +set -euo pipefail + +find_homebrew_lib() { + local base="$1" + find "${HOMEBREW_PREFIX:-/opt/homebrew}/Cellar" "${HOMEBREW_PREFIX:-/opt/homebrew}/opt" \ + -name "${base}" -path '*/lib/*' 2>/dev/null | head -1 +} + +resolve_dependency_path() { + local owner="$1" + local dep="$2" + local reference="${3:-${owner}}" + + if [[ "${dep}" != @* ]]; then + [[ -f "${dep}" ]] || return 1 + printf '%s\n' "${dep}" + return 0 + fi + + local owner_dir reference_dir + owner_dir="$(cd "$(dirname "${owner}")" && pwd)" + reference_dir="$(cd "$(dirname "${reference}")" && pwd)" + + case "${dep}" in + @loader_path/*) + local candidate="${owner_dir}/${dep#@loader_path/}" + [[ -f "${candidate}" ]] || return 1 + printf '%s\n' "${candidate}" + ;; + @executable_path/*) + local rel="${dep#@executable_path/}" + local candidate="${owner_dir}/${rel}" + if [[ ! -f "${candidate}" ]]; then + candidate="${reference_dir}/${rel}" + fi + if [[ ! -f "${candidate}" ]]; then + candidate="$(find_homebrew_lib "$(basename "${rel}")")" + fi + [[ -f "${candidate}" ]] || return 1 + printf '%s\n' "${candidate}" + ;; + @rpath/*) + local rel="${dep#@rpath/}" + local rpath candidate + while IFS= read -r rpath; do + case "${rpath}" in + @loader_path/*) candidate="${owner_dir}/${rpath#@loader_path/}/${rel}" ;; + @executable_path/*) candidate="${owner_dir}/${rpath#@executable_path/}/${rel}" ;; + *) candidate="${rpath}/${rel}" ;; + esac + if [[ -f "${candidate}" ]]; then + printf '%s\n' "${candidate}" + return 0 + fi + done < <( + otool -l "${owner}" | awk ' + /cmd LC_RPATH/ { want=1; next } + want && /path / { + sub(/^.*path /, "", $0) + sub(/ \(offset.*$/, "", $0) + print + want=0 + } + ' + ) + candidate="${owner_dir}/${rel}" + [[ -f "${candidate}" ]] || return 1 + printf '%s\n' "${candidate}" + ;; + *) + return 1 + ;; + esac +} + +list_load_dylibs() { + local target="$1" + otool -l "${target}" | awk ' + /cmd LC_LOAD_DYLIB/ { want=1; next } + want && /name / { + sub(/^.*name /, "", $0) + sub(/ \(offset.*$/, "", $0) + print + want=0 + } + ' +} + +bundle_mach_dylibs() { + local binary="${1:?binary path required}" + local lib_dir="${2:?lib directory required}" + local reference_binary="${3:-${binary}}" + + [[ -f "${binary}" ]] || { + echo "bundle_mach_dylibs: missing binary: ${binary}" >&2 + return 1 + } + + mkdir -p "${lib_dir}" + + local -a queue=("${binary}") + local seen_list=" " + local -a sources=() + + is_system_lib() { + case "$1" in + /usr/lib/*|/System/*|/Library/*) return 0 ;; + esac + return 1 + } + + already_seen() { + [[ "${seen_list}" == *" $1 "* ]] + } + + remember() { + seen_list+=" $1 " + } + + while ((${#queue[@]} > 0)); do + local target="${queue[0]}" + queue=("${queue[@]:1}") + [[ -f "${target}" ]] || continue + + local dep resolved + while IFS= read -r dep; do + [[ -n "${dep}" ]] || continue + is_system_lib "${dep}" && continue + already_seen "${dep}" && continue + remember "${dep}" + + resolved="$(resolve_dependency_path "${target}" "${dep}" "${reference_binary}")" || { + echo "bundle_mach_dylibs: missing dependency ${dep} for ${target}" >&2 + return 1 + } + already_seen "${resolved}" || remember "${resolved}" + + if [[ "${resolved}" != "${binary}" ]]; then + sources+=("${resolved}") + queue+=("${resolved}") + fi + done < <(list_load_dylibs "${target}") + done + + local src base + for src in "${sources[@]}"; do + base="$(basename "${src}")" + cp -f "${src}" "${lib_dir}/${base}" + chmod 755 "${lib_dir}/${base}" + done + + rewrite_dep() { + local target="$1" + local dep="$2" + local new_path="$3" + install_name_tool -change "${dep}" "${new_path}" "${target}" 2>/dev/null || true + } + + rewrite_targets() { + local target="$1" + local prefix="$2" + local dep base + while IFS= read -r dep; do + [[ -n "${dep}" ]] || continue + is_system_lib "${dep}" && continue + [[ "${dep}" == @* ]] && continue + base="$(basename "${dep}")" + [[ -f "${lib_dir}/${base}" ]] || continue + rewrite_dep "${target}" "${dep}" "${prefix}${base}" + done < <(list_load_dylibs "${target}") + } + + rewrite_rpath_targets() { + local target="$1" + local prefix="$2" + local dep base + while IFS= read -r dep; do + [[ -n "${dep}" ]] || continue + [[ "${dep}" == @rpath/* ]] || continue + base="$(basename "${dep}")" + [[ -f "${lib_dir}/${base}" ]] || continue + rewrite_dep "${target}" "${dep}" "${prefix}${base}" + done < <(list_load_dylibs "${target}") + } + + local changed=1 + while ((changed)); do + changed=0 + rewrite_targets "${binary}" "@executable_path/../lib/" + rewrite_rpath_targets "${binary}" "@executable_path/../lib/" + for staged in "${lib_dir}"/*.dylib; do + [[ -f "${staged}" ]] || continue + before="$(list_load_dylibs "${staged}" | { grep -E 'homebrew|^@rpath/' || true; } | wc -l | tr -d ' ')" + rewrite_targets "${staged}" "@loader_path/" + rewrite_rpath_targets "${staged}" "@loader_path/" + after="$(list_load_dylibs "${staged}" | { grep -E 'homebrew|^@rpath/' || true; } | wc -l | tr -d ' ')" + [[ "${before}" != "${after}" ]] && changed=1 + done + done + + local remaining + remaining="$(list_load_dylibs "${binary}" | grep homebrew || true)" + if [[ -n "${remaining}" ]]; then + echo "bundle_mach_dylibs: unresolved homebrew dependencies remain:" >&2 + printf '%s\n' "${remaining}" >&2 + return 1 + fi + + install_name_tool -add_rpath "@executable_path/../lib" "${binary}" 2>/dev/null || true + + local staged + for staged in "${lib_dir}"/*.dylib; do + [[ -f "${staged}" ]] || continue + codesign --force --sign - "${staged}" >/dev/null + done + codesign --force --sign - "${binary}" >/dev/null +} + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + bundle_mach_dylibs "$@" +fi diff --git a/scripts/release-imgproxy-native-prod.sh b/scripts/release-imgproxy-native-prod.sh new file mode 100755 index 0000000..b14e7c9 --- /dev/null +++ b/scripts/release-imgproxy-native-prod.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Release the independently versioned native imgproxy runtime (vendor binary) to 103. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HOST="${STUDIO_HOST:-john@58.38.22.103}" +REMOTE_ROOT="${STUDIO_REMOTE_ROOT:-/Users/john/Project}" +RUNTIME_BASE="${REMOTE_ROOT}/imgproxy-runtime" +INCOMING="${REMOTE_ROOT}/incoming/imgproxy-runtime" +RELEASE_ID="$(date +%Y%m%d-%H%M%S)-$(git -C "$ROOT" rev-parse --short HEAD)" +TMP="$(mktemp -d "${TMPDIR:-/tmp}/imgproxy-native-release.XXXXXX")" +DRY_RUN=0 + +cleanup() { rm -rf "$TMP"; } +trap cleanup EXIT + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=1 ;; + -h|--help) + cat <<'EOF' +Usage: bash scripts/release-imgproxy-native-prod.sh [--dry-run] + +Builds and installs the vendor imgproxy runtime on 103. Portal runtime is unchanged. +EOF + exit 0 + ;; + *) + echo "unknown argument: $1" >&2 + exit 1 + ;; + esac + shift +done + +git -C "$ROOT" fetch origin main --quiet +branch="$(git -C "$ROOT" branch --show-current)" +[[ "$branch" == release/* ]] || { echo 'imgproxy native production release must run from a release/* branch' >&2; exit 1; } +[[ -z "$(git -C "$ROOT" status --porcelain)" ]] || { echo 'worktree must be clean' >&2; exit 1; } +[[ "$(git -C "$ROOT" rev-parse HEAD)" == "$(git -C "$ROOT" rev-parse origin/main)" ]] || { + echo 'main must match origin/main' >&2 + exit 1 +} +bash "$ROOT/scripts/check-release-ready.sh" + +bash "$ROOT/scripts/build-imgproxy-runtime-native.sh" +RUNTIME_DIR="$ROOT/.runtime/imgproxy-native" +for file in \ + vendor/imgproxy-runtime/bin/imgproxy \ + vendor/imgproxy-runtime/SHA256SUMS \ + vendor/imgproxy-runtime/VERSION.txt \ + scripts/install-imgproxy-native-prod.sh \ + imgproxy-compat-proxy.mjs \ + RUNBOOK.txt; do + [[ -f "$RUNTIME_DIR/$file" ]] || { echo "missing runtime artifact: $file" >&2; exit 1; } +done + +MANIFEST="$TMP/release-manifest.txt" +{ + echo "release_id=${RELEASE_ID}" + echo "git_head=$(git -C "$ROOT" rev-parse HEAD)" + echo "imgproxy_version=$(cat "$RUNTIME_DIR/vendor/imgproxy-runtime/VERSION.txt")" + echo "runtime_mode=native-vendor" +} > "$MANIFEST" + +mkdir -p "$TMP/runtime" +cp -R "$RUNTIME_DIR/." "$TMP/runtime/" +cp "$MANIFEST" "$TMP/runtime/" +tar -C "$TMP" -czf "$TMP/imgproxy-native-runtime-$RELEASE_ID.tar.gz" runtime +(cd "$TMP" && shasum -a 256 "imgproxy-native-runtime-$RELEASE_ID.tar.gz" > "imgproxy-native-runtime-$RELEASE_ID.tar.gz.sha256") + +if [[ "$DRY_RUN" == 1 ]]; then + shasum -a 256 -c "$TMP/imgproxy-native-runtime-$RELEASE_ID.tar.gz.sha256" + echo "imgproxy native dry-run artifact verified: $TMP/imgproxy-native-runtime-$RELEASE_ID.tar.gz" + exit 0 +fi + +ssh -o BatchMode=yes "$HOST" "mkdir -p '$INCOMING' '$RUNTIME_BASE/releases' '$RUNTIME_BASE/backups'" +scp -q "$TMP/imgproxy-native-runtime-$RELEASE_ID.tar.gz" "$TMP/imgproxy-native-runtime-$RELEASE_ID.tar.gz.sha256" "$HOST:$INCOMING/" +ssh -o BatchMode=yes "$HOST" "RELEASE_ID='$RELEASE_ID' RUNTIME_BASE='$RUNTIME_BASE' INCOMING='$INCOMING' bash -s" <<'REMOTE' +set -euo pipefail +archive="$INCOMING/imgproxy-native-runtime-$RELEASE_ID.tar.gz" +sha="$archive.sha256" +release_dir="$RUNTIME_BASE/releases/$RELEASE_ID" +backup_dir="$RUNTIME_BASE/backups/$RELEASE_ID" +current_link="$RUNTIME_BASE/current" +previous_target="$(readlink "$current_link" 2>/dev/null || true)" + +mkdir -p "$release_dir" "$backup_dir" +cp "$HOME/Library/LaunchAgents/cn.tkmind.imgproxy.plist" "$backup_dir/imgproxy.plist" 2>/dev/null || true +cp "$HOME/Library/LaunchAgents/cn.tkmind.imgproxy-compat.plist" "$backup_dir/imgproxy-compat.plist" 2>/dev/null || true +cd "$INCOMING" +shasum -a 256 -c "$sha" +tar -xzf "$archive" -C "$release_dir" --strip-components=1 + +rollback() { + launchctl bootout "gui/$(id -u)/cn.tkmind.imgproxy" >/dev/null 2>&1 || true + launchctl bootout "gui/$(id -u)/cn.tkmind.imgproxy-compat" >/dev/null 2>&1 || true + if [[ -n "$previous_target" && -d "$previous_target" ]]; then + ln -sfn "$previous_target" "$current_link" + else + rm -f "$current_link" + fi + if [[ -f "$backup_dir/imgproxy.plist" ]]; then + cp "$backup_dir/imgproxy.plist" "$HOME/Library/LaunchAgents/cn.tkmind.imgproxy.plist" + launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/cn.tkmind.imgproxy.plist" >/dev/null 2>&1 || true + fi + if [[ -f "$backup_dir/imgproxy-compat.plist" ]]; then + cp "$backup_dir/imgproxy-compat.plist" "$HOME/Library/LaunchAgents/cn.tkmind.imgproxy-compat.plist" + launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/cn.tkmind.imgproxy-compat.plist" >/dev/null 2>&1 || true + fi +} +trap rollback ERR + +IMGPROXY_RUNTIME_DIR="$release_dir" bash "$release_dir/scripts/install-imgproxy-native-prod.sh" +ln -sfn "$release_dir" "$current_link" +curl -fsS --max-time 5 http://127.0.0.1:20082/health >/dev/null +curl -fsS --max-time 5 http://10.10.0.2:20081/health >/dev/null +trap - ERR +printf '%s\n' "$RELEASE_ID" > "$RUNTIME_BASE/current-release.txt" +REMOTE + +curl -kfsS --max-time 15 https://img.tkmind.cn/health >/dev/null +printf 'imgproxy native release verified: %s\n' "$RELEASE_ID" diff --git a/scripts/run-imgproxy-prod.sh b/scripts/run-imgproxy-prod.sh new file mode 100755 index 0000000..cf48688 --- /dev/null +++ b/scripts/run-imgproxy-prod.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +RUNTIME_BASE="${IMGPROXY_RUNTIME_BASE:-/Users/john/Project/imgproxy-runtime}" +RUNTIME_DIR="${IMGPROXY_RUNTIME_DIR:-${RUNTIME_BASE}/current}" +ROOT="${ROOT:-/Users/john/Project/Memind}" +IMGPROXY_BIN="${RUNTIME_DIR}/vendor/imgproxy-runtime/bin/imgproxy" + +[[ -x "${IMGPROXY_BIN}" ]] || { + echo "[imgproxy-start] missing binary: ${IMGPROXY_BIN}" >&2 + exit 1 +} + +if [[ -f "${ROOT}/.env" ]]; then + set -a + # shellcheck disable=SC1090 + source "${ROOT}/.env" + set +a +fi + +: "${IMGPROXY_SIGNING_KEY:?missing IMGPROXY_SIGNING_KEY}" +: "${IMGPROXY_SIGNING_SALT:?missing IMGPROXY_SIGNING_SALT}" +: "${MINDSPACE_STORAGE_ROOT:?missing MINDSPACE_STORAGE_ROOT}" + +export IMGPROXY_BIND="${IMGPROXY_BIND:-127.0.0.1:20082}" +export IMGPROXY_LOCAL_FILESYSTEM_ROOT="${MINDSPACE_STORAGE_ROOT}" +export IMGPROXY_KEY="${IMGPROXY_SIGNING_KEY}" +export IMGPROXY_SALT="${IMGPROXY_SIGNING_SALT}" +export IMGPROXY_USE_ETAG="${IMGPROXY_USE_ETAG:-true}" +export IMGPROXY_ENABLE_WEBP_DETECTION="${IMGPROXY_ENABLE_WEBP_DETECTION:-true}" + +exec "${IMGPROXY_BIN}"