ROS2 企业级构建脚本:告别繁琐的命令行操作

ROS2 企业级构建脚本:告别繁琐的命令行操作

引言

在 ROS2 开发过程中,我们经常需要重复执行相同的构建命令:

bash 复制代码
cd ~/ros_workspace
colcon build --symlink-install

随着项目规模的扩大,编译特定包、管理多个工作空间、清理旧日志等任务变得越来越繁琐。今天,我将分享一个精心打造的 ROS2 企业级构建脚本,它将彻底改变你的开发工作流程。

脚本概览

这个构建脚本集成了以下核心功能:

  1. 智能工作空间检测 - 自动查找 ROS2 工作空间
  2. 交互式编译管理 - 菜单驱动,无需记忆复杂命令
  3. 包选择系统 - 支持通配符、序号选择、批量选择
  4. 日志管理 - 自动清理旧日志,避免磁盘空间不足
  5. 配置持久化 - 保存常用设置,下次启动自动加载

核心功能详解

1. 智能工作空间检测

脚本能够自动检测当前目录的 ROS2 工作空间:

  • 自动从当前目录向上查找包含 src 目录的工作空间
  • 如果未找到,会自动检查常见位置
  • 最后还可以手动输入路径

2. 交互式编译菜单

启动脚本后,会看到一个清晰的交互菜单:

3. 灵活的包选择系统

选择编译特定包时,脚本会列出所有可用的包:

支持多种选择方式[a/s]:

  • 序号选择:1 3 5
  • 全选:all

4. 智能日志管理

每次构建都会生成带时间戳的日志文件,并自动清理旧日志:

bash 复制代码
# 自动清理策略
# 1. 删除超过3天的日志文件
# 2. 保持最多100个日志文件
# 3. 根据磁盘使用率动态调整清理策略

安装与使用

快速使用

1. 创建build.sh脚本:
bash 复制代码
#!/bin/bash

# =================================================================
# ROS2 企业级构建脚本
# 功能:
# 1. 自动检测工作空间
# 2. 选择编译模式
# 3. 选择编译范围(全部/指定包)
# 4. 包列表浏览和选择
# 5. 编译参数配置
# 6. 日志和错误处理
# 7. 编译历史记录
# =================================================================

# 配置参数
CONFIG_FILE="$HOME/.ros2_build/config"
HISTORY_FILE="$HOME/.ros2_build/history"
LOG_DIR="$HOME/.ros2_build/logs"
VERSION="1.0.0"
AUTHOR="zhongshaolei"

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'
DIM='\033[2m'

# 全局变量
WORKSPACE_ROOT=""
SELECTED_PACKAGES=()
COMPILE_MODE="symlink"
COMPILE_TYPE="all"
EXTRA_ARGS=""
DRY_RUN=false
VERBOSE=false
PARALLEL_JOBS=8
CLEAN_BUILD=false
CONTINUE_ON_ERROR=false
SAVE_CONFIG=false
SHOW_PACKAGE_INFO=false
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOG_FILE="${LOG_DIR}/build_${LOG_TIMESTAMP}.log"

# 创建必要的目录
mkdir -p "$LOG_DIR"

# =================================================================
# 函数定义
# =================================================================

print_banner() {
    clear
    echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
    echo -e "${CYAN}${BOLD}               ROS2 企业级构建系统 v${VERSION}        ${NC}${CYAN}${NC}"
    echo -e "${CYAN}${DIM}                   作者: ${AUTHOR}                    ${NC}${CYAN}${NC}"
    echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}\n"
}

print_usage() {
    echo -e "${YELLOW}使用说明:${NC}"
    echo "  $0 [选项]"
    echo ""
    echo -e "${YELLOW}选项:${NC}"
    echo "  -h, --help              显示此帮助信息"
    echo "  -w, --workspace PATH    指定工作空间路径"
    echo "  -a, --all               编译所有包"
    echo "  -p, --package NAME      编译指定包(可多次使用)"
    echo "  -s, --symlink           使用符号链接模式(默认)"
    echo "  -m, --merge             使用合并安装模式"
    echo "  -c, --clean             清理后编译"
    echo "  -j, --jobs N            并行任务数(默认: 8)"
    echo "  -d, --dry-run           只显示命令,不执行"
    echo "  -v, --verbose           详细输出"
    echo "  -e, --extra ARGS        额外传递给 colcon 的参数"
    echo "  --continue              出错时继续"
    echo "  -config, --save-config           保存当前配置"
    echo "  -l, --list-packages         列出所有包并退出"
    echo ""
    echo -e "${YELLOW}示例:${NC}"
    echo "  $0 -w ~/ros_ws -a -c          # 清理后编译整个工作空间"
    echo "  $0 -p package1 -p package2    # 编译指定包"
    echo "  $0 -l                         # 列出所有包"
}

