Files
2026-08-16 18:24:52 +07:00

299 lines
12 KiB
Bash
Executable File

#!/bin/bash
# HOT-Step CPP — macOS Release Packager
#
# Builds a portable .tar.gz release that requires ZERO prerequisites.
# End users just extract and run ./launch.sh — no Xcode, no brew, no npm.
#
# What gets bundled:
# - Pre-built C++ engine binaries (Metal GPU)
# - Bundled Node.js 22 runtime (~40 MB)
# - Pre-installed server dependencies (production only)
# - Pre-built React UI
# - Launch scripts
#
# Usage:
# ./package-release.sh # Build release package
# ./package-release.sh --skip-build # Package only (engine already built)
#
# Output:
# HOT-Step-CPP-v{VERSION}-macOS-arm64.tar.gz
set -e
cd "$(dirname "$0")"
ROOT_DIR="$(pwd)"
# ── Config ───────────────────────────────────────────────────────────
VERSION="${VERSION:-1.0.0}"
ARCH="arm64"
NODE_VERSION="22.22.2"
NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-darwin-${ARCH}.tar.xz"
RELEASE_NAME="HOT-Step-CPP-v${VERSION}-macOS-${ARCH}"
SKIP_BUILD=0
for arg in "$@"; do
case $arg in
--skip-build) SKIP_BUILD=1 ;;
--version=*) VERSION="${arg#*=}"; RELEASE_NAME="HOT-Step-CPP-v${VERSION}-macOS-${ARCH}" ;;
esac
done
STAGING_DIR="${ROOT_DIR}/release-staging/${RELEASE_NAME}"
OUTPUT_FILE="${ROOT_DIR}/${RELEASE_NAME}.tar.gz"
# ── Colors ───────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
step() { echo -e "\n${CYAN}${BOLD}[$1/$TOTAL_STEPS]${NC} ${BOLD}$2${NC}"; }
ok() { echo -e " ${GREEN}${NC} $1"; }
warn() { echo -e " ${YELLOW}${NC} $1"; }
fail() { echo -e " ${RED}$1${NC}"; exit 1; }
TOTAL_STEPS=7
echo ""
echo -e "${BOLD}╔══════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ HOT-Step CPP — Release Packager ║${NC}"
echo -e "${BOLD}║ macOS ${ARCH} Portable Build ║${NC}"
echo -e "${BOLD}╚══════════════════════════════════════════╝${NC}"
echo ""
echo " Version: ${VERSION}"
echo " Output: ${RELEASE_NAME}.tar.gz"
echo ""
# ── Find compatible Node for the build process ──────────────────────
find_node() {
for bp in "/opt/homebrew/opt/node@22/bin" "/usr/local/opt/node@22/bin"; do
if [ -x "${bp}/node" ]; then echo "$bp"; return 0; fi
done
if command -v node &>/dev/null; then
local ver; ver="$(node --version 2>/dev/null | tr -d 'v')"
local major="${ver%%.*}"
if [ "$major" -ge 18 ] && [ "$major" -lt 24 ] 2>/dev/null; then
echo "$(dirname "$(command -v node)")"; return 0
fi
fi
return 1
}
NODE_DIR=""
if NODE_DIR=$(find_node); then
export PATH="${NODE_DIR}:${PATH}"
else
fail "Node.js 18-22 required for building. Run: brew install node@22"
fi
# ── Step 1: Build engine ─────────────────────────────────────────────
step 1 "Building C++ engine..."
if [ "$SKIP_BUILD" -eq 1 ] && [ -f "engine/build/ace-server" ]; then
ok "Skipping build (--skip-build, binaries exist)"
else
cd engine
rm -rf build && mkdir build && cd build
cmake .. \
-DGGML_METAL=ON \
-DGGML_METAL_EMBED_LIBRARY=ON \
-DGGML_BACKEND_DL=OFF \
-DCMAKE_BUILD_TYPE=Release \
2>&1 | tail -3
NCPU=$(sysctl -n hw.ncpu 2>/dev/null || echo 4)
cmake --build . --config Release -j "${NCPU}" 2>&1 | grep -E '^\[100|Built target|error:'
cd "${ROOT_DIR}"
fi
# Verify binaries
for bin in ace-server mastering mp3-codec vst-host; do
[ -f "engine/build/${bin}" ] || fail "engine/build/${bin} not found"
done
ok "All engine binaries built"
# Fix rpaths: replace any absolute build-dir rpaths with @executable_path
# so dylibs are found relative to the binary (portable release requirement).
echo " Fixing dylib rpaths for portability..."
for bin in engine/build/ace-server engine/build/ace-lm engine/build/ace-synth \
engine/build/ace-understand engine/build/neural-codec engine/build/quantize; do
if [ -f "$bin" ]; then
# Remove all existing rpaths (may be absolute build paths)
for old_rpath in $(otool -l "$bin" 2>/dev/null | grep -A1 LC_RPATH | grep "path " | awk '{print $2}'); do
install_name_tool -delete_rpath "$old_rpath" "$bin" 2>/dev/null || true
done
# Add the portable @executable_path rpath
install_name_tool -add_rpath "@executable_path" "$bin" 2>/dev/null || true
fi
done
ok "Rpaths fixed (@executable_path)"
# ── Step 2: Install server deps (production) ─────────────────────────
step 2 "Installing server dependencies (production)..."
cd server
rm -rf node_modules
npm install --omit=dev --loglevel=warn 2>&1 | tail -3
# Rebuild native modules
npm rebuild better-sqlite3 2>&1 | tail -1
# tsx is in devDependencies but needed at runtime to run the TypeScript server
npm install tsx --save-optional --loglevel=warn 2>&1 | tail -1
ok "Server dependencies installed (production + tsx)"
cd "${ROOT_DIR}"
# ── Step 3: Build UI ─────────────────────────────────────────────────
step 3 "Building production UI..."
cd ui
[ -d "node_modules" ] || npm install --loglevel=warn
npx vite build 2>&1 | tail -3
ok "UI built"
cd "${ROOT_DIR}"
# ── Step 4: Download Node.js runtime ─────────────────────────────────
step 4 "Downloading Node.js ${NODE_VERSION} runtime..."
NODE_CACHE="${ROOT_DIR}/release-staging/node-cache"
NODE_ARCHIVE="${NODE_CACHE}/node-v${NODE_VERSION}-darwin-${ARCH}.tar.xz"
NODE_EXTRACTED="${NODE_CACHE}/node-v${NODE_VERSION}-darwin-${ARCH}"
mkdir -p "${NODE_CACHE}"
if [ -f "${NODE_EXTRACTED}/bin/node" ]; then
ok "Using cached Node.js download"
else
echo " Downloading from ${NODE_URL}..."
curl -L --progress-bar -o "${NODE_ARCHIVE}" "${NODE_URL}"
cd "${NODE_CACHE}"
tar xf "$(basename "${NODE_ARCHIVE}")"
cd "${ROOT_DIR}"
ok "Node.js ${NODE_VERSION} downloaded"
fi
# ── Step 5: Assemble release ─────────────────────────────────────────
step 5 "Assembling release package..."
rm -rf "${STAGING_DIR}"
mkdir -p "${STAGING_DIR}"
# Engine binaries + dylibs
mkdir -p "${STAGING_DIR}/engine/build"
for bin in ace-server ace-lm ace-synth ace-understand mastering mp3-codec neural-codec quantize vst-host; do
[ -f "engine/build/${bin}" ] && cp "engine/build/${bin}" "${STAGING_DIR}/engine/build/"
done
# Copy ggml dylibs
cp engine/build/libggml*.dylib "${STAGING_DIR}/engine/build/" 2>/dev/null || true
# Lua plugins — dual directory system:
# engine/plugins/ = native/built-in plugins (solvers, schedulers, guidance)
# plugins/ = community/user plugins (same structure, overrides native)
# ace-server scans both via engine_dir and project_dir resolution from binary path
if [ -d "engine/plugins" ]; then
mkdir -p "${STAGING_DIR}/engine/plugins"
cp -r engine/plugins/* "${STAGING_DIR}/engine/plugins/"
PLUGIN_COUNT=$(find "${STAGING_DIR}/engine/plugins" -name "*.lua" | wc -l | tr -d ' ')
ok "${PLUGIN_COUNT} native plugins copied (engine/plugins/)"
fi
if [ -d "plugins" ]; then
cp -r plugins "${STAGING_DIR}/"
ok "Community plugins dir copied (plugins/)"
fi
# Server (source + node_modules, no devDependencies)
mkdir -p "${STAGING_DIR}/server"
cp server/package.json server/package-lock.json "${STAGING_DIR}/server/"
cp server/tsconfig.json "${STAGING_DIR}/server/"
cp -r server/node_modules "${STAGING_DIR}/server/"
cp -r server/src "${STAGING_DIR}/server/"
[ -f server/migrate_lireek.cjs ] && cp server/migrate_lireek.cjs "${STAGING_DIR}/server/"
ok "Server copied"
# Pre-built UI
mkdir -p "${STAGING_DIR}/ui"
cp -r ui/dist "${STAGING_DIR}/ui/"
ok "UI copied"
# Node.js runtime (just the binary + npm essentials)
mkdir -p "${STAGING_DIR}/runtime"
cp "${NODE_EXTRACTED}/bin/node" "${STAGING_DIR}/runtime/node"
# Copy npm/npx for server to use tsx
mkdir -p "${STAGING_DIR}/runtime/lib"
cp -r "${NODE_EXTRACTED}/lib/node_modules" "${STAGING_DIR}/runtime/lib/"
# Create npx/npm symlinks
ln -sf "../lib/node_modules/npm/bin/npm-cli.js" "${STAGING_DIR}/runtime/npm"
ln -sf "../lib/node_modules/npm/bin/npx-cli.js" "${STAGING_DIR}/runtime/npx"
ok "Node.js runtime bundled"
# Empty directories for user content
mkdir -p "${STAGING_DIR}/models"
mkdir -p "${STAGING_DIR}/adapters"
# Scripts
cp launch.sh "${STAGING_DIR}/"
cp .env.example "${STAGING_DIR}/"
chmod +x "${STAGING_DIR}/launch.sh"
# README snippet for release
cat > "${STAGING_DIR}/README.txt" << 'HEREDOC'
HOT-Step CPP — macOS (Apple Silicon)
Quick Start:
1. Extract this archive
2. Run: ./launch.sh
3. Open http://localhost:3001 in your browser
4. Go to Models → Get More Models to download AI models (~7 GB)
If macOS blocks the app (unsigned binary):
xattr -cr /path/to/HOT-Step-CPP-macOS-arm64/
Requirements:
- macOS 13+ (Apple Silicon)
- ~10 GB free disk space
- No other software needed — Node.js is bundled
For the full README, source code, and updates:
https://github.com/scragnog/HOT-Step-CPP
HEREDOC
ok "Release assembled"
# ── Step 6: Set permissions ──────────────────────────────────────────
step 6 "Setting permissions..."
find "${STAGING_DIR}/engine/build" -type f -perm +0111 -exec chmod +x {} \; 2>/dev/null || true
chmod +x "${STAGING_DIR}/engine/build/"* 2>/dev/null || true
chmod +x "${STAGING_DIR}/runtime/node"
# npm/npx are symlinks to .js files — make targets executable
chmod +x "${STAGING_DIR}/runtime/lib/node_modules/npm/bin/npm-cli.js" 2>/dev/null || true
chmod +x "${STAGING_DIR}/runtime/lib/node_modules/npm/bin/npx-cli.js" 2>/dev/null || true
chmod +x "${STAGING_DIR}/launch.sh"
ok "Permissions set"
# ── Step 7: Create archive ──────────────────────────────────────────
step 7 "Creating archive..."
cd release-staging
tar czf "${OUTPUT_FILE}" "${RELEASE_NAME}"
cd "${ROOT_DIR}"
# Size info
SIZE_MB=$(du -sm "${OUTPUT_FILE}" | cut -f1)
ok "${RELEASE_NAME}.tar.gz (${SIZE_MB} MB)"
# ── Done ─────────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}${BOLD}╔══════════════════════════════════════════╗${NC}"
echo -e "${GREEN}${BOLD}║ Release package ready! 🎉 ║${NC}"
echo -e "${GREEN}${BOLD}╚══════════════════════════════════════════╝${NC}"
echo ""
echo " File: ${OUTPUT_FILE}"
echo " Size: ${SIZE_MB} MB"
echo ""
echo " Test locally:"
echo " cd release-staging/${RELEASE_NAME}"
echo " ./launch.sh"
echo ""
# Clean up staging (keep the archive)
# rm -rf release-staging