搭建Hyperf本地开发环境之Docker容器开发(二)

0 阅读22分钟

写在前面

《搭建Hyperf本地开发环境之Docker容器开发(二)》是继 《搭建Hyperf本地开发环境之Docker容器开发(一)》的补充说明;

为什么要做这个补充呢,是因为我发现我在安装 oracle 扩展的时候,发现不能成功,所以重新做了改动来满足我的需求;

初衷还是不变,都是通过修改配置文件来控制一切;

快闪回放

搭建Hyperf本地开发环境之Docker容器开发(一)

目录结构

project
|--config
   |--build.conf
   |--extension_deps.conf
   |--extension_special.conf
   |--system_tools.conf
|--extension
|--logs
|--build.sh
|--Dockerfile

文件内容及说明

build.conf
# ========================================
# PHP Hyperf Docker 构建配置文件
# ========================================

# PHP 基础镜像(只能选择一个,其他注释掉)
[base_image]
#php:8.3-cli
#php:8.3-cli-alpine
#php:8.2-cli
#php:8.2-cli-alpine
#php:8.1-cli
#php:8.1-cli-alpine
php:7.4-cli
#php:7.4-cli-alpine

# ========================================
# 扩展配置
# 格式说明:
#   扩展名           - 安装最新版本
#   扩展名-版本号    - 安装指定版本
#   local:扩展名     - 强制从本地安装
#   pecl:扩展名      - 强制从PECL安装
# ========================================
[extension]
# 核心扩展(docker-php-ext-install)
pcntl
pdo_mysql
bcmath
sockets

# GD库(自动处理依赖)
gd

# 压缩扩展
zip

# PECL扩展
redis
swoole-4.8.13

# 可选扩展(按需取消注释)
#mongodb
#xlswriter
#protobuf
#grpc
#imagick
#memcached
#oci8
pdo_oci
# SQL Server php7.4 对应 pdo_sqlsrv-5.10.1
pdo_sqlsrv-5.10.1
#sqlsrv
pdo_pgsql
#pgsql
#amqp
#rdkafka
#igbinary
#msgpack
#yaf
#yar
#apcu
#xdebug

# ========================================
# 构建模式:prod / dev
# prod: 生产模式,优化构建,清理缓存
# dev:  开发模式,保留调试信息
# ========================================
[mode]
prod

# ========================================
# 项目工作目录
# ========================================
[workdir]
/opt/www

# ========================================
# APT/APK 系统镜像源(用于加速系统包下载)
# 可选:default / aliyun / ustc / tsinghua / 163
# default: 使用官方源
# ========================================
[system_mirror]
aliyun

# ========================================
# Composer 镜像源
# 可选:auto / aliyun / tencent / ustc / packagist / huaweicloud
# auto: 自动检测最快的源
# ========================================
[composer_mirror]
auto

# ========================================
# PHP 配置(php.ini)
# 格式:配置项=值
# ========================================
[php]
swoole.use_shortname=Off
memory_limit=1G
opcache.enable_cli=On
opcache.jit_buffer_size=128M
date.timezone=Asia/Shanghai
max_execution_time=0
post_max_size=100M
upload_max_filesize=100M

# ========================================
# Docker CMD 命令配置
# 留空则不设置CMD
# ========================================
[cmd]
#php /opt/www/bin/hyperf.php start

# ========================================
# 常用系统工具
# 支持的工具列表见 config/system_tools.conf
# 脚本会自动识别系统类型安装对应的包
# ========================================
[system_tools]
# 网络诊断工具
#curl
wget
telnet
#netcat
#ping
#traceroute
#dig
#nslookup
ss
netstat
lsof
#tcpdump
#iptables

# 编辑器和文本处理
vim
#nano
#less
#jq
#tree

# 系统监控工具
#htop
#top
#ps
#iostat
#vmstat
#strace

# 文件和压缩工具
#tar
#gzip
#unzip
#zip
#file

# 其他实用工具
#git
#procps
#coreutils
#findutils
#grep
#sed
#awk

# ========================================
# 额外的系统包(空格分隔,直接指定包名)
# ========================================
[extra_packages]


# ========================================
# 环境变量配置
# 格式:变量名=值
# ========================================
[env]
#APP_ENV=dev
#SCAN_CACHEABLE=false

# ========================================
# 构建参数
# ========================================
[build_args]
# 启用构建缓存
enable_cache=true
# 并行编译数(0为自动检测)
parallel_jobs=0
# 内存限制(防止OOM,单位MB,0为不限制)
memory_limit=2048
# 清理构建缓存
clean_cache=true
extension_deps.conf
# ========================================
# PHP 扩展依赖配置文件
# 格式:扩展名|Debian依赖|Alpine依赖
# 多个依赖用逗号分隔
# ========================================

# 核心扩展
pcntl||
pdo_mysql||
bcmath||
sockets||
calendar||
exif||
gettext||
shmop||
sysvmsg||
sysvsem||
sysvshm||

# GD库
gd|libpng-dev,libjpeg-dev,libfreetype6-dev,libwebp-dev,libxpm-dev|libpng-dev,libjpeg-turbo-dev,freetype-dev,libwebp-dev,libxpm-dev

# 压缩相关
zip|libzip-dev|libzip-dev
bz2|libbz2-dev|bzip2-dev
zlib||zlib-dev

# 图像处理
imagick|libmagickwand-dev|imagemagick-dev

# 数据库扩展
pdo_pgsql|libpq-dev|postgresql-dev
pgsql|libpq-dev|postgresql-dev
pdo_sqlite|libsqlite3-dev|sqlite-dev
sqlite3|libsqlite3-dev|sqlite-dev

# Redis/Memcached
redis||
memcached|libmemcached-dev,zlib1g-dev|libmemcached-dev,zlib-dev

# 消息队列
amqp|librabbitmq-dev|rabbitmq-c-dev
rdkafka|librdkafka-dev|librdkafka-dev

# 其他常用扩展
swoole|libcurl4-openssl-dev,libssl-dev,libc-ares-dev|curl-dev,openssl-dev,c-ares-dev
mongodb|libssl-dev|openssl-dev
xlswriter|zlib1g-dev|zlib-dev
protobuf||
grpc||
igbinary||
msgpack||
apcu||
xdebug||
yaf||
yar|libcurl4-openssl-dev|curl-dev

# 国际化
intl|libicu-dev|icu-dev
iconv||

# 加密
sodium|libsodium-dev|libsodium-dev
mcrypt|libmcrypt-dev|libmcrypt-dev
openssl||

# XML相关
xml|libxml2-dev|libxml2-dev
dom|libxml2-dev|libxml2-dev
xmlrpc|libxml2-dev|libxml2-dev
xmlreader|libxml2-dev|libxml2-dev
xmlwriter|libxml2-dev|libxml2-dev
xsl|libxslt-dev|libxslt-dev
simplexml|libxml2-dev|libxml2-dev
soap|libxml2-dev|libxml2-dev

# LDAP
ldap|libldap2-dev|openldap-dev

# 其他
curl|libcurl4-openssl-dev|curl-dev
gmp|libgmp-dev|gmp-dev
imap|libc-client-dev,libkrb5-dev|imap-dev,krb5-dev
snmp|libsnmp-dev|net-snmp-dev
tidy|libtidy-dev|tidyhtml-dev
ftp||
fileinfo||
phar||
posix||
readline|libreadline-dev|readline-dev
tokenizer||
ctype||
filter||
json||
mbstring|libonig-dev|oniguruma-dev
pdo||
reflection||
session||
standard||
opcache||

extension_special.conf
# ========================================
# PHP 扩展特殊配置文件
# 需要特殊处理的扩展配置
# ========================================

# ----------------------------------------
# GD 库配置
# ----------------------------------------
[gd]
# Debian 配置命令
debian_configure=docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp --with-xpm
# Alpine 配置命令
alpine_configure=docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp --with-xpm
# PHP 7.x 兼容配置(不同的参数格式)
php7_debian_configure=docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-webp-dir=/usr/include/ --with-xpm-dir=/usr/include/
php7_alpine_configure=docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-webp-dir=/usr/include/ --with-xpm-dir=/usr/include/

# ----------------------------------------
# Oracle 扩展配置 (oci8, pdo_oci)
# ----------------------------------------
[oci8]
# 依赖:需要 Oracle Instant Client
# 注意:oci8 需要单独安装,不能和其他核心扩展一起批量安装
install_method=separate
pre_install=<<EOF
# 下载并安装 Oracle Instant Client
log_info "安装 Oracle Instant Client..."
mkdir -p /opt/oracle
cd /opt/oracle

# 设置 Oracle 版本
ORACLE_VERSION="21_14"
ORACLE_MAJOR="21"

# Debian
if command -v apt-get &>/dev/null; then
    apt-get update && apt-get install -y libaio1 libaio-dev unzip wget
    
    # 下载 Oracle Instant Client
    if [ ! -f "instantclient-basic-linux.x64-${ORACLE_MAJOR}.*.zip" ]; then
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-basic-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-basic.zip || \
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" -O instantclient-basic.zip
    fi
    if [ ! -f "instantclient-sdk-linux.x64-${ORACLE_MAJOR}.*.zip" ]; then
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-sdk-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-sdk.zip || \
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" -O instantclient-sdk.zip
    fi
    
    unzip -o instantclient-basic.zip
    unzip -o instantclient-sdk.zip
    rm -f *.zip
    
    # 获取实际安装目录
    INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
    if [ -z "$INSTANT_CLIENT_DIR" ]; then
        log_error "Oracle Instant Client 安装失败"
    fi
    
    # 配置链接库
    echo "$INSTANT_CLIENT_DIR" > /etc/ld.so.conf.d/oracle-instantclient.conf
    ldconfig
fi

