#!/bin/zsh set -e # Exit on any error set -o pipefail # Exit if any command in a pipeline fails # Get the project directory if [ -z "${SRCROOT}" ]; then # If SRCROOT is not set (running outside Xcode), determine it from script location SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" else PROJECT_DIR="${SRCROOT}" fi WEB_DIR="${PROJECT_DIR}/../web" HASH_FILE="${BUILT_PRODUCTS_DIR}/.web-content-hash" PREVIOUS_HASH_FILE="${BUILT_PRODUCTS_DIR}/.web-content-hash.previous" PUBLIC_DIR="${WEB_DIR}/public" # Set destination directory if [ -z "${BUILT_PRODUCTS_DIR}" ]; then # Default for testing outside Xcode DEST_DIR="/tmp/vibetunnel-web-build" else DEST_DIR="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/web/public" fi APP_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" # In CI with pre-built artifacts, skip the entire build process if [ "${CI}" = "true" ] && [ -f "${WEB_DIR}/dist/server/server.js" ]; then echo "✓ CI environment detected with pre-built web artifacts" echo "✓ Skipping web frontend build entirely" # Still need to copy the pre-built files to the app bundle # Clean and create destination directory echo "Cleaning destination directory..." rm -rf "${DEST_DIR}" mkdir -p "${DEST_DIR}" # Copy built files to Resources echo "Copying pre-built web files to app bundle..." if [ -d "${PUBLIC_DIR}" ]; then cp -R "${PUBLIC_DIR}/"* "${DEST_DIR}/" fi # Copy native executable and modules to app bundle if they exist NATIVE_DIR="${WEB_DIR}/native" if [ -f "${NATIVE_DIR}/vibetunnel" ]; then echo "Copying native executable to app bundle..." cp "${NATIVE_DIR}/vibetunnel" "${APP_RESOURCES}/" chmod +x "${APP_RESOURCES}/vibetunnel" fi if [ -f "${NATIVE_DIR}/pty.node" ]; then cp "${NATIVE_DIR}/pty.node" "${APP_RESOURCES}/" fi if [ -f "${NATIVE_DIR}/spawn-helper" ]; then cp "${NATIVE_DIR}/spawn-helper" "${APP_RESOURCES}/" chmod +x "${APP_RESOURCES}/spawn-helper" fi if [ -f "${NATIVE_DIR}/authenticate_pam.node" ]; then cp "${NATIVE_DIR}/authenticate_pam.node" "${APP_RESOURCES}/" fi if [ -f "${WEB_DIR}/bin/vt" ]; then cp "${WEB_DIR}/bin/vt" "${APP_RESOURCES}/" chmod +x "${APP_RESOURCES}/vt" fi echo "✓ Pre-built web artifacts copied successfully" exit 0 fi # Read the current hash if [ -f "${HASH_FILE}" ]; then CURRENT_HASH=$(cat "${HASH_FILE}") else # If hash file doesn't exist, we need to rebuild # Generate a unique hash to force rebuild CURRENT_HASH="no-hash-file-$(date +%s)" echo "Hash file not found at ${HASH_FILE}. Will rebuild..." fi # Check if we need to rebuild NEED_REBUILD=1 # Check if previous hash exists and matches current if [ -f "${PREVIOUS_HASH_FILE}" ]; then PREVIOUS_HASH=$(cat "${PREVIOUS_HASH_FILE}") if [ "${CURRENT_HASH}" = "${PREVIOUS_HASH}" ]; then # Also check if the built files actually exist if [ -d "${DEST_DIR}" ] && [ -f "${APP_RESOURCES}/vibetunnel" ] && [ -f "${APP_RESOURCES}/pty.node" ] && [ -f "${APP_RESOURCES}/spawn-helper" ]; then echo "Web content unchanged and build outputs exist. Skipping rebuild." NEED_REBUILD=0 else echo "Web content unchanged but build outputs missing. Rebuilding..." fi else echo "Web content changed. Hash: ${PREVIOUS_HASH} -> ${CURRENT_HASH}" fi else echo "No previous build hash found. Building web frontend..." fi if [ ${NEED_REBUILD} -eq 0 ]; then echo "Skipping web frontend build (no changes detected)" exit 0 fi echo "Building web frontend..." # Setup Node.js PATH (Homebrew, nvm, Volta, fnm) SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )" # Set environment variable to use clean build environment export VIBETUNNEL_BUILD_CLEAN_ENV=true source "${SCRIPT_DIR}/node-path-setup.sh" # Export CI to prevent interactive prompts export CI=true # Check if pnpm is available (skip in CI when web artifacts are pre-built) if [ "${SKIP_NODE_CHECK}" = "true" ] && [ "${CI}" = "true" ]; then echo "✓ Skipping pnpm check in CI (web artifacts are pre-built)" echo "✓ This script should not be running in CI - web build should already be complete" exit 0 fi if ! command -v pnpm &> /dev/null; then echo "error: pnpm not found. Please install pnpm" exit 1 fi echo "Using pnpm version: $(pnpm --version)" echo "Using Node.js version: $(node --version)" # Check if web directory exists if [ ! -d "${WEB_DIR}" ]; then echo "error: Web directory not found at ${WEB_DIR}" exit 1 fi # Change to web directory cd "${WEB_DIR}" # Clean build artifacts echo "Cleaning build artifacts..." rm -rf dist public/bundle public/output.css native # Install dependencies echo "Installing dependencies..." # For Xcode builds, ensure C++20 standard for native modules export MACOSX_DEPLOYMENT_TARGET="14.0" export CXXFLAGS="-std=c++20 -stdlib=libc++ -mmacosx-version-min=14.0" export CXX="${CXX:-clang++}" export CC="${CC:-clang}" # Filter common non-actionable warnings from build output filter_build_output() { # Allow bypassing filter with VERBOSE_BUILD environment variable if [ "${VERBOSE_BUILD:-false}" = "true" ]; then cat # Pass through unfiltered return fi local patterns=( # C++ compiler warnings from node-gyp builds 'missing field .* initializer' 'expanded from macro' 'converts to incompatible function type' 'instantiation of function template' # Deprecation warnings 'is deprecated.*Use.*instead' 'has been explicitly marked deprecated' # npm/pnpm configuration warnings 'npm warn Unknown.*config' # Tailwind CSS content configuration warnings 'warn - Your.*content.*configuration' 'warn - Pattern:.*\*\.js' 'warn - See our documentation' # Build tool information messages 'gyp info spawn args' '\.\.\.\/node_modules\/.*install:' # ESBuild bundle size warnings 'exceeds recommended size limit' 'This can impact web performance' ) # Combine patterns with OR operator local pattern=$(IFS='|'; echo "${patterns[*]}") grep -v -E "$pattern" || true } # Run pnpm install with filtered output pnpm install --frozen-lockfile 2>&1 | filter_build_output # Determine build configuration BUILD_CONFIG="${CONFIGURATION:-Debug}" echo "Build configuration: $BUILD_CONFIG" # Check for custom Node.js build echo "Searching for custom Node.js builds..." # Find all Node build directories if [ -d "${WEB_DIR}/.node-builds" ]; then ALL_NODE_DIRS=$(find "${WEB_DIR}/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V) else ALL_NODE_DIRS="" fi if [ -n "$ALL_NODE_DIRS" ]; then echo "Found Node.js build directories:" echo "$ALL_NODE_DIRS" | while read -r dir; do if [ -f "$dir/out/Release/node" ]; then VERSION=$(basename "$dir" | sed 's/node-v\(.*\)-minimal/\1/') echo " ✓ $VERSION (complete build)" else VERSION=$(basename "$dir" | sed 's/node-v\(.*\)-minimal/\1/') echo " ✗ $VERSION (incomplete build - missing binary)" fi done fi # Find directories with complete builds (containing the actual node binary) if [ -d "${WEB_DIR}/.node-builds" ]; then CUSTOM_NODE_DIR=$(find "${WEB_DIR}/.node-builds" -name "node-v*-minimal" -type d -exec test -f {}/out/Release/node \; -print 2>/dev/null | sort -V | tail -n1) else CUSTOM_NODE_DIR="" fi CUSTOM_NODE_PATH="${CUSTOM_NODE_DIR}/out/Release/node" if [ -n "$CUSTOM_NODE_DIR" ]; then SELECTED_VERSION=$(basename "$CUSTOM_NODE_DIR" | sed 's/node-v\(.*\)-minimal/\1/') echo "Selected custom Node.js v$SELECTED_VERSION" else echo "No complete custom Node.js builds found" fi # Build the web frontend if [ "$BUILD_CONFIG" = "Release" ]; then echo "Release build - checking for custom Node.js..." # Skip custom Node.js build in CI to avoid timeout if [ "${CI:-false}" = "true" ]; then echo "CI environment detected - skipping custom Node.js build to avoid timeout" echo "The app will be larger than optimal but will build within CI time limits." pnpm run build 2>&1 | filter_build_output elif [ ! -f "$CUSTOM_NODE_PATH" ]; then echo "Custom Node.js not found, building it for optimal size..." echo "This will take 10-20 minutes on first run but will be cached." node build-custom-node.js --latest 2>&1 | filter_build_output if [ -d "${WEB_DIR}/.node-builds" ]; then CUSTOM_NODE_DIR=$(find "${WEB_DIR}/.node-builds" -name "node-v*-minimal" -type d -exec test -f {}/out/Release/node \; -print 2>/dev/null | sort -V | tail -n1) else CUSTOM_NODE_DIR="" fi CUSTOM_NODE_PATH="${CUSTOM_NODE_DIR}/out/Release/node" fi if [ "${CI:-false}" != "true" ] && [ -f "$CUSTOM_NODE_PATH" ]; then CUSTOM_NODE_VERSION=$("$CUSTOM_NODE_PATH" --version 2>/dev/null || echo "unknown") CUSTOM_NODE_SIZE=$(ls -lh "$CUSTOM_NODE_PATH" 2>/dev/null | awk '{print $5}' || echo "unknown") echo "Using custom Node.js for release build:" echo " Version: $CUSTOM_NODE_VERSION" echo " Size: $CUSTOM_NODE_SIZE (vs ~110MB for standard Node.js)" echo " Path: $CUSTOM_NODE_PATH" pnpm run build -- --custom-node 2>&1 | filter_build_output else echo "WARNING: Custom Node.js build failed, using system Node.js" echo "The app will be larger than optimal." pnpm run build 2>&1 | filter_build_output fi else # Debug build if [ -f "$CUSTOM_NODE_PATH" ]; then CUSTOM_NODE_VERSION=$("$CUSTOM_NODE_PATH" --version 2>/dev/null || echo "unknown") echo "Debug build - found existing custom Node.js $CUSTOM_NODE_VERSION, using it for consistency" pnpm run build -- --custom-node 2>&1 | filter_build_output else echo "Debug build - using system Node.js for faster builds" echo "System Node.js: $(node --version)" echo "To use custom Node.js in debug builds, run: cd web && node build-custom-node.js --latest" pnpm run build 2>&1 | filter_build_output fi fi # Clean and create destination directory echo "Cleaning destination directory..." rm -rf "${DEST_DIR}" mkdir -p "${DEST_DIR}" # Copy built files to Resources echo "Copying web files to app bundle..." cp -R "${PUBLIC_DIR}/"* "${DEST_DIR}/" # Copy native executable and modules to app bundle NATIVE_DIR="${WEB_DIR}/native" if [ -f "${NATIVE_DIR}/vibetunnel" ]; then echo "Copying native executable to app bundle..." EXEC_SIZE=$(ls -lh "${NATIVE_DIR}/vibetunnel" | awk '{print $5}') echo " Executable size: $EXEC_SIZE" cp "${NATIVE_DIR}/vibetunnel" "${APP_RESOURCES}/" chmod +x "${APP_RESOURCES}/vibetunnel" else echo "error: Native executable not found at ${NATIVE_DIR}/vibetunnel" exit 1 fi if [ -f "${NATIVE_DIR}/pty.node" ]; then echo "Copying pty.node..." cp "${NATIVE_DIR}/pty.node" "${APP_RESOURCES}/" else echo "error: pty.node not found" exit 1 fi if [ -f "${NATIVE_DIR}/spawn-helper" ]; then echo "Copying spawn-helper..." cp "${NATIVE_DIR}/spawn-helper" "${APP_RESOURCES}/" chmod +x "${APP_RESOURCES}/spawn-helper" else echo "error: spawn-helper not found" exit 1 fi # Copy authenticate_pam.node if it exists if [ -f "${NATIVE_DIR}/authenticate_pam.node" ]; then echo "Copying authenticate_pam.node..." cp "${NATIVE_DIR}/authenticate_pam.node" "${APP_RESOURCES}/" else echo "Warning: authenticate_pam.node not found. PAM authentication may not work." fi # Copy unified vt script if [ -f "${WEB_DIR}/bin/vt" ]; then echo "Copying unified vt script..." cp "${WEB_DIR}/bin/vt" "${APP_RESOURCES}/" chmod +x "${APP_RESOURCES}/vt" else echo "error: Unified vt script not found at ${WEB_DIR}/bin/vt" exit 1 fi echo "✓ Native executable, modules, and vt script copied successfully" # Sanity check: Verify all required binaries are present in the app bundle echo "Performing final sanity check..." MISSING_FILES=() # Check for vibetunnel executable if [ ! -f "${APP_RESOURCES}/vibetunnel" ]; then MISSING_FILES+=("vibetunnel executable") fi # Check for pty.node if [ ! -f "${APP_RESOURCES}/pty.node" ]; then MISSING_FILES+=("pty.node native module") fi # Check for spawn-helper (Unix only) if [ ! -f "${APP_RESOURCES}/spawn-helper" ]; then MISSING_FILES+=("spawn-helper") fi # Check if vibetunnel is executable if [ -f "${APP_RESOURCES}/vibetunnel" ] && [ ! -x "${APP_RESOURCES}/vibetunnel" ]; then MISSING_FILES+=("vibetunnel is not executable") fi # Check if spawn-helper is executable if [ -f "${APP_RESOURCES}/spawn-helper" ] && [ ! -x "${APP_RESOURCES}/spawn-helper" ]; then MISSING_FILES+=("spawn-helper is not executable") fi # Check for vt script if [ ! -f "${APP_RESOURCES}/vt" ]; then MISSING_FILES+=("vt script") fi # Check if vt script is executable if [ -f "${APP_RESOURCES}/vt" ] && [ ! -x "${APP_RESOURCES}/vt" ]; then MISSING_FILES+=("vt script is not executable") fi # If any files are missing, fail the build if [ ${#MISSING_FILES[@]} -gt 0 ]; then echo "error: Build sanity check failed! Missing required files:" for file in "${MISSING_FILES[@]}"; do echo " - $file" done echo "Build artifacts in ${NATIVE_DIR}:" ls -la "${NATIVE_DIR}" || echo " Directory does not exist" echo "App resources in ${APP_RESOURCES}:" ls -la "${APP_RESOURCES}/vibetunnel" "${APP_RESOURCES}/pty.node" "${APP_RESOURCES}/spawn-helper" "${APP_RESOURCES}/vt" 2>/dev/null || true exit 1 fi # Verify the executable works echo "Verifying vibetunnel executable..." echo "Full path: ${APP_RESOURCES}/vibetunnel" if "${APP_RESOURCES}/vibetunnel" version &>/dev/null; then VERSION_OUTPUT=$("${APP_RESOURCES}/vibetunnel" version 2>&1 | head -1) echo "✓ VibeTunnel executable verified: $VERSION_OUTPUT" else echo "error: VibeTunnel executable failed verification (version command failed)" echo "Full executable path: ${APP_RESOURCES}/vibetunnel" echo "Checking if file exists and is executable:" ls -la "${APP_RESOURCES}/vibetunnel" || echo "File not found!" echo "Attempting to run with error output:" "${APP_RESOURCES}/vibetunnel" version 2>&1 || true exit 1 fi echo "✓ All sanity checks passed" # Save the current hash as the previous hash for next build if [ -f "${HASH_FILE}" ]; then cp "${HASH_FILE}" "${PREVIOUS_HASH_FILE}" else # If hash file doesn't exist, create it with the current hash value echo "${CURRENT_HASH}" > "${PREVIOUS_HASH_FILE}" fi echo "Web frontend build completed successfully"