534d9a54创建于 1月15日历史提交
#!/bin/bash

# Exit on any error, but handle errors gracefully
set -e

# Enable error trapping
trap 'handle_error $? $LINENO' ERR

# =============================================================================
# Configuration
# =============================================================================

# Project configuration
readonly APP_NAME="octopus"
readonly MAIN_DIR="./"
readonly OUTPUT_DIR="build"

# Build metadata
readonly BUILD_TIME="$(TZ='Asia/Shanghai' date +'%F %T %z')"
readonly GIT_AUTHOR="bestrui"
readonly GIT_VERSION="$(git describe --tags --abbrev=0 2>/dev/null || echo 'dev')"
readonly COMMIT_ID="$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"

# Build flags
readonly LDFLAGS="-X 'github.com/bestruirui/octopus/internal/conf.Version=${GIT_VERSION}' \
                  -X 'github.com/bestruirui/octopus/internal/conf.BuildTime=${BUILD_TIME}' \
                  -X 'github.com/bestruirui/octopus/internal/conf.Author=${GIT_AUTHOR}' \
                  -X 'github.com/bestruirui/octopus/internal/conf.Commit=${COMMIT_ID}' \
                  -s -w"

# =============================================================================
# Utility Functions
# =============================================================================

log_info() {
    echo "ℹ️  $1"
}

log_success() {
    echo "✅ $1"
}

log_error() {
    echo "❌ $1" >&2
}

log_warning() {
    echo "⚠️  $1" >&2
}

log_step() {
    echo ""
    echo "🔧 $1"
    echo "────────────────────────────────────────"
}

# Error handling function
handle_error() {
    local exit_code=$1
    local line_number=$2
    log_error "Build failed at line ${line_number} with exit code ${exit_code}"
    log_error "Command that failed: $(sed -n "${line_number}p" "$0" | xargs)"
    log_error "Check the output above for more details"
    exit $exit_code
}

# =============================================================================
# Setup Functions
# =============================================================================
command_exists() {
    command -v "$1" >/dev/null 2>&1
}

prepare_environment() {
    log_step "Preparing build environment"

    # Check and install required commands
    log_info "Checking required commands..."

    # Check Go
    if ! command_exists go; then
        log_error "Go is not installed. Please install Go from https://golang.org/dl/"
        return 1
    fi

    local go_version=$(go version 2>/dev/null | grep -o 'go[0-9]\+\.[0-9]\+' | head -1)
    log_success "Go version: $go_version"

    # Check Python
    if ! command_exists python3; then
        log_error "Python is not installed. Please install Python from https://www.python.org/downloads/"
        return 1
    fi

    local python_version=$(python3 --version 2>/dev/null)
    log_success "Python version: $python_version"

    # Check Node.js
    if ! command_exists node; then
        log_error "Node.js is not installed. Please install Node.js from https://nodejs.org/"
        return 1
    fi

    local node_version=$(node --version 2>/dev/null)
    log_success "Node.js version: $node_version"

    # Check pnpm
    if ! command_exists pnpm; then
        log_error "pnpm is not installed. Please install pnpm: npm install -g pnpm"
        return 1
    fi

    local pnpm_version=$(pnpm --version 2>/dev/null)
    log_success "pnpm version: $pnpm_version"

    # Check git
    if ! command_exists git; then
        log_error "git is not installed."
        return 1
    fi

    # Check curl
    if ! command_exists curl; then
        log_error "curl is not installed."
        return 1
    fi

    # Check unzip
    if ! command_exists unzip; then
        log_error "unzip is not installed."
        return 1
    fi

    # Check tar
    if ! command_exists tar; then
        log_error "tar is not installed."
        return 1
    fi

    # Check zip
    if ! command_exists zip; then
        log_error "zip is not installed."
        return 1
    fi

    # Check md5sum (or md5 on macOS)
    if ! command_exists md5sum && ! command_exists md5; then
        log_error "md5sum or md5 is not installed."
        return 1
    fi

    log_success "All required commands installed"

    # Create output directory and subdirectories
    log_info "Creating output directory structure: ${OUTPUT_DIR}"

    # Check if OUTPUT_DIR exists (including symlinks)
    if [ -e "${OUTPUT_DIR}" ]; then
        if [ -d "${OUTPUT_DIR}" ]; then
            log_success "Output directory already exists: ${OUTPUT_DIR}"
        else
            log_error "Output path exists but is not a directory: ${OUTPUT_DIR}"
            log_error "Path type: $(ls -la "${OUTPUT_DIR}" 2>/dev/null || echo 'Cannot determine type')"
            return 1
        fi
    else
        # Try to create the directory
        if ! mkdir -p "${OUTPUT_DIR}"; then
            log_error "Failed to create output directory: ${OUTPUT_DIR}"
            log_error "Current working directory: $(pwd)"
            log_error "Directory permissions: $(ls -la . 2>/dev/null || echo 'Cannot list directory')"
            return 1
        fi
        log_success "Created output directory: ${OUTPUT_DIR}"
    fi

    # Create subdirectories for organized output
    local subdirs=("bin" "docker" "archives")
    for subdir in "${subdirs[@]}"; do
        if ! mkdir -p "${OUTPUT_DIR}/${subdir}"; then
            log_error "Failed to create subdirectory: ${OUTPUT_DIR}/${subdir}"
            return 1
        fi
    done
    log_success "Created output subdirectories: bin, docker, archives"

    log_info "Tidying Go modules..."
    if ! go mod tidy >/dev/null 2>&1; then
        log_error "Failed to tidy Go modules"
        return 1
    fi

    log_success "Build environment ready"
}

