#!/usr/bin/env bash #==================================================== # OpenWrt 构建管理脚本 # 功能:提供交互式菜单,可选择执行特定操作 # 更新:2024-02-26 #==================================================== #---------------基础配置---------------# # 日志目录设置 LOGS_ROOT="openwrt_logs" SESSION_DIR="${LOGS_ROOT}/$(date +%Y%m%d_%H%M%S)" LOG_FILE="${SESSION_DIR}/build.log" #---------------清理配置---------------# # 需要移除的目录列表 declare -a REMOVE_DIRS=( ) # 需要移除的包列表 declare -a REMOVE_PACKAGES=( "feeds/luci/applications/luci-app-mosdns" "feeds/packages/net/alist" "feeds/packages/net/adguardhome" "feeds/packages/net/smartdns" ) #---------------自定义包配置---------------# # 定义自定义包配置,格式:[包名]=[目标目录]=[仓库URL]=[分支]=[是否使用--depth 1] declare -A CUSTOM_PACKAGES=( ["golang"]="feeds/packages/lang/golang=https://github.com/kenzok8/golang=main=true" ["partexp"]="package/luci-app-partexp=https://github.com/sirpdboy/luci-app-partexp=main=false" ["openclash"]="package/openclash=https://github.com/vernesong/OpenClash=dev=true" ["zerotier"]="package/luci-app-zerotier=https://github.com/zhengmz/luci-app-zerotier=master=false" ["5gsupport"]="package/openwrt-app-actions=https://github.com/Siriling/openwrt-app-actions.git=main=true" ["kucat"]="package/luci-theme-kucat=https://github.com/sirpdboy/luci-theme-kucat.git=main=false" ["mt76"]="package/firmware/mt76=https://github.com/openwrt/mt76.git=master=false" ) #---------------日志系统---------------# # 保存原始的标准输出和错误输出 exec {STDOUT_ORIG}>&1 exec {STDERR_ORIG}>&2 # 初始化日志系统 init_logger() { # 创建日志目录结构 mkdir -p "$SESSION_DIR" || { echo "无法创建日志目录: $SESSION_DIR" exit 1 } # 设置全局变量标记是否记录日志 LOGGING_ENABLED=true echo "日志保存在: $LOG_FILE" if [ "$LOGGING_ENABLED" = true ]; then # 标准输出重定向 exec > >(while read -r line; do printf "[%(%Y-%m-%d %H:%M:%S)T] %s\n" -1 "$line" | tee -a "$LOG_FILE" done) # 标准错误重定向 exec 2> >(while read -r line; do printf "[%(%Y-%m-%d %H:%M:%S)T] %s\n" -1 "$line" | tee -a "$LOG_FILE" done >&2) fi } # 创建新的编译日志文件 create_build_log() { local build_type=$1 local timestamp=$(date +%Y%m%d_%H%M%S) BUILD_LOG_FILE="${SESSION_DIR}/${build_type}_${timestamp}.log" echo "创建新的编译日志: $BUILD_LOG_FILE" return 0 } # 重定向输出到指定的编译日志文件 redirect_to_build_log() { local log_file=$1 # 保存当前的输出重定向状态 LOGGING_TO_BUILD_LOG=true # 重定向输出到编译日志文件 exec > >(while read -r line; do printf "[%(%Y-%m-%d %H:%M:%S)T] %s\n" -1 "$line" >> "$log_file" done) # 重定向错误输出到编译日志文件 exec 2> >(while read -r line; do printf "[%(%Y-%m-%d %H:%M:%S)T] %s\n" -1 "$line" >> "$log_file" done >&2) } # 恢复输出到主日志 restore_main_logging() { LOGGING_TO_BUILD_LOG=false # 恢复到主日志 if [ "$LOGGING_ENABLED" = true ]; then # 标准输出重定向 exec > >(while read -r line; do printf "[%(%Y-%m-%d %H:%M:%S)T] %s\n" -1 "$line" | tee -a "$LOG_FILE" done) # 标准错误重定向 exec 2> >(while read -r line; do printf "[%(%Y-%m-%d %H:%M:%S)T] %s\n" -1 "$line" | tee -a "$LOG_FILE" done >&2) fi } # 禁用日志输出 disable_logging() { LOGGING_ENABLED=false exec >&$STDOUT_ORIG exec 2>&$STDERR_ORIG } # 启用日志输出 enable_logging() { LOGGING_ENABLED=true # 标准输出重定向 exec > >(while read -r line; do printf "[%(%Y-%m-%d %H:%M:%S)T] %s\n" -1 "$line" | tee -a "$LOG_FILE" done) # 标准错误重定向 exec 2> >(while read -r line; do printf "[%(%Y-%m-%d %H:%M:%S)T] %s\n" -1 "$line" | tee -a "$LOG_FILE" done >&2) } # 设置终端颜色 setup_colors() { if [[ -t 2 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' else RED='' GREEN='' YELLOW='' BLUE='' NC='' fi } # 记录日志函数 - 使用此函数来明确日志级别 log() { local level=$1 local message=$2 local timestamp=$(date "+%Y-%m-%d %H:%M:%S") local color="" level_str="" case $level in "INFO") color="$GREEN"; level_str="INFO" ;; "WARN") color="$YELLOW"; level_str="WARN" ;; "ERROR") color="$RED"; level_str="ERROR" ;; "DEBUG") color="$BLUE"; level_str="DEBUG" ;; esac printf "[%s] ${color}[%s]${NC}: %s\n" "$timestamp" "$level_str" "$message" } # 同时记录到主日志和编译日志的函数 log_to_both() { local level=$1 local message=$2 local build_log=$3 # 记录到主日志 log "$level" "$message" # 记录到编译日志 local timestamp=$(date "+%Y-%m-%d %H:%M:%S") echo "[$timestamp] [$level]: $message" >> "$build_log" } #---------------自定义包管理函数---------------# # 更新单个自定义包 update_single_package() { local pkg_name=$1 local config=$2 # 解析配置 local target_dir=$(echo "$config" | cut -d= -f1) local repo_url=$(echo "$config" | cut -d= -f2) local branch=$(echo "$config" | cut -d= -f3) local use_depth=$(echo "$config" | cut -d= -f4) log "INFO" "更新 $pkg_name..." # 检查目标目录 if [ -d "$target_dir" ]; then log "INFO" "移除现有 $target_dir 目录" rm -rf "$target_dir" || { log "ERROR" "无法删除 $target_dir 目录" return 1 } fi # 准备克隆命令 local clone_cmd="git clone --progress" # 添加分支参数 (如果指定) if [ -n "$branch" ] && [ "$branch" != "master" ]; then clone_cmd="$clone_cmd -b $branch" fi # 添加 depth 参数 (如果需要) if [ "$use_depth" = "true" ]; then clone_cmd="$clone_cmd --depth 1" fi # 完成克隆命令 clone_cmd="$clone_cmd $repo_url $target_dir" # 执行克隆 log "INFO" "执行: $clone_cmd" eval "$clone_cmd" || { log "ERROR" "克隆 $pkg_name 失败" return 1 } log "INFO" "$pkg_name 更新完成" return 0 } # 更新自定义包的主函数 update_custom_package() { local failed_packages=() local success_count=0 local total_packages=${#CUSTOM_PACKAGES[@]} log "INFO" "开始更新 $total_packages 个自定义包..." for pkg_name in "${!CUSTOM_PACKAGES[@]}"; do update_single_package "$pkg_name" "${CUSTOM_PACKAGES[$pkg_name]}" || { failed_packages+=("$pkg_name") continue } ((success_count++)) done # 显示更新结果 if [ ${#failed_packages[@]} -eq 0 ]; then log "INFO" "所有自定义包更新成功 ($success_count/$total_packages)" return 0 else log "WARN" "部分包更新失败 (成功: $success_count/$total_packages)" log "ERROR" "更新失败的包: ${failed_packages[*]}" return 1 fi } # 更新特定自定义包 update_specific_package() { local pkg_name=$1 if [ -z "$pkg_name" ]; then log "ERROR" "未指定包名" return 1 fi if [ -z "${CUSTOM_PACKAGES[$pkg_name]}" ]; then log "ERROR" "未找到包配置:$pkg_name" log "INFO" "可用的包: ${!CUSTOM_PACKAGES[*]}" return 1 fi update_single_package "$pkg_name" "${CUSTOM_PACKAGES[$pkg_name]}" } #---------------基础功能函数---------------# # 清理工作区 clean_workspace() { log "INFO" "开始清理工作区..." log "INFO" "执行./scripts/feeds clean..." ./scripts/feeds clean || log "WARN" "feeds clean 失败" log "INFO" "执行rm -rf ./tmp ./staging_dir ./build_dir ./bin ./dl..." rm -rf ./tmp ./staging_dir ./build_dir ./bin ./dl || log "WARN" "删除临时目录失败" log "INFO" "工作区清理完成" } # 清理编译临时文件 clean_build_temp() { log "INFO" "开始清理编译临时文件..." log "INFO" "删除临时编译目录和缓存..." rm -rf ./tmp ./staging_dir ./build_dir || log "WARN" "删除临时编译目录失败" log "INFO" "编译临时文件清理完成" } # 更新源码 update_source() { log "INFO" "开始更新源码..." git pull || { log "ERROR" "源码更新失败"; return 1; } log "INFO" "源码更新完成" } # 更新feeds update_feeds() { log "INFO" "开始更新feeds..." ./scripts/feeds update -a && ./scripts/feeds install -a || { log "ERROR" "Feeds更新失败" return 1 } log "INFO" "Feeds更新完成" return 0 } # 执行编译命令并仅记录到指定日志文件 run_build_command() { local cmd=$1 local log_file=$2 local msg=$3 log_to_both "INFO" "$msg" "$log_file" # 重定向输出到编译日志 redirect_to_build_log "$log_file" # 执行编译命令 local result=0 eval "$cmd" || result=1 # 恢复输出到主日志 restore_main_logging return $result } # 编译固件 build_firmware() { # 创建新的编译日志 create_build_log "firmware" local current_log="$BUILD_LOG_FILE" log "INFO" "开始编译固件..." log "INFO" "编译详细日志位置: $current_log" # 下载依赖 log "INFO" "下载依赖包..." run_build_command "make download -j$(nproc) V=s" "$current_log" "正在下载依赖包..." || { log "ERROR" "依赖下载失败,详见日志: $current_log" return 1 } # 使用多线程编译 local cpu_cores=$(nproc) log "INFO" "使用 $cpu_cores 线程开始编译..." if run_build_command "make -j$cpu_cores V=s" "$current_log" "多线程编译进行中..."; then log "INFO" "多线程编译成功完成" return 0 else log "WARN" "多线程编译失败,清理临时文件后切换为单线程编译..." # 清理编译临时文件 clean_build_temp # 创建一个新日志用于单线程编译 create_build_log "firmware_singlethread" current_log="$BUILD_LOG_FILE" log "INFO" "单线程编译详细日志位置: $current_log" # 重新下载依赖 log "INFO" "重新下载依赖包..." run_build_command "make download -j1 V=s" "$current_log" "单线程模式重新下载依赖..." || { log "ERROR" "单线程依赖下载也失败,详见日志: $current_log" return 1 } if run_build_command "make -j1 V=s" "$current_log" "单线程编译进行中..."; then log "INFO" "单线程编译成功完成" return 0 else log "ERROR" "编译失败,即使在单线程模式下。详见日志: $current_log" return 1 fi fi } # 清理旧包 clean_packages() { log "INFO" "开始清理旧包..." local removed_count=0 local failed_count=0 # 清理目录 for dir in "${REMOVE_DIRS[@]}"; do if [ -d "$dir" ]; then rm -rf "$dir" && { log "INFO" "已删除: $dir" ((removed_count++)) } || { log "ERROR" "删除失败: $dir" ((failed_count++)) } fi done # 清理包 for pkg in "${REMOVE_PACKAGES[@]}"; do if [ -d "$pkg" ]; then rm -rf "$pkg" && { log "INFO" "已移除: $pkg" ((removed_count++)) } || { log "ERROR" "删除失败: $pkg" ((failed_count++)) } fi done if [ $failed_count -eq 0 ]; then log "INFO" "包清理完成,共移除 $removed_count 个包/目录" return 0 else log "WARN" "包清理部分完成,成功: $removed_count, 失败: $failed_count" return 1 fi } # 更新所有组件 update_all_components() { log "INFO" "开始更新所有组件..." local failed=false # 更新源码 log "INFO" "执行源码更新..." update_source || failed=true # 清理旧包 log "INFO" "执行清理旧包..." clean_packages || log "WARN" "清理旧包部分失败,继续执行..." # 更新自定义包 log "INFO" "执行自定义包更新..." update_custom_package || failed=true # 更新feeds log "INFO" "执行feeds更新..." update_feeds || failed=true if [ "$failed" = true ]; then log "WARN" "部分组件更新失败,请查看日志了解详情" return 1 else log "INFO" "所有组件更新完成" return 0 fi } # 菜单配置 menuconfig() { log "INFO" "开始菜单配置..." disable_logging make menuconfig enable_logging log "INFO" "菜单配置完成" } #---------------组合功能函数---------------# # 完整构建流程 full_build() { log "INFO" "启动完整构建流程..." clean_workspace || log "WARN" "工作区清理部分失败,继续执行..." update_source || { log "ERROR" "源码更新失败,终止构建"; return 1; } clean_packages || log "WARN" "清理旧包部分失败,继续执行..." update_custom_package || { log "ERROR" "自定义包更新失败,终止构建"; return 1; } update_feeds || { log "ERROR" "feeds更新失败,终止构建"; return 1; } log "INFO" "进入菜单配置..." menuconfig log "INFO" "开始编译固件..." build_firmware || { log "ERROR" "固件编译失败" return 1 } log "INFO" "完整构建流程完成" return 0 } #---------------菜单函数---------------# print_menu() { echo -e "\n${BLUE}OpenWrt 构建管理菜单${NC}" echo -e "${YELLOW}================================${NC}" echo -e "${GREEN}1.${NC} 完整构建流程" echo -e "${GREEN}2.${NC} 清理工作区" echo -e "${GREEN}3.${NC} 更新所有组件" echo -e "${GREEN}4.${NC} 清理旧包" echo -e "${GREEN}5.${NC} 编译固件" echo -e "${GREEN}6.${NC} 调整配置(menuconfig)" echo -e "${GREEN}7.${NC} 更新自定义包" echo -e "${GREEN}8.${NC} 更新特定自定义包" echo -e "${GREEN}9.${NC} 更新源码" echo -e "${GREEN}10.${NC} 更新feeds" echo -e "${GREEN}0.${NC} 退出" echo -e "${YELLOW}================================${NC}" echo -ne "请选择操作 [0-10]: " } # 显示可用的自定义包 print_package_menu() { echo -e "\n${BLUE}可用的自定义包${NC}" echo -e "${YELLOW}================================${NC}" local i=1 for pkg_name in "${!CUSTOM_PACKAGES[@]}"; do echo -e "${GREEN}$i.${NC} $pkg_name" ((i++)) done echo -e "${YELLOW}================================${NC}" echo -ne "请选择要更新的包 [1-$((i-1))]: " } # 选择特定自定义包更新 select_specific_package() { local pkg_names=("${!CUSTOM_PACKAGES[@]}") print_package_menu read -r choice # 验证选择 if [[ ! "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#pkg_names[@]} ]; then log "ERROR" "无效选择: $choice" return 1 fi local selected_pkg=${pkg_names[$((choice-1))]} log "INFO" "选择更新包: $selected_pkg" update_specific_package "$selected_pkg" } #---------------主函数---------------# main() { # 初始化日志目录和日志系统 init_logger setup_colors log "INFO" "OpenWrt 构建管理脚本启动 - 会话日志目录: $SESSION_DIR" while true; do print_menu read -r choice case $choice in 1) full_build ;; 2) clean_workspace ;; 3) update_all_components ;; 4) clean_packages ;; 5) build_firmware ;; 6) menuconfig ;; 7) update_custom_package ;; 8) select_specific_package ;; 9) update_source ;; 10) update_feeds ;; 0) log "INFO" "退出脚本" exit 0 ;; *) log "WARN" "无效选择: $choice,请重试" ;; esac # 操作完成后暂停 if [[ $choice != 0 ]]; then echo -e "\n${YELLOW}操作完成,按回车键继续...${NC}" read -r fi done } # 执行主函数 main "$@"