# Alpine
if command -v apk &>/dev/null; then
    apk add --no-cache libaio libnsl libc6-compat unzip wget
    
    if [ ! -f "instantclient-basic.zip" ]; then
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-basic-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-basic.zip || \
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" -O instantclient-basic.zip
    fi
    if [ ! -f "instantclient-sdk.zip" ]; then
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-sdk-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-sdk.zip || \
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" -O instantclient-sdk.zip
    fi
    
    unzip -o instantclient-basic.zip
    unzip -o instantclient-sdk.zip
    rm -f *.zip
    
    INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
    if [ -z "$INSTANT_CLIENT_DIR" ]; then
        log_error "Oracle Instant Client 安装失败"
    fi
    
    # Alpine 兼容性
    ln -sf /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2 2>/dev/null || true
fi

# 导出环境变量(写入 profile 以便后续脚本使用)
export ORACLE_HOME="$INSTANT_CLIENT_DIR"
export LD_LIBRARY_PATH="$INSTANT_CLIENT_DIR:$LD_LIBRARY_PATH"
echo "export ORACLE_HOME=$INSTANT_CLIENT_DIR" >> /etc/profile
echo "export LD_LIBRARY_PATH=$INSTANT_CLIENT_DIR:\$LD_LIBRARY_PATH" >> /etc/profile

log_info "Oracle Instant Client 安装完成: $INSTANT_CLIENT_DIR"

# 直接安装 oci8 扩展
log_step "安装 oci8 扩展..."
docker-php-ext-configure oci8 --with-oci8=instantclient,$INSTANT_CLIENT_DIR
docker-php-ext-install -j$(nproc) oci8
log_info "oci8 扩展安装成功"
EOF
# 标记为已在 pre_install 中完成安装
skip_install=true

[pdo_oci]
# pdo_oci 需要单独安装
install_method=separate
pre_install=<<EOF
# 检查 Oracle Instant Client 是否已安装(如果没有安装 oci8 的话)
INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)

if [ -z "$INSTANT_CLIENT_DIR" ]; then
    log_info "安装 Oracle Instant Client for pdo_oci..."
    mkdir -p /opt/oracle
    cd /opt/oracle
    
    ORACLE_VERSION="21_14"
    ORACLE_MAJOR="21"
    
    if command -v apt-get &>/dev/null; then
        apt-get update && apt-get install -y libaio1 libaio-dev unzip wget
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-basic-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-basic.zip || \
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" -O instantclient-basic.zip
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-sdk-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-sdk.zip || \
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" -O instantclient-sdk.zip
        unzip -o instantclient-basic.zip
        unzip -o instantclient-sdk.zip
        rm -f *.zip
        INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
        echo "$INSTANT_CLIENT_DIR" > /etc/ld.so.conf.d/oracle-instantclient.conf
        ldconfig
    fi
    
    if command -v apk &>/dev/null; then
        apk add --no-cache libaio libnsl libc6-compat unzip wget
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-basic-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-basic.zip || \
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-basic-linuxx64.zip" -O instantclient-basic.zip
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/${ORACLE_VERSION}/instantclient-sdk-linux.x64-${ORACLE_MAJOR}.14.0.0.0dbru.zip" -O instantclient-sdk.zip || \
        wget -q "https://download.oracle.com/otn_software/linux/instantclient/instantclient-sdk-linuxx64.zip" -O instantclient-sdk.zip
        unzip -o instantclient-basic.zip
        unzip -o instantclient-sdk.zip
        rm -f *.zip
        INSTANT_CLIENT_DIR=$(ls -d /opt/oracle/instantclient_* 2>/dev/null | head -1)
        ln -sf /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2 2>/dev/null || true
    fi
    
    echo "export ORACLE_HOME=$INSTANT_CLIENT_DIR" >> /etc/profile
    echo "export LD_LIBRARY_PATH=$INSTANT_CLIENT_DIR:\$LD_LIBRARY_PATH" >> /etc/profile
fi

export ORACLE_HOME="$INSTANT_CLIENT_DIR"
export LD_LIBRARY_PATH="$INSTANT_CLIENT_DIR:$LD_LIBRARY_PATH"

log_info "Oracle Instant Client 路径: $INSTANT_CLIENT_DIR"

# 直接安装 pdo_oci 扩展
log_step "安装 pdo_oci 扩展..."
docker-php-ext-configure pdo_oci --with-pdo-oci=instantclient,$INSTANT_CLIENT_DIR
docker-php-ext-install -j$(nproc) pdo_oci
log_info "pdo_oci 扩展安装成功"
EOF
# 标记为已在 pre_install 中完成安装
skip_install=true

# ----------------------------------------
# SQL Server 扩展配置 (sqlsrv, pdo_sqlsrv)
# ----------------------------------------
[sqlsrv]
pre_install=<<EOF
# 安装 Microsoft ODBC Driver
if command -v apt-get &>/dev/null; then
    # 安装 gnupg 和 curl
    apt-get update
    apt-get install -y gnupg curl

    curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg
    #curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
    curl -fsSL https://packages.microsoft.com/config/debian/$(cat /etc/debian_version | cut -d. -f1)/prod.list > /etc/apt/sources.list.d/mssql-release.list
    apt-get update
    ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
fi
if command -v apk &>/dev/null; then
    apk add --no-cache unixodbc-dev
    # Alpine 需要从 edge 仓库安装 msodbcsql
    echo "Note: SQL Server driver on Alpine requires manual setup"
fi
EOF
# PECL 安装
install_method=pecl

[pdo_sqlsrv]
pre_install=<<EOF
# 复用 sqlsrv 的 ODBC 驱动安装
if ! command -v sqlcmd &>/dev/null; then
    if command -v apt-get &>/dev/null; then
        # 安装 gnupg 和 curl
        apt-get update
        apt-get install -y gnupg curl

        curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg
        #curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
        curl -fsSL https://packages.microsoft.com/config/debian/$(cat /etc/debian_version | cut -d. -f1)/prod.list > /etc/apt/sources.list.d/mssql-release.list
        apt-get update
        ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
    fi
    if command -v apk &>/dev/null; then
        apk add --no-cache unixodbc-dev
    fi
fi
EOF
install_method=pecl

# ----------------------------------------
# PostgreSQL 扩展配置
# ----------------------------------------
[pdo_pgsql]
# Debian 额外配置
debian_extra=ln -sf /usr/include/postgresql /usr/include/pgsql

[pgsql]
debian_extra=ln -sf /usr/include/postgresql /usr/include/pgsql

# ----------------------------------------
# IMAP 扩展配置
# ----------------------------------------
[imap]
debian_configure=docker-php-ext-configure imap --with-kerberos --with-imap-ssl
alpine_configure=docker-php-ext-configure imap --with-kerberos --with-imap-ssl

# ----------------------------------------
# LDAP 扩展配置
# ----------------------------------------
[ldap]
debian_extra=ln -sf /usr/lib/x86_64-linux-gnu/libldap.so /usr/lib/libldap.so && ln -sf /usr/lib/x86_64-linux-gnu/liblber.so /usr/lib/liblber.so
debian_configure=docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu

# ----------------------------------------
# Swoole 扩展配置
# ----------------------------------------
[swoole]
# Swoole 编译参数
pecl_configure=--enable-openssl --enable-sockets --enable-mysqlnd --enable-swoole-curl --enable-cares
# PHP 7.x 兼容配置
php7_pecl_configure=--enable-openssl --enable-sockets --enable-mysqlnd --enable-http2

# ----------------------------------------
# GrPC 扩展配置
# ----------------------------------------
[grpc]
# grpc 需要大量内存编译
build_memory=4096
# 编译超时时间(秒)
build_timeout=3600

# ----------------------------------------
# xlswriter 扩展配置
# ----------------------------------------
[xlswriter]
pecl_configure=--enable-reader

# ----------------------------------------
# imagick 扩展配置
# ----------------------------------------
[imagick]
# 需要 ImageMagick 开发库
post_install=<<EOF
# 确保 ImageMagick 策略允许 PDF 处理
if [ -f /etc/ImageMagick-6/policy.xml ]; then
    sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
fi
if [ -f /etc/ImageMagick-7/policy.xml ]; then
    sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-7/policy.xml
fi
EOF

# ----------------------------------------
# memcached 扩展配置
# ----------------------------------------
[memcached]
pecl_configure=--enable-memcached-igbinary --enable-memcached-json --enable-memcached-session --enable-memcached-sasl

# ----------------------------------------
# xdebug 扩展配置
# ----------------------------------------
[xdebug]
# 仅在 dev 模式安装
mode_only=dev
# xdebug 3.x 配置
php_ini_config=<<EOF
xdebug.mode=develop,debug
xdebug.start_with_request=yes
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
EOF

# ----------------------------------------
# opcache 扩展配置
# ----------------------------------------
[opcache]
php_ini_config=<<EOF
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=100000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1
EOF
# 生产模式特殊配置
prod_php_ini_config=<<EOF
opcache.enable=1
opcache.enable_cli=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=100000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1
opcache.jit_buffer_size=256M
opcache.jit=1255
EOF

system_tools.conf
# ========================================
# 系统工具映射配置
# 格式: 工具名|debian包名|alpine包名|说明
# 如果包名为 - 表示该系统不支持或不需要安装
# ========================================

