diff --git a/scripts/build.sh b/scripts/build.sh index 418fee1..daea079 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -29,4 +29,8 @@ fi PACKAGES=(flexiv_description arm_msgs arm_description arm_control arm_moveit_config arm_bringup arm_application) colcon build --symlink-install --packages-select "${PACKAGES[@]}" "$@" +# shellcheck source=lib/fix_shebangs.sh +source "${ROOT}/scripts/lib/fix_shebangs.sh" +fix_shebangs "${ROOT}/install" "${CONDA_PREFIX:-/usr}/bin/python3" + echo "构建完成。请执行: source ${ROOT}/install/setup.bash" diff --git a/scripts/build_sim.sh b/scripts/build_sim.sh index f18e5eb..d635351 100644 --- a/scripts/build_sim.sh +++ b/scripts/build_sim.sh @@ -80,5 +80,17 @@ fi colcon --log-base log_sim "${ARGS[@]}" 2>&1 \ | grep -vE 'anaconda-auth|conda-content-trust|Unknown distribution option|warnings\.warn' +# --- 修正入口脚本的 shebang ------------------------------------------------ +# setuptools 写 console_scripts 时用的是**构建那一刻**的 python。如果在真机 +# 环境(ros_flexiv)里跑过本脚本,install_sim/ 里的脚本 shebang 就会指向 +# ros_flexiv —— 于是 ros2 run 起来的仿真节点其实跑在真机 conda 环境里。 +# 两个环境都是 Humble + py3.11,所以多数时候"居然能跑",只有偶发的 +# dlopen / 消息反序列化怪错误。这里统一改回仿真环境的 python。 +# shellcheck source=lib/fix_shebangs.sh +source "${ROOT}/scripts/lib/fix_shebangs.sh" +echo +echo "检查入口脚本 shebang(是否误指向真机 conda 环境)…" +fix_shebangs "${ROOT}/install_sim" "${CONDA_PREFIX:-/usr}/bin/python3" + echo echo "构建完成。启动仿真: bash scripts/run_sim_gz.sh" diff --git a/scripts/lib/conda_switch.sh b/scripts/lib/conda_switch.sh index 758f63d..4fe6620 100644 --- a/scripts/lib/conda_switch.sh +++ b/scripts/lib/conda_switch.sh @@ -52,5 +52,52 @@ conda_switch_clean() { return 0 } +# path_drop_patterns ... +# 从环境变量 的冒号分隔列表里删掉所有匹配任一 glob 的条目(保持剩余顺序)。 +# 用于清理"同一个 shell 里先 source 过另一套环境"留下的路径残留。 +# +# 为什么必须清:真机环境(scripts/env.sh)与仿真环境(scripts/sim_env.sh) +# 共用同一个工作区,各自的 setup.bash 都会往 PATH / PYTHONPATH / +# LD_LIBRARY_PATH / AMENT_PREFIX_PATH 里追加自己的产物目录。conda 的 +# deactivate 只会撤掉 conda 自己那部分,**不会**撤掉 setup.bash 追加的条目。 +# 于是"先真机后仿真"的 shell 里会长成这样: +# PYTHONPATH = .../install/arm_msgs/... : .../install_sim/arm_msgs/... +# 此时 `import arm_msgs` 会拿到**真机那份**,与 install_sim 下的运行库混用, +# 症状是消息反序列化莫名失败、字段对不上、dlopen 找不到 typesupport —— +# 报错信息完全不提"环境脏了"这个真实原因。 +# +# 实测踩过:sim_tactile_adapter 订阅 ros_gz_interfaces/msg/Contacts 时崩在 +# rcutils_set_error_state: Type support not from this implementation ... +# Could not load library libros_gz_interfaces__rosidl_typesupport_fastrtps_c.so +# 库其实就在 $CONDA_PREFIX/lib 下(ldd 全齐),根因是 LD_LIBRARY_PATH 里 +# 既残留着 ros_flexiv 的 lib、又**没有** $CONDA_PREFIX/lib。 +path_drop_patterns() { + local _var="$1" + shift + local -a _pats=("$@") + local _cur="${!_var:-}" + [[ -z "${_cur}" ]] && return 0 + + local _out="" _entry _pat _hit + local IFS=':' + for _entry in ${_cur}; do + _hit=0 + for _pat in "${_pats[@]}"; do + # 故意不加引号:让右侧当 glob 用(这里就是要通配匹配) + # shellcheck disable=SC2053 + if [[ "${_entry}" == ${_pat} ]]; then + _hit=1 + break + fi + done + [[ "${_hit}" -eq 1 ]] && continue + _out="${_out:+${_out}:}${_entry}" + done + + printf -v "${_var}" '%s' "${_out}" + export "${_var?}" + return 0 +} + # 供调用方按需取消设置,避免污染使用者的变量空间。 # (bash 没有"私有函数"概念,命名上统一加 conda_ 前缀降低撞名概率。) diff --git a/scripts/lib/fix_shebangs.sh b/scripts/lib/fix_shebangs.sh new file mode 100644 index 0000000..9a6a721 --- /dev/null +++ b/scripts/lib/fix_shebangs.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# ============================================================================= +# 修 console_scripts 的 shebang(真机 / 仿真两套构建产物都会踩的坑) +# ============================================================================= +# +# 现象: +# source scripts/sim_env.sh && ros2 run arm_sim_gazebo sim_camera_adapter +# 进程列表里却是 +# /home/cc/anaconda3/envs/ros_flexiv/bin/python3.11 ... sim_camera_adapter +# 即**仿真节点跑在真机的 conda 环境里**。 +# +# 为什么: +# setuptools 的 console_scripts 入口脚本,shebang 用的是**构建那一刻**的 +# sys.executable。本工作区只有一份源码,真机与仿真各有自己的 conda 环境 +# (ros_flexiv / ros_gz_sim)和各自的 install 目录。如果在真机环境里跑过 +# build_sim.sh(或反之),写进 install_sim/ 的脚本 shebang 就指向了另一个 +# 环境。`ros2 run` 是**照 shebang 执行**的,不看当前 CONDA_PREFIX。 +# +# 为什么危险: +# 两个环境都是 ROS 2 Humble + Python 3.11,所以多数时候"居然能跑", +# 只是 import 到的是另一个环境的包。表现为偶发的 ABI/`dlopen`/消息反序列化 +# 报错,报错信息完全不提"shebang 指错环境"。 +# 实测:LD_LIBRARY_PATH、PYTHONPATH 都清干净了,节点仍然跑在 ros_flexiv 里。 +# +# 对策: +# 构建收尾时扫一遍本次产物,把指向"别的 conda 环境"的 shebang 改回 +# **本环境**的 python。已指向本环境的文件不动(幂等)。 +# +# 用法: +# fix_shebangs +# ============================================================================= + +# fix_shebangs +# 把 下所有 console_scripts 里"指向其它 conda 环境"的 shebang +# 改写成 。返回 0;打印改了哪些文件(没改就不打印)。 +fix_shebangs() { + local install_dir="$1" + local expected="$2" + + if [[ ! -d "${install_dir}" || ! -x "${expected}" ]]; then + return 0 + fi + + # 只扫这两类位置:包自带的 console_scripts 目录、以及 bin/。 + # 用 find 而不是递归 rg:要按"是不是可执行文件 + 首行是 shebang"判断。 + local -a files=() + local f + while IFS= read -r -d '' f; do + files+=("${f}") + done < <(find "${install_dir}" \ + \( -path "*/lib/*" -o -path "*/bin/*" \) \ + -type f -perm -u+x -print0 2>/dev/null) + + local fixed=0 first + for f in "${files[@]}"; do + # 读首行(最多 200 字节);不是 #! 开头就跳过(二进制、数据文件等) + first="$(head -c 200 "${f}" | head -1)" + [[ "${first:0:2}" == "#!" ]] || continue + # 只动"指向某个 conda 环境"的 shebang;/usr/bin/python3 这类系统解释器不动 + [[ "${first}" == *"/envs/"*"python"* ]] || continue + [[ "${first}" == *"${expected}"* ]] && continue + + # 保留原 shebang 里的参数(例如 `python3 -u`) + local interp="${first#\#!}" # 去掉 #! + interp="${interp%% *}" # 第一个 token = 解释器路径 + local rest="${first#"#!${interp}"}" # 剩下的是参数(含前导空格) + # 就地重写首行:用 printf + tail 组装,避免 sed -i 在符号链接上的坑 + local tmp="${f}.shebang.$$" + { + printf '#!%s%s\n' "${expected}" "${rest}" + tail -n +2 "${f}" + } > "${tmp}" && cat "${tmp}" > "${f}" && rm -f "${tmp}" + echo " 修正 shebang: ${f#$PWD/} → ${expected}" + fixed=$((fixed + 1)) + done + + if [[ "${fixed}" -gt 0 ]]; then + echo " (共修正 ${fixed} 个入口脚本:它们的 shebang 原本指向别的 conda 环境," + echo " 会导致 ros2 run 起来的节点跑在错误的环境里)" + fi + return 0 +} diff --git a/scripts/sim_env.sh b/scripts/sim_env.sh index 10fe5ac..a123869 100644 --- a/scripts/sim_env.sh +++ b/scripts/sim_env.sh @@ -54,6 +54,32 @@ else echo "提示: install_sim/ 还不存在,先跑 bash scripts/build_sim.sh" >&2 fi +# --- 清掉真机环境留下的路径残留 --------------------------------------------- +# 同一个 shell 里先 source 过 scripts/env.sh(真机)再 source 本文件时,install/ +# 与 ros_flexiv 的路径不会被 conda deactivate 撤掉,会长在 install_sim/ 前面, +# 导致 import 到真机那份包、dlopen 到真机那份库。详见 conda_switch.sh 里 +# path_drop_patterns 的注释(那里记了 sim_tactile_adapter 崩溃的实测症状)。 +path_drop_patterns PYTHONPATH "${ROOT}/install/*" "${ROOT}/build/*" "*/envs/${REAL_ENV_NAME}/*" +path_drop_patterns LD_LIBRARY_PATH "${ROOT}/install/*" "*/envs/${REAL_ENV_NAME}/*" +path_drop_patterns PATH "${ROOT}/install/*" "*/envs/${REAL_ENV_NAME}/*" +path_drop_patterns AMENT_PREFIX_PATH "${ROOT}/install" "*/envs/${REAL_ENV_NAME}" +path_drop_patterns CMAKE_PREFIX_PATH "${ROOT}/install" "*/envs/${REAL_ENV_NAME}" + +# --- 补上 conda 环境自己的 lib ---------------------------------------------- +# conda 激活**不会**把 $CONDA_PREFIX/lib 放进 LD_LIBRARY_PATH(见下面 GZ 插件 +# 路径那段长注释)。对 gz 我们靠 GZ_SIM_SYSTEM_PLUGIN_PATH 兜住了,但 +# rosidl 的 typesupport 是靠 dlopen("lib<包名>__rosidl_typesupport_*.so") 找的, +# 只认 ld.so 的搜索路径。于是 conda 里带的(不在本工作区的)消息包会在 +# create_publisher/create_subscription 时抛 +# Type support not from this implementation ... cannot open shared object file +# 追加在**末尾**(优先级最低),避免压过系统/工作区里已有的同名库。 +if [[ -n "${CONDA_PREFIX:-}" && -d "${CONDA_PREFIX}/lib" ]]; then + case ":${LD_LIBRARY_PATH:-}:" in + *":${CONDA_PREFIX}/lib:"*) ;; + *) export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}${CONDA_PREFIX}/lib" ;; + esac +fi + [[ "${_sim_had_u}" -eq 1 ]] && set -u unset _sim_had_u