mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-15 12:55:52 +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;
|
||||
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 */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
|
|
@ -270,7 +270,7 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
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 */
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "Building web frontend..."
|
||||
|
||||
# Get the project directory
|
||||
if [ -z "${SRCROOT}" ]; then
|
||||
# If SRCROOT is not set (running outside Xcode), determine it from script location
|
||||
|
|
@ -13,6 +11,8 @@ else
|
|||
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
|
||||
|
|
@ -23,6 +23,44 @@ 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"
|
||||
|
||||
|
|
@ -55,15 +93,56 @@ cd "${WEB_DIR}"
|
|||
|
||||
# Clean 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
|
||||
echo "Installing dependencies..."
|
||||
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
|
||||
echo "Building web frontend..."
|
||||
npm run build
|
||||
if [ "$BUILD_CONFIG" = "Release" ]; then
|
||||
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
|
||||
echo "Cleaning destination directory..."
|
||||
|
|
@ -74,20 +153,22 @@ mkdir -p "${DEST_DIR}"
|
|||
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..."
|
||||
# Copy native executable and modules to app bundle
|
||||
NATIVE_DIR="${WEB_DIR}/native"
|
||||
APP_RESOURCES="${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||
|
||||
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: vibetunnel executable not found"
|
||||
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"
|
||||
|
|
@ -95,6 +176,7 @@ else
|
|||
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
|
||||
|
|
@ -102,4 +184,65 @@ else
|
|||
exit 1
|
||||
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"
|
||||
|
|
@ -27,7 +27,7 @@ cd "${WEB_DIR}"
|
|||
|
||||
# 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
|
||||
# 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 . \
|
||||
-type f \
|
||||
\( -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 "./coverage/*" \
|
||||
-not -path "./.cache/*" \
|
||||
-not -path "./.node-builds/*" \
|
||||
-not -path "./build/*" \
|
||||
-not -path "./native/*" \
|
||||
-not -name "package-lock.json" \
|
||||
-exec stat -f "%m %z %p" {} \; \
|
||||
-exec shasum -a 256 {} \; | \
|
||||
|
|
|
|||
|
|
@ -150,6 +150,16 @@ if [[ "$CLEAN_ALL" == "true" ]]; then
|
|||
remove_item "web/.next" "Next.js build cache"
|
||||
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
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} + 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 -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
|
||||
if [[ "$RELEASE_TYPE" != "stable" ]]; then
|
||||
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
|
||||
* ```bash
|
||||
* node build-native-node.js # Build without sourcemaps (default)
|
||||
* node build-native-node.js --sourcemap # Build with inline sourcemaps
|
||||
* node build-native.js # Build with system Node.js
|
||||
* 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
|
||||
|
|
@ -61,9 +66,45 @@ const path = require('path');
|
|||
|
||||
// Parse command line arguments
|
||||
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(`Node.js version: ${process.version}`);
|
||||
console.log(`System Node.js version: ${process.version}`);
|
||||
if (includeSourcemaps) {
|
||||
console.log('Including sourcemaps in build');
|
||||
}
|
||||
|
|
@ -228,11 +269,38 @@ async function main() {
|
|||
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();
|
||||
|
||||
// 1. Bundle TypeScript with esbuild using custom loader
|
||||
console.log('Bundling TypeScript with esbuild...');
|
||||
// 2. Bundle TypeScript with esbuild using custom loader
|
||||
console.log('\nBundling TypeScript with esbuild...');
|
||||
const buildDate = new Date().toISOString();
|
||||
const buildTimestamp = Date.now();
|
||||
|
||||
|
|
@ -272,7 +340,6 @@ async function main() {
|
|||
|
||||
// 4. Create executable
|
||||
console.log('\nCreating executable...');
|
||||
const nodeExe = process.execPath;
|
||||
const targetExe = process.platform === 'win32' ? 'native/vibetunnel.exe' : 'native/vibetunnel';
|
||||
|
||||
// Copy node binary
|
||||
|
|
@ -292,18 +359,32 @@ async function main() {
|
|||
|
||||
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') {
|
||||
console.log('Signing executable...');
|
||||
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...');
|
||||
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' });
|
||||
|
||||
// 8. Copy only necessary native files
|
||||
// 9. Copy only necessary native files
|
||||
console.log('Copying native modules...');
|
||||
const nativeModulesDir = 'node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release';
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,16 @@ execSync('tsc', { stdio: 'inherit' });
|
|||
|
||||
// Build 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!');
|
||||
|
|
@ -6,6 +6,16 @@ import { VERSION } from './server/version.js';
|
|||
|
||||
// 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
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('Uncaught exception:', error);
|
||||
|
|
@ -21,14 +31,25 @@ process.on('unhandledRejection', (reason, promise) => {
|
|||
process.exit(1);
|
||||
});
|
||||
|
||||
if (process.argv[2] === 'version') {
|
||||
console.log(`VibeTunnel Linux 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);
|
||||
});
|
||||
// Only execute if this is the main module (or in SEA where require.main is undefined)
|
||||
if (!module.parent && (require.main === module || require.main === undefined)) {
|
||||
console.log('Main module check passed, proceeding with server startup');
|
||||
console.log('module.parent:', module.parent);
|
||||
console.log('require.main === module:', require.main === module);
|
||||
console.log('require.main:', require.main);
|
||||
|
||||
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 {
|
||||
startVibeTunnelServer();
|
||||
console.log('Not main module, skipping server startup');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,7 +247,17 @@ interface AppInstance {
|
|||
activityMonitor: ActivityMonitor;
|
||||
}
|
||||
|
||||
// Track if app has been created
|
||||
let appCreated = false;
|
||||
|
||||
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();
|
||||
|
||||
// Check if help was requested
|
||||
|
|
@ -271,6 +281,7 @@ export function createApp(): AppInstance {
|
|||
|
||||
validateConfig(config);
|
||||
|
||||
console.log('Creating Express app and HTTP server...');
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
|
@ -406,6 +417,28 @@ export function createApp(): AppInstance {
|
|||
// Start server function
|
||||
const startServer = () => {
|
||||
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, () => {
|
||||
const address = server.address();
|
||||
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 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
|
||||
const appInstance = createApp();
|
||||
const {
|
||||
|
|
@ -499,6 +544,7 @@ export function startVibeTunnelServer() {
|
|||
activityMonitor,
|
||||
} = appInstance;
|
||||
|
||||
console.log('Starting server...');
|
||||
startServer();
|
||||
|
||||
// Cleanup old terminals every 5 minutes
|
||||
|
|
|
|||
Loading…
Reference in a new issue