# 网络诊断工具
curl|curl|curl|命令行URL工具
wget|wget|wget|文件下载工具
telnet|telnet|busybox-extras|远程登录协议客户端
netcat|netcat-openbsd|netcat-openbsd|网络连接工具
nc|netcat-openbsd|netcat-openbsd|netcat别名
ping|iputils-ping|iputils|网络连通性测试
traceroute|traceroute|traceroute|路由追踪
tracepath|iputils-tracepath|iputils|路径MTU发现
dig|dnsutils|bind-tools|DNS查询工具
nslookup|dnsutils|bind-tools|DNS查询工具
host|dnsutils|bind-tools|DNS查询工具
ss|iproute2|iproute2|套接字统计工具
netstat|net-tools|net-tools|网络统计工具(旧版)
ip|iproute2|iproute2|IP配置工具
ifconfig|net-tools|net-tools|网络接口配置(旧版)
lsof|lsof|lsof|列出打开的文件
tcpdump|tcpdump|tcpdump|网络抓包工具
iptables|iptables|iptables|防火墙规则管理
nmap|nmap|nmap|网络扫描工具
mtr|mtr|mtr|网络诊断工具

# 编辑器和文本处理
vim|vim|vim|Vi改进版编辑器
vi|vim|vim|vim别名
nano|nano|nano|简易文本编辑器
less|less|less|分页查看器
more|util-linux|util-linux|分页查看器
cat|coreutils|coreutils|文件查看
head|coreutils|coreutils|查看文件头部
tail|coreutils|coreutils|查看文件尾部
jq|jq|jq|JSON处理工具
yq|yq|yq|YAML处理工具
tree|tree|tree|目录树显示
diff|diffutils|diffutils|文件比较

# 系统监控工具
htop|htop|htop|交互式进程查看器
top|procps|procps|进程监控
ps|procps|procps|进程状态
free|procps|procps|内存使用情况
uptime|procps|procps|系统运行时间
watch|procps|procps|周期性执行命令
pstree|psmisc|psmisc|进程树显示
killall|psmisc|psmisc|按名称终止进程
fuser|psmisc|psmisc|显示文件使用者
iostat|sysstat|sysstat|IO统计
vmstat|procps|procps|虚拟内存统计
sar|sysstat|sysstat|系统活动报告
mpstat|sysstat|sysstat|CPU统计
pidstat|sysstat|sysstat|进程统计
strace|strace|strace|系统调用追踪
ltrace|ltrace|-|库调用追踪
dstat|dstat|-|资源统计工具
iotop|iotop|iotop|IO监控
iftop|iftop|iftop|网络带宽监控
nload|nload|nload|网络流量监控

# 文件和压缩工具
tar|tar|tar|归档工具
gzip|gzip|gzip|gzip压缩
gunzip|gzip|gzip|gzip解压
bzip2|bzip2|bzip2|bzip2压缩
bunzip2|bzip2|bzip2|bzip2解压
xz|xz-utils|xz|xz压缩
unxz|xz-utils|xz|xz解压
zip|zip|zip|zip压缩
unzip|unzip|unzip|zip解压
7z|p7zip-full|p7zip|7zip压缩
rar|unrar|-|RAR解压
file|file|file|文件类型识别
stat|coreutils|coreutils|文件状态
du|coreutils|coreutils|磁盘使用统计
df|coreutils|coreutils|磁盘空间统计
ln|coreutils|coreutils|链接创建
cp|coreutils|coreutils|文件复制
mv|coreutils|coreutils|文件移动
rm|coreutils|coreutils|文件删除
mkdir|coreutils|coreutils|创建目录
rmdir|coreutils|coreutils|删除目录
chmod|coreutils|coreutils|权限修改
chown|coreutils|coreutils|所有者修改
touch|coreutils|coreutils|修改时间戳

# 查找和搜索工具
find|findutils|findutils|文件查找
locate|mlocate|mlocate|快速文件定位
which|debianutils|which|命令路径查找
whereis|util-linux|util-linux|命令位置查找
grep|grep|grep|文本搜索
egrep|grep|grep|扩展正则搜索
fgrep|grep|grep|固定字符串搜索
ack|ack|ack|代码搜索工具
ag|silversearcher-ag|the_silver_searcher|快速代码搜索
rg|ripgrep|-|更快的代码搜索

# 文本处理
sed|sed|sed|流编辑器
awk|gawk|gawk|文本处理语言
gawk|gawk|gawk|GNU AWK
cut|coreutils|coreutils|文本切割
sort|coreutils|coreutils|排序
uniq|coreutils|coreutils|去重
wc|coreutils|coreutils|字数统计
tr|coreutils|coreutils|字符转换
tee|coreutils|coreutils|输出分流
xargs|findutils|findutils|参数传递
column|bsdmainutils|util-linux|列格式化

# 版本控制
git|git|git|Git版本控制
svn|subversion|subversion|SVN版本控制

# 系统管理
sudo|sudo|sudo|权限提升
su|login|shadow|用户切换
useradd|passwd|shadow|添加用户
usermod|passwd|shadow|修改用户
userdel|passwd|shadow|删除用户
groupadd|passwd|shadow|添加组
passwd|passwd|passwd|修改密码
crontab|cron|busybox|定时任务
systemctl|systemd|-|服务管理(Debian)
service|sysvinit-utils|openrc|服务管理
rc-service|-|openrc|服务管理(Alpine)

# 开发工具
make|make|make|构建工具
gcc|gcc|gcc|C编译器
g++|g++|g++|C++编译器
python|python3|python3|Python解释器
python3|python3|python3|Python3解释器
pip|python3-pip|py3-pip|Python包管理
node|nodejs|nodejs|Node.js
npm|npm|npm|Node包管理
perl|perl|perl|Perl解释器

# 数据库客户端
mysql|mariadb-client|mariadb-client|MySQL客户端
psql|postgresql-client|postgresql-client|PostgreSQL客户端
redis-cli|redis-tools|redis|Redis客户端
mongo|mongodb-clients|-|MongoDB客户端
sqlite3|sqlite3|sqlite|SQLite客户端

# 其他实用工具
procps|procps|procps|进程工具集
coreutils|coreutils|coreutils|核心工具集
util-linux|util-linux|util-linux|系统工具集
ca-certificates|ca-certificates|ca-certificates|CA证书
openssl|openssl|openssl|SSL工具
tzdata|tzdata|tzdata|时区数据
locales|locales|-|语言环境(仅Debian)
bash|bash|bash|Bash shell
zsh|zsh|zsh|Zsh shell
tmux|tmux|tmux|终端复用器
screen|screen|screen|终端复用器
rsync|rsync|rsync|文件同步
sshpass|sshpass|sshpass|SSH密码工具
expect|expect|expect|自动化交互工具
bc|bc|bc|计算器
xxd|xxd|vim|十六进制查看
hexdump|bsdmainutils|util-linux|十六进制转储
base64|coreutils|coreutils|Base64编解码
md5sum|coreutils|coreutils|MD5校验
sha256sum|coreutils|coreutils|SHA256校验

build.sh
#!/bin/bash
# ========================================
# PHP Hyperf Docker 构建脚本
# 用法: ./build.sh 镜像名称:标签
# ========================================

set -e
set -o pipefail  # 确保管道中任何一步失败,整个命令都返回错误

# ========================================
# 全局变量
# ========================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="${SCRIPT_DIR}/config"
BUILD_SCRIPTS_DIR="${SCRIPT_DIR}/build_scripts"
EXTENSION_DIR="${SCRIPT_DIR}/extension"
LOGS_DIR="${SCRIPT_DIR}/logs"
LOG_FILE="${LOGS_DIR}/build_$(date +%Y%m%d_%H%M%S).log"

# 配置文件
BUILD_CONF="${CONFIG_DIR}/build.conf"
EXTENSION_DEPS_CONF="${CONFIG_DIR}/extension_deps.conf"
EXTENSION_SPECIAL_CONF="${CONFIG_DIR}/extension_special.conf"

# 默认值
DEFAULT_WORKDIR="/opt/www"
DEFAULT_MODE="prod"
DEFAULT_COMPOSER_MIRROR="auto"

# Bash 版本检查
BASH_VERSION_MAJOR="${BASH_VERSION%%.*}"

# ========================================
# 颜色定义
# ========================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m' # No Color
BOLD='\033[1m'
BLINK='\033[5m'

# ========================================
# 日志函数
# ========================================
log_info() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $1"
    echo -e "${GREEN}${msg}${NC}"
    echo "$msg" >> "$LOG_FILE"
}

log_warn() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $1"
    echo -e "${YELLOW}${msg}${NC}"
    echo "$msg" >> "$LOG_FILE"
}

log_error() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1"
    echo -e "${RED}${BOLD}${msg}${NC}"
    echo "$msg" >> "$LOG_FILE"
}

log_success() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS] $1"
    echo -e "${GREEN}${BOLD}${msg}${NC}"
    echo "$msg" >> "$LOG_FILE"
}

log_step() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [STEP] $1"
    echo -e "${CYAN}${BOLD}══════════════════════════════════════════════════════════${NC}"
    echo -e "${CYAN}${BOLD}  $1${NC}"
    echo -e "${CYAN}${BOLD}══════════════════════════════════════════════════════════${NC}"
    echo "$msg" >> "$LOG_FILE"
}

log_debug() {
    if [ "$DEBUG" = "true" ]; then
        local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [DEBUG] $1"
        echo -e "${MAGENTA}${msg}${NC}"
        echo "$msg" >> "$LOG_FILE"
    fi
}

# ========================================
# 进度显示函数
# ========================================
SPINNER_PID=""

