299 lines
12 KiB
Bash
Executable File
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
|