# =============================================================================
# Build Functions
# =============================================================================

build_frontend() {
    log_step "Building frontend"

    local web_dir="web"

    # Check if web directory exists
    if [ ! -d "$web_dir" ]; then
        log_error "Web directory not found: $web_dir"
        log_error "Please run this script from the project root directory"
        return 1
    fi

    # Change to web directory
    cd "$web_dir" || return 1

    # Install dependencies
    log_info "Installing frontend dependencies..."
    if ! pnpm install; then
        log_error "Failed to install frontend dependencies"
        cd ..
        return 1
    fi
    log_success "Frontend dependencies installed"

    # Build the project
    log_info "Building frontend project..."
    if ! NEXT_PUBLIC_APP_VERSION="$GIT_VERSION" pnpm run build; then
        log_error "Failed to build frontend project"
        cd ..
        return 1
    fi
    log_success "Frontend build completed"

    # Return to original directory
    cd ..

    # Move out directory to static directory
    log_info "Moving frontend output to static directory..."
    
    # Remove old static/out if exists
    if [ -d "static/out" ]; then
        rm -rf "static/out"
        log_info "Removed old static/out directory"
    fi
    
    # Move web/out to static/out
    if [ -d "${web_dir}/out" ]; then
        mv "${web_dir}/out" "static/"
        log_success "Moved frontend output to static/out"
    else
        log_error "Frontend output directory not found: ${web_dir}/out"
        return 1
    fi

    return 0
}

update_price() {
    log_step "Updating price"
    if ! python3 scripts/updatePrice.py; then
        log_error "Failed to update price"
        return 1
    fi
    log_success "Price updated"
}


get_go_arch() {
    case "$1" in
    "x86_64") echo "amd64" ;;
    "arm64") echo "arm64" ;;
    "x86") echo "386" ;;
    "armv7") echo "arm" ;;
    *)
        log_error "Unsupported architecture: $1"
        return 1
        ;;
    esac
}

build_standard() {
    local os="$1"
    local arch="$2"
    local go_arch

    if ! go_arch="$(get_go_arch "${arch}")"; then
        log_error "Failed to get Go architecture: ${arch}"
        return 1
    fi

    local output_file="${OUTPUT_DIR}/bin/${APP_NAME}-${os}-${arch}"

    log_info "Building ${os}/${arch}..."

    if ! GOOS="${os}" GOARCH="${go_arch}" CGO_ENABLED=0 \
        go build -o "${output_file}" -ldflags="${LDFLAGS}" -tags=jsoniter "${MAIN_DIR}" 2>&1; then
        log_error "Failed to build ${os}/${arch}"
        log_error "Build command: GOOS=${os} GOARCH=${go_arch} CGO_ENABLED=0 go build -o ${output_file} -ldflags=\"${LDFLAGS}\" -tags=jsoniter ${MAIN_DIR}"
        return 1
    fi

    if [ ! -f "${output_file}" ]; then
        log_error "Build completed but output file not found: ${output_file}"
        return 1
    fi

    log_success "Built ${os}/${arch} → bin/$(basename "${output_file}")"
}

# =============================================================================
# Post-build Functions
# =============================================================================

