제4부 빌드·패키징·배포
4.8 크로스 컴파일과 arm64
4.8.1 이 장에서 배우는 것 / 선수 지식
로봇의 온보드 컴퓨터는 대개 arm64다 — Jetson·Raspberry Pi·각종 임베디드 보드. 개발 머신은 대개 x86_64다. 이 불일치가 빌드 파이프라인의 난제가 된다.
보드에서 직접 빌드하면 되지 않느냐 — 되기는 한다. 다만 라즈베리파이에서 Nav2를 빌드하면 몇 시간이 걸리고, 메모리가 부족해 종료되고(4.5.5), SD 카드 수명을 단축한다. 그래서 대안을 찾게 된다.
이 장은 각 접근의 실제 비용을 정직하게 비교한다. 결론을 먼저 말하면 — ROS 2의 전통적 크로스 컴파일은 어렵고, 대부분의 팀에게 Docker 다중 아키텍처 빌드가 더 실용적이다.
배우는 것:
- ROS 2 크로스 컴파일이 어려운 구조적 이유 — 메시지 생성이 걸림돌인 이유.
- 네 가지 접근 — 타깃 네이티브·QEMU 에뮬레이션·크로스 툴체인·Docker buildx.
- Docker 다중 아키텍처 빌드 — 실제 절차.
- 빌드 시간 최적화 — ccache·캐시 마운트.
- 배포 전략 — 이미지·deb·워크스페이스 복사.
- 검증 — 크로스 빌드 결과를 어떻게 믿는가.
선수 지식: 4.5(colcon), 4.6(rosdep), 4.7(소스 빌드), Docker 기본.
정직: 크로스 컴파일 도구·절차는 빠르게 변하며, ROS 2 공식 크로스 컴파일 도구의 유지 상태는 시점에 따라 다르다
(확인 필요). 아래 Docker 기반 절차는 일반적으로 통용되는 방식이며 이 환경에서 실행 검증하지 않았다(목차 3). 빌드 시간·성능 수치는 인용하지 않는다(3.6.1의 방침).
검증 상태(추가): 130번째 회차(151차 갭 점검)까지 이 장은 실행 검증이 전혀 없었다. 130번째 회차가 ②·④(QEMU 에뮬레이션·Docker buildx — 이 장이 대부분의 팀에 권장하는 경로)를 실제 arm64 빌드로 끝까지 확인해 본문에 (실행 확인) 표기를 처음 추가했고, 그 과정에서 이 장의 "
--load다중 플랫폼 불가" 함정 서술에 반례를 하나 찾았다(→ 아래 정정). ③(크로스 툴체인)은 부분적으로만 진행했다 — 호스트 툴체인 설치는 쉬웠지만 arm64 sysroot 구성 단계에서 실제 장애물 두 가지에 부딪혔고, 이 장이 말하는 rosidl 코드 생성 문제 자체까지는 이번 회차에 도달하지 못했다. 4.8.4·4.8.7을 보라. 131번째 회차(152차 갭 점검)가 이 미완료를 이어받아 실제로 rosidl 코드 생성 크로스 컴파일을 끝까지 실행했고, 이 장의 "①이 핵심 문제다" 서술을 상당 부분 재구성했다(→ 아래 4.8.2·4.8.3 정정). 4.8.2·4.8.3을 보라.
4.8.2 ROS 2 크로스 컴파일이 어려운 이유
일반적인 C++ 프로젝트의 크로스 컴파일은 어렵지 않다. 크로스 툴체인을 지정하고 sysroot를 주면 된다. ROS 2가 어려운 이유는 별도로 있다.
| 걸림돌 | 설명 |
|---|---|
| ① 코드 생성 도구 | rosidl 생성기(1.3.2)가 빌드 중 실행된다. 타깃용으로 빌드된 도구는 호스트에서 실행할 수 없다 |
| ② Python 확장 | rclpy의 C 확장(2.8.2)이 타깃 Python 헤더·라이브러리를 요구 |
| ③ sysroot | 모든 의존 라이브러리의 타깃 아키텍처 버전이 필요 |
| ④ 빌드 시 실행 도구 | ament의 여러 도구가 빌드 과정에서 실행됨 |
①이 핵심 문제다. 일반적인 해법은 "호스트용 도구를 따로 빌드해 두고 크로스 빌드에서 그것을 쓰게 하는" 것인데, 이 구성이 복잡하고 깨지기 쉽다.
실행 확인(152차, Jazzy) — "①이 핵심 문제다"는 이 절이 말하는 형태로는 재현되지 않았다. rosidl 코드 생성 자체는 크로스 컴파일에서 별도 조치 없이 그대로 작동했다(→ 정정). 4.8.3 ③의 "부분 검증"(151차)을 이어받아, 이번에는 sysroot를 끝까지 조립하고 실제로
colcon build --cmake-args -DCMAKE_TOOLCHAIN_FILE=...을 커스텀 메시지 패키지(.msg정의 포함)에 실행했다. 결과: 첫 시도에서 그대로 성공했다(Finished my_msgs [3.61s], 별도의 "호스트용 도구 따로 빌드" 작업 없음). 원인을 소스에서 확인하니 —rosidl_generator_c등 코드 생성기의 실제 진입점이 컴파일된 실행 파일이 아니라 파이썬 스크립트였다(file확인: "Python script, ASCII text executable"). 이 절의 예시 토대인CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER(프로그램은 sysroot가 아니라 호스트 경로에서 찾음)가 CMake로 하여금 호스트에 이미 설치된python3와, 그python3가 실행하는 (역시 호스트에 이미 있는) rosidl 생성기 스크립트를 그대로 쓰게 만들었다 — 로그에Found rosidl_default_generators: ... /opt/sysroot-arm64/...(패키지 설정은 sysroot에서)와Found Python3: /usr/bin/python3(실행기는 호스트에서)가 나란히 출력됐다. "코드 생성 도구를 호스트용으로 따로 빌드해야 한다"는 이 절의 서술은 Jazzy의 rosidl 생성기 구현(파이썬 기반)에는 해당하지 않는다 — 개발자가 평소 쓰는 호스트 ROS 2 설치가 이미 "호스트용 도구"였다. 실제로 만든 마찰은 ③(sysroot 준비)이었다 — 아래 4.8.3에서 자세히 다룬다. 생성된 라이브러리(libmy_msgs__rosidl_typesupport_cpp.so등 8종)는 ELF 헤더로 실제 arm64(EM_AARCH64)임을 확인했다. 원시 로그는tools/gap_harness/gap152/logs/cross_build_full_detail.log. 목차 3.
견해: 이 복잡도 때문에 "실제 크로스 컴파일"은 대부분의 팀에게 비용이 이득을 넘는다. 아래 네 접근 중 ④(Docker buildx)가 실무에서 가장 널리 쓰이는 이유가 이것이다 — 크로스 컴파일 문제를 에뮬레이션으로 우회한다.
견해에 대한 실행 확인(152차) — 이 견해의 결론(대부분의 팀에게 ④가 정답)은 152차 실측과도 여전히 부합한다. ③이 "핵심 문제(코드 생성)"에서는 뜻밖에 쉬웠지만, sysroot 준비 자체의 마찰(4.8.3을 보라)은 여전히 ④보다 훨씬 컸다 — 이번 회차에 289개 패키지를 개별 내려받아 수동으로 조립해야 했다. "왜 ③이 어려운가"의 정확한 지점이 이 절의 설명과는 다르게 밝혀졌을 뿐, ③ 자체가 ④보다 복잡하다는 결론은 바뀌지 않는다.
4.8.3 네 가지 접근
① 타깃에서 네이티브 빌드
# 보드에 SSH로 들어가서
colcon build --parallel-workers 1 --executor sequential \
--cmake-args -DCMAKE_BUILD_TYPE=Release
| 축 | 평가 |
|---|---|
| 정확성 | ✅ 가장 확실 — 실제 환경 그대로 |
| 설정 복잡도 | ✅ 없음 |
| 속도 | ❌ 매우 느림 |
| 메모리 | ❌ 부족하기 쉬움(4.5.5) |
| 저장장치 수명 | ❌ SD 카드 마모 |
| CI 적합성 | ❌ 어려움 |
완화: 스왑 추가, --parallel-workers 1, SSD·NVMe 사용, ccache.
# 스왑 임시 추가 (메모리 부족 대응)
sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
② QEMU 에뮬레이션
x86_64 머신에서 arm64 바이너리를 에뮬레이션해 실행한다. 크로스 컴파일이 아니라 "느린 네이티브 빌드" 다.
# binfmt 등록 (호스트에서 arm64 바이너리 실행 가능하게)
docker run --privileged --rm tonistiigi/binfmt --install all
| 축 | 평가 |
|---|---|
| 정확성 | ✅ 네이티브와 동일한 빌드 절차 |
| 설정 복잡도 | ✅ 낮음 |
| 속도 | ⚠️ 느리지만 강력한 x86 머신이면 보드보다 빠를 수 있다 |
| CI 적합성 | ✅ 좋음 |
③ 크로스 툴체인
실제 크로스 컴파일. CMake 툴체인 파일과 sysroot를 준비한다.
# toolchain-arm64.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_SYSROOT /opt/sysroot-arm64)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
colcon build --cmake-args -DCMAKE_TOOLCHAIN_FILE=$PWD/toolchain-arm64.cmake
| 축 | 평가 |
|---|---|
| 정확성 | ⚠️ 미묘한 차이 가능 |
| 설정 복잡도 | ❌ 매우 높음(4.8.2의 걸림돌들) |
| 속도 | ✅ 가장 빠름 |
| 유지보수 | ❌ 깨지기 쉬움 |
실행 확인(151차, 부분적) — "설정 복잡도가 매우 높다"를 실제로 시도해 확인했지만, 4.8.2가 지목한 ①(rosidl 코드 생성) 문제 자체까지는 이번 회차에 도달하지 못했다(정직하게 명시). 호스트 툴체인 설치(
crossbuild-essential-arm64)는 어렵지 않았다(즉시 성공). 그러나 sysroot를 준비하는 단계에서 이 절이 언급하지 않는 실제 장애물 두 가지에 부딪혔다. ①archive.ubuntu.com에는 arm64 패키지가 없다 — Ubuntu는 amd64·i386만archive.ubuntu.com에 두고, arm64를 포함한 나머지 아키텍처는ports.ubuntu.com에 별도로 둔다.dpkg --add-architecture arm64후 그대로apt-get update를 실행하면noble/restricted arm64 Packages 404 Not Found류 오류가 난다 —ports.ubuntu.com을 가리키는 별도 소스 항목을Architectures: arm64로 명시해야 한다(4.8.4의 Docker 이미지는 arm64 베이스 이미지 자체가 이미 이렇게 구성돼 있어 이 문제를 안 겪는다 — 이 절 ③처럼 호스트에서 직접 sysroot를 만들 때만 나온다). ② 여러 ROS 2 Python 보조 패키지가arm64로 설치 불가능하다고 나온다 —apt-get install --download-only ros-jazzy-rosidl-default-generators:arm64등을 시도하면python3-catkin-pkg-modules:arm64 but it is not installable류 오류가 여럿 난다. 원인을dpkg-query로 추적하니python3-catkin-pkg-modules가Architecture: all인데Multi-Arch: no였다(foreign이어야 다른 아키텍처의 의존을 만족시킬 수 있다) — rosidl 코드 생성 도구 체인을 구성하는 여러 Python 패키지의 멀티아크 메타데이터가 크로스 아키텍처 의존 해석과 맞지 않는다. 이 두 장애물 자체가 "설정 지옥"이라는 이 절의 평가를 뒷받침하지만, 이것을 우회해 실제로colcon build --cmake-args -DCMAKE_TOOLCHAIN_FILE=...을 실행하고 4.8.2의 ①(rosidl 생성기가 호스트에서 실행되는 문제)이 정확히 어떻게 나타나는지까지는 이번 회차 예산 안에서 도달하지 못했다(확인 필요, 다음 회차 후보). 원시 로그는tools/gap_harness/gap151/logs/cross_toolchain_sysroot_apt_failure.log. 목차 3.
실행 확인(152차) — 151차가 남긴
(확인 필요)를 이어받아 sysroot를 끝까지 조립하고, 실제 rosidl 코드 생성 크로스 컴파일까지 도달했다(→ 4.8.2 정정과 연결). ②(Multi-Arch: no문제)의 우회법은 단순했다 —apt-get install(설치 가능 여부를 전부 검증) 대신apt-get download(이름으로 단순히 받아오기, 조인트 설치 가능성을 안 따짐)를 쓰면 된다.apt-cache depends --recurse로 추출한 필요 패키지 289개를 전부apt-get download <pkg>:arm64(실패 시 아키텍처 접미사 없이 재시도 —Architecture: all패키지는 접미사를 안 받는다) 방식으로 시도하니 289개 전부 성공했다(실패 0건) —Multi-Arch: no는 apt의 설치 시점 의존성 그래프 검증에서만 걸리는 문제였지, 개별 패키지 파일 자체를 받는 데는 아무 지장이 없었다.dpkg-deb -x로 전부/opt/sysroot-arm64에 풀고(503MB), 이 절의 예시 그대로(CMAKE_SYSROOT·CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER등) 툴체인 파일을 써서.msg정의가 있는 커스텀 메시지 패키지를colcon build했다. 결과는 4.8.2에 적었듯 첫 시도에 성공 — sysroot만 다 갖추면 그 다음은 뜻밖에 순조로웠다. 이번 회차에 실제로 겪은 "설정 지옥"은 ①·②(sysroot 조립 자체)뿐이었고, 코드 생성 실행 문제는 발생하지 않았다. 원시 데이터는tools/gap_harness/gap152/data/sysroot_packages_downloaded.txt(289개 목록)·toolchain-arm64.cmake. 목차 3.
④ Docker 다중 아키텍처(buildx)
②를 컨테이너로 포장한 것이며, 결과가 배포 가능한 이미지라는 점이 다르다.
| 축 | 평가 |
|---|---|
| 정확성 | ✅ 좋음 |
| 설정 복잡도 | ✅ 낮음 |
| 속도 | ⚠️ 에뮬레이션 속도 |
| 배포 통합 | ✅ 이미지가 곧 배포물 |
| CI 적합성 | ✅ 가장 좋음 |
견해: 대부분의 팀에게 ④가 정답이다. 설정이 단순하고, CI에 자연스럽게 들어가며, 배포 아티팩트가 바로 나온다. 속도가 정말 문제일 때만 ③을 검토하되, 그 전에 ccache·캐시 마운트(4.8.5)로 얼마나 개선되는지 먼저 보라. ①은 "빠르게 한 번 시험"할 때, ②는 이미지가 필요 없을 때 쓴다.
4.8.4 Docker 다중 아키텍처 빌드
준비:
# QEMU binfmt 등록 (호스트 1회)
docker run --privileged --rm tonistiigi/binfmt --install arm64
# buildx 빌더 생성
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap
Dockerfile:
FROM ros:jazzy-ros-base AS builder
ARG DEBIAN_FRONTEND=noninteractive
WORKDIR /ws
# ① 의존 해석용으로 package.xml만 먼저 (4.6.6)
COPY src/ /tmp/src/
RUN mkdir -p src && \
find /tmp/src -name package.xml -exec bash -c \
'd=src/$(basename $(dirname {})); mkdir -p $d; cp {} $d/' \; && \
apt-get update && \
rosdep install --from-paths src -y --ignore-src && \
rm -rf /var/lib/apt/lists/*
# ② 소스 복사 후 빌드
COPY src/ /ws/src/
RUN . /opt/ros/jazzy/setup.sh && \
colcon build --merge-install \
--cmake-args -DCMAKE_BUILD_TYPE=Release \
--parallel-workers 2
# ③ 런타임 이미지 (빌드 산출물만)
FROM ros:jazzy-ros-core AS runtime
COPY --from=builder /ws/install /ws/install
COPY --from=builder /opt/ros /opt/ros
WORKDIR /ws
ENTRYPOINT ["/ros_entrypoint.sh"]
CMD ["ros2", "launch", "my_robot", "bringup.launch.py"]
빌드:
# arm64용
docker buildx build --platform linux/arm64 \
-t registry.example.com/my_robot:1.2.0-arm64 --push .
# 다중 아키텍처 매니페스트 (하나의 태그로 둘 다)
docker buildx build --platform linux/amd64,linux/arm64 \
-t registry.example.com/my_robot:1.2.0 --push .
멀티스테이지가 중요하다. 빌드 도구·소스·중간 산출물이 최종 이미지에 남지 않아 이미지가 작아지고 공격면이 줄어든다(13.4).
함정:
--push없이 다중 플랫폼 빌드를 하면 로컬에 저장할 수 없다(확인 필요: buildx 버전에 따라--load는 단일 플랫폼만 지원). 레지스트리가 필요하다. 로컬 시험은 단일 플랫폼으로:
docker buildx build --platform linux/arm64 --load -t my_robot:test .
docker run --rm --platform linux/arm64 my_robot:test ros2 pkg list | head
실행 확인(151차, Jazzy) — 이 절의 준비·빌드 절차 전체를 실제 arm64로 끝까지 실행했다.
docker run --privileged --rm tonistiigi/binfmt --install arm64가 이 환경(중첩 Docker, 샌드박스)에서도 그대로 성공했고(installing: arm64 OK),docker buildx create --use+--bootstrap도 정상 동작했다. 이 절의 Dockerfile 구조 그대로(패키지.xml만 먼저 복사해rosdep install→ 소스 복사 →colcon build→ 멀티스테이지)rclcpp·std_msgs에 의존하는 최소 노드 패키지를docker buildx build --platform linux/arm64 --load로 빌드하니rosdep install이ports.ubuntu.com에서 실제 arm64 패키지 인덱스를 받아왔고,colcon build가 QEMU 에뮬레이션 위에서 실제로 컴파일을 수행해(58.7초, 스킵되지 않음) 성공했다. 결과 바이너리를file이 없는 최소 이미지에서 ELF 헤더를 직접 읽어(오프셋 18~19바이트e_machine) 확인하니b7 00=EM_AARCH64로 실제 arm64 바이너리였다.docker run --platform linux/arm64로 직접 실행하니rclcpp노드가 실제로 기동해 로그를 냈다(ros2 run으로 감쌌을 때는 에뮬레이션 오버헤드로 짧은 타임아웃 안에 출력이 안 나온 적이 있었다 — 바이너리를 직접 실행하면 재현됨, 아래에서 정리).다만 이 함정 서술 자체는 정정이 필요하다(→ 이번 회차 반례로 정정) — "
--load는 단일 플랫폼만 지원"은 이 환경에서 재현되지 않았다. 같은 Dockerfile을--platform linux/amd64,linux/arm64(양쪽 다) +--load로 빌드하니 실패하지 않고 정상 종료했으며, 로컬에 실제 다중 아키텍처 이미지가 저장됐다 — 같은 이미지 태그를--platform linux/arm64로 실행하면 ELF가EM_AARCH64,--platform linux/amd64로 실행하면EM_X86_64로, 하나의 태그가 두 아키텍처를 정확히 구분해 서빙했다. 원인은 Docker 서버(버전 29.5.2)가 containerd 이미지 스토어(driver-type: io.containerd.snapshotter.v1)를 쓰고 있었기 때문으로 보인다 — 이 스토어가 활성화되면 다중 플랫폼 이미지를 로컬 인덱스로 저장할 수 있다. book의 원래 서술이 틀린 것이 아니라 버전·설정에 따라 갈린다 — classic(비-containerd) 이미지 스토어에서는 여전히 책이 설명한 대로 실패할 수 있다(확인 필요: classic 스토어에서의 재현). 원시 로그는tools/gap_harness/gap151/logs/buildx_arm64_build.log·buildx_multiload.log, ELF 확인 원문은tools/gap_harness/gap151/data/elf_arch_verification.txt. 목차 3.
4.8.5 빌드 시간 최적화
에뮬레이션 빌드는 느리므로 캐시가 중요하다.
① ccache
RUN apt-get update && apt-get install -y ccache
ENV PATH="/usr/lib/ccache:${PATH}"
ENV CCACHE_DIR=/ccache
RUN --mount=type=cache,target=/ccache \
. /opt/ros/jazzy/setup.sh && \
colcon build --merge-install --mixin release
# BuildKit 필요
DOCKER_BUILDKIT=1 docker buildx build ...
② 레이어 캐시 활용 — package.xml만 먼저 복사(4.6.6). 소스 변경 시 의존 설치를 다시 하지 않는다.
③ apt 캐시 마운트
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt/lists \
apt-get update && rosdep install --from-paths src -y --ignore-src
④ 베이스 이미지에 의존 미리 설치
의존이 자주 바뀌지 않는다면, 의존이 설치된 중간 이미지를 따로 만들어 두고 CI는 그것을 쓴다.
# my_robot_deps:jazzy — 주기적으로만 재빌드
FROM ros:jazzy-ros-base
COPY deps.repos /tmp/
RUN ... rosdep install ...
# 실제 빌드 — 매 커밋
FROM registry.example.com/my_robot_deps:jazzy
COPY src/ /ws/src/
RUN colcon build ...
견해: ④가 CI 빌드 시간에 가장 큰 영향을 준다. 의존 설치는 대개 빌드의 상당 부분을 차지하는데, 매 커밋마다 반복할 이유가 없다. 4.6.6에서 "CI는 의존 설치 이미지를 쓰라"고 한 것과 같은 논리다.
⑤ 병렬도 조정 — 에뮬레이션에서는 병렬도가 높아도 이득이 적을 수 있다. 실측해서 정한다(4.5.5).
4.8.6 배포 전략
빌드 산출물을 로봇에 배포하는 방법 세 가지다.
① 컨테이너 이미지
# 로봇에서
docker pull registry.example.com/my_robot:1.2.0
docker run --network=host --restart=always ... my_robot:1.2.0
| 축 | 평가 |
|---|---|
| 재현성 | ✅ 최고 |
| 롤백 | ✅ 태그 전환 |
| 네트워크 설정 | ⚠️ host 모드 필요(3.9.5) |
| 하드웨어 접근 | ⚠️ 디바이스 매핑 필요 |
| 이미지 크기 | ⚠️ 대역·저장공간 |
② deb 패키지
# 빌드 머신에서 deb 생성 (4.9)
# 로봇에서
sudo apt install ./my-robot-package_1.2.0_arm64.deb
| 축 | 평가 |
|---|---|
| 통합 | ✅ OS 패키지 관리와 일관 |
| 크기 | ✅ 작음(증분 업데이트) |
| 구축 비용 | ❌ 사내 apt 저장소·bloom 설정 필요 |
③ 워크스페이스 복사
rsync -avz --delete install/ robot:/opt/my_robot/install/
| 축 | 평가 |
|---|---|
| 단순함 | ✅ 가장 쉬움 |
| 의존 | ❌ 시스템 의존은 별도 관리 |
| 재현성 | ❌ 낮음 |
| 롤백 | ❌ 수동 |
견해: 개발 중에는 ③, 운영에서는 ① 또는 ② 가 흔한 조합이다. ③은 빠른 반복에 좋지만 "로봇에 무엇이 설치돼 있는지" 추적이 안 되어 운영에는 부적합하다. ③으로 배포한 로봇이 여러 대가 되면 반드시 문제가 생긴다 — 어느 로봇이 어느 버전인지 알 수 없게 된다(14.9).
함정: ③을 쓸 때
--symlink-install로 빌드한 워크스페이스를 복사하면 깨진다(4.3.5). 배포용은 심링크 없이 빌드해야 한다.
4.8.7 검증
크로스 빌드 결과를 어떻게 믿는가.
# ① 아키텍처 확인
file install/my_pkg/lib/my_pkg/my_node
# ELF 64-bit LSB pie executable, ARM aarch64, ...
# ② 동적 링크 확인 (타깃에서)
ldd /ws/install/my_pkg/lib/my_pkg/my_node | grep 'not found'
# ③ 실제 실행
docker run --rm --platform linux/arm64 my_robot:test \
bash -c 'source /ws/install/setup.bash && ros2 pkg list | wc -l'
# ④ 테스트 실행 (에뮬레이션에서)
docker run --rm --platform linux/arm64 my_robot:test \
bash -c 'source /opt/ros/jazzy/setup.bash && cd /ws && colcon test && colcon test-result --verbose'
④가 중요하다. 에뮬레이션 환경에서 테스트를 실행하면 아키텍처 의존 버그(엔디언·정렬·부동소수점 차이)를 검출할 수 있다.
실행 확인(151차) — ①·③은 실제로 확인했고, ②·④는 다루지 못했다.
file명령이 없는 최소 이미지에서는 ELF 헤더를 직접 읽어(오프셋 18~19바이트) 같은 결론에 도달했다 — arm64 빌드는b7 00(EM_AARCH64), amd64는3e 00(EM_X86_64).docker run --platform linux/arm64로rclcpp노드를 실제 기동해 로그까지 확인했다(③).ldd로 링크 누락 확인(②)과colcon test(④)는 이번 회차 범위 밖이었다(확인 필요, 다음 회차 후보). 자세한 수치는 4.8.4를 보라. 목차 3.
아키텍처 차이가 실제로 문제가 되는 경우:
| 문제 | 설명 |
|---|---|
char의 부호 | x86에서는 signed, ARM에서는 unsigned가 기본 |
| 구조체 정렬 | 패딩 차이(1.3.4와 연결) |
| 부동소수점 | 미세한 반올림 차이 |
| SIMD 명령 | SSE ↔ NEON 이식 |
| 원자 연산 | 메모리 모델 차이(2.9.4) |
함정:
char의 부호 차이가 조용한 버그를 만든다. x86에서 개발한 코드가 ARM에서 다르게 동작하는 전형적 원인이며, 컴파일 경고도 안 난다. 명시적으로int8_t·uint8_t를 쓰는 습관이 방어가 된다.
# 부호를 명시적으로 고정 (이식성)
add_compile_options(-fsigned-char)
에뮬레이션이 검출하지 못하는 것:
| 항목 | 이유 |
|---|---|
| 실시간 성능 | 에뮬레이션은 속도가 완전히 다르다 |
| 하드웨어 접근 | GPIO·CAN·카메라 |
| GPU·가속기 | CUDA·NPU |
| 열·전력 특성 | 물리 환경 |
따라서 실기 시험은 여전히 필요하다. 크로스 빌드는 "빌드가 되는가"를 빠르게 확인하는 수단이지 실기 검증의 대체가 아니다.
4.8.8 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | 보드에서 전체 빌드 | OOM·수 시간 | 스왑·병렬도 제한·Docker 빌드 |
| 2 | binfmt 미등록 | exec format error | tonistiigi/binfmt --install |
| 3 | 다중 플랫폼 + --load | 저장 불가 | 레지스트리 push 또는 단일 플랫폼 |
| 4 | 멀티스테이지 미사용 | 이미지 비대·공격면 | builder/runtime 분리 |
| 5 | 캐시 미활용 | CI 빌드 시간 폭증 | ccache·레이어·의존 이미지 |
| 6 | --symlink-install로 빌드 후 복사 배포 | 대상에서 깨짐 | 심링크 없이 빌드 |
| 7 | char 부호 가정 | 조용한 동작 차이 | int8_t·-fsigned-char |
| 8 | 에뮬레이션 성능으로 실기 판단 | 잘못된 결론 | 실기 측정 필수 |
| 9 | rsync 배포로 운영 | 버전 추적 불가 | 이미지·deb |
| 10 | 크로스 툴체인을 성급히 도입 | 설정 지옥 | Docker 방식 먼저 |
| 11 | 하드웨어 의존 테스트를 에뮬레이션에서 | 통과했는데 실기 실패 | HIL 시험(14.6) |
| 12 | 타깃 아키텍처 확인 생략 | 잘못된 바이너리 배포 | file 명령 |
4.8.9 체크포인트
- ROS 2 크로스 컴파일이 일반 C++ 프로젝트보다 어려운 구조적 이유 네 가지를 말할 수 있는가. 핵심 걸림돌은?
- 네 가지 접근의 정확성·복잡도·속도·CI 적합성을 비교할 수 있는가. 대부분의 팀에게 권장되는 것은 무엇이며 왜인가.
- QEMU 에뮬레이션이 "크로스 컴파일"이 아니라 무엇인지 정확히 말할 수 있는가.
- Docker 다중 아키텍처 빌드의 준비 단계와, 멀티스테이지가 주는 이점 두 가지는?
- 빌드 시간을 줄이는 다섯 가지 방법 중 CI에서 가장 효과가 큰 것은 무엇이며 왜인가.
- 배포 전략 세 가지의 장단을 비교하고, rsync 방식이 운영에 부적합한 이유를 말할 수 있는가.
- 크로스 빌드 결과를 검증하는 네 단계를 말할 수 있는가.
- 아키텍처 차이로 생기는 문제 다섯 가지 중, 컴파일 경고 없이 조용히 발생하는 것은 무엇인가.
- 에뮬레이션이 검출하지 못하는 것 네 가지를 들 수 있는가. 그래서 크로스 빌드의 역할을 어떻게 규정해야 하는가.
다음 장 4.9는 빌드한 결과를 공식적으로 릴리스하는 절차다 — bloom·rosdistro·빌드팜을 거쳐 apt install ros-jazzy-*가 되기까지, 그리고 사내 빌드팜을 세우는 선택지.