4.4 在容器中开发
备注
本节仅适用于 X2 Ultra(旗舰版) 与 X2 Ultra(new version)(旗舰焕新版)。X2 EDU(人人造版)无开发计算单元,不适用本节。
使用容器隔离开发环境,避免污染机器人主系统、便于多版本并存。
容器通过 bind-mount 直接挂载宿主机(即开发计算单元)的 SDK 目录,在开发计算单元上的修改会实时同步到容器内,编译产物亦然。
注意
在开发计算单元上开发时,自 v1.2.0 起二开程序须在容器内构建与运行,非容器开发模式(在开发计算单元宿主机上直接使用 SDK)的权限将受限,系统软件包(apt / pip 等)的安装仅支持在容器开发模式下进行。
v1.1.0 两种模式均可用,属过渡版本。若二开程序目前仍直接跑在开发计算单元宿主机上,请在 v1.1.0 期间按本章完成迁移:
按下文构建镜像并启动容器;
把二开程序源码放到 SDK 目录
/home/agi/aimdk下(该目录已挂载进容器,宿主机与容器内是同一份文件);在容器内重新构建 SDK 与二开程序,把宿主机上手动装过的依赖改为写进 Dockerfile 或在容器内安装;
在容器内运行并确认功能与非容器模式一致。
4.4.1 准备文件
在开发计算单元上创建一个目录存放镜像相关脚本,例如 ~/aimdk-image/:
mkdir -p ~/aimdk-image
cd ~/aimdk-image
下面两份文件需手动保存到该目录中。
构建前必查:镜像源可达性
下面的 Dockerfile 全部使用国内镜像源。构建前确认四行都返回 200:
for u in https://ngc.nju.edu.cn/v2/nvidia/l4t-base/tags/list \
https://mirrors.aliyun.com/ubuntu-ports/dists/jammy/Release \
https://mirrors.aliyun.com/ros2/ubuntu/dists/jammy/Release \
https://pypi.tuna.tsinghua.edu.cn/simple/ ; do
printf "%-58s %s\n" "$(echo "$u" | cut -c1-56)" \
"$(curl -s -o /dev/null -w '%{http_code}' -m 15 "$u")"
done
构建前必查:/etc/subgid 尾随空格
rootless podman 建容器靠 /etc/subgid 授权做 GID 映射。若某行行尾带了尾随空格,解析器会整行丢弃,导致该 GID 授权失效——而报错只点名那个 GID(如 gid range ... -> [1000-1001) not allowed),极具误导性。换机器 / 刷机后容易复发,构建前先查:
cat -A /etc/subgid
看有没有 $ 前带空格 / tab 的行。正常应是 agi:1000:1$(1 直接到 $,无空格)。若看到 agi:1000:1 $ 这种坏行,修复:
sudo sed -i 's/[[:space:]]*$//' /etc/subgid
cat -A /etc/subgid # Recheck; the bad line should become agi:1000:1$
podman system migrate
修完再继续构建。
Dockerfile
将以下内容保存为 ~/aimdk-image/Dockerfile。可直接用下面的 cat 命令写入(EOF 必须加单引号,避免 $(...) 被提前展开):
cat > ~/aimdk-image/Dockerfile <<'EOF'
# Pull the base image via the NJU NGC mirror; nvcr.io is often unreachable in China.
FROM ngc.nju.edu.cn/nvidia/l4t-base:r36.2.0
LABEL agi.image.purpose="ROS Humble base, no CUDA bundled (mount /usr/local/cuda + /usr/lib/.../libcudnn,libnvinfer at runtime)"
LABEL agi.image.base="l4t-base:r36.2.0"
LABEL agi.image.note="Mount /opt/ros, CUDA toolkit, cuDNN/TensorRT shared libs from host at runtime"
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Asia/Shanghai
# Point the container's system apt at the Aliyun ubuntu-ports mirror: the base image
# defaults to ports.ubuntu.com, which is slow in China, and the hundreds of megabytes
# of build-essential / cmake / OpenCV dependencies below all come from this source.
# Also disable the NVIDIA L4T apt source; this image installs no l4t packages.
RUN for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do \
[ -f "$f" ] || continue; \
sed -i -E \
-e 's@https?://ports\.ubuntu\.com/ubuntu-ports@https://mirrors.aliyun.com/ubuntu-ports@g' \
-e 's@https?://(archive|security)\.ubuntu\.com/ubuntu@https://mirrors.aliyun.com/ubuntu@g' \
"$f"; \
done && \
if [ -f /etc/apt/sources.list.d/nvidia-l4t-apt-source.list ]; then \
sed -i -E 's@^[[:space:]]*deb@# deb@' /etc/apt/sources.list.d/nvidia-l4t-apt-source.list; \
fi && \
grep -rhE '^[[:space:]]*deb ' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null
# Install ROS 2 apt source and ros-humble-ros-core to pull in system-level
# dependencies (libspdlog1, libtinyxml2-9, python3-packaging, etc.).
# /opt/ros is overridden by a host bind-mount at runtime.
# The ROS apt source uses the Aliyun mirror, reachable from within China.
# The genuine ROS 2 GPG key is fetched by fingerprint F42ED6FBAB17C654 from a keyserver;
# do NOT use mirrors.aliyun.com/ros/ros.key (it lacks the ROS 2 key and causes NO_PUBKEY).
# Before using a keyserver, install dirmngr and create /root/.gnupg (absent in the base
# image), otherwise gpg fails with "No dirmngr".
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl gnupg2 dirmngr lsb-release ca-certificates && \
mkdir -p /root/.gnupg && chmod 700 /root/.gnupg && \
echo "standard-resolver" >> /root/.gnupg/dirmngr.conf && \
echo "disable-ipv6" >> /root/.gnupg/dirmngr.conf && \
gpg --no-default-keyring --keyring /usr/share/keyrings/ros-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 --recv-keys F42ED6FBAB17C654 && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] https://mirrors.aliyun.com/ros2/ubuntu $(lsb_release -cs) main" \
> /etc/apt/sources.list.d/ros2.list && \
apt-get update && \
apt-get install -y --no-install-recommends ros-humble-ros-core && \
apt-get install -y --no-install-recommends \
usbutils iproute2 tini python3-pip \
build-essential binutils git \
python3-opencv python3-numpy \
libjpeg-dev libpng-dev libtiff-dev \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \
libgtk-3-dev \
openssh-client \
libboost-python1.74.0 \
bluez \
cmake ninja-build \
python3-colcon-common-extensions \
libcurl4-openssl-dev \
libncurses-dev \
pkg-config && \
printf '%s\n' \
'[global]' \
'index-url = https://pypi.tuna.tsinghua.edu.cn/simple' \
'extra-index-url = https://mirrors.aliyun.com/pypi/simple/' \
' https://mirrors.cloud.tencent.com/pypi/simple/' \
'timeout = 60' \
'retries = 5' \
> /etc/pip.conf && \
pip3 install --no-cache-dir -U pip packaging && \
pip3 install --no-cache-dir "scikit-build-core<0.10" nanobind && \
pip3 install --no-cache-dir --no-build-isolation ruckig==0.15.3 && \
cd /opt && \
( timeout 900 git clone --depth 1 -b 4.8.0 https://gitee.com/mirrors/opencv.git opencv-4.8.0 || \
( echo "gitee clone failed, falling back to a GitHub mirror in China" && rm -rf /opt/opencv-4.8.0 && \
curl -fSL --retry 3 --retry-delay 5 -m 1800 -o /tmp/opencv-4.8.0.tar.gz \
https://gh-proxy.com/https://github.com/opencv/opencv/archive/refs/tags/4.8.0.tar.gz && \
tar -xzf /tmp/opencv-4.8.0.tar.gz -C /opt && rm -f /tmp/opencv-4.8.0.tar.gz ) ) && \
cd /opt/opencv-4.8.0 && mkdir -p build && cd build && \
cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr/local \
-DBUILD_PYTHON_SUPPORT=OFF \
-DBUILD_EXAMPLES=OFF -DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF \
-DWITH_GTK=ON \
-DBUILD_LIST=core,imgproc,imgcodecs,videoio,highgui,calib3d,features2d,photo,video,objdetect \
-DOPENCV_GENERATE_PKGCONFIG=ON .. && \
make -j$(nproc) && make install && \
rm -rf /opt/opencv-4.8.0 && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
# Auto-source the host-mounted ROS setup in interactive shells.
RUN echo 'if [ -f /opt/ros/humble/setup.bash ]; then source /opt/ros/humble/setup.bash; fi' \
>> /etc/bash.bashrc
# Make the dynamic linker aware of the host-mounted CUDA paths.
RUN echo "/usr/local/cuda/lib64" > /etc/ld.so.conf.d/cuda.conf && \
echo "/usr/local/cuda/targets/aarch64-linux/lib" >> /etc/ld.so.conf.d/cuda.conf
WORKDIR /home/agi
# tini handles PID 1 signal forwarding.
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["sleep", "infinity"]
EOF
run.sh
将以下内容保存为 ~/aimdk-image/run.sh,并赋予执行权限 chmod +x run.sh。可直接用下面的 cat 命令写入:
cat > ~/aimdk-image/run.sh <<'EOF'
#!/bin/bash
# Launch agi/aimdk-dev:dev with host ROS, CUDA, cuDNN/TRT and aima mounted in.
set -euo pipefail
IMAGE="${IMAGE:-agi/aimdk-dev:dev}"
NAME="${NAME:-aimdk-dev}"
# ---- helper: only add a -v mount if the host source exists ----
# Usage: m <src> <dst> [opts:ro|rw|rslave] (default ro)
MOUNTS=()
m() {
local src="$1" dst="$2" opts="${3:-ro}"
if [ -e "$src" ]; then
MOUNTS+=("-v" "$src:$dst:$opts")
else
echo " mount $src NOT present, skipping" >&2
fi
}
# Host GIDs to add to the container (groups owning device nodes).
HOST_GIDS=(
20 # dialout - /dev/ttyUSB*
29 # audio - /dev/snd
44 # video - /dev/nvmap, /dev/nvhost-* (required by CUDA)
106 # input - /dev/input/event*
109 # render - /dev/dri/renderD*
995 # debug - /dev/nvhost-dbg-*
1000 # run - /agibot/data/var/MapManagerModule
)
# Translate host GIDs to container-side GIDs via the rootless ID mapping.
declare -a GROUP_ADDS
MAP_RAW=$(podman info --format '{{.Host.IDMappings.GIDMap}}')
for HG in "${HOST_GIDS[@]}"; do
CG=$(echo "$MAP_RAW" | grep -oE '\{[0-9]+ '"$HG"' [0-9]+\}' | head -1 \
| grep -oE '^\{[0-9]+' | tr -d '{')
if [ -z "$CG" ]; then
echo "GID $HG not mapped in /etc/subgid, skipping" >&2
else
GROUP_ADDS+=("--group-add" "$CG")
echo " $HG (host) -> $CG (container)" >&2
fi
done
# Enumerate userspace CUDA shared libraries to bind-mount (cuDNN, TensorRT, cuBLAS, cuFFT, ...).
for f in /usr/lib/aarch64-linux-gnu/libcudnn*.so* \
/usr/lib/aarch64-linux-gnu/libnvinfer*.so* \
/usr/lib/aarch64-linux-gnu/libnvonnxparser*.so* \
/usr/lib/aarch64-linux-gnu/libnvparsers*.so* \
/usr/lib/aarch64-linux-gnu/libnvcaffe_parser*.so* \
/usr/lib/aarch64-linux-gnu/libcublas*.so* \
/usr/lib/aarch64-linux-gnu/libcufft*.so* \
/usr/lib/aarch64-linux-gnu/libcurand*.so* \
/usr/lib/aarch64-linux-gnu/libcusolver*.so* \
/usr/lib/aarch64-linux-gnu/libcusparse*.so* \
/usr/lib/aarch64-linux-gnu/libcupti*.so* \
/usr/lib/aarch64-linux-gnu/libnpp*.so* \
/usr/lib/aarch64-linux-gnu/libnvjpeg*.so* \
/usr/lib/aarch64-linux-gnu/libnvToolsExt*.so* \
/usr/lib/aarch64-linux-gnu/libnvrtc*.so* ; do
m "$f" "$f"
done
echo " CUDA libs to bind-mount: $(ls /usr/lib/aarch64-linux-gnu/libcuda*.so* 2>/dev/null | wc -l) files" >&2
# ncurses dev files (headers, link-time .so symlinks, pkg-config) for the keyboard example.
for f in /usr/include/curses.h /usr/include/ncurses*.h /usr/include/term.h \
/usr/include/termcap.h /usr/include/unctrl.h /usr/include/eti.h \
/usr/lib/aarch64-linux-gnu/libncurses*.so* /usr/lib/aarch64-linux-gnu/libtinfo*.so* \
/usr/lib/aarch64-linux-gnu/pkgconfig/ncurses*.pc /usr/lib/aarch64-linux-gnu/pkgconfig/tinfo*.pc; do
m "$f" "$f"
done
# Python include dirs (numpy, ...) that the host /opt/ros CMake configs reference by absolute path.
while read -r d; do
m "$d" "$d"
done < <(grep -rhoE '/[A-Za-z0-9_./+-]*(dist|site)-packages/[A-Za-z0-9_./+-]*include' \
--include='*.cmake' /opt/ros/humble/share 2>/dev/null | sort -u)
# Extract AGIBOT_* env vars from the firmware-managed bashrc into an env-file.
AGIBOT_BASHRC="/home/agi/.aima/env/bashrc"
ENV_FILE=$(mktemp /tmp/aimdk-env.XXXXXX)
trap 'rm -f "$ENV_FILE"' EXIT
if [ -r "$AGIBOT_BASHRC" ]; then
sed -nE "s/^export (AGIBOT_[A-Z_]+)='(.*)'\$/\1=\2/p" "$AGIBOT_BASHRC" > "$ENV_FILE"
echo " AGIBOT env vars: $(wc -l < "$ENV_FILE") loaded from $AGIBOT_BASHRC" >&2
else
echo " $AGIBOT_BASHRC not readable, skipping AGIBOT_* env" >&2
fi
# Remove any existing container with the same name.
if podman ps -a --format '{{.Names}}' | grep -qx "$NAME"; then
echo "stopping & removing old $NAME..."
podman rm -f "$NAME" >/dev/null
fi
# If the DDS config exported by the firmware exists, mount it at the container path
# referenced by FASTRTPS_DEFAULT_PROFILES_FILE (shared discovery with the robot).
m /home/agi/.aima/env/ros_dds_configuration.xml /etc/ros_dds_configuration.xml
# ---- single fixed mount: the SDK dir is always present and must be rw ----
m /home/agi/aimdk /home/agi/aimdk rw
echo "starting $NAME from $IMAGE..."
podman run -d --name "$NAME" \
--ipc=host --network=host --pid=host \
--device nvidia.com/gpu=all \
"${GROUP_ADDS[@]}" \
-e HOME=/home/agi \
-e FASTRTPS_DEFAULT_PROFILES_FILE=/etc/ros_dds_configuration.xml \
-e PATH=/opt/ros/humble/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
-e LD_LIBRARY_PATH=/usr/local/lib:/usr/local/cuda/lib64:/opt/ros/humble/lib:/host-libs/aarch64-linux-gnu \
--env-file "$ENV_FILE" \
-v /etc/machine-id:/etc/machine-id:ro \
-v /usr/lib/aarch64-linux-gnu:/host-libs/aarch64-linux-gnu:ro \
-v /opt/ros:/opt/ros:ro \
-v /usr/local/bin/aima:/usr/local/bin/aima:ro \
-v /dev:/dev:rslave \
-v /run/dbus:/run/dbus:rslave \
"${MOUNTS[@]}" \
"$IMAGE"
EOF
4.4.2 构建镜像
cd ~/aimdk-image
podman build -t agi/aimdk-dev:dev .
4.4.3 启动容器
cd ~/aimdk-image
./run.sh
脚本会自动完成:
清理同名旧容器
把宿主机
dialout、audio、video、input、render、debug等组转译到容器侧 GID 并--group-add加入容器挂载
/opt/ros、CUDA 工具链、cuDNN/TensorRT 等共享库挂载
/dev,支持访问 USB、声卡、输入设备、外接存储挂载 SDK 目录
/home/agi/aimdk到容器内同名路径(读写)若存在 DDS 配置
/home/agi/.aima/env/ros_dds_configuration.xml,挂到容器内/etc/ros_dds_configuration.xml(与机器人共享话题发现)其余宿主路径(CUDA 库、ncurses 开发文件等)存在才挂(
m()函数),缺失自动跳过,跨设备可移植启动一个长驻容器
aimdk-dev,使用--ipc=host --network=host --pid=host与宿主机共享命名空间
备注
默认镜像名 agi/aimdk-dev:dev、容器名 aimdk-dev,可通过环境变量 IMAGE / NAME 覆盖:
IMAGE=agi/aimdk-dev:dev NAME=my-dev ./run.sh
4.4.4 进入容器
podman exec -it aimdk-dev bash
# 容器交互 shell 已自动 source /opt/ros/humble/setup.bash
ros2 topic list # 应能看到机器人话题
4.4.5 构建并运行
容器内的 /home/agi/aimdk 与宿主机同名目录是同一份文件。在容器中按标准流程构建:
备注
若此前在宿主机上构建过,需先清理旧产物再构建,否则会报 gmake: /usr/local/bin/cmake: No such file or directory(旧产物中记录的 cmake 路径在容器内不存在):
rm -rf ~/aimdk/build ~/aimdk/install ~/aimdk/log
该目录宿主机与容器内是同一份文件,清理后宿主机侧亦需重新构建。
# 非上位机/外接算力包时此步可跳过
source /opt/ros/humble/setup.bash
# 进入 SDK 目录构建(上位机模式请将 SDK 放至 ~/aimdk)
cd ~/aimdk && colcon build
# 加载构建产物
source install/local_setup.bash
之后可前往 运行一个代码示例 继续。