start_spinner() {
    local msg="$1"
    local delay=0.1
    local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
    
    (
        while true; do
            for (( i=0; i<${#spinstr}; i++ )); do
                echo -ne "\r${CYAN}${BLINK}${spinstr:$i:1}${NC} ${msg}   "
                sleep $delay
            done
        done
    ) &
    SPINNER_PID=$!
    disown
}

stop_spinner() {
    local status="$1"
    if [ -n "$SPINNER_PID" ]; then
        kill $SPINNER_PID 2>/dev/null || true
        wait $SPINNER_PID 2>/dev/null || true
        SPINNER_PID=""
    fi
    
    if [ "$status" = "success" ]; then
        echo -e "\r${GREEN}✔${NC} $2                    "
    elif [ "$status" = "fail" ]; then
        echo -e "\r${RED}✘${NC} $2                    "
    else
        echo -e "\r  $2                    "
    fi
}

# 进度条
show_progress() {
    local current=$1
    local total=$2
    local msg="$3"
    local width=50
    local percent=$((current * 100 / total))
    local filled=$((current * width / total))
    local empty=$((width - filled))
    
    printf "\r${CYAN}[${NC}"
    printf "%${filled}s" | tr ' ' '█'
    printf "%${empty}s" | tr ' ' '░'
    printf "${CYAN}]${NC} ${percent}%% ${msg}"
    
    if [ "$current" -eq "$total" ]; then
        echo ""
    fi
}

# ========================================
# 配置解析函数
# ========================================
declare -A CONFIG
declare -a EXTENSIONS
declare -a SYSTEM_TOOLS
declare -A PHP_CONFIG
declare -A ENV_VARS
declare -A BUILD_ARGS
declare -A TOOLS_DEBIAN
declare -A TOOLS_ALPINE

parse_config() {
    log_step "解析配置文件"
    
    if [ ! -f "$BUILD_CONF" ]; then
        log_error "配置文件不存在: $BUILD_CONF"
        exit 1
    fi
    
    start_spinner "正在解析 build.conf..."
    
    local current_section=""
    local line_num=0
    
    while IFS= read -r line || [ -n "$line" ]; do
        line_num=$((line_num + 1))
        
        # 去除首尾空白
        line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
        
        # 跳过空行和注释
        if [ -z "$line" ] || [[ "$line" =~ ^# ]]; then
            continue
        fi
        
        # 检测段落
        if [[ "$line" =~ ^\[([a-zA-Z_]+)\]$ ]]; then
            current_section="${BASH_REMATCH[1]}"
            log_debug "进入段落: $current_section"
            continue
        fi
        
        # 根据段落解析内容
        case "$current_section" in
            base_image)
                if [[ ! "$line" =~ ^# ]]; then
                    CONFIG["base_image"]="$line"
                    log_debug "基础镜像: $line"
                fi
                ;;
            extension)
                EXTENSIONS+=("$line")
                log_debug "扩展: $line"
                ;;
            mode)
                CONFIG["mode"]="$line"
                ;;
            workdir)
                CONFIG["workdir"]="$line"
                ;;
            composer_mirror)
                CONFIG["composer_mirror"]="$line"
                ;;
            system_mirror)
                CONFIG["system_mirror"]="$line"
                ;;
            php)
                if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
                    local key="${BASH_REMATCH[1]}"
                    local value="${BASH_REMATCH[2]}"
                    PHP_CONFIG["$key"]="$value"
                fi
                ;;
            cmd)
                CONFIG["cmd"]="$line"
                ;;
            system_tools)
                SYSTEM_TOOLS+=("$line")
                log_debug "系统工具: $line"
                ;;
            extra_packages)
                if [ -n "$line" ]; then
                    CONFIG["extra_packages"]="${CONFIG["extra_packages"]} $line"
                fi
                ;;
            env)
                if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
                    ENV_VARS["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
                fi
                ;;
            build_args)
                if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
                    BUILD_ARGS["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
                fi
                ;;
        esac
    done < "$BUILD_CONF"
    
    # 设置默认值
    CONFIG["base_image"]="${CONFIG["base_image"]:-php:8.2-cli}"
    CONFIG["mode"]="${CONFIG["mode"]:-$DEFAULT_MODE}"
    CONFIG["workdir"]="${CONFIG["workdir"]:-$DEFAULT_WORKDIR}"
    CONFIG["composer_mirror"]="${CONFIG["composer_mirror"]:-$DEFAULT_COMPOSER_MIRROR}"
    CONFIG["system_mirror"]="${CONFIG["system_mirror"]:-default}"
    
    stop_spinner "success" "配置解析完成"
    
    # 显示解析结果
    log_info "基础镜像: ${CONFIG["base_image"]}"
    log_info "构建模式: ${CONFIG["mode"]}"
    log_info "工作目录: ${CONFIG["workdir"]}"
    log_info "系统镜像源: ${CONFIG["system_mirror"]}"
    log_info "Composer源: ${CONFIG["composer_mirror"]}"
    log_info "扩展数量: ${#EXTENSIONS[@]}"
    log_info "系统工具数量: ${#SYSTEM_TOOLS[@]}"
}

# ========================================
# 检测镜像类型
# ========================================
detect_image_type() {
    local image="${CONFIG["base_image"]}"
    
    if [[ "$image" =~ alpine ]]; then
        CONFIG["os_type"]="alpine"
        CONFIG["pkg_manager"]="apk"
    else
        CONFIG["os_type"]="debian"
        CONFIG["pkg_manager"]="apt-get"
    fi
    
    # 提取 PHP 版本
    if [[ "$image" =~ php:([0-9]+\.[0-9]+) ]]; then
        CONFIG["php_version"]="${BASH_REMATCH[1]}"
        CONFIG["php_major"]="${CONFIG["php_version"]%%.*}"
    else
        CONFIG["php_version"]="8.2"
        CONFIG["php_major"]="8"
    fi
    
    log_info "操作系统类型: ${CONFIG["os_type"]}"
    log_info "PHP 版本: ${CONFIG["php_version"]}"
}

# ========================================
# 扩展依赖解析
# ========================================
declare -A EXT_DEPS_DEBIAN
declare -A EXT_DEPS_ALPINE