log_message() {
    local level=$1
    local message=$2
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    case $level in
        "INFO") color=$GREEN ;;
        "WARN") color=$YELLOW ;;
        "ERROR") color=$RED ;;
        "DEBUG") color=$PURPLE ;;
        *) color=$NC ;;
    esac
    
    echo -e "${color}[${timestamp}] ${level}: ${message}${NC}"
    echo "[${timestamp}] ${level}: ${message}" >> "$LOG_FILE"
    
    if [[ "$level" == "ERROR" && "$CONTINUE_ON_ERROR" == false ]]; then
        echo -e "\n${RED}错误发生,编译停止。使用 --continue 选项可继续编译。${NC}"
        exit 1
    fi
}

# 清理旧日志文件
cleanup_old_logs() {
    echo -e "${CYAN}检查并清理旧日志文件...${NC}" >&2
    
    if [[ ! -d "$LOG_DIR" ]]; then
        echo -e "${YELLOW}日志目录不存在: $LOG_DIR${NC}" >&2
        return 0
    fi
    
    # 获取日志文件总数
    local total_logs=$(find "$LOG_DIR" -name "build_*.log" -type f 2>/dev/null | wc -l)
    echo -e "当前日志文件数: $total_logs" >&2
    
    # 1. 删除超过3天的日志文件
    echo -e "${CYAN}清理超过3天的日志文件...${NC}" >&2
    local old_logs=$(find "$LOG_DIR" -name "build_*.log" -type f -mtime +3 2>/dev/null | wc -l)
    
    if [[ $old_logs -gt 0 ]]; then
        echo -e "找到 $old_logs 个超过3天的日志文件" >&2
        find "$LOG_DIR" -name "build_*.log" -type f -mtime +3 2>/dev/null | \
            while read -r file; do
                echo -e "删除: $(basename "$file")" >&2
                rm -f "$file"
            done
    else
        echo -e "没有超过3天的日志文件" >&2
    fi
    
    # 2. 如果日志文件仍超过100个,删除最旧的文件
    local remaining_logs=$(find "$LOG_DIR" -name "build_*.log" -type f 2>/dev/null | wc -l)
    echo -e "清理后剩余日志文件: $remaining_logs" >&2
    
    if [[ $remaining_logs -gt 100 ]]; then
        local to_delete=$((remaining_logs - 100))
        echo -e "${CYAN}日志文件超过100个,删除最旧的 $to_delete 个...${NC}" >&2
        
        # 按修改时间排序,删除最旧的
        find "$LOG_DIR" -name "build_*.log" -type f -exec ls -1t {} + 2>/dev/null | \
            tail -n "$to_delete" | \
            while read -r file; do
                echo -e "删除: $(basename "$file")" >&2
                rm -f "$file"
            done
    fi
    
    # 3. 清理空日志目录
    find "$LOG_DIR" -type d -empty 2>/dev/null | while read -r dir; do
        if [[ "$dir" != "$LOG_DIR" ]]; then
            rmdir "$dir" 2>/dev/null
        fi
    done
    
    local final_logs=$(find "$LOG_DIR" -name "build_*.log" -type f 2>/dev/null | wc -l)
    echo -e "${GREEN}日志清理完成,剩余 $final_logs 个日志文件${NC}" >&2
    echo "" >&2
}