create_archives() {
    log_step "Creating distribution archives"

    local archives_dir="${OUTPUT_DIR}/archives"

    # Copy documentation files to archives directory
    cp README.md LICENSE "${archives_dir}/" 2>/dev/null || log_info "Documentation files not found, skipping"

    # Archive all binaries (zip format for all platforms)
    while IFS= read -r -d '' file; do
        local basename_file
        basename_file=$(basename "$file")
        local extension=""

        # Add .exe extension for Windows binaries
        if [[ "$basename_file" == *"-windows-"* ]]; then
            extension=".exe"
        fi

        if ! cp "$file" "${archives_dir}/${APP_NAME}${extension}" 2>/dev/null; then
            log_error "Failed to copy $file to ${archives_dir}/${APP_NAME}${extension}"
            continue
        fi

        if (cd "${archives_dir}" && zip -q "${basename_file}.zip" "${APP_NAME}${extension}" README.md LICENSE 2>/dev/null); then
            rm -f "${archives_dir}/${APP_NAME}${extension}"
            log_success "Archived: archives/${basename_file}.zip"
        else
            log_error "Failed to create archive: ${basename_file}.zip"
            rm -f "${archives_dir}/${APP_NAME}${extension}"
        fi
    done < <(find "${OUTPUT_DIR}/bin/" -name "${APP_NAME}-*" -type f -print0 2>/dev/null)

    # Cleanup documentation files from archives directory
    rm -f "${archives_dir}/README.md" "${archives_dir}/LICENSE"

    if ! cd .. 2>/dev/null; then
        log_error "Failed to return to parent directory"
        return 1
    fi

    log_success "Created archives in ${archives_dir}/"
}

generate_checksums() {
    log_step "Generating checksums"

    local bin_dir="${OUTPUT_DIR}/bin"

    if ! cd "${bin_dir}" 2>/dev/null; then
        log_error "Failed to change to bin directory: ${bin_dir}"
        return 1
    fi

    if ! find . -maxdepth 1 -name "${APP_NAME}-*" -type f | head -1 | grep -q .; then
        log_info "No build artifacts found in bin directory, skipping checksums"
        cd ../.. 2>/dev/null || true
        return 0
    fi

    # Use appropriate checksum command based on OS
    local checksum_cmd
    if command_exists md5sum; then
        checksum_cmd="md5sum"
    elif command_exists md5; then
        checksum_cmd="md5 -r" # -r for BSD md5 to match md5sum format
    else
        log_error "No checksum command available (md5sum or md5)"
        cd ../.. 2>/dev/null || true
        return 1
    fi

    if find . -maxdepth 1 -name "${APP_NAME}-*" -type f -print0 | xargs -0 $checksum_cmd >md5.txt 2>/dev/null; then
        local checksum_count=$(wc -l <md5.txt 2>/dev/null || echo "0")
        log_success "Generated checksums for ${checksum_count} files in bin/"
    else
        log_error "Failed to generate checksums"
        cd ../.. 2>/dev/null || true
        return 1
    fi

    if ! cd ../.. 2>/dev/null; then
        log_error "Failed to return to parent directory"
        return 1
    fi
}

prepare_docker_binaries() {
    log_step "Preparing Docker binaries"

    local docker_dir="${OUTPUT_DIR}/docker"

    # Create docker directory under OUTPUT_DIR
    if ! mkdir -p "${docker_dir}"; then
        log_error "Failed to create docker directory: ${docker_dir}"
        log_error "Current working directory: $(pwd)"
        log_error "Directory permissions: $(ls -la . 2>/dev/null || echo 'Cannot list directory')"
        return 1
    fi

    local platforms=(
        "x86_64:linux/amd64"
        "x86:linux/386"
        "armv7:linux/arm/v7"
        "arm64:linux/arm64"
    )

    local copied_count=0

    for platform in "${platforms[@]}"; do
        local arch="${platform%%:*}"
        local docker_platform="${platform#*:}"
        local binary_name="${APP_NAME}-linux-${arch}"
        local platform_dir="${docker_dir}/${docker_platform}"

        if ! mkdir -p "${platform_dir}"; then
            log_error "Failed to create directory: ${platform_dir}"
            log_error "Docker platform: ${docker_platform}"
            continue
        fi

        # Try to copy from binary file first
        if [ -f "${OUTPUT_DIR}/bin/${binary_name}" ]; then
            if cp "${OUTPUT_DIR}/bin/${binary_name}" "${platform_dir}/${APP_NAME}" 2>/dev/null; then
                log_success "Copied bin/${binary_name} → docker/${docker_platform}/${APP_NAME}"
                ((copied_count++))
            else
                log_error "Failed to copy bin/${binary_name} to ${platform_dir}/${APP_NAME}"
            fi
        else
            log_warning "Binary not found: bin/${binary_name}"
        fi
    done

    if [ $copied_count -gt 0 ]; then
        log_success "Prepared ${copied_count} Docker binaries in ${docker_dir}/"
    else
        log_warning "No Docker binaries prepared"
    fi
}

# =============================================================================
# Main Execution
# =============================================================================