parse_extension_deps() {
    log_step "解析扩展依赖配置"
    
    if [ ! -f "$EXTENSION_DEPS_CONF" ]; then
        log_warn "扩展依赖配置文件不存在: $EXTENSION_DEPS_CONF"
        return
    fi
    
    while IFS='|' read -r ext debian alpine; do
        # 跳过注释和空行
        [[ "$ext" =~ ^# ]] && continue
        [ -z "$ext" ] && continue
        
        ext="$(echo "$ext" | tr -d '[:space:]')"
        EXT_DEPS_DEBIAN["$ext"]="$debian"
        EXT_DEPS_ALPINE["$ext"]="$alpine"
    done < "$EXTENSION_DEPS_CONF"
    
    log_info "已加载 ${#EXT_DEPS_DEBIAN[@]} 个扩展依赖配置"
}

# ========================================
# 扩展特殊配置解析
# ========================================
declare -A EXT_SPECIAL

parse_extension_special() {
    log_step "解析扩展特殊配置"
    
    if [ ! -f "$EXTENSION_SPECIAL_CONF" ]; then
        log_warn "扩展特殊配置文件不存在: $EXTENSION_SPECIAL_CONF"
        return
    fi
    
    local current_ext=""
    local current_key=""
    local in_heredoc=false
    local heredoc_content=""
    
    while IFS= read -r line || [ -n "$line" ]; do
        # 检测扩展段落
        if [[ "$line" =~ ^\[([a-zA-Z0-9_]+)\]$ ]]; then
            current_ext="${BASH_REMATCH[1]}"
            continue
        fi
        
        # 跳过注释和空行
        [[ "$line" =~ ^[[:space:]]*# ]] && continue
        
        if [ -n "$current_ext" ]; then
            # 处理 heredoc
            if [ "$in_heredoc" = true ]; then
                if [[ "$line" =~ ^EOF$ ]]; then
                    EXT_SPECIAL["${current_ext}_${current_key}"]="$heredoc_content"
                    in_heredoc=false
                    heredoc_content=""
                else
                    heredoc_content="${heredoc_content}${line}\n"
                fi
                continue
            fi
            
            # 检测 heredoc 开始
            if [[ "$line" =~ ^([a-zA-Z_]+)=\<\<EOF$ ]]; then
                current_key="${BASH_REMATCH[1]}"
                in_heredoc=true
                heredoc_content=""
                continue
            fi
            
            # 普通键值对
            if [[ "$line" =~ ^([a-zA-Z_]+)=(.+)$ ]]; then
                local key="${BASH_REMATCH[1]}"
                local value="${BASH_REMATCH[2]}"
                EXT_SPECIAL["${current_ext}_${key}"]="$value"
            fi
        fi
    done < "$EXTENSION_SPECIAL_CONF"
    
    log_info "已加载扩展特殊配置"
}

# ========================================
# 解析系统工具配置
# ========================================
resolve_system_tools() {
    log_step "解析系统工具映射"

    local tools_conf="${CONFIG_DIR}/system_tools.conf"
    if [ ! -f "$tools_conf" ]; then
        log_warn "系统工具映射文件不存在: $tools_conf"
        return
    fi

    local os_type="${CONFIG["os_type"]}"
    local tool_packages=""

    # 读取配置文件建立映射
    declare -A TOOLS_MAP
    while IFS='|' read -r tool debian alpine desc; do
        [[ "$tool" =~ ^# ]] && continue
        [ -z "$tool" ] && continue

        tool="$(echo "$tool" | tr -d '[:space:]')"

        if [ "$os_type" = "alpine" ]; then
            TOOLS_MAP["$tool"]="$(echo "$alpine" | tr -d '[:space:]')"
        else
            TOOLS_MAP["$tool"]="$(echo "$debian" | tr -d '[:space:]')"
        fi
    done < "$tools_conf"

    # 匹配选中的工具
    for tool in "${SYSTEM_TOOLS[@]}"; do
        # 去除注释
        [[ "$tool" =~ ^# ]] && continue

        local pkg_name="${TOOLS_MAP[$tool]}"

        if [ -n "$pkg_name" ] && [ "$pkg_name" != "-" ]; then
            tool_packages="$tool_packages $pkg_name"
            log_debug "映射工具: $tool -> $pkg_name"
        else
            log_warn "未找到工具 $tool 的对应包名或该系统不支持"
        fi
    done

    # 将结果保存到全局配置中,供后续使用
    CONFIG["resolved_tools"]="$tool_packages"
    log_info "已解析系统工具包: $tool_packages"
}

# ========================================
# 检查本地扩展
# ========================================
declare -A LOCAL_EXTENSIONS

check_local_extensions() {
    log_step "检查本地扩展目录"
    
    if [ ! -d "$EXTENSION_DIR" ]; then
        log_info "本地扩展目录不存在,跳过本地扩展检查"
        return
    fi
    
    local ext_count=0
    
    shopt -s nullglob
    local ext_files=("$EXTENSION_DIR"/*.tgz "$EXTENSION_DIR"/*.tar.gz "$EXTENSION_DIR"/*.zip)
    shopt -u nullglob
    
    for ext_file in "${ext_files[@]}"; do
        [ -f "$ext_file" ] || continue
        
        local filename=$(basename "$ext_file")
        local ext_name=""
        local ext_version=""
        
        # 解析扩展名和版本
        if [[ "$filename" =~ ^([a-zA-Z_]+)-([0-9.]+)\.(tgz|tar\.gz|zip)$ ]]; then
            ext_name="${BASH_REMATCH[1]}"
            ext_version="${BASH_REMATCH[2]}"
        elif [[ "$filename" =~ ^([a-zA-Z_]+)\.(tgz|tar\.gz|zip)$ ]]; then
            ext_name="${BASH_REMATCH[1]}"
            ext_version="latest"
        fi
        
        if [ -n "$ext_name" ]; then
            # 检查是否已存在该扩展的记录
            if [ -n "${LOCAL_EXTENSIONS[$ext_name]}" ]; then
                # 比较版本,保留更高版本
                local existing_version="${LOCAL_EXTENSIONS[$ext_name]%%|*}"
                if version_compare "$ext_version" "$existing_version"; then
                    LOCAL_EXTENSIONS["$ext_name"]="${ext_version}|${ext_file}"
                    log_debug "更新本地扩展: $ext_name 版本 $existing_version -> $ext_version"
                fi
            else
                LOCAL_EXTENSIONS["$ext_name"]="${ext_version}|${ext_file}"
                log_debug "发现本地扩展: $ext_name 版本 $ext_version"
            fi
            ext_count=$((ext_count + 1))
        fi
    done
    
    log_info "发现 ${#LOCAL_EXTENSIONS[@]} 个本地扩展"
}

# 版本比较函数
version_compare() {
    local v1="$1"
    local v2="$2"
    
    [ "$v1" = "latest" ] && return 0
    [ "$v2" = "latest" ] && return 1
    
    if [ "$(printf '%s\n' "$v1" "$v2" | sort -V | tail -n1)" = "$v1" ]; then
        return 0
    else
        return 1
    fi
}

# ========================================
# 判断扩展类型
# ========================================
# 核心扩展列表(使用 docker-php-ext-install)
CORE_EXTENSIONS=(
    "bcmath" "bz2" "calendar" "ctype" "curl" "dba" "dom" "enchant"
    "exif" "fileinfo" "filter" "ftp" "gd" "gettext" "gmp" "hash"
    "iconv" "imap" "intl" "json" "ldap" "mbstring" "mysqli" "oci8"
    "odbc" "opcache" "pcntl" "pdo" "pdo_dblib" "pdo_firebird" "pdo_mysql"
    "pdo_oci" "pdo_odbc" "pdo_pgsql" "pdo_sqlite" "pgsql" "phar" "posix"
    "pspell" "readline" "reflection" "session" "shmop" "simplexml"
    "snmp" "soap" "sockets" "sodium" "spl" "standard" "sysvmsg"
    "sysvsem" "sysvshm" "tidy" "tokenizer" "xml" "xmlreader" "xmlrpc"
    "xmlwriter" "xsl" "zip" "zlib"
)

is_core_extension() {
    local ext="$1"
    for core_ext in "${CORE_EXTENSIONS[@]}"; do
        [ "$ext" = "$core_ext" ] && return 0
    done
    return 1
}

# ========================================
# 生成安装脚本
# ========================================
generate_install_scripts() {
    log_step "生成安装脚本"
    
    # 创建脚本目录
    mkdir -p "$BUILD_SCRIPTS_DIR"
    
    local os_type="${CONFIG["os_type"]}"
    local php_major="${CONFIG["php_major"]}"
    local total_ext=${#EXTENSIONS[@]}
    local current_ext=0

    # 解析系统工具
    resolve_system_tools
    
    # 收集所有依赖
    local all_deps=""
    local install_core=""
    local install_pecl=""
    local install_local=""
    local configure_cmds=""
    local pre_install_cmds=""
    local post_install_cmds=""
    
    for ext_entry in "${EXTENSIONS[@]}"; do
        current_ext=$((current_ext + 1))
        show_progress $current_ext $total_ext "处理扩展: $ext_entry"
        
        # 解析扩展名和版本
        local ext_name=""
        local ext_version=""
        local force_local=false
        local force_pecl=false
        
        # 检查强制标记
        if [[ "$ext_entry" =~ ^local:(.+)$ ]]; then
            ext_entry="${BASH_REMATCH[1]}"
            force_local=true
        elif [[ "$ext_entry" =~ ^pecl:(.+)$ ]]; then
            ext_entry="${BASH_REMATCH[1]}"
            force_pecl=true
        fi
        
        # 解析版本号
        if [[ "$ext_entry" =~ ^([a-zA-Z_]+)-([0-9.]+)$ ]]; then
            ext_name="${BASH_REMATCH[1]}"
            ext_version="${BASH_REMATCH[2]}"
        else
            ext_name="$ext_entry"
            ext_version=""
        fi
        
        # 检查是否仅限特定模式
        local mode_only="${EXT_SPECIAL["${ext_name}_mode_only"]}"
        if [ -n "$mode_only" ] && [ "$mode_only" != "${CONFIG["mode"]}" ]; then
            log_debug "跳过扩展 $ext_name (仅限 $mode_only 模式)"
            continue
        fi
        
        # 获取依赖
        local deps=""
        if [ "$os_type" = "alpine" ]; then
            deps="${EXT_DEPS_ALPINE[$ext_name]}"
        else
            deps="${EXT_DEPS_DEBIAN[$ext_name]}"
        fi
        
        if [ -n "$deps" ]; then
            all_deps="$all_deps $deps"
        fi
        
        # 检查预安装命令
        local pre_install="${EXT_SPECIAL["${ext_name}_pre_install"]}"
        if [ -n "$pre_install" ]; then
            pre_install_cmds="${pre_install_cmds}\n# Pre-install for $ext_name\n$(echo -e "$pre_install")"
        fi
        
        # 检查额外配置
        local extra_cmd=""
        if [ "$os_type" = "debian" ]; then
            extra_cmd="${EXT_SPECIAL["${ext_name}_debian_extra"]}"
        fi
        if [ -n "$extra_cmd" ]; then
            pre_install_cmds="${pre_install_cmds}\n$extra_cmd"
        fi
        
        # 确定安装方式
        local install_method="${EXT_SPECIAL["${ext_name}_install_method"]}"
        
        # 检查是否跳过安装(已在 pre_install 中完成)
        local skip_install="${EXT_SPECIAL["${ext_name}_skip_install"]}"
        if [ "$skip_install" = "true" ]; then
            log_debug "扩展 $ext_name 将在 pre_install 脚本中安装"
            continue
        fi
        
        # 检查本地扩展
        if [ "$force_local" = true ] || [ -n "${LOCAL_EXTENSIONS[$ext_name]}" ]; then
            local local_info="${LOCAL_EXTENSIONS[$ext_name]}"
            if [ -n "$local_info" ]; then
                local local_file="${local_info#*|}"
                install_local="${install_local}\n# Install $ext_name from local\ninstall_local_extension \"$ext_name\" \"$local_file\""
                continue
            fi
        fi
        
        # 检查是否为核心扩展
        if is_core_extension "$ext_name" && [ "$force_pecl" != true ] && [ "$install_method" != "pecl" ]; then
            # 检查配置命令
            local configure_cmd=""
            if [ "$php_major" -lt 8 ]; then
                configure_cmd="${EXT_SPECIAL["${ext_name}_php7_${os_type}_configure"]}"
            fi
            if [ -z "$configure_cmd" ]; then
                configure_cmd="${EXT_SPECIAL["${ext_name}_${os_type}_configure"]}"
            fi
            
            if [ -n "$configure_cmd" ]; then
                configure_cmds="${configure_cmds}\n$configure_cmd"
            fi
            
            install_core="${install_core} $ext_name"
        else
            # PECL 安装
            local pecl_name="$ext_name"
            if [ -n "$ext_version" ]; then
                pecl_name="${ext_name}-${ext_version}"
            fi
            
            # 检查 PECL 配置参数
            local pecl_configure=""
            if [ "$php_major" -lt 8 ]; then
                pecl_configure="${EXT_SPECIAL["${ext_name}_php7_pecl_configure"]}"
            fi
            if [ -z "$pecl_configure" ]; then
                pecl_configure="${EXT_SPECIAL["${ext_name}_pecl_configure"]}"
            fi
            
            if [ -n "$pecl_configure" ]; then
                install_pecl="${install_pecl}\ninstall_pecl_extension \"$pecl_name\" \"$pecl_configure\""
            else
                install_pecl="${install_pecl}\ninstall_pecl_extension \"$pecl_name\" \"\""
            fi
        fi
        
        # 检查后置安装命令
        local post_install="${EXT_SPECIAL["${ext_name}_post_install"]}"
        if [ -n "$post_install" ]; then
            post_install_cmds="${post_install_cmds}\n# Post-install for $ext_name\n$(echo -e "$post_install")"
        fi
        
        # 检查 PHP 配置
        local php_ini_config=""
        if [ "${CONFIG["mode"]}" = "prod" ]; then
            php_ini_config="${EXT_SPECIAL["${ext_name}_prod_php_ini_config"]}"
        fi
        if [ -z "$php_ini_config" ]; then
            php_ini_config="${EXT_SPECIAL["${ext_name}_php_ini_config"]}"
        fi
        if [ -n "$php_ini_config" ]; then
            # 添加到 PHP 配置
            while IFS= read -r ini_line; do
                [ -z "$ini_line" ] && continue
                if [[ "$ini_line" =~ ^([^=]+)=(.*)$ ]]; then
                    PHP_CONFIG["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
                fi
            done <<< "$(echo -e "$php_ini_config")"
        fi
    done
    
    # 去重依赖
    all_deps=$(echo "$all_deps" | tr ',' ' ' | tr ' ' '\n' | sort -u | tr '\n' ' ')
    
#    log_info "生成依赖安装脚本..."
#    generate_deps_script "$all_deps"
    # 合并扩展依赖和系统工具包
    local final_deps="${all_deps} ${CONFIG["resolved_tools"]}"
    # 去重处理
    final_deps=$(echo "$final_deps" | tr ' ' '\n' | sort -u | tr '\n' ' ')

    log_info "生成依赖安装脚本(包含系统工具)..."
    generate_deps_script "$final_deps"
    
    log_info "生成扩展安装脚本..."
    generate_extensions_script "$install_core" "$install_pecl" "$install_local" "$configure_cmds" "$pre_install_cmds"
    
    log_info "生成后置安装脚本..."
    generate_post_install_script "$post_install_cmds"
    
    log_info "生成 Composer 安装脚本..."
    generate_composer_script
    
    log_info "生成 PHP 配置文件..."
    generate_php_ini
    
    log_info "生成环境变量文件..."
    generate_env_file
    
#    log_info "生成入口点脚本..."
#    generate_entrypoint_script
    
    log_success "所有安装脚本生成完成"
}

# ========================================
# 生成依赖安装脚本
# ========================================
generate_deps_script() {
    local deps="$1"
    local os_type="${CONFIG["os_type"]}"
    local system_mirror="${CONFIG["system_mirror"]}"
    
    cat > "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }

log_info "开始安装系统依赖..."

SCRIPT_HEADER

    if [ "$os_type" = "alpine" ]; then
        # 生成 Alpine 镜像源切换脚本
        if [ "$system_mirror" != "default" ]; then
            cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << SCRIPT_ALPINE_MIRROR
# 切换 Alpine 镜像源
log_info "切换 Alpine 镜像源到: $system_mirror"
SCRIPT_ALPINE_MIRROR
            case "$system_mirror" in
                aliyun)
                    cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_ALPINE_ALIYUN'
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
SCRIPT_ALPINE_ALIYUN
                    ;;
                ustc)
                    cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_ALPINE_USTC'
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
SCRIPT_ALPINE_USTC
                    ;;
                tsinghua)
                    cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_ALPINE_TSINGHUA'
sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
SCRIPT_ALPINE_TSINGHUA
                    ;;
                163)
                    cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_ALPINE_163'
sed -i 's/dl-cdn.alpinelinux.org/mirrors.163.com/g' /etc/apk/repositories
SCRIPT_ALPINE_163
                    ;;
            esac
        fi
        
        cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << SCRIPT_ALPINE
# Alpine 系统依赖
apk update
apk add --no-cache --virtual .build-deps \\
    \$PHPIZE_DEPS \\
    autoconf \\
    automake \\
    libtool \\
    make \\
    gcc \\
    g++ \\
    linux-headers

# 安装运行时依赖
apk add --no-cache \\
    $deps

log_info "Alpine 系统依赖安装完成"
SCRIPT_ALPINE
    else
        # 生成 Debian 镜像源切换脚本
        if [ "$system_mirror" != "default" ]; then
            cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << SCRIPT_DEBIAN_MIRROR
# 切换 Debian 镜像源
log_info "切换 Debian/Ubuntu 镜像源到: $system_mirror"

# 备份原始源
cp /etc/apt/sources.list /etc/apt/sources.list.bak 2>/dev/null || true

# 检测 Debian 版本
DEBIAN_VERSION=\$(cat /etc/os-release | grep VERSION_CODENAME | cut -d= -f2)
if [ -z "\$DEBIAN_VERSION" ]; then
    DEBIAN_VERSION="bullseye"
fi

log_info "检测到 Debian 版本: \$DEBIAN_VERSION"

SCRIPT_DEBIAN_MIRROR
            case "$system_mirror" in
                aliyun)
                    cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_DEBIAN_ALIYUN'
cat > /etc/apt/sources.list << EOF
deb https://mirrors.aliyun.com/debian/ $DEBIAN_VERSION main contrib non-free
deb https://mirrors.aliyun.com/debian/ $DEBIAN_VERSION-updates main contrib non-free
deb https://mirrors.aliyun.com/debian-security $DEBIAN_VERSION-security main contrib non-free
EOF
SCRIPT_DEBIAN_ALIYUN
                    ;;
                ustc)
                    cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_DEBIAN_USTC'
cat > /etc/apt/sources.list << EOF
deb https://mirrors.ustc.edu.cn/debian/ $DEBIAN_VERSION main contrib non-free
deb https://mirrors.ustc.edu.cn/debian/ $DEBIAN_VERSION-updates main contrib non-free
deb https://mirrors.ustc.edu.cn/debian-security $DEBIAN_VERSION-security main contrib non-free
EOF
SCRIPT_DEBIAN_USTC
                    ;;
                tsinghua)
                    cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_DEBIAN_TSINGHUA'
cat > /etc/apt/sources.list << EOF
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ $DEBIAN_VERSION main contrib non-free
deb https://mirrors.tuna.tsinghua.edu.cn/debian/ $DEBIAN_VERSION-updates main contrib non-free
deb https://mirrors.tuna.tsinghua.edu.cn/debian-security $DEBIAN_VERSION-security main contrib non-free
EOF
SCRIPT_DEBIAN_TSINGHUA
                    ;;
                163)
                    cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << 'SCRIPT_DEBIAN_163'
cat > /etc/apt/sources.list << EOF
deb http://mirrors.163.com/debian/ $DEBIAN_VERSION main contrib non-free
deb http://mirrors.163.com/debian/ $DEBIAN_VERSION-updates main contrib non-free
deb http://mirrors.163.com/debian-security $DEBIAN_VERSION-security main contrib non-free
EOF
SCRIPT_DEBIAN_163
                    ;;
            esac
        fi
        
        cat >> "$BUILD_SCRIPTS_DIR/install_deps.sh" << SCRIPT_DEBIAN
# Debian 系统依赖
apt-get update
apt-get install -y --no-install-recommends \\
    \$PHPIZE_DEPS \\
    autoconf \\
    automake \\
    libtool \\
    make \\
    gcc \\
    g++ \\
    $deps

log_info "Debian 系统依赖安装完成"
SCRIPT_DEBIAN
    fi
    
    chmod +x "$BUILD_SCRIPTS_DIR/install_deps.sh"
}

# ========================================
# 生成扩展安装脚本
# ========================================
generate_extensions_script() {
    local core_exts="$1"
    local pecl_exts="$2"
    local local_exts="$3"
    local configure_cmds="$4"
    local pre_install_cmds="$5"
    
    cat > "$BUILD_SCRIPTS_DIR/install_extensions.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
log_step() { echo -e "${CYAN}[STEP]${NC} $1"; }

# PECL 扩展安装函数
install_pecl_extension() {
    local ext_name="$1"
    local configure_opts="$2"
    
    log_step "安装 PECL 扩展: $ext_name"
    
    if [ -n "$configure_opts" ]; then
        printf "\n" | pecl install $ext_name <<< "$configure_opts" || {
            log_warn "带配置安装失败,尝试默认安装..."
            printf "\n" | pecl install $ext_name
        }
    else
        printf "\n" | pecl install $ext_name || log_error "安装 $ext_name 失败"
    fi
    
    # 获取纯扩展名(去除版本号)
    local pure_name="${ext_name%%-*}"
    docker-php-ext-enable $pure_name || log_error "启用 $pure_name 失败"
    
    log_info "PECL 扩展 $ext_name 安装成功"
}

# 本地扩展安装函数
install_local_extension() {
    local ext_name="$1"
    local ext_file="$2"
    
    log_step "从本地安装扩展: $ext_name"
    
    local ext_dir="/tmp/ext_$ext_name"
    mkdir -p "$ext_dir"
    
    case "$ext_file" in
        *.tgz|*.tar.gz)
            tar -xzf "$ext_file" -C "$ext_dir" --strip-components=1
            ;;
        *.zip)
            unzip -q "$ext_file" -d "$ext_dir"
            ;;
        *)
            log_error "不支持的扩展格式: $ext_file"
            ;;
    esac
    
    cd "$ext_dir"
    phpize
    ./configure
    make -j$(nproc)
    make install
    docker-php-ext-enable $ext_name
    
    rm -rf "$ext_dir"
    log_info "本地扩展 $ext_name 安装成功"
}

log_info "开始安装 PHP 扩展..."

SCRIPT_HEADER

    # 添加预安装命令
    if [ -n "$pre_install_cmds" ]; then
        echo -e "\n# 预安装命令" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
        echo -e "$pre_install_cmds" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
    fi
    
    # 添加配置命令
    if [ -n "$configure_cmds" ]; then
        echo -e "\n# 扩展配置命令" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
        echo -e "$configure_cmds" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
    fi
    
    # 添加核心扩展安装
    if [ -n "$core_exts" ]; then
        cat >> "$BUILD_SCRIPTS_DIR/install_extensions.sh" << SCRIPT_CORE

# 安装核心扩展
log_step "安装核心扩展:$core_exts"
docker-php-ext-install -j\$(nproc)$core_exts || log_error "核心扩展安装失败"
log_info "核心扩展安装成功"
SCRIPT_CORE
    fi
    
    # 添加 PECL 扩展安装
    if [ -n "$pecl_exts" ]; then
        echo -e "\n# 安装 PECL 扩展" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
        echo -e "$pecl_exts" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
    fi
    
    # 添加本地扩展安装
    if [ -n "$local_exts" ]; then
        echo -e "\n# 安装本地扩展" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
        echo -e "$local_exts" >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
    fi
    
    echo -e '\nlog_info "所有 PHP 扩展安装完成"' >> "$BUILD_SCRIPTS_DIR/install_extensions.sh"
    
    chmod +x "$BUILD_SCRIPTS_DIR/install_extensions.sh"
}

# ========================================
# 生成后置安装脚本
# ========================================
generate_post_install_script() {
    local post_cmds="$1"
    
    cat > "$BUILD_SCRIPTS_DIR/post_install.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e

log_info() { echo -e "\033[0;32m[INFO]\033[0m $1"; }

log_info "执行后置安装脚本..."

SCRIPT_HEADER

    if [ -n "$post_cmds" ]; then
        echo -e "$post_cmds" >> "$BUILD_SCRIPTS_DIR/post_install.sh"
    fi
    
    echo -e '\nlog_info "后置安装脚本执行完成"' >> "$BUILD_SCRIPTS_DIR/post_install.sh"
    
    chmod +x "$BUILD_SCRIPTS_DIR/post_install.sh"
}

# ========================================
# 生成 Composer 安装脚本
# ========================================
generate_composer_script() {
    local mirror="${CONFIG["composer_mirror"]}"
    
    cat > "$BUILD_SCRIPTS_DIR/install_composer.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e

log_info() { echo -e "\033[0;32m[INFO]\033[0m $1"; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $1"; exit 1; }

MIRROR="$1"

log_info "安装 Composer..."

# 下载并安装 Composer
EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')"
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")"

if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
    rm composer-setup.php
    log_error 'Composer installer 校验失败'
fi

php composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm composer-setup.php

# 配置镜像源
configure_mirror() {
    case "$MIRROR" in
        aliyun)
            composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
            ;;
        tencent)
            composer config -g repo.packagist composer https://mirrors.cloud.tencent.com/composer/
            ;;
        ustc)
            composer config -g repo.packagist composer https://mirrors.ustc.edu.cn/composer/
            ;;
        huaweicloud)
            composer config -g repo.packagist composer https://repo.huaweicloud.com/repository/php/
            ;;
        packagist)
            composer config -g repo.packagist composer https://packagist.org
            ;;
        auto)
            # 自动检测最快的源
            log_info "自动检测最快的 Composer 源..."
            MIRRORS=(
                "https://mirrors.aliyun.com/composer/"
                "https://mirrors.cloud.tencent.com/composer/"
                "https://packagist.org"
            )
            FASTEST=""
            FASTEST_TIME=999
            for m in "${MIRRORS[@]}"; do
                TIME=$(curl -o /dev/null -s -w '%{time_total}' --connect-timeout 3 "$m" 2>/dev/null || echo "999")
                if (( $(echo "$TIME < $FASTEST_TIME" | bc -l 2>/dev/null || echo 0) )); then
                    FASTEST_TIME=$TIME
                    FASTEST=$m
                fi
            done
            if [ -n "$FASTEST" ]; then
                composer config -g repo.packagist composer "$FASTEST"
                log_info "使用最快源: $FASTEST"
            fi
            ;;
    esac
}

configure_mirror

log_info "Composer 安装完成"
composer --version
SCRIPT_HEADER

    chmod +x "$BUILD_SCRIPTS_DIR/install_composer.sh"
}

# ========================================
# 生成 PHP 配置文件
# ========================================
generate_php_ini() {
    local ini_file="$BUILD_SCRIPTS_DIR/php.ini"
    
    echo "; PHP Custom Configuration" > "$ini_file"
    echo "; Generated by build.sh at $(date)" >> "$ini_file"
    echo "" >> "$ini_file"
    
    for key in "${!PHP_CONFIG[@]}"; do
        echo "$key=${PHP_CONFIG[$key]}" >> "$ini_file"
    done
    
    log_debug "PHP 配置文件生成完成"
}

# ========================================
# 生成环境变量文件
# ========================================
generate_env_file() {
    local env_file="$BUILD_SCRIPTS_DIR/env.sh"
    
    echo "# Environment Variables" > "$env_file"
    echo "# Generated by build.sh at $(date)" >> "$env_file"
    echo "" >> "$env_file"
    
    for key in "${!ENV_VARS[@]}"; do
        echo "export $key=\"${ENV_VARS[$key]}\"" >> "$env_file"
    done
}

# ========================================
# 生成入口点脚本
# ========================================
generate_entrypoint_script() {
    local cmd="${CONFIG["cmd"]}"
    
    cat > "$BUILD_SCRIPTS_DIR/entrypoint.sh" << 'SCRIPT_HEADER'
#!/bin/bash
set -e

# 加载环境变量
if [ -f /etc/profile ]; then
    source /etc/profile
fi

# 如果传入了命令,执行传入的命令
if [ $# -gt 0 ]; then
    exec "$@"
fi

SCRIPT_HEADER

    # 添加默认命令
    if [ -n "$cmd" ]; then
        echo "# 默认命令" >> "$BUILD_SCRIPTS_DIR/entrypoint.sh"
        echo "exec $cmd" >> "$BUILD_SCRIPTS_DIR/entrypoint.sh"
    else
        echo "# 无默认命令,保持容器运行" >> "$BUILD_SCRIPTS_DIR/entrypoint.sh"
        echo 'exec tail -f /dev/null' >> "$BUILD_SCRIPTS_DIR/entrypoint.sh"
    fi
    
    chmod +x "$BUILD_SCRIPTS_DIR/entrypoint.sh"
}

# ========================================
# 验证配置
# ========================================
validate_config() {
    log_step "验证配置"
    
    local errors=0
    
    # 验证基础镜像
    if [ -z "${CONFIG["base_image"]}" ]; then
        log_error "未配置基础镜像"
        errors=$((errors + 1))
    fi
    
    # 验证扩展数量
    if [ ${#EXTENSIONS[@]} -eq 0 ]; then
        log_warn "未配置任何扩展"
    fi
    
    # 验证工作目录
    if [ -z "${CONFIG["workdir"]}" ]; then
        log_warn "未配置工作目录,使用默认值: $DEFAULT_WORKDIR"
        CONFIG["workdir"]="$DEFAULT_WORKDIR"
    fi
    
    if [ $errors -gt 0 ]; then
        log_error "配置验证失败,发现 $errors 个错误"
        exit 1
    fi
    
    log_success "配置验证通过"
}

# ========================================
# Docker 构建
# ========================================
build_docker_image() {
    local image_tag="$1"
    
    log_step "开始构建 Docker 镜像: $image_tag"
    
    # 准备构建参数
    local build_args=""
    build_args="$build_args --build-arg BASE_IMAGE=${CONFIG["base_image"]}"
    build_args="$build_args --build-arg WORKDIR=${CONFIG["workdir"]}"
    build_args="$build_args --build-arg BUILD_MODE=${CONFIG["mode"]}"
    build_args="$build_args --build-arg COMPOSER_MIRROR=${CONFIG["composer_mirror"]}"

    # 传递额外包参数
    if [ -n "${CONFIG["extra_packages"]}" ]; then
        build_args="$build_args --build-arg EXTRA_PACKAGES=\"${CONFIG["extra_packages"]}\""
    fi
    
    # 内存限制(防止 OOM)
    local memory_limit="${BUILD_ARGS["memory_limit"]:-2048}"
    if [ "$memory_limit" != "0" ]; then
        build_args="$build_args --memory=${memory_limit}m"
    fi
    
    # 并行编译数
    local parallel_jobs="${BUILD_ARGS["parallel_jobs"]:-0}"
    if [ "$parallel_jobs" = "0" ]; then
        parallel_jobs=$(nproc)
    fi
    build_args="$build_args --build-arg PARALLEL_JOBS=$parallel_jobs"
    
    # 缓存设置
    local enable_cache="${BUILD_ARGS["enable_cache"]:-true}"
    if [ "$enable_cache" != "true" ]; then
        build_args="$build_args --no-cache"
    fi
    
    # 复制本地扩展到构建上下文
    if [ -d "$EXTENSION_DIR" ] && [ "$(ls -A "$EXTENSION_DIR" 2>/dev/null)" ]; then
        mkdir -p "$BUILD_SCRIPTS_DIR/extensions"
        cp -r "$EXTENSION_DIR"/* "$BUILD_SCRIPTS_DIR/extensions/" 2>/dev/null || true
    fi
    
    log_info "构建参数: $build_args"
    
    # 执行构建
    start_spinner "正在构建 Docker 镜像..."

    # 修改判断逻辑,确保报错立即退出,不打印成功信息
    if docker build $build_args -t "$image_tag" -f "$SCRIPT_DIR/Dockerfile" "$SCRIPT_DIR" 2>&1 | tee -a "$LOG_FILE"; then
        stop_spinner "success" "Docker 镜像构建成功"
        log_success "镜像构建完成: $image_tag"
    else
        # 只要 docker build 失败,进入这里
        stop_spinner "fail" "Docker 镜像构建失败"
        log_error "构建过程中发生错误,请查看上方日志或检查: $LOG_FILE"
        exit 1  # 强制退出,不执行后续逻辑
    fi
    
    # 清理临时文件
    if [ "${BUILD_ARGS["clean_cache"]}" = "true" ]; then
        log_info "清理构建缓存..."
        rm -rf "$BUILD_SCRIPTS_DIR/extensions" 2>/dev/null || true
    fi
}

# ========================================
# 显示帮助信息
# ========================================
show_help() {
    cat << EOF
${CYAN}${BOLD}PHP Hyperf Docker 构建工具${NC}

${YELLOW}用法:${NC}
    ./build.sh <镜像名称:标签>
    ./build.sh [选项]

${YELLOW}选项:${NC}
    -h, --help          显示帮助信息
    -v, --version       显示版本信息
    -d, --debug         启用调试模式
    --dry-run           仅解析配置,不执行构建
    --validate          仅验证配置

${YELLOW}示例:${NC}
    ./build.sh myapp:latest
    ./build.sh myapp:v1.0.0
    ./build.sh --debug myapp:dev

${YELLOW}配置文件:${NC}
    config/build.conf           主配置文件
    config/extension_deps.conf  扩展依赖配置
    config/extension_special.conf 扩展特殊配置

${YELLOW}本地扩展:${NC}
    将扩展文件放入 extension/ 目录
    支持格式: .tgz, .tar.gz, .zip
    命名格式: 扩展名-版本号.tgz (如: swoole-4.8.13.tgz)

${YELLOW}日志文件:${NC}
    logs/build_YYYYMMDD_HHMMSS.log

EOF
}

# ========================================
# 主函数
# ========================================
main() {
    # 创建日志目录
    mkdir -p "$LOGS_DIR"
    
    # 参数解析
    local image_tag=""
    local dry_run=false
    local validate_only=false
    
    while [[ $# -gt 0 ]]; do
        case "$1" in
            -h|--help)
                show_help
                exit 0
                ;;
            -v|--version)
                echo "PHP Hyperf Docker Builder v1.0.0"
                exit 0
                ;;
            -d|--debug)
                DEBUG=true
                shift
                ;;
            --dry-run)
                dry_run=true
                shift
                ;;
            --validate)
                validate_only=true
                shift
                ;;
            -*)
                log_error "未知选项: $1"
                show_help
                exit 1
                ;;
            *)
                image_tag="$1"
                shift
                ;;
        esac
    done
    
    # 检查镜像标签
    if [ -z "$image_tag" ] && [ "$validate_only" != true ]; then
        log_error "请指定镜像名称和标签"
        echo ""
        show_help
        exit 1
    fi
    
    echo ""
    echo -e "${CYAN}${BOLD}╔════════════════════════════════════════════════════════════╗${NC}"
    echo -e "${CYAN}${BOLD}║         PHP Hyperf Docker 构建工具 v1.0.0                  ║${NC}"
    echo -e "${CYAN}${BOLD}╚════════════════════════════════════════════════════════════╝${NC}"
    echo ""
    
    log_info "日志文件: $LOG_FILE"
    
    # 解析配置
    parse_config
    detect_image_type
    parse_extension_deps
    parse_extension_special
    check_local_extensions
    
    # 验证配置
    validate_config
    
    if [ "$validate_only" = true ]; then
        log_success "配置验证完成"
        exit 0
    fi
    
    # 生成安装脚本
    generate_install_scripts
    
    if [ "$dry_run" = true ]; then
        log_success "Dry run 完成,脚本已生成到: $BUILD_SCRIPTS_DIR"
        exit 0
    fi
    
    # 构建 Docker 镜像
    #build_docker_image "$image_tag"
    build_docker_image "$image_tag" || {
        echo -e "${RED}构建失败,请检查上方日志${NC}"
        exit 1
    }
    
    echo ""
    log_success "构建完成!"
    echo ""
    echo -e "${GREEN}运行容器:${NC}"
    echo -e "  docker run -d --name hyperf -p 9501:9501 $image_tag"
    echo ""
    echo -e "${GREEN}查看日志:${NC}"
    echo -e "  docker logs -f hyperf"
    echo ""
}

# 执行主函数
main "$@"

Dockerfile
# ========================================
# PHP Hyperf Docker 构建文件
# 所有参数通过 ARG 传入,不直接修改此文件
# ========================================

# 基础镜像参数
ARG BASE_IMAGE=php:8.2-cli
FROM ${BASE_IMAGE}

# 构建参数
ARG WORKDIR=/opt/www
ARG BUILD_MODE=prod
ARG PARALLEL_JOBS=4
ARG COMPOSER_MIRROR=auto
ARG TIMEZONE=Asia/Shanghai

# 环境变量
ARG ENV_VARS=""
ARG PHP_INI_CONFIG=""
ARG EXTRA_PACKAGES=""
ARG CMD_COMMAND=""

# 扩展安装脚本(由 build.sh 生成)
ARG INSTALL_DEPS_SCRIPT=""
ARG INSTALL_EXTENSIONS_SCRIPT=""
ARG POST_INSTALL_SCRIPT=""

# 标签
LABEL maintainer="PHP Hyperf Builder"
LABEL description="PHP Hyperf Application Container"
LABEL build.mode="${BUILD_MODE}"

# 设置工作目录
WORKDIR ${WORKDIR}

# 设置时区
ENV TZ=${TIMEZONE}

# 检测系统类型并安装基础依赖
RUN set -eux; \
    # 检测是否为 Alpine
    if [ -f /etc/alpine-release ]; then \
        echo "Detected Alpine Linux"; \
        apk update; \
        apk add --no-cache \
            bash \
            curl \
            wget \
            git \
            unzip \
            tzdata \
            $EXTRA_PACKAGES; \
        cp /usr/share/zoneinfo/${TIMEZONE} /etc/localtime; \
        echo "${TIMEZONE}" > /etc/timezone; \
    else \
        echo "Detected Debian/Ubuntu"; \
        apt-get update; \
        apt-get install -y --no-install-recommends \
            bash \
            curl \
            wget \
            git \
            unzip \
            ca-certificates \
            $EXTRA_PACKAGES; \
        ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime; \
        echo "${TIMEZONE}" > /etc/timezone; \
    fi

# 复制依赖安装脚本
COPY build_scripts/install_deps.sh /tmp/install_deps.sh
COPY build_scripts/install_extensions.sh /tmp/install_extensions.sh
COPY build_scripts/post_install.sh /tmp/post_install.sh
COPY build_scripts/install_composer.sh /tmp/install_composer.sh

# 安装系统依赖
RUN chmod +x /tmp/install_deps.sh && /tmp/install_deps.sh

# 安装 PHP 扩展
RUN chmod +x /tmp/install_extensions.sh && /tmp/install_extensions.sh

# 安装 Composer
RUN chmod +x /tmp/install_composer.sh && /tmp/install_composer.sh ${COMPOSER_MIRROR}

# 执行后置安装脚本
RUN chmod +x /tmp/post_install.sh && /tmp/post_install.sh

# 复制 PHP 配置
COPY build_scripts/php.ini /usr/local/etc/php/conf.d/99-custom.ini

# 清理
RUN set -eux; \
    rm -rf /tmp/*; \
    if [ -f /etc/alpine-release ]; then \
        rm -rf /var/cache/apk/*; \
    else \
        apt-get clean; \
        rm -rf /var/lib/apt/lists/*; \
    fi; \
    # 生产模式额外清理
    if [ "${BUILD_MODE}" = "prod" ]; then \
        rm -rf /usr/src/php*; \
        rm -rf /usr/local/include/php; \
    fi

# 设置环境变量
COPY build_scripts/env.sh /tmp/env.sh
RUN if [ -f /tmp/env.sh ]; then cat /tmp/env.sh >> /etc/profile; fi

# 暴露端口
#EXPOSE 9501

# 设置工作目录权限
RUN mkdir -p ${WORKDIR} && chmod -R 755 ${WORKDIR}

# 健康检查
#HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
#    CMD curl -f http://localhost:9501/health || exit 1

# 入口点(可被覆盖)
#COPY build_scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
#RUN chmod +x /usr/local/bin/entrypoint.sh
#ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

# CMD 命令(如果配置了)
# 这行会被 build.sh 动态添加

执行构建

# 进入项目目录
cd project

# 添加文件执行权限
chmod +x ./build.sh

# 执行构建
./build.sh 新的镜像名称

结语

该份套餐可以做到:

  • 构建逻辑通用:不同版本、不同扩展的构建只需要修改 build.conf 配置文件
  • 扩展自由度高:可自由增减扩展信息,可指定版本,也可不指定,不指定时安装最新版本
  • 构建模式切换:分为 prod/dev,prod 构建会清楚构建缓存等信息,减少镜像体积;dev模式不会,方便多次构建,减少构建时间
  • 自定义系统依赖源:自由配置是否使用自定义系统依赖源,减少构建成功率和构建速度
  • 自定义php配置:不同的开发需求,可能需要不通过的php配置,可以自由配置,构建后覆盖原有的默认配置
  • 构建目标自由:可自由配置构建 基础镜像(base)、最终镜像(final)、基础+最终(all)
  • 系统依赖自动检测:可根据配置的基础镜像,自动识别镜像系统(debian/alpine)、并安装对应的依赖,如:系统依赖、系统工具等
  • GD 扩展自动配置:如果 local 扩展中存在 GD 扩展,则自动做扩展特殊配置
  • 镜像名称自定义:基础镜像名称自动生成,最终镜像名称可以自由定义
  • 扩展安装重试机制:安装扩展容易因为各种原因失败,追加重试机制(3次)减少失败次数
  • ... ... ...

okk,提升到此结束,又可以愉快的做牛马了 ... ... ...