find_workspace() {
    ### 自动检测工作空间并给WORKSPACE_ROOT赋值
    ### 如果没有指定工作空间,尝试自动查找当前目录向上查找的ROS2工作空间根目录
    local start_dir="${1:-$(pwd)}"
    local current_dir="$start_dir"
    
    # 记录搜索路径
    local search_paths=()
    
    echo -e "${CYAN}搜索工作空间...${NC}"
    echo -e "开始目录: $start_dir\n"
    
    # 定义工作空间标志特征
    local workspace_indicators=(
        "src"                     # ROS工作空间必须有src目录
        "src/CMakeLists.txt"     # ROS1工作空间的src/CMakeLists.txt
        "build"                   # 构建目录
        "install"                 # 安装目录
        "log"                     # 日志目录
        ".vscode"                 # VSCode配置目录
    )
    
    # 从当前目录向上查找
    while [[ "$current_dir" != "/" ]]; do
        echo -e "检查: $current_dir"
        
        # 检查是否是工作空间根目录的标志
        if [[ -d "$current_dir/src" ]]; then
            echo -e "  ✓ 发现 src 目录"
            
            # 检查是否有包存在
            local has_packages=false
            local package_count=0
            local packages=()
            
            # 在src目录中查找package.xml文件
            if find "$current_dir/src" -maxdepth 5 -name "package.xml" 2>/dev/null | read; then
                has_packages=true
                package_count=$(find "$current_dir/src" -maxdepth 5 -name "package.xml" 2>/dev/null | wc -l)
            fi
            
            if [[ "$has_packages" == true ]]; then
                echo -e "  ✓ 发现 $package_count 个包"
                
                # 检查其他工作空间特征
                local indicator_count=0
                for indicator in "${workspace_indicators[@]}"; do
                    if [[ -e "$current_dir/$indicator" ]]; then
                        ((indicator_count++))
                    fi
                done
                
                echo -e "  ✓ 满足 $indicator_count/${#workspace_indicators[@]} 个工作空间特征"
                
                # 如果满足至少2个特征,认为是工作空间
                if [[ $indicator_count -ge 2 ]]; then
                    echo -e "\n${GREEN}发现工作空间: $current_dir${NC}"
                    
                    # 列出找到的包
                    echo -e "\n${YELLOW}工作空间包:${NC}"
                    find "$current_dir/src" -maxdepth 5 -name "package.xml" 2>/dev/null | \
                        xargs grep -l "<name>" 2>/dev/null | \
                        while read -r pkg_file; do
                            local pkg_name=$(grep "<name>" "$pkg_file" | head -1 | sed 's/.*<name>//;s/<\/name>.*//')
                            local relative_path=$(dirname "${pkg_file#$current_dir/src/}")
                            echo "  - $pkg_name (${relative_path})"
                        done
                    WORKSPACE_ROOT=$current_dir
                    return 0
                fi
            fi
        fi
        
        # 检查是否是子目录但包含工作空间特征
        for indicator in "${workspace_indicators[@]}"; do
            if [[ -e "$current_dir/$indicator" ]] && [[ -d "$current_dir/src" || -d "$current_dir/../../src" ]]; then
                # 可能是工作空间,但不是根目录
                search_paths+=("$current_dir (可能的子目录)")
            fi
        done
        
        current_dir=$(dirname "$current_dir")
    done
    
    # 如果没找到,检查常见的工作空间位置
    echo -e "\n${YELLOW}在标准位置搜索工作空间...${NC}"
    
    local common_ws_locations=(
        "$HOME/ros_workspaces/ros2_ws"
        "$HOME/ros_workspaces/ros_ws"
    )
    
    for location in "${common_ws_locations[@]}"; do
        if [[ -d "$location/src" ]]; then
            echo -e "  ✓ 发现工作空间: $location"
            
            # 检查是否有包
            if find "$location/src" -maxdepth 5 -name "package.xml" 2>/dev/null | read; then
                echo -e "${GREEN}使用已知工作空间: $location${NC}"
                WORKSPACE_ROOT=$location
                return 0
            fi
            return 1
        fi
    done
    
    # 如果没有找到,尝试通过环境变量查找
    echo -e "\n${YELLOW}通过环境变量搜索...${NC}"
    
    if [[ -n "$ROS_WORKSPACE" && -d "$ROS_WORKSPACE/src" ]]; then
        echo -e "  ✓ ROS_WORKSPACE 环境变量: $ROS_WORKSPACE"
        WORKSPACE_ROOT=$ROS_WORKSPACE
        return 0
    fi
    
    if [[ -n "$COLCON_PREFIX_PATH" ]]; then
        # 从colcon路径推断工作空间
        local ws_path=$(echo "$COLCON_PREFIX_PATH" | cut -d: -f1 | xargs dirname 2>/dev/null || true)
        if [[ -d "$ws_path/src" ]]; then
            echo -e "  ✓ 从 COLCON_PREFIX_PATH 推断: $ws_path"
            WORKSPACE_ROOT=$ws_path
            return 0
        fi
    fi
    
    # 最后尝试:查找包含CMakeLists.txt的src目录
    echo -e "\n${YELLOW}尝试深度搜索...${NC}"
    
    # 从用户主目录开始查找
    local found_ws=$(find "$HOME" -name "package.xml" -path "*/src/*" 2>/dev/null | \
        head -1 | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null || true)
    
    if [[ -n "$found_ws" && -d "$found_ws/src" ]]; then
        echo -e "  ✓ 深度搜索找到: $found_ws"
        WORKSPACE_ROOT=$found_ws
        return 0
    fi
    
    echo -e "\n${RED}未找到ROS2工作空间${NC}"
    
    if [[ ${#search_paths[@]} -gt 0 ]]; then
        echo -e "\n${YELLOW}可能的工作空间位置:${NC}"
        for path in "${search_paths[@]}"; do
            echo "  - $path"
        done
    fi
    
    return 1
}

load_config() {
    if [[ -f "$CONFIG_FILE" ]]; then
        source "$CONFIG_FILE"
        log_message "INFO" "加载配置文件: $CONFIG_FILE"
    fi
}

save_config() {
    cat > "$CONFIG_FILE" << EOF
# ROS2 构建配置
# 生成时间: $(date)
WORKSPACE_ROOT="$WORKSPACE_ROOT"
COMPILE_MODE="$COMPILE_MODE"
PARALLEL_JOBS="$PARALLEL_JOBS"
LAST_BUILD_TIME="$(date)"
SELECTED_PACKAGES="${SELECTED_PACKAGES[*]}"
COMPILE_TYPE="$COMPILE_TYPE"
EOF
    log_message "INFO" "配置已保存到: $CONFIG_FILE"
}

save_history() {
    local status=$1
    local duration=$2
    echo "$(date),$WORKSPACE_ROOT,$COMPILE_MODE,$COMPILE_TYPE,${SELECTED_PACKAGES[*]},$status,$duration" >> "$HISTORY_FILE"
}

list_packages() {
    if [[ -z "$WORKSPACE_ROOT" ]]; then
        log_message "ERROR" "未设置工作空间"
        return 1
    fi
    
    cd "$WORKSPACE_ROOT" || return 1
    
    echo -e "\n${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
    echo -e "${CYAN}${BOLD}                     工作空间包列表                      ${NC}${CYAN}${NC}"
    echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}\n"
    
    if command -v colcon > /dev/null 2>&1; then
        colcon list --names-only | cat -n
    else
        # 如果没有 colcon,手动查找
        find src -name package.xml -type f | xargs grep -l "<name>" | while read -r pkg; do
            name=$(grep "<name>" "$pkg" | head -1 | sed 's/.*<name>//;s/<\/name>.*//')
            echo "$name"
        done | cat -n
    fi
    
    echo -e "\n${YELLOW}总包数: $(find src -name package.xml 2>/dev/null | wc -l)${NC}"
}

select_packages_interactive() {
    local packages=()
    local package_names=()
    
    cd "$WORKSPACE_ROOT" || return 1
    
    # 获取包列表
    if command -v colcon > /dev/null 2>&1; then
        mapfile -t packages < <(colcon list --names-only)
    else
        while IFS= read -r pkg; do
            name=$(grep "<name>" "$pkg" | head -1 | sed 's/.*<name>//;s/<\/name>.*//')
            packages+=("$name")
        done < <(find src -name package.xml -type f)
    fi
    
    if [[ ${#packages[@]} -eq 0 ]]; then
        log_message "ERROR" "未找到任何包"
        return 1
    fi
    
    echo -e "\n${CYAN}请选择要编译的包:${NC}"
    echo "a) 所有包"
    echo -e "s) 选择特定包\n"
    
    local choice
    read -p "选择 [a/s]: " choice
    
    case $choice in
        a|A)
            SELECTED_PACKAGES=("${packages[@]}")
            COMPILE_TYPE="all"
            echo -e "${GREEN}已选择所有包 (${#packages[@]}个)${NC}"
            ;;
        s|S)
            echo -e "\n${CYAN}包列表:${NC}"
            for i in "${!packages[@]}"; do
                printf "%3d) %s\n" $((i+1)) "${packages[i]}"
            done
            
            echo -e "\n${YELLOW}选择包 (输入序号,多个用空格分隔,如: 1 3 5):${NC}"
            read -p "> " -a indices
            
            for idx in "${indices[@]}"; do
                if [[ "$idx" =~ ^[0-9]+$ ]] && [[ $idx -ge 1 ]] && [[ $idx -le ${#packages[@]} ]]; then
                    SELECTED_PACKAGES+=("${packages[idx-1]}")
                fi
            done
            
            if [[ ${#SELECTED_PACKAGES[@]} -eq 0 ]]; then
                log_message "WARN" "没有选择任何包,将编译所有包"
                SELECTED_PACKAGES=("${packages[@]}")
            fi
            COMPILE_TYPE="selected"
            ;;
        *)
            log_message "WARN" "无效选择,将编译所有包"
            SELECTED_PACKAGES=("${packages[@]}")
            COMPILE_TYPE="all"
            ;;
    esac
}

detect_system_info() {
    echo -e "\n${CYAN}系统信息:${NC}"
    echo "ROS2 版本: $ROS_DISTRO"
    echo "系统: $(lsb_release -ds 2>/dev/null || uname -o)"
    echo "内核: $(uname -r)"
    echo "CPU: $(nproc) 核心"
    echo "内存: $(free -h | awk '/^Mem:/ {print $2}')"
    echo "工作空间: $WORKSPACE_ROOT"
}

build_packages() {
    local start_time=$(date +%s)
    
    cd "$WORKSPACE_ROOT" || {
        log_message "ERROR" "无法进入工作空间目录: $WORKSPACE_ROOT"
        return 1
    }
    
    # 构建命令基础
    local build_cmd="colcon build"
    
    # 添加编译模式
    if [[ "$COMPILE_MODE" == "symlink" ]]; then
        build_cmd="$build_cmd --symlink-install"
    else
        build_cmd="$build_cmd --merge-install"
    fi
    
    # 添加包选择
    if [[ "$COMPILE_TYPE" == "selected" && ${#SELECTED_PACKAGES[@]} -gt 0 ]]; then
        build_cmd="$build_cmd --packages-select ${SELECTED_PACKAGES[*]}"
    fi
    
    # 添加并行参数
    build_cmd="$build_cmd --parallel-workers $PARALLEL_JOBS"
    
    # 清理构建
    if [[ "$CLEAN_BUILD" == true ]]; then
        log_message "INFO" "清理构建目录..."
        if [[ "$DRY_RUN" == false ]]; then
            rm -rf build install log
        else
            echo "[DRY RUN] rm -rf build install log"
        fi
    fi
    
    # 添加额外参数
    if [[ -n "$EXTRA_ARGS" ]]; then
        build_cmd="$build_cmd $EXTRA_ARGS"
    fi
    
    # 添加事件处理器
    if [[ "$CONTINUE_ON_ERROR" == true ]]; then
        build_cmd="$build_cmd --event-handlers console_direct+"
    else
        build_cmd="$build_cmd --event-handlers console_cohesion+"
    fi
    
    # 显示构建信息
    echo -e "\n${CYAN}════════════════════ 构建信息 ════════════════════${NC}"
    echo "工作空间:    $WORKSPACE_ROOT"
    echo "编译模式:    $COMPILE_MODE"
    echo "编译类型:    $COMPILE_TYPE"
    if [[ "$COMPILE_TYPE" == "selected" ]]; then
        echo "选择包:      ${SELECTED_PACKAGES[*]}"
    fi
    echo "并行任务:    $PARALLEL_JOBS"
    echo "额外参数:    $EXTRA_ARGS"
    echo "日志文件:    $LOG_FILE"
    echo -e "${CYAN}═══════════════════════════════════════════════════${NC}\n"
    
    # 执行构建
    log_message "INFO" "开始构建..."
    echo -e "${GREEN}执行命令:${NC} $build_cmd\n"
    
    if [[ "$DRY_RUN" == true ]]; then
        echo "[DRY RUN] 将执行: $build_cmd"
        return 0
    fi
    
    # 实际执行构建命令
    eval $build_cmd 2>&1 | tee -a "$LOG_FILE"
    local build_status=${PIPESTATUS[0]}
    
    local end_time=$(date +%s)
    local duration=$((end_time - start_time))
    local duration_str=$(printf "%02d:%02d" $((duration/60)) $((duration%60)))
    
    # 保存历史记录
    if [[ $build_status -eq 0 ]]; then
        log_message "INFO" "构建成功完成! 耗时: $duration_str"
        save_history "SUCCESS" "$duration_str"
        
        # 询问是否source
        echo -e "\n${YELLOW}是否要 source 安装文件? [y/N]:${NC}"
        read -n 1 -r
        if [[ $REPLY =~ ^[Yy]$ ]]; then
            source install/setup.bash
            echo -e "${GREEN}已 source 安装文件${NC}"
        fi
    else
        log_message "ERROR" "构建失败! 耗时: $duration_str"
        save_history "FAILED" "$duration_str"
        
        # 显示错误日志最后10行
        echo -e "\n${RED}最后10行错误日志:${NC}"
        tail -10 "$LOG_FILE"
    fi
    
    return $build_status
}

show_menu() {
    while true; do
        print_banner
        
        echo -e "${CYAN}当前配置:${NC}"
        echo -e  "1. 工作空间:   ${GREEN}$WORKSPACE_ROOT${NC}"
        echo -e  "2. 编译模式:   ${GREEN}$COMPILE_MODE${NC}"
        echo -e  "3. 并行任务:   ${GREEN}$PARALLEL_JOBS${NC}"
        echo -e  "4. 编译范围:   ${GREEN}$COMPILE_TYPE${NC}"
        if [[ "$COMPILE_TYPE" == "selected" ]]; then
            echo -e  "   选择包:     ${GREEN}${SELECTED_PACKAGES[*]}${NC}"
        fi
        echo -e "5. 额外参数:   ${GREEN}$EXTRA_ARGS${NC}"
        echo -e "6. 清理构建:   ${GREEN}$CLEAN_BUILD${NC}"
        echo -e "7. 出错继续:   ${GREEN}$CONTINUE_ON_ERROR${NC}"
        echo ""
        echo -e "${CYAN}操作菜单:${NC}"
        echo "w) 设置工作空间"
        echo "m) 选择编译模式 (symlink/merge)"
        echo "p) 选择要编译的包"
        echo "j) 设置并行任务数"
        echo "e) 设置额外参数"
        echo "c) 切换清理构建"
        echo "o) 切换出错继续"
        echo "i) 显示系统信息"
        echo "l) 列出所有包"
        echo "s) 保存当前配置"
        echo "r) 开始构建"
        echo "q) 退出"
        echo ""
        
        read -p "请选择操作: " choice
        
        case $choice in
            1) 
                read -p "输入工作空间路径: " WORKSPACE_ROOT
                WORKSPACE_ROOT=${WORKSPACE_ROOT//\~/$HOME}
                ;;
            2)
                echo -e "\n编译模式:"
                echo "1) symlink (符号链接,推荐)"
                echo "2) merge (合并安装)"
                read -p "选择 [1/2]: " mode_choice
                case $mode_choice in
                    1) COMPILE_MODE="symlink" ;;
                    2) COMPILE_MODE="merge" ;;
                    *) echo "无效选择" ;;
                esac
                ;;
            3)
                read -p "输入并行任务数 (默认: 8): " jobs_input
                if [[ -n "$jobs_input" && "$jobs_input" =~ ^[0-9]+$ ]]; then
                    PARALLEL_JOBS=$jobs_input
                fi
                ;;
            4)
                echo -e "\n编译范围:"
                echo "1) 所有包"
                echo "2) 选择包"
                read -p "选择 [1/2]: " type_choice
                case $type_choice in
                    1) COMPILE_TYPE="all" ;;
                    2) COMPILE_TYPE="selected" ;;
                    *) echo "无效选择" ;;
                esac
                ;;
            5)
                read -p "输入额外参数: " EXTRA_ARGS
                ;;
            6)
                if [[ "$CLEAN_BUILD" == true ]]; then
                    CLEAN_BUILD=false
                else
                    CLEAN_BUILD=true
                fi
                ;;
            7)
                if [[ "$CONTINUE_ON_ERROR" == true ]]; then
                    CONTINUE_ON_ERROR=false
                else
                    CONTINUE_ON_ERROR=true
                fi
                ;;
            w|W)
                read -p "输入工作空间路径: " WORKSPACE_ROOT
                WORKSPACE_ROOT=${WORKSPACE_ROOT//\~/$HOME}
                ;;
            m|M)
                echo -e "\n编译模式:"
                echo "s) symlink (符号链接,推荐)"
                echo "m) merge (合并安装)"
                read -p "选择 [s/m]: " mode_choice
                case $mode_choice in
                    s|S) COMPILE_MODE="symlink" ;;
                    m|M) COMPILE_MODE="merge" ;;
                    *) echo "无效选择" ;;
                esac
                ;;
            p|P)
                if [[ -n "$WORKSPACE_ROOT" ]]; then
                    select_packages_interactive
                else
                    echo -e "${RED}请先设置工作空间${NC}"
                    sleep 2
                fi
                ;;
            j|J)
                read -p "输入并行任务数 (默认: 8): " jobs_input
                if [[ -n "$jobs_input" && "$jobs_input" =~ ^[0-9]+$ ]]; then
                    PARALLEL_JOBS=$jobs_input
                fi
                ;;
            e|E)
                read -p "输入额外参数: " EXTRA_ARGS
                ;;
            c|C)
                if [[ "$CLEAN_BUILD" == true ]]; then
                    CLEAN_BUILD=false
                    echo -e "${GREEN}已禁用清理构建${NC}"
                else
                    CLEAN_BUILD=true
                    echo -e "${GREEN}已启用清理构建${NC}"
                fi
                sleep 1
                ;;
            o|O)
                if [[ "$CONTINUE_ON_ERROR" == true ]]; then
                    CONTINUE_ON_ERROR=false
                    echo -e "${GREEN}已禁用在出错时继续${NC}"
                else
                    CONTINUE_ON_ERROR=true
                    echo -e "${GREEN}已启用在出错时继续${NC}"
                fi
                sleep 1
                ;;
            i|I)
                detect_system_info
                echo -e "\n按任意键继续..."
                read -n 1
                ;;
            l|L)
                if [[ -n "$WORKSPACE_ROOT" ]]; then
                    list_packages
                else
                    echo -e "${RED}请先设置工作空间${NC}"
                fi
                echo -e "\n按任意键继续..."
                read -n 1
                ;;
            s|S)
                save_config
                sleep 1
                ;;
            r|R)
                if [[ -n "$WORKSPACE_ROOT" && -d "$WORKSPACE_ROOT" ]]; then
                    build_packages
                else
                    echo -e "${RED}工作空间不存在或未设置${NC}"
                fi
                echo -e "\n按任意键继续..."
                read -n 1
                ;;
            q|Q)
                echo -e "\n${GREEN}再见!${NC}"
                exit 0
                ;;
            *)
                echo -e "${RED}无效选择${NC}"
                sleep 1
                ;;
        esac
    done
}

parse_arguments() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help)
                print_usage
                exit 0
                ;;
            -w|--workspace)
                WORKSPACE_ROOT="$2"
                shift 2
                ;;
            -a|--all)
                COMPILE_TYPE="all"
                shift
                ;;
            -p|--package)
                SELECTED_PACKAGES+=("$2")
                COMPILE_TYPE="selected"
                shift 2
                ;;
            -s|--symlink)
                COMPILE_MODE="symlink"
                shift
                ;;
            -m|--merge)
                COMPILE_MODE="merge"
                shift
                ;;
            -c|--clean)
                CLEAN_BUILD=true
                shift
                ;;
            -j|--jobs)
                PARALLEL_JOBS="$2"
                shift 2
                ;;
            -d|--dry-run)
                DRY_RUN=true
                shift
                ;;
            -v|--verbose)
                VERBOSE=true
                shift
                ;;
            -e|--extra)
                EXTRA_ARGS="$2"
                shift 2
                ;;
            --continue)
                CONTINUE_ON_ERROR=true
                shift
                ;;
            -config|--save-config)
                SAVE_CONFIG=true
                shift
                ;;
            -l|--list-packages)
                if [[ -z "$WORKSPACE_ROOT" ]]; then
                    find_workspace
                fi
                if [[ -n "$WORKSPACE_ROOT" ]]; then
                    list_packages
                else
                    log_message "ERROR" "未找到工作空间"
                fi
                exit 0
                ;;
            *)
                log_message "ERROR" "未知参数: $1"
                print_usage
                exit 1
                ;;
        esac
    done
}