show_usage() {
    echo "Usage: $0 <command> [os] [arch]"
    echo ""
    echo "Commands:"
    echo "  release              Build all platforms and create distribution packages"
    echo "  build <os> <arch>    Build for specific OS and architecture"
    echo "  help                 Show this help message"
    echo ""
    echo "Supported OS:"
    echo "  linux, windows, darwin, android"
    echo ""
    echo "Supported architectures:"
    echo "  x86_64, arm64, armv7, x86"
    echo ""
    echo "Examples:"
    echo "  $0 build windows x86_64"
    echo "  $0 build linux x86_64"
    echo "  $0 build android arm64"
    echo "  $0 release"
}

validate_os_arch() {
    local os="$1"
    local arch="$2"

    # Validate OS
    case "$os" in
    "linux" | "windows" | "darwin" | "android") ;;
    *)
        log_error "Unsupported OS: $os"
        log_error "Supported OS: linux, windows, darwin, android"
        return 1
        ;;
    esac

    # Validate architecture
    case "$arch" in
    "x86_64" | "arm64" | "armv7" | "x86") ;;
    *)
        log_error "Unsupported architecture: $arch"
        log_error "Supported architectures: x86_64, arm64, armv7, x86"
        return 1
        ;;
    esac

    return 0
}

main() {
    case "${1:-}" in
    "build")
        if [ $# -ne 3 ]; then
            log_error "Build command requires OS and architecture"
            log_error "Usage: $0 build <os> <arch>"
            show_usage
            exit 1
        fi

        local os="$2"
        local arch="$3"

        if ! validate_os_arch "$os" "$arch"; then
            exit 1
        fi

        log_step "Starting single platform build"
        echo "📦 Building ${APP_NAME} ${GIT_VERSION} (${COMMIT_ID}) for ${os}/${arch}"
        echo ""

        # Setup
        if ! prepare_environment; then
            log_error "Failed to prepare build environment"
            exit 1
        fi

        # Build frontend
        if ! build_frontend; then
            log_error "Failed to build frontend"
            exit 1
        fi

        # Update price
        if ! update_price; then
            log_error "Failed to update price"
            exit 1
        fi

        # Build for specified platform
        log_step "Building binary"

        if ! build_standard "$os" "$arch"; then
            log_error "Failed to build ${os}/${arch}"
            exit 1
        fi

        log_step "Build completed"
        log_success "Binary ready: ${OUTPUT_DIR}/bin/${APP_NAME}-${os}-${arch}"
        ;;
    "release")
        log_step "Starting release build"
        echo "📦 Building ${APP_NAME} ${GIT_VERSION} (${COMMIT_ID})"
        echo ""

        # Setup
        if ! prepare_environment; then
            log_error "Failed to prepare build environment"
            exit 1
        fi

        # Build frontend
        if ! build_frontend; then
            log_error "Failed to build frontend"
            exit 1
        fi

        # Update price
        if ! update_price; then
            log_error "Failed to update price"
            exit 1
        fi

        # Build for different platforms
        log_step "Building binaries"

        # Standard builds (pure Go, static binaries)
        if ! build_standard linux x86_64; then
            log_error "Failed to build Linux x86_64"
        fi
        if ! build_standard linux arm64; then
            log_error "Failed to build Linux arm64"
        fi
        if ! build_standard linux armv7; then
            log_error "Failed to build Linux armv7"
        fi
        if ! build_standard linux x86; then
            log_error "Failed to build Linux x86"
        fi
        if ! build_standard windows x86_64; then
            log_error "Failed to build Windows x86_64"
        fi
        if ! build_standard windows x86; then
            log_error "Failed to build Windows x86"
        fi
        if ! build_standard darwin arm64; then
            log_error "Failed to build Darwin arm64"
        fi
        if ! build_standard darwin x86_64; then
            log_error "Failed to build Darwin arm64"
        fi

        # Post-processing
        if ! prepare_docker_binaries; then
            log_warning "Failed to prepare Docker binaries, but continuing..."
        fi

        if ! generate_checksums; then
            log_warning "Failed to generate checksums, but continuing..."
        fi

        if ! create_archives; then
            log_warning "Failed to create archives, but continuing..."
        fi

        log_step "Build completed"
        log_success "All artifacts ready in ${OUTPUT_DIR}/"
        log_info "  • Binaries: ${OUTPUT_DIR}/bin/"
        log_info "  • Docker binaries: ${OUTPUT_DIR}/docker/"
        log_info "  • Archives: ${OUTPUT_DIR}/archives/"
        ;;
    "help" | "-h" | "--help")
        show_usage
        ;;
    "")
        log_error "No command specified"
        show_usage
        exit 1
        ;;
    *)
        log_error "Unknown command: $1"
        show_usage
        exit 1
        ;;
    esac
}

main "$@"