ROS2 企业级构建脚本:告别繁琐的命令行操作
引言
在 ROS2 开发过程中,我们经常需要重复执行相同的构建命令:
bash
cd ~/ros_workspace
colcon build --symlink-install
随着项目规模的扩大,编译特定包、管理多个工作空间、清理旧日志等任务变得越来越繁琐。今天,我将分享一个精心打造的 ROS2 企业级构建脚本,它将彻底改变你的开发工作流程。
脚本概览
这个构建脚本集成了以下核心功能:
- 智能工作空间检测 - 自动查找 ROS2 工作空间
- 交互式编译管理 - 菜单驱动,无需记忆复杂命令
- 包选择系统 - 支持通配符、序号选择、批量选择
- 日志管理 - 自动清理旧日志,避免磁盘空间不足
- 配置持久化 - 保存常用设置,下次启动自动加载
核心功能详解
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 # 列出所有包

高级功能
- 并行编译:
bash
./build.sh -j 12 # 使用12个并行任务
- 不同编译模式:
bash
./build.sh -s # 符号链接模式(默认)
./build.sh -m # 合并安装模式
- 构建历史查看:
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
故障排除
常见问题
-
找不到工作空间
检查脚本文件所在当前目录是否包含 src 目录 或使用 -w 参数指定工作空间路径 -
编译失败
查看详细日志:tail -f ~/.ros2_build/logs/latest_build.log 检查依赖是否完整 -
权限问题
bashchmod +x build.sh sudo chown $USER ~/.ros2_build
调试模式
bash
ros2-build --verbose
ros2-build --dry-run # 只显示命令,不执行
结语
这个 ROS2 企业级构建脚本将原本繁琐的构建过程简化为几个简单的命令。它不仅仅是一个工具,更是一套完整的工作流解决方案。通过自动化和智能化的设计,它能够:
- 提高开发效率
- 减少人为错误
- 提供一致的构建环境
- 简化新成员上手难度
- 便于团队协作
许可证
本项目采用 MIT 许可证,允许自由使用、修改和分发。
开始享受更高效的 ROS2 开发体验吧!