main() {
    # 每次运行先清理旧日志和旧文件
    cleanup_old_logs()

    # 检查是否在ROS2环境中
    if [[ -z "$ROS_DISTRO" ]]; then
        echo -e "${YELLOW}警告: 未检测到ROS2环境${NC}"
        echo "请确保已source ROS2环境: source /opt/ros/你的版本/setup.bash"
        read -p "是否继续? [y/N]: " -n 1 -r
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            exit 1
        fi
    fi
    
    # 解析命令行参数
    parse_arguments "$@"
    
    # 加载配置
    load_config
    
    # 如果没有指定工作空间,尝试自动查找
    if [[ -z "$WORKSPACE_ROOT" ]]; then
        find_workspace
        echo -e "${GREEN}自动找到工作空间: $WORKSPACE_ROOT${NC}"
        if [[ -z "$WORKSPACE_ROOT" ]]; then
            echo -e "${YELLOW}未找到工作空间,请手动指定${NC}"
            read -p "输入工作空间路径: " WORKSPACE_ROOT
            WORKSPACE_ROOT=${WORKSPACE_ROOT//\~/$HOME}
        fi
    fi
    
    # 验证工作空间
    if [[ ! -d "$WORKSPACE_ROOT" ]]; then
        log_message "ERROR" "工作空间不存在: $WORKSPACE_ROOT"
        exit 1
    fi
    
    if [[ ! -d "$WORKSPACE_ROOT/src" ]]; then
        log_message "ERROR" "工作空间无效 (缺少src目录): $WORKSPACE_ROOT"
        exit 1
    fi
    
    # 如果没有包被选择且指定了selected模式,进入交互式选择
    if [[ "$COMPILE_TYPE" == "selected" && ${#SELECTED_PACKAGES[@]} -eq 0 ]]; then
        select_packages_interactive
    fi
    
    # 如果从命令行启动且有参数,直接构建
    if [[ $# -gt 0 ]] && [[ "$DRY_RUN" == false ]]; then
        build_packages
    else
        # 否则进入交互式菜单
        show_menu
    fi
    
    # 如果需要保存配置
    if [[ "$SAVE_CONFIG" == true ]]; then
        save_config
    fi
}

# 设置退出时的清理
trap 'echo -e "\n${YELLOW}用户中断,退出...${NC}"; exit 1' INT

# 启动主函数
main "$@"
2. 运行使用
bash 复制代码
# 添加执行权限
chmod +x build.sh

# 创建软链接到系统路径
sudo ln -s $(pwd)/build.sh /usr/local/bin/ros2-build

基本使用

bash 复制代码
# 交互式模式
./build.sh

# 命令行模式
./build.sh -w ~/ros_ws -a -c  # 清理后编译所有包
./build.sh -p package1 -p package2  # 编译特定包
./build.sh --list-packages  # 列出所有包

高级功能

  1. 并行编译
bash 复制代码
./build.sh -j 12  # 使用12个并行任务
  1. 不同编译模式
bash 复制代码
./build.sh -s  # 符号链接模式(默认)
./build.sh -m  # 合并安装模式
  1. 构建历史查看
bash 复制代码
# 查看构建历史
cat ~/.ros2_build/history

脚本特色

1. 错误恢复机制

构建失败时,脚本会显示详细的错误信息,并可以选择继续或停止。

2. 颜色编码输出

不同级别的信息使用不同颜色,提高可读性:

  • 绿色:成功信息
  • 黄色:警告信息
  • 红色:错误信息
  • 青色:普通信息

3. 配置文件管理

所有配置保存在 ~/.ros2_build/config 中,包括:

  • 最近使用的工作空间
  • 偏好的编译模式
  • 并行任务数设置

4. 智能包发现

自动检测包的依赖关系,确保编译顺序正确。

实际应用场景

场景1:日常开发

bash 复制代码
# 快速编译正在开发的包
./build.sh -p my_package

# 查看编译输出
tail -f ~/.ros2_build/logs/latest_build.log

场景2:团队协作

bash 复制代码
# 新成员快速上手
git clone project_repo
./build.sh  # 自动检测并编译所有包

场景3:持续集成

bash 复制代码
# CI/CD 流水线中使用
./build.sh -a -c 2>&1 | tee build.log
# 检查构建状态
if ./build.sh --check-status; then
    echo "构建成功"
else
    echo "构建失败"
    # 发送通知...
fi

性能优化

并行编译

脚本默认使用 nproc 命令检测 CPU 核心数,自动设置最优的并行任务数。

增量编译

只有当文件发生变化时才重新编译相关包,大幅缩短编译时间。

缓存利用

利用 ccache 加速重复编译任务。

与现有工具集成

与 VSCode 集成

json 复制代码
// .vscode/tasks.json
{
    "label": "ROS2 Build",
    "type": "shell",
    "command": "ros2-build", // 或者指定build.sh的路径
    "group": "build"
}

与 Docker 集成

dockerfile 复制代码
# Dockerfile
COPY build.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/build.sh

最佳实践

1. 工作空间组织

复制代码
~/ros_workspaces/
├── ros2_ws/          # 主工作空间
├── navigation_ws/    # 导航相关包
└── perception_ws/    # 感知相关包

2. 配置管理

bash 复制代码
# 为不同项目使用不同配置
export ROS2_BUILD_CONFIG=~/.ros2_build/config.project_a
ros2-build

3. 自动化脚本

bash 复制代码
#!/bin/bash
# auto_build.sh
set -e
ros2-build -a -c
ros2 test
ros2 launch demo.launch.py

故障排除

常见问题

  1. 找不到工作空间

    复制代码
    检查脚本文件所在当前目录是否包含 src 目录
    或使用 -w 参数指定工作空间路径
  2. 编译失败

    复制代码
    查看详细日志:tail -f ~/.ros2_build/logs/latest_build.log
    检查依赖是否完整
  3. 权限问题

    bash 复制代码
    chmod +x build.sh
    sudo chown $USER ~/.ros2_build

调试模式

bash 复制代码
ros2-build --verbose
ros2-build --dry-run  # 只显示命令,不执行

结语

这个 ROS2 企业级构建脚本将原本繁琐的构建过程简化为几个简单的命令。它不仅仅是一个工具,更是一套完整的工作流解决方案。通过自动化和智能化的设计,它能够:

  1. 提高开发效率
  2. 减少人为错误
  3. 提供一致的构建环境
  4. 简化新成员上手难度
  5. 便于团队协作

许可证

本项目采用 MIT 许可证,允许自由使用、修改和分发。


开始享受更高效的 ROS2 开发体验吧!

相关推荐
kobesdu2 天前
ORB-SLAM3:从特征提取到稠密点云建图
机器人·ros·orbslam
kobesdu2 天前
FAST-LIO2 + 蓝海M300激光雷达:从建图到实时栅格图的完整流程
算法·机器人·ros·slam·fast lio
kobesdu4 天前
综合强度信息的激光雷达去拖尾算法解析和源码实现
算法·机器人·ros·slam·激光雷达
ZTL-NPU4 天前
Jetbrains开发ros
ide·python·pycharm·编辑器·ros·clion
kobesdu5 天前
ROS Flutter GUI App:跨平台机器人可视化控制界面使用记录
flutter·机器人·ros
kobesdu6 天前
LeRobot “机器人万能接口”:现状与前沿进展
机器人·ros
kobesdu7 天前
laser_line_extraction线段提取开源功能包解读和使用例程
人工智能·算法·机器人·ros
kyle~7 天前
C++---Boost库(准标准库)
开发语言·c++·机器人·ros·boost
康谋自动驾驶11 天前
从数据采集到回放验证:ADTF 适配 ROS2 的 ADAS 测试实践
汽车·ros·数据采集·测试