mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Add logic to use custom node compiler
This commit is contained in:
parent
dbe280a68e
commit
180caf7e81
14 changed files with 812 additions and 335 deletions
115
docs/custom-node.md
Normal file
115
docs/custom-node.md
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
# Custom Node.js Build
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
VibeTunnel uses Node.js Single Executable Applications (SEA) to create a standalone terminal server. However, the standard Node.js binary is quite large:
|
||||||
|
|
||||||
|
- **Standard Node.js binary**: ~110MB
|
||||||
|
- **Custom minimal Node.js**: ~43MB (61% reduction)
|
||||||
|
- **Final executable size**: ~45MB (down from ~105MB)
|
||||||
|
- **Final app size impact**: Reduces app from ~130MB to ~88MB
|
||||||
|
|
||||||
|
We don't need many Node.js features for VibeTunnel:
|
||||||
|
- No internationalization (ICU) support needed
|
||||||
|
- No npm package manager in the binary
|
||||||
|
- No inspector/debugging protocol
|
||||||
|
- No V8 snapshots or code cache
|
||||||
|
|
||||||
|
By building a custom Node.js without these features, we achieve a significantly smaller app bundle while maintaining full functionality.
|
||||||
|
|
||||||
|
## Build Behavior
|
||||||
|
|
||||||
|
### Debug Mode (Xcode)
|
||||||
|
- Uses system Node.js for faster iteration
|
||||||
|
- No custom Node.js compilation required
|
||||||
|
- Build output shows: `"Debug build - using system Node.js for faster builds"`
|
||||||
|
- If a custom Node.js was previously built, it will be reused for consistency
|
||||||
|
|
||||||
|
### Release Mode (Xcode)
|
||||||
|
- Automatically builds custom minimal Node.js on first run
|
||||||
|
- Compilation takes 10-20 minutes but is cached for future builds
|
||||||
|
- Uses the custom Node.js to create a smaller executable
|
||||||
|
- Build output shows version and size comparison
|
||||||
|
|
||||||
|
## Build Automation
|
||||||
|
|
||||||
|
### Release Builds
|
||||||
|
The release script (`mac/scripts/release.sh`) automatically checks for and builds custom Node.js if needed. You don't need to manually build it before releases.
|
||||||
|
|
||||||
|
### Manual Custom Node.js Build
|
||||||
|
|
||||||
|
To build the custom Node.js manually (outside of Xcode):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
node build-custom-node.js --latest
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Download the latest Node.js source
|
||||||
|
2. Configure it without unnecessary features
|
||||||
|
3. Build with optimizations (`-Os`, `-flto`, etc.)
|
||||||
|
4. Cache the result in `web/.node-builds/`
|
||||||
|
|
||||||
|
To use the custom Node.js for building the executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
npm run build -- --custom-node
|
||||||
|
```
|
||||||
|
|
||||||
|
Or directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node build-native.js --custom-node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Process Details
|
||||||
|
|
||||||
|
### Automatic Detection
|
||||||
|
The build system automatically searches for custom Node.js builds in `.node-builds/` when `--custom-node` is passed without a path. It finds the most recent build by checking directory modification times.
|
||||||
|
|
||||||
|
### Code Signing on macOS
|
||||||
|
When building the executable:
|
||||||
|
1. The Node.js binary is injected with our JavaScript code (SEA process)
|
||||||
|
2. The binary is stripped to remove debug symbols
|
||||||
|
3. The executable is re-signed with an ad-hoc signature
|
||||||
|
|
||||||
|
Note: You may see a warning about "invalidating the code signature" during the strip process - this is expected and harmless since we re-sign immediately after.
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Features Disabled
|
||||||
|
- `--without-intl` - Removes internationalization support
|
||||||
|
- `--without-npm` - Excludes npm from the binary
|
||||||
|
- `--without-corepack` - Removes package manager wrapper
|
||||||
|
- `--without-inspector` - Disables debugging protocol
|
||||||
|
- `--without-node-snapshot` - Skips V8 snapshot (~2-3MB)
|
||||||
|
- `--without-node-code-cache` - Skips code cache (~1-2MB)
|
||||||
|
|
||||||
|
### Optimization Flags
|
||||||
|
- `-Os` - Optimize for size
|
||||||
|
- `-flto` - Link-time optimization
|
||||||
|
- `-ffunction-sections` / `-fdata-sections` - Enable dead code elimination
|
||||||
|
- `-Wl,-dead_strip` - Remove unused code at link time
|
||||||
|
|
||||||
|
### Build Cache
|
||||||
|
Custom Node.js builds are stored in `web/.node-builds/` and are excluded from git via `.gitignore`. The build system automatically detects and reuses existing builds.
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
|
||||||
|
- Build script: `web/build-custom-node.js`
|
||||||
|
- Native executable builder: `web/build-native.js`
|
||||||
|
- Xcode integration: `mac/scripts/build-web-frontend.sh`
|
||||||
|
- Build output: `web/.node-builds/node-v*-minimal/`
|
||||||
|
- Final executable: `web/native/vibetunnel`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Custom Node.js not detected
|
||||||
|
- Ensure the build completed successfully: check for `.node-builds/node-v*-minimal/out/Release/node`
|
||||||
|
- In Debug mode, the system will use custom Node.js if already built
|
||||||
|
- In Release mode, it will build custom Node.js automatically if not present
|
||||||
|
|
||||||
|
### Code signature warnings
|
||||||
|
The warning "changes being made to the file will invalidate the code signature" is expected and handled automatically. The build process re-signs the executable after all modifications.
|
||||||
|
|
@ -251,7 +251,7 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/zsh;
|
shellPath = /bin/zsh;
|
||||||
shellScript = "# Build web frontend conditionally based on hash\necho \"Checking if web frontend needs rebuild...\"\n\n# Run the conditional build script\n\"${SRCROOT}/scripts/build-web-frontend-conditional.sh\"\n\nif [ $? -ne 0 ]; then\n echo \"error: Web frontend build failed\"\n exit 1\nfi\n";
|
shellScript = "# Build web frontend conditionally based on hash\necho \"Checking if web frontend needs rebuild...\"\n\n# Run the conditional build script\n\"${SRCROOT}/scripts/build-web-frontend.sh\"\n\nif [ $? -ne 0 ]; then\n echo \"error: Web frontend build failed\"\n exit 1\nfi\n";
|
||||||
};
|
};
|
||||||
C3D4E5F6A7B8C9D0E1F23456 /* Install Build Dependencies */ = {
|
C3D4E5F6A7B8C9D0E1F23456 /* Install Build Dependencies */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
|
@ -270,7 +270,7 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/zsh;
|
shellPath = /bin/zsh;
|
||||||
shellScript = "# Install build dependencies\necho \"Checking build dependencies...\"\n\n# Run the install script\n\"${SRCROOT}/scripts/install-bun.sh\"\n\nif [ $? -ne 0 ]; then\n echo \"error: Failed to install build dependencies\"\n exit 1\nfi\n";
|
shellScript = "# Check for Node.js availability\necho \"Checking build dependencies...\"\n\n# Run the install script\n\"${SRCROOT}/scripts/install-node.sh\"\n\nif [ $? -ne 0 ]; then\n echo \"error: Node.js is required to build VibeTunnel\"\n exit 1\nfi\n";
|
||||||
};
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# Build and copy Node.js SEA executable and native modules to the app bundle
|
|
||||||
# ARM64 only - VibeTunnel requires Apple Silicon
|
|
||||||
#
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Add common Node.js installation paths to PATH
|
|
||||||
# Homebrew on Apple Silicon
|
|
||||||
if [ -d "/opt/homebrew/bin" ]; then
|
|
||||||
export PATH="/opt/homebrew/bin:$PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Homebrew on Intel Macs
|
|
||||||
if [ -d "/usr/local/bin" ]; then
|
|
||||||
export PATH="/usr/local/bin:$PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# NVM default location
|
|
||||||
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
|
||||||
. "$NVM_DIR/nvm.sh"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Node Version Manager (n)
|
|
||||||
if [ -d "/usr/local/n/versions" ]; then
|
|
||||||
export PATH="/usr/local/bin:$PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# MacPorts
|
|
||||||
if [ -d "/opt/local/bin" ]; then
|
|
||||||
export PATH="/opt/local/bin:$PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Export CI environment variable to prevent interactive prompts
|
|
||||||
export CI=true
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Script directory
|
|
||||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
||||||
PROJECT_ROOT="$SCRIPT_DIR/../.."
|
|
||||||
WEB_DIR="$PROJECT_ROOT/web"
|
|
||||||
NATIVE_DIR="$WEB_DIR/native"
|
|
||||||
|
|
||||||
# Destination from Xcode (passed as argument or use BUILT_PRODUCTS_DIR)
|
|
||||||
if [ $# -eq 0 ]; then
|
|
||||||
if [ -z "${BUILT_PRODUCTS_DIR:-}" ]; then
|
|
||||||
echo -e "${RED}Error: No destination path provided and BUILT_PRODUCTS_DIR not set${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
DEST_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
|
||||||
else
|
|
||||||
DEST_RESOURCES="$1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}Building and copying Node.js SEA executable (ARM64 only)...${NC}"
|
|
||||||
|
|
||||||
# Change to web directory
|
|
||||||
cd "$WEB_DIR"
|
|
||||||
|
|
||||||
# Check if native directory exists, if not use prebuilts
|
|
||||||
if [ ! -d "$NATIVE_DIR" ] || [ ! -f "$NATIVE_DIR/vibetunnel" ]; then
|
|
||||||
echo -e "${YELLOW}Native directory not found. Checking for prebuilt binaries...${NC}"
|
|
||||||
|
|
||||||
# Use prebuilt binaries if available
|
|
||||||
PREBUILTS_DIR="$PROJECT_ROOT/mac/Resources/BunPrebuilts"
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
|
|
||||||
if [ "$ARCH" != "arm64" ]; then
|
|
||||||
echo -e "${RED}Error: VibeTunnel requires Apple Silicon (ARM64)${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -d "$PREBUILTS_DIR/arm64" ] && [ -f "$PREBUILTS_DIR/arm64/vibetunnel" ]; then
|
|
||||||
echo -e "${GREEN}Using prebuilt binaries for ARM64${NC}"
|
|
||||||
mkdir -p "$NATIVE_DIR"
|
|
||||||
cp "$PREBUILTS_DIR/arm64/vibetunnel" "$NATIVE_DIR/"
|
|
||||||
cp "$PREBUILTS_DIR/arm64/pty.node" "$NATIVE_DIR/"
|
|
||||||
cp "$PREBUILTS_DIR/arm64/spawn-helper" "$NATIVE_DIR/"
|
|
||||||
chmod +x "$NATIVE_DIR/vibetunnel"
|
|
||||||
chmod +x "$NATIVE_DIR/spawn-helper"
|
|
||||||
else
|
|
||||||
# Try to build with Node.js
|
|
||||||
echo -e "${YELLOW}Prebuilt binaries not found. Attempting to build...${NC}"
|
|
||||||
|
|
||||||
# Check if build-native.js exists
|
|
||||||
if [ -f "build-native.js" ]; then
|
|
||||||
# Use Node.js to build
|
|
||||||
if command -v node &> /dev/null; then
|
|
||||||
echo "Using Node.js to build SEA executable..."
|
|
||||||
echo "Using Node.js version: $(node --version)"
|
|
||||||
echo "PATH: $PATH"
|
|
||||||
node build-native.js
|
|
||||||
else
|
|
||||||
echo -e "${RED}Error: Node.js not found in PATH${NC}"
|
|
||||||
echo -e "${RED}PATH is: $PATH${NC}"
|
|
||||||
echo -e "${RED}Please install Node.js 20+ or ensure prebuilt binaries are available in:${NC}"
|
|
||||||
echo -e "${RED} $PREBUILTS_DIR/arm64/${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo -e "${RED}Error: build-native.js not found and no prebuilt binaries available${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify native files exist
|
|
||||||
if [ ! -f "$NATIVE_DIR/vibetunnel" ]; then
|
|
||||||
echo -e "${RED}Error: Executable not found at $NATIVE_DIR/vibetunnel${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy executable
|
|
||||||
echo "Copying executable to app bundle..."
|
|
||||||
cp "$NATIVE_DIR/vibetunnel" "$DEST_RESOURCES/"
|
|
||||||
chmod +x "$DEST_RESOURCES/vibetunnel"
|
|
||||||
|
|
||||||
# Copy native modules
|
|
||||||
if [ -f "$NATIVE_DIR/pty.node" ]; then
|
|
||||||
echo "Copying pty.node..."
|
|
||||||
cp "$NATIVE_DIR/pty.node" "$DEST_RESOURCES/"
|
|
||||||
else
|
|
||||||
echo -e "${RED}Error: pty.node not found${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$NATIVE_DIR/spawn-helper" ]; then
|
|
||||||
echo "Copying spawn-helper..."
|
|
||||||
cp "$NATIVE_DIR/spawn-helper" "$DEST_RESOURCES/"
|
|
||||||
chmod +x "$DEST_RESOURCES/spawn-helper"
|
|
||||||
else
|
|
||||||
echo -e "${RED}Error: spawn-helper not found${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}✓ Executable and native modules copied successfully${NC}"
|
|
||||||
|
|
||||||
# Verify the files
|
|
||||||
echo "Verifying copied files:"
|
|
||||||
ls -la "$DEST_RESOURCES/vibetunnel" || echo "vibetunnel not found!"
|
|
||||||
ls -la "$DEST_RESOURCES/pty.node" || echo "pty.node not found!"
|
|
||||||
ls -la "$DEST_RESOURCES/spawn-helper" || echo "spawn-helper not found!"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}Note: VibeTunnel requires Apple Silicon (M1/M2/M3) Macs.${NC}"
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
#!/bin/zsh
|
|
||||||
set -e # Exit on any error
|
|
||||||
|
|
||||||
# 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}"
|
|
||||||
|
|
||||||
# Read the current hash
|
|
||||||
if [ -f "${HASH_FILE}" ]; then
|
|
||||||
CURRENT_HASH=$(cat "${HASH_FILE}")
|
|
||||||
else
|
|
||||||
echo "error: Hash file not found. Run 'Calculate Web Hash' build phase first."
|
|
||||||
exit 1
|
|
||||||
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 PATH for Node.js
|
|
||||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
|
||||||
|
|
||||||
# Load NVM if available
|
|
||||||
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
|
||||||
. "$NVM_DIR/nvm.sh"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Put volta on the path if it exists
|
|
||||||
export PATH="$HOME/.volta/bin:$PATH"
|
|
||||||
|
|
||||||
# Trigger rehash
|
|
||||||
rehash
|
|
||||||
|
|
||||||
# Export CI to prevent interactive prompts
|
|
||||||
export CI=true
|
|
||||||
|
|
||||||
# Check if npm is available
|
|
||||||
if ! command -v npm &> /dev/null; then
|
|
||||||
echo "error: npm not found. Please install Node.js"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Using npm version: $(npm --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
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
echo "Installing dependencies..."
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Build the web frontend
|
|
||||||
echo "Building web frontend..."
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# 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 root
|
|
||||||
echo "Copying native executable and modules..."
|
|
||||||
NATIVE_DIR="${WEB_DIR}/native"
|
|
||||||
|
|
||||||
if [ -f "${NATIVE_DIR}/vibetunnel" ]; then
|
|
||||||
cp "${NATIVE_DIR}/vibetunnel" "${APP_RESOURCES}/"
|
|
||||||
chmod +x "${APP_RESOURCES}/vibetunnel"
|
|
||||||
else
|
|
||||||
echo "error: vibetunnel executable not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "${NATIVE_DIR}/pty.node" ]; then
|
|
||||||
cp "${NATIVE_DIR}/pty.node" "${APP_RESOURCES}/"
|
|
||||||
else
|
|
||||||
echo "error: pty.node not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "${NATIVE_DIR}/spawn-helper" ]; then
|
|
||||||
cp "${NATIVE_DIR}/spawn-helper" "${APP_RESOURCES}/"
|
|
||||||
chmod +x "${APP_RESOURCES}/spawn-helper"
|
|
||||||
else
|
|
||||||
echo "error: spawn-helper not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Save the current hash as the previous hash for next build
|
|
||||||
cp "${HASH_FILE}" "${PREVIOUS_HASH_FILE}"
|
|
||||||
|
|
||||||
echo "Web frontend build completed successfully"
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
#!/bin/zsh
|
#!/bin/zsh
|
||||||
set -e # Exit on any error
|
set -e # Exit on any error
|
||||||
|
|
||||||
echo "Building web frontend..."
|
|
||||||
|
|
||||||
# Get the project directory
|
# Get the project directory
|
||||||
if [ -z "${SRCROOT}" ]; then
|
if [ -z "${SRCROOT}" ]; then
|
||||||
# If SRCROOT is not set (running outside Xcode), determine it from script location
|
# If SRCROOT is not set (running outside Xcode), determine it from script location
|
||||||
|
|
@ -13,6 +11,8 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
WEB_DIR="${PROJECT_DIR}/../web"
|
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"
|
PUBLIC_DIR="${WEB_DIR}/public"
|
||||||
|
|
||||||
# Set destination directory
|
# Set destination directory
|
||||||
|
|
@ -23,6 +23,44 @@ else
|
||||||
DEST_DIR="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/web/public"
|
DEST_DIR="${BUILT_PRODUCTS_DIR}/${CONTENTS_FOLDER_PATH}/Resources/web/public"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
APP_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||||
|
|
||||||
|
# Read the current hash
|
||||||
|
if [ -f "${HASH_FILE}" ]; then
|
||||||
|
CURRENT_HASH=$(cat "${HASH_FILE}")
|
||||||
|
else
|
||||||
|
echo "error: Hash file not found. Run 'Calculate Web Hash' build phase first."
|
||||||
|
exit 1
|
||||||
|
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 PATH for Node.js
|
# Setup PATH for Node.js
|
||||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||||
|
|
||||||
|
|
@ -55,15 +93,56 @@ cd "${WEB_DIR}"
|
||||||
|
|
||||||
# Clean build artifacts
|
# Clean build artifacts
|
||||||
echo "Cleaning build artifacts..."
|
echo "Cleaning build artifacts..."
|
||||||
rm -rf dist public/bundle public/output.css
|
rm -rf dist public/bundle public/output.css native
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
echo "Installing dependencies..."
|
echo "Installing dependencies..."
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
|
# Determine build configuration
|
||||||
|
BUILD_CONFIG="${CONFIGURATION:-Debug}"
|
||||||
|
echo "Build configuration: $BUILD_CONFIG"
|
||||||
|
|
||||||
|
# Check for custom Node.js build
|
||||||
|
CUSTOM_NODE_PATH=$(find "${WEB_DIR}/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node
|
||||||
|
|
||||||
# Build the web frontend
|
# Build the web frontend
|
||||||
echo "Building web frontend..."
|
if [ "$BUILD_CONFIG" = "Release" ]; then
|
||||||
npm run build
|
echo "Release build - checking for custom Node.js..."
|
||||||
|
|
||||||
|
if [ ! -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
|
||||||
|
CUSTOM_NODE_PATH=$(find "${WEB_DIR}/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -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"
|
||||||
|
npm run build -- --custom-node
|
||||||
|
else
|
||||||
|
echo "WARNING: Custom Node.js build failed, using system Node.js"
|
||||||
|
echo "The app will be larger than optimal."
|
||||||
|
npm run build
|
||||||
|
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"
|
||||||
|
npm run build -- --custom-node
|
||||||
|
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"
|
||||||
|
npm run build
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Clean and create destination directory
|
# Clean and create destination directory
|
||||||
echo "Cleaning destination directory..."
|
echo "Cleaning destination directory..."
|
||||||
|
|
@ -74,20 +153,22 @@ mkdir -p "${DEST_DIR}"
|
||||||
echo "Copying web files to app bundle..."
|
echo "Copying web files to app bundle..."
|
||||||
cp -R "${PUBLIC_DIR}/"* "${DEST_DIR}/"
|
cp -R "${PUBLIC_DIR}/"* "${DEST_DIR}/"
|
||||||
|
|
||||||
# Copy native executable and modules to app bundle root
|
# Copy native executable and modules to app bundle
|
||||||
echo "Copying native executable and modules..."
|
|
||||||
NATIVE_DIR="${WEB_DIR}/native"
|
NATIVE_DIR="${WEB_DIR}/native"
|
||||||
APP_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
|
||||||
|
|
||||||
if [ -f "${NATIVE_DIR}/vibetunnel" ]; then
|
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}/"
|
cp "${NATIVE_DIR}/vibetunnel" "${APP_RESOURCES}/"
|
||||||
chmod +x "${APP_RESOURCES}/vibetunnel"
|
chmod +x "${APP_RESOURCES}/vibetunnel"
|
||||||
else
|
else
|
||||||
echo "error: vibetunnel executable not found"
|
echo "error: Native executable not found at ${NATIVE_DIR}/vibetunnel"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${NATIVE_DIR}/pty.node" ]; then
|
if [ -f "${NATIVE_DIR}/pty.node" ]; then
|
||||||
|
echo "Copying pty.node..."
|
||||||
cp "${NATIVE_DIR}/pty.node" "${APP_RESOURCES}/"
|
cp "${NATIVE_DIR}/pty.node" "${APP_RESOURCES}/"
|
||||||
else
|
else
|
||||||
echo "error: pty.node not found"
|
echo "error: pty.node not found"
|
||||||
|
|
@ -95,6 +176,7 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${NATIVE_DIR}/spawn-helper" ]; then
|
if [ -f "${NATIVE_DIR}/spawn-helper" ]; then
|
||||||
|
echo "Copying spawn-helper..."
|
||||||
cp "${NATIVE_DIR}/spawn-helper" "${APP_RESOURCES}/"
|
cp "${NATIVE_DIR}/spawn-helper" "${APP_RESOURCES}/"
|
||||||
chmod +x "${APP_RESOURCES}/spawn-helper"
|
chmod +x "${APP_RESOURCES}/spawn-helper"
|
||||||
else
|
else
|
||||||
|
|
@ -102,4 +184,65 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "✓ Native executable and modules 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
|
||||||
|
|
||||||
|
# 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" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optional: Verify the executable works
|
||||||
|
echo "Verifying vibetunnel executable..."
|
||||||
|
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 test failed)"
|
||||||
|
echo "This might indicate a corrupted or incompatible binary"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✓ All sanity checks passed"
|
||||||
|
|
||||||
|
# Save the current hash as the previous hash for next build
|
||||||
|
cp "${HASH_FILE}" "${PREVIOUS_HASH_FILE}"
|
||||||
|
|
||||||
echo "Web frontend build completed successfully"
|
echo "Web frontend build completed successfully"
|
||||||
|
|
@ -27,7 +27,7 @@ cd "${WEB_DIR}"
|
||||||
|
|
||||||
# Find all relevant files and calculate their size, modification time, and content hash
|
# Find all relevant files and calculate their size, modification time, and content hash
|
||||||
# This approach is more reliable than just content hash as it catches permission changes
|
# This approach is more reliable than just content hash as it catches permission changes
|
||||||
# Exclude: node_modules, dist, public (all build outputs), package-lock.json
|
# Exclude: node_modules, dist, public (all build outputs), package-lock.json, and build directories
|
||||||
CONTENT_HASH=$(find . \
|
CONTENT_HASH=$(find . \
|
||||||
-type f \
|
-type f \
|
||||||
\( -name "*.ts" -o -name "*.js" -o -name "*.json" -o -name "*.css" -o -name "*.html" \
|
\( -name "*.ts" -o -name "*.js" -o -name "*.json" -o -name "*.css" -o -name "*.html" \
|
||||||
|
|
@ -39,6 +39,9 @@ CONTENT_HASH=$(find . \
|
||||||
-not -path "./.next/*" \
|
-not -path "./.next/*" \
|
||||||
-not -path "./coverage/*" \
|
-not -path "./coverage/*" \
|
||||||
-not -path "./.cache/*" \
|
-not -path "./.cache/*" \
|
||||||
|
-not -path "./.node-builds/*" \
|
||||||
|
-not -path "./build/*" \
|
||||||
|
-not -path "./native/*" \
|
||||||
-not -name "package-lock.json" \
|
-not -name "package-lock.json" \
|
||||||
-exec stat -f "%m %z %p" {} \; \
|
-exec stat -f "%m %z %p" {} \; \
|
||||||
-exec shasum -a 256 {} \; | \
|
-exec shasum -a 256 {} \; | \
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,16 @@ if [[ "$CLEAN_ALL" == "true" ]]; then
|
||||||
remove_item "web/.next" "Next.js build cache"
|
remove_item "web/.next" "Next.js build cache"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Clean web build artifacts (always clean these)
|
||||||
|
if [[ -d "$PROJECT_ROOT/../web" ]]; then
|
||||||
|
remove_item "$PROJECT_ROOT/../web/native" "Web native executables"
|
||||||
|
remove_item "$PROJECT_ROOT/../web/dist" "Web dist directory"
|
||||||
|
remove_item "$PROJECT_ROOT/../web/public/bundle" "Web bundle directory"
|
||||||
|
remove_item "$PROJECT_ROOT/../web/public/output.css" "Web CSS output"
|
||||||
|
remove_item "$PROJECT_ROOT/../web/build" "Web build directory"
|
||||||
|
remove_item "$PROJECT_ROOT/../web/.node-builds" "Custom Node.js builds"
|
||||||
|
fi
|
||||||
|
|
||||||
# Clean Python caches
|
# Clean Python caches
|
||||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||||
|
|
|
||||||
69
mac/scripts/install-node.sh
Executable file
69
mac/scripts/install-node.sh
Executable file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Check for Node.js availability for the build process
|
||||||
|
#
|
||||||
|
# This script ensures Node.js is available for building VibeTunnel
|
||||||
|
#
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# Script directory and paths
|
||||||
|
if [ -n "${BASH_SOURCE[0]:-}" ]; then
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
else
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking for Node.js..."
|
||||||
|
|
||||||
|
# Add common Node.js installation paths to PATH
|
||||||
|
# Homebrew on Apple Silicon
|
||||||
|
if [ -d "/opt/homebrew/bin" ]; then
|
||||||
|
export PATH="/opt/homebrew/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Homebrew on Intel Macs
|
||||||
|
if [ -d "/usr/local/bin" ]; then
|
||||||
|
export PATH="/usr/local/bin:$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# NVM default location
|
||||||
|
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
export NVM_DIR="$HOME/.nvm"
|
||||||
|
. "$NVM_DIR/nvm.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Node.js is available
|
||||||
|
if command -v node &> /dev/null; then
|
||||||
|
echo "✓ Node.js found: $(which node)"
|
||||||
|
echo " Version: $(node --version)"
|
||||||
|
|
||||||
|
# Check Node.js version (need v20+)
|
||||||
|
NODE_VERSION=$(node --version | cut -d'v' -f2)
|
||||||
|
NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d'.' -f1)
|
||||||
|
|
||||||
|
if [ "$NODE_MAJOR" -lt 20 ]; then
|
||||||
|
echo "Warning: Node.js v20+ is recommended (found v$NODE_VERSION)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if npm is available
|
||||||
|
if command -v npm &> /dev/null; then
|
||||||
|
echo "✓ npm found: $(which npm)"
|
||||||
|
echo " Version: $(npm --version)"
|
||||||
|
else
|
||||||
|
echo "Error: npm not found. Please ensure Node.js is properly installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "Error: Node.js not found in PATH"
|
||||||
|
echo ""
|
||||||
|
echo "Please install Node.js 20+ using one of these methods:"
|
||||||
|
echo " - Homebrew: brew install node"
|
||||||
|
echo " - Download from: https://nodejs.org/"
|
||||||
|
echo " - Using nvm: nvm install 20"
|
||||||
|
echo ""
|
||||||
|
echo "PATH checked: $PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -319,6 +319,38 @@ fi
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}📋 Step 4/8: Building universal application...${NC}"
|
echo -e "${BLUE}📋 Step 4/8: Building universal application...${NC}"
|
||||||
|
|
||||||
|
# Check for custom Node.js build
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Checking for custom Node.js build..."
|
||||||
|
WEB_DIR="$PROJECT_ROOT/../web"
|
||||||
|
CUSTOM_NODE_PATH=$(find "$WEB_DIR/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node
|
||||||
|
|
||||||
|
if [[ ! -f "$CUSTOM_NODE_PATH" ]]; then
|
||||||
|
echo -e "${YELLOW}⚠️ Custom Node.js not found. Building for optimal app size...${NC}"
|
||||||
|
echo " This will take 10-20 minutes on first run."
|
||||||
|
|
||||||
|
# Build custom Node.js
|
||||||
|
pushd "$WEB_DIR" > /dev/null
|
||||||
|
if node build-custom-node.js --latest; then
|
||||||
|
echo -e "${GREEN}✅ Custom Node.js built successfully${NC}"
|
||||||
|
CUSTOM_NODE_PATH=$(find "$WEB_DIR/.node-builds" -name "node-v*-minimal" -type d 2>/dev/null | sort -V | tail -n1)/out/Release/node
|
||||||
|
if [[ -f "$CUSTOM_NODE_PATH" ]]; then
|
||||||
|
CUSTOM_NODE_SIZE=$(ls -lh "$CUSTOM_NODE_PATH" | awk '{print $5}')
|
||||||
|
echo " Size: $CUSTOM_NODE_SIZE (vs ~110MB for standard Node.js)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Failed to build custom Node.js${NC}"
|
||||||
|
echo " Continuing with standard Node.js (larger app size)"
|
||||||
|
fi
|
||||||
|
popd > /dev/null
|
||||||
|
else
|
||||||
|
CUSTOM_NODE_SIZE=$(ls -lh "$CUSTOM_NODE_PATH" | awk '{print $5}')
|
||||||
|
CUSTOM_NODE_VERSION=$("$CUSTOM_NODE_PATH" --version 2>/dev/null || echo "unknown")
|
||||||
|
echo -e "${GREEN}✅ Found custom Node.js${NC}"
|
||||||
|
echo " Version: $CUSTOM_NODE_VERSION"
|
||||||
|
echo " Size: $CUSTOM_NODE_SIZE"
|
||||||
|
fi
|
||||||
|
|
||||||
# For pre-release builds, set the environment variable
|
# For pre-release builds, set the environment variable
|
||||||
if [[ "$RELEASE_TYPE" != "stable" ]]; then
|
if [[ "$RELEASE_TYPE" != "stable" ]]; then
|
||||||
echo "📝 Marking build as pre-release..."
|
echo "📝 Marking build as pre-release..."
|
||||||
|
|
|
||||||
250
web/build-custom-node.js
Executable file
250
web/build-custom-node.js
Executable file
|
|
@ -0,0 +1,250 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a custom Node.js binary with reduced size by excluding features we don't need.
|
||||||
|
*
|
||||||
|
* This creates a Node.js build without:
|
||||||
|
* - International support (ICU) - saves ~28MB
|
||||||
|
* - npm/npx - saves ~5MB
|
||||||
|
* - corepack
|
||||||
|
* - dtrace/etw instrumentation
|
||||||
|
*
|
||||||
|
* The resulting binary is typically 50-60MB instead of 110+MB.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```bash
|
||||||
|
* node build-custom-node.js # Build for current Node.js version
|
||||||
|
* node build-custom-node.js --latest # Build latest Node.js version
|
||||||
|
* node build-custom-node.js --version=24.2.0 # Build specific version
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The custom Node.js will be built in:
|
||||||
|
* .node-builds/node-vXX-minimal/out/Release/node
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
let targetVersion = null;
|
||||||
|
let useLatest = false;
|
||||||
|
|
||||||
|
for (const arg of args) {
|
||||||
|
if (arg.startsWith('--version=')) {
|
||||||
|
targetVersion = arg.split('=')[1];
|
||||||
|
} else if (arg === '--latest') {
|
||||||
|
useLatest = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to download files
|
||||||
|
function downloadFile(url, destPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const file = fs.createWriteStream(destPath);
|
||||||
|
https.get(url, (response) => {
|
||||||
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||||
|
// Handle redirect
|
||||||
|
downloadFile(response.headers.location, destPath).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close(resolve);
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
fs.unlink(destPath, () => {});
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get latest Node.js version
|
||||||
|
async function getLatestNodeVersion() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
https.get('https://nodejs.org/dist/latest/SHASUMS256.txt', (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', chunk => data += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
// Extract version from first line like: "1234567890abcdef node-v24.2.0-darwin-arm64.tar.gz"
|
||||||
|
const match = data.match(/node-v(\d+\.\d+\.\d+)/);
|
||||||
|
if (match) {
|
||||||
|
resolve(match[1]);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Could not determine latest Node.js version'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildCustomNode() {
|
||||||
|
// Determine version to build
|
||||||
|
let nodeSourceVersion;
|
||||||
|
if (useLatest) {
|
||||||
|
console.log('Fetching latest Node.js version...');
|
||||||
|
nodeSourceVersion = await getLatestNodeVersion();
|
||||||
|
console.log(`Latest Node.js version: ${nodeSourceVersion}`);
|
||||||
|
} else if (targetVersion) {
|
||||||
|
nodeSourceVersion = targetVersion;
|
||||||
|
} else {
|
||||||
|
// Use current Node.js version
|
||||||
|
const nodeVersionMatch = process.version.match(/^v(\d+)\.(\d+)\.(\d+)/);
|
||||||
|
nodeSourceVersion = `${nodeVersionMatch[1]}.${nodeVersionMatch[2]}.${nodeVersionMatch[3]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Building custom Node.js ${nodeSourceVersion} without intl and npm...`);
|
||||||
|
console.log('This will take 10-20 minutes on first run, but will be cached for future builds.');
|
||||||
|
|
||||||
|
const nodeSourceUrl = `https://nodejs.org/dist/v${nodeSourceVersion}/node-v${nodeSourceVersion}.tar.gz`;
|
||||||
|
const majorVersion = nodeSourceVersion.split('.')[0];
|
||||||
|
|
||||||
|
const buildDir = path.join(__dirname, '.node-builds');
|
||||||
|
const versionDir = path.join(buildDir, `node-v${nodeSourceVersion}-minimal`);
|
||||||
|
const markerFile = path.join(versionDir, '.build-complete');
|
||||||
|
const customNodePath = path.join(versionDir, 'out', 'Release', 'node');
|
||||||
|
|
||||||
|
// Check if already built
|
||||||
|
if (fs.existsSync(markerFile) && fs.existsSync(customNodePath)) {
|
||||||
|
console.log(`Using cached custom Node.js build from ${customNodePath}`);
|
||||||
|
const stats = fs.statSync(customNodePath);
|
||||||
|
console.log(`Cached custom Node.js size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(`\nTo use this custom Node.js with build-native.js:`);
|
||||||
|
console.log(`node build-native.js --custom-node="${customNodePath}"`);
|
||||||
|
return customNodePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create build directory
|
||||||
|
if (!fs.existsSync(buildDir)) {
|
||||||
|
fs.mkdirSync(buildDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old version directory if exists
|
||||||
|
if (fs.existsSync(versionDir)) {
|
||||||
|
console.log('Cleaning up incomplete build...');
|
||||||
|
fs.rmSync(versionDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tarPath = path.join(buildDir, `node-v${nodeSourceVersion}.tar.gz`);
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Download Node.js source if not cached
|
||||||
|
if (!fs.existsSync(tarPath)) {
|
||||||
|
console.log(`Downloading Node.js source from ${nodeSourceUrl}...`);
|
||||||
|
await downloadFile(nodeSourceUrl, tarPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract source
|
||||||
|
console.log('Extracting Node.js source...');
|
||||||
|
execSync(`tar -xzf "${tarPath}" -C "${buildDir}"`, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
// Rename to version-specific directory
|
||||||
|
const extractedDir = path.join(buildDir, `node-v${nodeSourceVersion}`);
|
||||||
|
fs.renameSync(extractedDir, versionDir);
|
||||||
|
|
||||||
|
// Configure and build
|
||||||
|
process.chdir(versionDir);
|
||||||
|
|
||||||
|
console.log('Configuring Node.js build (without intl and npm)...');
|
||||||
|
const configureArgs = [
|
||||||
|
'--without-intl',
|
||||||
|
'--without-npm',
|
||||||
|
'--without-corepack',
|
||||||
|
'--without-inspector',
|
||||||
|
'--without-node-snapshot', // Disable V8 snapshot (saves ~2-3MB)
|
||||||
|
'--without-node-code-cache', // Disable code cache (saves ~1-2MB)
|
||||||
|
'--ninja', // Use ninja if available for faster builds
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set optimization flags for smaller binary
|
||||||
|
// -Os: Optimize for size
|
||||||
|
// -flto: Link Time Optimization (can save 10-20%)
|
||||||
|
// -ffunction-sections -fdata-sections: Allow linker to remove unused code
|
||||||
|
process.env.CFLAGS = '-Os -flto -ffunction-sections -fdata-sections';
|
||||||
|
process.env.CXXFLAGS = '-Os -flto -ffunction-sections -fdata-sections';
|
||||||
|
process.env.LDFLAGS = '-Wl,-dead_strip'; // macOS equivalent of --gc-sections
|
||||||
|
|
||||||
|
// Check if ninja is available, install if not
|
||||||
|
try {
|
||||||
|
execSync('which ninja', { stdio: 'ignore' });
|
||||||
|
console.log('Using Ninja for faster builds...');
|
||||||
|
} catch {
|
||||||
|
console.log('Ninja not found, installing via Homebrew...');
|
||||||
|
try {
|
||||||
|
execSync('brew install ninja', { stdio: 'inherit' });
|
||||||
|
console.log('Ninja installed successfully');
|
||||||
|
} catch (brewError) {
|
||||||
|
console.log('Failed to install ninja, falling back to Make...');
|
||||||
|
// Remove --ninja if not available
|
||||||
|
configureArgs.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execSync(`./configure ${configureArgs.join(' ')}`, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
console.log('Building Node.js (this will take a while)...');
|
||||||
|
const cores = require('os').cpus().length;
|
||||||
|
|
||||||
|
// Check if we're using ninja or make
|
||||||
|
const buildSystem = configureArgs.includes('--ninja') ? 'ninja' : 'make';
|
||||||
|
if (buildSystem === 'ninja') {
|
||||||
|
execSync(`ninja -C out/Release -j ${cores}`, { stdio: 'inherit' });
|
||||||
|
} else {
|
||||||
|
execSync(`make -j${cores}`, { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the build
|
||||||
|
if (!fs.existsSync(customNodePath)) {
|
||||||
|
throw new Error('Node.js build failed - binary not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the binary
|
||||||
|
console.log('Stripping Node.js binary...');
|
||||||
|
execSync(`strip -S "${customNodePath}"`, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
// Check final size
|
||||||
|
const stats = fs.statSync(customNodePath);
|
||||||
|
console.log(`\n✅ Custom Node.js built successfully!`);
|
||||||
|
console.log(`Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
|
// Compare with system Node.js
|
||||||
|
try {
|
||||||
|
const systemNodeStats = fs.statSync(process.execPath);
|
||||||
|
const reduction = ((systemNodeStats.size - stats.size) / systemNodeStats.size * 100).toFixed(1);
|
||||||
|
console.log(`Size reduction: ${reduction}% compared to system Node.js`);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if we can't stat system node
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark build as complete
|
||||||
|
fs.writeFileSync(markerFile, JSON.stringify({
|
||||||
|
version: nodeSourceVersion,
|
||||||
|
buildDate: new Date().toISOString(),
|
||||||
|
size: stats.size,
|
||||||
|
configureArgs: configureArgs
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
// Change back to original directory
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
|
||||||
|
console.log(`\nCustom Node.js location: ${customNodePath}`);
|
||||||
|
console.log(`\nTo use this custom Node.js with build-native.js:`);
|
||||||
|
console.log(`node build-native.js --custom-node="${customNodePath}"`);
|
||||||
|
|
||||||
|
return customNodePath;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
console.error('Failed to build custom Node.js:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the build
|
||||||
|
buildCustomNode().catch(err => {
|
||||||
|
console.error('Build failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -41,8 +41,13 @@
|
||||||
*
|
*
|
||||||
* ## Usage
|
* ## Usage
|
||||||
* ```bash
|
* ```bash
|
||||||
* node build-native-node.js # Build without sourcemaps (default)
|
* node build-native.js # Build with system Node.js
|
||||||
* node build-native-node.js --sourcemap # Build with inline sourcemaps
|
* node build-native.js --sourcemap # Build with inline sourcemaps
|
||||||
|
* node build-native.js --custom-node=/path/to/node # Use custom Node.js binary
|
||||||
|
*
|
||||||
|
* # Build custom Node.js first:
|
||||||
|
* node build-custom-node.js # Build minimal Node.js for current version
|
||||||
|
* node build-custom-node.js --version=24.2.0 # Build specific version
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* ## Requirements
|
* ## Requirements
|
||||||
|
|
@ -61,9 +66,45 @@ const path = require('path');
|
||||||
|
|
||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
const includeSourcemaps = process.argv.includes('--sourcemap');
|
const includeSourcemaps = process.argv.includes('--sourcemap');
|
||||||
|
let customNodePath = null;
|
||||||
|
|
||||||
|
// Parse --custom-node argument
|
||||||
|
for (let i = 0; i < process.argv.length; i++) {
|
||||||
|
const arg = process.argv[i];
|
||||||
|
if (arg.startsWith('--custom-node=')) {
|
||||||
|
customNodePath = arg.split('=')[1];
|
||||||
|
} else if (arg === '--custom-node') {
|
||||||
|
// Check if next argument is a path
|
||||||
|
if (i + 1 < process.argv.length && !process.argv[i + 1].startsWith('--')) {
|
||||||
|
customNodePath = process.argv[i + 1];
|
||||||
|
} else {
|
||||||
|
// No path provided, search for custom Node.js build
|
||||||
|
console.log('Searching for custom Node.js build...');
|
||||||
|
const customBuildsDir = path.join(__dirname, '.node-builds');
|
||||||
|
if (fs.existsSync(customBuildsDir)) {
|
||||||
|
const dirs = fs.readdirSync(customBuildsDir)
|
||||||
|
.filter(dir => dir.startsWith('node-v') && dir.endsWith('-minimal'))
|
||||||
|
.map(dir => ({
|
||||||
|
name: dir,
|
||||||
|
path: path.join(customBuildsDir, dir, 'out/Release/node'),
|
||||||
|
mtime: fs.statSync(path.join(customBuildsDir, dir)).mtime
|
||||||
|
}))
|
||||||
|
.filter(item => fs.existsSync(item.path))
|
||||||
|
.sort((a, b) => b.mtime - a.mtime); // Sort by modification time, newest first
|
||||||
|
|
||||||
|
if (dirs.length > 0) {
|
||||||
|
customNodePath = dirs[0].path;
|
||||||
|
console.log(`Found custom Node.js at: ${customNodePath}`);
|
||||||
|
} else {
|
||||||
|
console.log('No custom Node.js builds found in .node-builds/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Building standalone vibetunnel executable using Node.js SEA...');
|
console.log('Building standalone vibetunnel executable using Node.js SEA...');
|
||||||
console.log(`Node.js version: ${process.version}`);
|
console.log(`System Node.js version: ${process.version}`);
|
||||||
if (includeSourcemaps) {
|
if (includeSourcemaps) {
|
||||||
console.log('Including sourcemaps in build');
|
console.log('Including sourcemaps in build');
|
||||||
}
|
}
|
||||||
|
|
@ -228,11 +269,38 @@ async function main() {
|
||||||
fs.mkdirSync('native');
|
fs.mkdirSync('native');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0. Patch node-pty
|
// 0. Determine which Node.js to use
|
||||||
|
let nodeExe = process.execPath;
|
||||||
|
if (customNodePath) {
|
||||||
|
// Validate custom node exists
|
||||||
|
if (!fs.existsSync(customNodePath)) {
|
||||||
|
console.error(`Error: Custom Node.js not found at ${customNodePath}`);
|
||||||
|
console.error('Build one using: node build-custom-node.js');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
nodeExe = customNodePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Using Node.js binary: ${nodeExe}`);
|
||||||
|
const nodeStats = fs.statSync(nodeExe);
|
||||||
|
console.log(`Node.js binary size: ${(nodeStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
|
// Get version of the Node.js we're using
|
||||||
|
if (customNodePath) {
|
||||||
|
try {
|
||||||
|
const customVersion = execSync(`"${nodeExe}" --version`, { encoding: 'utf8' }).trim();
|
||||||
|
console.log(`Custom Node.js version: ${customVersion}`);
|
||||||
|
console.log('This minimal build excludes intl, npm, inspector, and other unused features.');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not determine custom Node.js version');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Patch node-pty
|
||||||
patchNodePty();
|
patchNodePty();
|
||||||
|
|
||||||
// 1. Bundle TypeScript with esbuild using custom loader
|
// 2. Bundle TypeScript with esbuild using custom loader
|
||||||
console.log('Bundling TypeScript with esbuild...');
|
console.log('\nBundling TypeScript with esbuild...');
|
||||||
const buildDate = new Date().toISOString();
|
const buildDate = new Date().toISOString();
|
||||||
const buildTimestamp = Date.now();
|
const buildTimestamp = Date.now();
|
||||||
|
|
||||||
|
|
@ -272,7 +340,6 @@ async function main() {
|
||||||
|
|
||||||
// 4. Create executable
|
// 4. Create executable
|
||||||
console.log('\nCreating executable...');
|
console.log('\nCreating executable...');
|
||||||
const nodeExe = process.execPath;
|
|
||||||
const targetExe = process.platform === 'win32' ? 'native/vibetunnel.exe' : 'native/vibetunnel';
|
const targetExe = process.platform === 'win32' ? 'native/vibetunnel.exe' : 'native/vibetunnel';
|
||||||
|
|
||||||
// Copy node binary
|
// Copy node binary
|
||||||
|
|
@ -292,18 +359,32 @@ async function main() {
|
||||||
|
|
||||||
execSync(postjectCmd, { stdio: 'inherit' });
|
execSync(postjectCmd, { stdio: 'inherit' });
|
||||||
|
|
||||||
// 6. Sign on macOS
|
// 6. Strip the executable first (before signing)
|
||||||
|
console.log('Stripping final executable...');
|
||||||
|
// Note: This will show a warning about invalidating code signature, which is expected
|
||||||
|
// since we're modifying a signed Node.js binary. We'll re-sign it in the next step.
|
||||||
|
execSync(`strip -S ${targetExe} 2>&1 | grep -v "warning: changes being made" || true`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Sign on macOS (after stripping)
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
console.log('Signing executable...');
|
console.log('Signing executable...');
|
||||||
execSync(`codesign --sign - ${targetExe}`, { stdio: 'inherit' });
|
execSync(`codesign --sign - ${targetExe}`, { stdio: 'inherit' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check final size
|
||||||
|
const finalStats = fs.statSync(targetExe);
|
||||||
|
console.log(`Final executable size: ${(finalStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(`Size reduction: ${((nodeStats.size - finalStats.size) / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
// 7. Restore original node-pty
|
// 8. Restore original node-pty
|
||||||
console.log('Restoring original node-pty...');
|
console.log('Restoring original node-pty...');
|
||||||
execSync('rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch', { stdio: 'inherit' });
|
execSync('rm -rf node_modules/@homebridge/node-pty-prebuilt-multiarch', { stdio: 'inherit' });
|
||||||
execSync('npm install @homebridge/node-pty-prebuilt-multiarch --silent --no-fund --no-audit', { stdio: 'inherit' });
|
execSync('npm install @homebridge/node-pty-prebuilt-multiarch --silent --no-fund --no-audit', { stdio: 'inherit' });
|
||||||
|
|
||||||
// 8. Copy only necessary native files
|
// 9. Copy only necessary native files
|
||||||
console.log('Copying native modules...');
|
console.log('Copying native modules...');
|
||||||
const nativeModulesDir = 'node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release';
|
const nativeModulesDir = 'node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,16 @@ execSync('tsc', { stdio: 'inherit' });
|
||||||
|
|
||||||
// Build native executable
|
// Build native executable
|
||||||
console.log('Building native executable...');
|
console.log('Building native executable...');
|
||||||
execSync('node build-native.js', { stdio: 'inherit' });
|
|
||||||
|
// Check for --custom-node flag
|
||||||
|
const useCustomNode = process.argv.includes('--custom-node');
|
||||||
|
|
||||||
|
if (useCustomNode) {
|
||||||
|
console.log('Using custom Node.js for smaller binary size...');
|
||||||
|
execSync('node build-native.js --custom-node', { stdio: 'inherit' });
|
||||||
|
} else {
|
||||||
|
console.log('Using system Node.js...');
|
||||||
|
execSync('node build-native.js', { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Build completed successfully!');
|
console.log('Build completed successfully!');
|
||||||
|
|
@ -6,6 +6,16 @@ import { VERSION } from './server/version.js';
|
||||||
|
|
||||||
// Source maps are only included if built with --sourcemap flag
|
// Source maps are only included if built with --sourcemap flag
|
||||||
|
|
||||||
|
// Prevent double execution in SEA context where require.main might be undefined
|
||||||
|
// Use a global flag to ensure we only run once
|
||||||
|
if ((global as any).__vibetunnelStarted) {
|
||||||
|
console.log('VibeTunnel already started, skipping duplicate execution');
|
||||||
|
console.log('Global flag was already set, exiting to prevent duplicate server');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
(global as any).__vibetunnelStarted = true;
|
||||||
|
console.log('Setting global flag to prevent duplicate execution');
|
||||||
|
|
||||||
// Handle uncaught exceptions
|
// Handle uncaught exceptions
|
||||||
process.on('uncaughtException', (error) => {
|
process.on('uncaughtException', (error) => {
|
||||||
console.error('Uncaught exception:', error);
|
console.error('Uncaught exception:', error);
|
||||||
|
|
@ -21,14 +31,25 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (process.argv[2] === 'version') {
|
// Only execute if this is the main module (or in SEA where require.main is undefined)
|
||||||
console.log(`VibeTunnel Linux v${VERSION}`);
|
if (!module.parent && (require.main === module || require.main === undefined)) {
|
||||||
process.exit(0);
|
console.log('Main module check passed, proceeding with server startup');
|
||||||
} else if (process.argv[2] === 'fwd') {
|
console.log('module.parent:', module.parent);
|
||||||
startVibeTunnelForward(process.argv.slice(3)).catch((error) => {
|
console.log('require.main === module:', require.main === module);
|
||||||
console.error('Fatal error:', error);
|
console.log('require.main:', require.main);
|
||||||
process.exit(1);
|
|
||||||
});
|
if (process.argv[2] === 'version') {
|
||||||
|
console.log(`VibeTunnel Server v${VERSION}`);
|
||||||
|
process.exit(0);
|
||||||
|
} else if (process.argv[2] === 'fwd') {
|
||||||
|
startVibeTunnelForward(process.argv.slice(3)).catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Starting VibeTunnel server...');
|
||||||
|
startVibeTunnelServer();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
startVibeTunnelServer();
|
console.log('Not main module, skipping server startup');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,17 @@ interface AppInstance {
|
||||||
activityMonitor: ActivityMonitor;
|
activityMonitor: ActivityMonitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track if app has been created
|
||||||
|
let appCreated = false;
|
||||||
|
|
||||||
export function createApp(): AppInstance {
|
export function createApp(): AppInstance {
|
||||||
|
// Prevent multiple app instances
|
||||||
|
if (appCreated) {
|
||||||
|
console.error(chalk.red('ERROR: App already created, preventing duplicate instance'));
|
||||||
|
throw new Error('Duplicate app creation detected');
|
||||||
|
}
|
||||||
|
appCreated = true;
|
||||||
|
|
||||||
const config = parseArgs();
|
const config = parseArgs();
|
||||||
|
|
||||||
// Check if help was requested
|
// Check if help was requested
|
||||||
|
|
@ -271,6 +281,7 @@ export function createApp(): AppInstance {
|
||||||
|
|
||||||
validateConfig(config);
|
validateConfig(config);
|
||||||
|
|
||||||
|
console.log('Creating Express app and HTTP server...');
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
const wss = new WebSocketServer({ server });
|
const wss = new WebSocketServer({ server });
|
||||||
|
|
@ -406,6 +417,28 @@ export function createApp(): AppInstance {
|
||||||
// Start server function
|
// Start server function
|
||||||
const startServer = () => {
|
const startServer = () => {
|
||||||
const requestedPort = config.port !== null ? config.port : Number(process.env.PORT) || 4020;
|
const requestedPort = config.port !== null ? config.port : Number(process.env.PORT) || 4020;
|
||||||
|
|
||||||
|
console.log(`Attempting to start server on port ${requestedPort}`);
|
||||||
|
|
||||||
|
// Remove all existing error listeners first to prevent duplicates
|
||||||
|
server.removeAllListeners('error');
|
||||||
|
|
||||||
|
// Add error handler for port already in use
|
||||||
|
server.on('error', (error: any) => {
|
||||||
|
if (error.code === 'EADDRINUSE') {
|
||||||
|
console.error(chalk.red(`Error: Port ${requestedPort} is already in use`));
|
||||||
|
console.error(
|
||||||
|
chalk.yellow(
|
||||||
|
'Please use a different port with --port <number> or stop the existing server'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
process.exit(9); // Exit with code 9 to indicate port conflict
|
||||||
|
} else {
|
||||||
|
console.error(chalk.red('Server error:'), error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.listen(requestedPort, () => {
|
server.listen(requestedPort, () => {
|
||||||
const address = server.address();
|
const address = server.address();
|
||||||
const actualPort =
|
const actualPort =
|
||||||
|
|
@ -485,8 +518,20 @@ export function createApp(): AppInstance {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track if server has been started
|
||||||
|
let serverStarted = false;
|
||||||
|
|
||||||
// Export a function to start the server
|
// Export a function to start the server
|
||||||
export function startVibeTunnelServer() {
|
export function startVibeTunnelServer() {
|
||||||
|
// Prevent multiple server instances
|
||||||
|
if (serverStarted) {
|
||||||
|
console.error(chalk.red('ERROR: Server already started, preventing duplicate instance'));
|
||||||
|
console.error('This should not happen - duplicate server startup detected');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
serverStarted = true;
|
||||||
|
|
||||||
|
console.log('Creating app instance...');
|
||||||
// Create and configure the app
|
// Create and configure the app
|
||||||
const appInstance = createApp();
|
const appInstance = createApp();
|
||||||
const {
|
const {
|
||||||
|
|
@ -499,6 +544,7 @@ export function startVibeTunnelServer() {
|
||||||
activityMonitor,
|
activityMonitor,
|
||||||
} = appInstance;
|
} = appInstance;
|
||||||
|
|
||||||
|
console.log('Starting server...');
|
||||||
startServer();
|
startServer();
|
||||||
|
|
||||||
// Cleanup old terminals every 5 minutes
|
// Cleanup old terminals every 5 minutes
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue