#!/bin/bash
set -e
# ChunkHound Unified Build Script
# The ONE script that replaces the complex multi-script build system
# Builds for macOS (native) and Ubuntu (Docker) using PyInstaller onedir format
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
DIST_DIR="$PROJECT_ROOT/dist"
# Build configuration
BUILD_TIMESTAMP=$(date +%Y%m%d-%H%M%S)
HOST_PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
HOST_ARCH=$(uname -m)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
# Default options
CLEAN_BUILD=false
VALIDATE_BINARIES=false
VERBOSE=false
PLATFORM=""
# Version info
VERSION=$(grep '^version = ' "$PROJECT_ROOT/pyproject.toml" | sed 's/version = "\(.*\)"/\1/')
# Logging functions
log_info() {
echo -e "${BLUE}ℹ️ $1${NC}"
}
log_success() {
echo -e "${GREEN}✅ $1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}❌ $1${NC}"
}
log_step() {
echo -e "${BOLD}🚀 $1${NC}"
}
# Usage information
show_usage() {
cat << EOF
ChunkHound Unified Build Script v$VERSION
USAGE:
./scripts/build.sh [PLATFORM] [OPTIONS]
PLATFORMS:
mac Build macOS binary (native, requires macOS host)
ubuntu Build Ubuntu binary (Docker, works on any host)
all Build both platforms (requires macOS host)
OPTIONS:
--clean Clean previous artifacts before building
--validate Test binaries after build (startup time, basic functionality)
--verbose Show detailed build output
--help Show this help message
EXAMPLES:
./scripts/build.sh all # Build both platforms
./scripts/build.sh ubuntu --clean # Clean build Ubuntu only
./scripts/build.sh mac --validate # Build and test macOS binary
./scripts/build.sh all --clean --validate # Full clean build with testing
ARTIFACTS:
dist/
├── chunkhound-macos-universal/ # macOS onedir
├── chunkhound-ubuntu-amd64/ # Ubuntu onedir
├── chunkhound-macos-universal.tar.gz # Compressed macOS
├── chunkhound-ubuntu-amd64.tar.gz # Compressed Ubuntu
└── SHA256SUMS # Checksums
REQUIREMENTS:
- Python 3.10+ with uv package manager
- Docker (for Ubuntu builds)
- macOS host (for macOS builds)
EOF
}
# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
mac|ubuntu|all)
if [[ -n "$PLATFORM" ]]; then
log_error "Multiple platforms specified. Use 'all' to build both."
exit 1
fi
PLATFORM="$1"
shift
;;
--clean)
CLEAN_BUILD=true
shift
;;
--validate)
VALIDATE_BINARIES=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
--help|-h)
show_usage
exit 0
;;
*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Default to current platform if none specified
if [[ -z "$PLATFORM" ]]; then
if [[ "$HOST_PLATFORM" == "darwin" ]]; then
PLATFORM="mac"
else
PLATFORM="ubuntu"
fi
log_info "No platform specified, defaulting to: $PLATFORM"
fi
}
# Setup build environment
setup_build_env() {
log_step "Setting up build environment"
cd "$PROJECT_ROOT"
# Verify uv is available
if ! command -v uv &> /dev/null; then
log_error "uv package manager not found. Install from: https://docs.astral.sh/uv/"
exit 1
fi
# Clean artifacts if requested
if [[ "$CLEAN_BUILD" == true ]]; then
log_info "Cleaning previous artifacts..."
rm -rf "$DIST_DIR"
fi
# Create dist directory
mkdir -p "$DIST_DIR"
# Sync dependencies
log_info "Syncing dependencies with uv..."
if [[ "$VERBOSE" == true ]]; then
uv sync --dev
else
uv sync --dev > /dev/null 2>&1
fi
log_success "Build environment ready"
}
# Build macOS binary (native)
build_macos() {
log_step "Building macOS binary (native)"
if [[ "$HOST_PLATFORM" != "darwin" ]]; then
log_error "macOS builds require macOS host. Current host: $HOST_PLATFORM"
exit 1
fi
cd "$PROJECT_ROOT"
local output_dir="$DIST_DIR/chunkhound-macos-universal"
local spec_file="chunkhound-optimized.spec"
# Verify spec file exists
if [[ ! -f "$spec_file" ]]; then
log_error "PyInstaller spec file not found: $spec_file"
exit 1
fi
log_info "Building with PyInstaller (onedir format)..."
# Build with PyInstaller
local build_cmd="uv run pyinstaller $spec_file --clean --noconfirm --distpath $DIST_DIR"
if [[ "$VERBOSE" == true ]]; then
$build_cmd
else
$build_cmd > /dev/null 2>&1
fi
# Verify output directory exists and rename if needed
if [[ -d "$DIST_DIR/chunkhound-optimized" ]]; then
# Clean target directory if it exists
if [[ -d "$output_dir" ]]; then
rm -rf "$output_dir"
fi
# Rename the PyInstaller output directory
mv "$DIST_DIR/chunkhound-optimized" "$output_dir"
elif [[ ! -d "$output_dir" ]]; then
log_error "Build failed - output directory not found"
exit 1
fi
# Verify binary exists and is executable
local binary="$output_dir/chunkhound-optimized"
if [[ ! -f "$binary" ]]; then
log_error "Binary not found: $binary"
exit 1
fi
chmod +x "$binary"
# Create compressed archive
log_info "Creating compressed archive..."
cd "$DIST_DIR"
tar -czf "chunkhound-macos-universal.tar.gz" "chunkhound-macos-universal/"
log_success "macOS binary built successfully"
log_info "Binary: $output_dir/chunkhound-optimized"
log_info "Archive: $DIST_DIR/chunkhound-macos-universal.tar.gz"
}
# Build Ubuntu binary (Docker)
build_ubuntu() {
log_step "Building Ubuntu binary (Docker)"
# Verify Docker is available
if ! command -v docker &> /dev/null; then
log_error "Docker not found. Install from: https://docs.docker.com/get-docker/"
exit 1
fi
# Verify Docker is running
if ! docker info > /dev/null 2>&1; then
log_error "Docker daemon is not running. Start Docker and try again."
exit 1
fi
cd "$PROJECT_ROOT"
local output_dir="$DIST_DIR/chunkhound-ubuntu-amd64"
local docker_tag="chunkhound:build-$BUILD_TIMESTAMP"
log_info "Building with Docker (linux/amd64) using Ubuntu 16.04 for GLIBC compatibility..."
# CRITICAL: Build Docker image using Ubuntu 16.04 Dockerfile for maximum GLIBC compatibility
# Ubuntu 16.04 is the MINIMUM SYSTEM REQUIREMENT - do not regress this version
local build_cmd="docker build --platform linux/amd64 --tag $docker_tag"
if [[ "$VERBOSE" == true ]]; then
$build_cmd .
else
$build_cmd . > /dev/null 2>&1
fi
# Extract binary from container
log_info "Extracting binary from Docker container..."
local container_name="temp-ubuntu-$BUILD_TIMESTAMP"
docker create --name "$container_name" "$docker_tag" > /dev/null
# Create output directory
mkdir -p "$output_dir"
# Copy binary from container (Ubuntu 16.04 build)
docker cp "$container_name":/artifacts/binaries/chunkhound-optimized/. "$output_dir/"
# Clean up container
docker rm "$container_name" > /dev/null
# Clean up image
docker rmi "$docker_tag" > /dev/null 2>&1 || true
# Verify binary exists
local binary="$output_dir/chunkhound-optimized"
if [[ ! -f "$binary" ]]; then
log_error "Binary not found after Docker extraction: $binary"
exit 1
fi
chmod +x "$binary"
# Create compressed archive
log_info "Creating compressed archive..."
cd "$DIST_DIR"
tar -czf "chunkhound-ubuntu16-amd64.tar.gz" "chunkhound-ubuntu-amd64/"
log_success "Ubuntu 16.04 compatible binary built successfully"
log_info "Binary: $output_dir/chunkhound-optimized"
log_info "Archive: $DIST_DIR/chunkhound-ubuntu16-amd64.tar.gz"
log_info "GLIBC compatibility: Ubuntu 16.04 LTS and newer (MINIMUM SYSTEM REQUIREMENT)"
}
# Validate built binaries
validate_binaries() {
log_step "Validating built binaries"
local validation_failed=false
# Test macOS binary
if [[ -f "$DIST_DIR/chunkhound-macos-universal/chunkhound-optimized" ]]; then
log_info "Testing macOS binary..."
local binary="$DIST_DIR/chunkhound-macos-universal/chunkhound-optimized"
# Test version command
if ! "$binary" --version > /dev/null 2>&1; then
log_error "macOS binary version check failed"
validation_failed=true
else
local version_output=$("$binary" --version 2>&1)
log_info "macOS binary version: $version_output"
fi
# Test help command (quick startup test)
log_info "Testing macOS binary startup time..."
local start_time=$(date +%s.%N)
if ! "$binary" --help > /dev/null 2>&1; then
log_error "macOS binary help command failed"
validation_failed=true
else
local end_time=$(date +%s.%N)
local startup_time=$(echo "$end_time - $start_time" | bc -l 2>/dev/null || echo "< 1.0")
log_info "macOS binary startup time: ${startup_time}s"
fi
# Check binary size
local size=$(ls -lh "$binary" | awk '{print $5}')
log_info "macOS binary size: $size"
fi
# Test Ubuntu binary (only if we can run Linux binaries)
if [[ -f "$DIST_DIR/chunkhound-ubuntu-amd64/chunkhound-optimized" ]]; then
log_info "Testing Ubuntu binary..."
local binary="$DIST_DIR/chunkhound-ubuntu-amd64/chunkhound-optimized"
# Check if we can run Linux binaries (e.g., on Linux or via Docker)
if [[ "$HOST_PLATFORM" == "linux" ]]; then
# Test version command
if ! "$binary" --version > /dev/null 2>&1; then
log_error "Ubuntu binary version check failed"
validation_failed=true
else
local version_output=$("$binary" --version 2>&1)
log_info "Ubuntu binary version: $version_output"
fi
# Test startup time
log_info "Testing Ubuntu binary startup time..."
local start_time=$(date +%s.%N)
if ! "$binary" --help > /dev/null 2>&1; then
log_error "Ubuntu binary help command failed"
validation_failed=true
else
local end_time=$(date +%s.%N)
local startup_time=$(echo "$end_time - $start_time" | bc -l 2>/dev/null || echo "< 1.0")
log_info "Ubuntu binary startup time: ${startup_time}s"
fi
else
log_info "Skipping Ubuntu binary runtime test (wrong host platform)"
log_info "Verifying binary file properties instead..."
if file "$binary" | grep -q "ELF.*executable"; then
log_info "Ubuntu binary is valid ELF executable"
else
log_error "Ubuntu binary is not a valid ELF executable"
validation_failed=true
fi
fi
# Check binary size
local size=$(ls -lh "$binary" | awk '{print $5}')
log_info "Ubuntu binary size: $size"
fi
if [[ "$validation_failed" == true ]]; then
log_error "Binary validation failed"
exit 1
else
log_success "All binaries validated successfully"
fi
}
# Generate checksums
generate_checksums() {
log_step "Generating checksums"
cd "$DIST_DIR"
# Remove old checksums
rm -f SHA256SUMS
# Generate checksums for all tar.gz files
for file in *.tar.gz; do
if [[ -f "$file" ]]; then
sha256sum "$file" >> SHA256SUMS
fi
done
if [[ -f "SHA256SUMS" ]]; then
log_success "Checksums generated: $DIST_DIR/SHA256SUMS"
if [[ "$VERBOSE" == true ]]; then
cat SHA256SUMS
fi
fi
}
# Build summary
build_summary() {
log_step "Build Summary"
echo
log_info "Build completed successfully!"
log_info "Timestamp: $(date)"
log_info "Platform(s): $PLATFORM"
log_info "Output directory: $DIST_DIR"
echo
if [[ -d "$DIST_DIR" ]]; then
log_info "Generated artifacts:"
ls -la "$DIST_DIR" | grep -E '\.(tar\.gz|SHA256SUMS)$' || true
echo
# Show directory sizes
for dir in "$DIST_DIR"/chunkhound-*; do
if [[ -d "$dir" ]]; then
local size=$(du -sh "$dir" 2>/dev/null | cut -f1)
local name=$(basename "$dir")
log_info "$name: $size"
fi
done
fi
echo
log_success "Ready for deployment! 🚀"
}
# Main execution
main() {
# Show header
echo -e "${BOLD}ChunkHound Unified Build Script v$VERSION${NC}"
echo -e "${BLUE}Building high-performance code search binaries${NC}"
echo
# Parse arguments
parse_args "$@"
# Setup build environment
setup_build_env
# Build based on platform selection
case "$PLATFORM" in
mac)
build_macos
;;
ubuntu)
build_ubuntu
;;
all)
build_macos
build_ubuntu
;;
*)
log_error "Invalid platform: $PLATFORM"
show_usage
exit 1
;;
esac
# Generate checksums
generate_checksums
# Validate binaries if requested
if [[ "$VALIDATE_BINARIES" == true ]]; then
validate_binaries
fi
# Show build summary
build_summary
}
# Execute main function with all arguments
main "$@"