AEGLOSLAB ROS 2 Jazzy 대전

제4부 빌드·패키징·배포

4.1 ament 빌드 시스템의 구조

4.1.1 이 장에서 배우는 것 / 선수 지식

여기까지 ROS 2가 어떻게 동작하는지를 봤다. 제4부는 어떻게 만들어지는지를 다룬다. 소스 파일이 패키지가 되고, 패키지가 워크스페이스가 되고, 워크스페이스가 로봇 위의 바이너리가 되기까지.

첫 장의 주제는 ament다. 그런데 ament는 흔히 오해된다 — "ROS 2의 빌드 시스템"이라고 부르지만, 정확히는 빌드 시스템이 아니다. 빌드는 CMake와 setuptools가 하고, ament는 그 위에 패키지를 발견하고 연결하는 규약을 추가한다. 이 구분을 이해하면 "왜 빌드는 됐는데 런타임에 못 찾지" 같은 문제가 즉시 설명된다.

배우는 것:

  • ament의 정체 — 무엇을 하고 무엇을 하지 않는가.
  • ament 인덱스 — ROS 2가 패키지를 찾는 실제 메커니즘.
  • 설치 레이아웃 — lib·share·include에 무엇이 왜 들어가는가.
  • 환경 훅 — setup.bash가 자동 생성되는 원리.
  • 빌드 타입 — ament_cmake·ament_python·혼합.
  • merge vs isolated 설치 — 두 레이아웃의 차이와 함정.
  • catkin과의 관계 — ROS 1 경험자가 알아야 할 변화.

선수 지식: 0.5(워크스페이스·오버레이), 0.6(첫 패키지). colcon은 4.5, package.xml 상세는 4.4에서 다룬다.

정직: ament의 CMake 매크로·인덱스 구조는 ament/ament_cmake·ament/ament_package 저장소에 근거한다. 매크로 이름·권장 사용법은 배포판마다 변해 왔으며(특히 ament_target_dependencies의 위치 — 4.2), 아래 서술은 Jazzy 기준으로 작성했으나 이 환경에서 빌드 검증하지 않았다(목차 3). 145차(124번째 회차)가 이 장의 핵심 주장들을 Humble·Jazzy 양쪽에서 실행 확인했다 — DESTINATION bin 함정은 정확히 맞았고, ament_package() 위치 함정은 서술이 부정확하다는 것도 함께 확인됐다. 4.1.4·4.1.10을 보라.

4.1.2 ament는 무엇이고 무엇이 아닌가

빌드는 성공 소스 패키지 colcon 순서 결정 · 병렬 실행 CMake · setuptools 컴파일 · 설치 ament 인덱스 · 의존 전파 환경 훅 · 설치 규약 런타임 발견 ros2 run · launch 그래도 실행 실패 가능 발견은 ament의 몫
층하는 일하지 않는 일
colcon패키지 순서 결정, 병렬 실행, 환경 구성컴파일
CMake·setuptools실제 컴파일·설치패키지 발견
ament인덱스 등록·의존 전파·환경 훅·설치 규약컴파일, 워크스페이스 관리

ament가 실제로 제공하는 것 넷:

  1. 패키지 인덱스 — "이 접두어에 어떤 ROS 패키지가 설치되어 있는가"를 빠르게 알 수 있는 색인(4.1.3).
  2. 의존 전파 규약 — 패키지 A가 B를 쓸 때 헤더 경로·라이브러리·컴파일 옵션이 자동으로 따라오게 하는 CMake 규약(4.2).
  3. 환경 훅 — 설치된 패키지가 필요로 하는 환경변수(PYTHONPATH·LD_LIBRARY_PATH 등)를 선언하고, 그것을 모아 setup.bash를 생성하는 체계.
  4. 설치 레이아웃 규약 — 무엇을 어디에 설치할지의 합의(4.1.4).

이 넷은 전부 "빌드 이후"의 문제다. 그래서 빌드가 성공했는데 런타임에 실패하는 상황이 ROS 2에서 흔하다 — 컴파일은 CMake가 했고, 발견은 ament가 하는데, 후자만 잘못될 수 있기 때문이다.

증상대개 어느 층
컴파일 오류·링크 오류CMake·컴파일러
빌드는 되는데 ros2 run이 실행 파일을 못 찾음ament 설치 규약
패키지가 ros2 pkg list에 안 나옴ament 인덱스
launch 파일·설정 파일을 런타임에 못 찾음설치 누락(4.1.4)
다른 패키지의 헤더를 못 찾음의존 전파(4.2)

4.1.3 ament 인덱스 — 패키지를 찾는 메커니즘

ros2 pkg list는 어떻게 패키지를 아는가? 디렉터리를 뒤지는 것이 아니라 색인 파일을 읽는다.

인덱스는 <install prefix>/share/ament_index/resource_index/ 아래에 자원 종류별 디렉터리를 두고, 그 안에 패키지 이름을 파일명으로 둔다.

경로파일 내용
resource_index/packages/my_robot_driver빈 파일 — 존재 자체가 등록
resource_index/packages/my_robot_msgs빈 파일
resource_index/rclcpp_components/my_pkg등록된 컴포넌트 목록(1.13)
resource_index/rosidl_interfaces/my_robot_msgs제공하는 인터페이스 목록(1.2)
자원 종류용도
packages패키지 존재 등록. ros2 pkg list의 출처
rosidl_interfaces이 패키지가 제공하는 msg·srv·action(1.2.10)
rclcpp_components등록된 컴포넌트(1.13.3)
ros2cli.command·ros2cli.verb.*CLI 확장(0.7.2)
pluginlib 관련플러그인 등록(4.2)

빈 파일의 존재가 등록이다 — 내용을 파싱할 필요 없이 디렉터리 목록만 읽으면 되므로 매우 빠르다. 이것이 ROS 1의 패키지 탐색(디렉터리 재귀 탐색)보다 개선된 점이다.

탐색 경로는 AMENT_PREFIX_PATH가 정한다(0.5).

echo $AMENT_PREFIX_PATH
# /home/user/ws/install/my_pkg:/opt/ros/jazzy

# 특정 패키지의 설치 위치
ros2 pkg prefix my_robot_driver

# 인덱스 직접 확인
ls /opt/ros/jazzy/share/ament_index/resource_index/packages | head

함정: 패키지가 목록에 없다면 인덱스에 등록이 안 된 것이다. 대개 원인은 셋 — ① 워크스페이스를 소싱하지 않음(0.5), ② ament_package() 호출 누락, ③ 빌드가 실제로는 실패했는데 알아채지 못함. 이 순서로 확인한다.

# ② 확인: install 디렉터리에 등록됐는가
find install -path '*resource_index/packages/*' | head

4.1.4 설치 레이아웃

ament는 FHS(파일 시스템 계층 표준)에 가까운 레이아웃을 규정한다.

경로내용누가 쓰나
lib/공유 라이브러리(.so)링커·dlopen
lib/<pkg>/실행 파일ros2 run
include/<pkg>/헤더의존 패키지 컴파일
share/<pkg>/launch·config·URDF·메시지 정의 등런타임 리소스
share/<pkg>/cmake/CMake config 파일find_package
share/ament_index/인덱스(4.1.3)ROS 도구
lib/python3.X/site-packages/Python 모듈Python 임포트

lib/<pkg>/에 실행 파일을 두는 것이 ROS 2의 규약이다. ros2 run my_pkg my_node는 lib/my_pkg/my_node를 찾는다.

install(TARGETS my_node
  DESTINATION lib/${PROJECT_NAME})      # ← 이 경로여야 ros2 run이 찾는다

함정: DESTINATION bin에 설치하면 ros2 run이 못 찾는다. 컴파일도 되고 파일도 있는데 실행이 안 되는 전형적 사례다. 직접 경로로 실행하면 되므로 더 혼란스럽다.

실행 확인(145차, Jazzy). install(TARGETS mynode DESTINATION bin)으로 빌드한 뒤 정확히 이 함정 그대로 재현됐다 — ros2 run cpptest_pkg mynode는 No executable found로 실패했고 ros2 pkg executables cpptest_pkg도 빈 목록을 냈지만, install/cpptest_pkg/bin/mynode를 직접 실행하니 정상 동작했다(MYNODE_RAN). 목차 3.

런타임 리소스는 반드시 명시적으로 설치해야 한다.

install(DIRECTORY launch config urdf rviz
  DESTINATION share/${PROJECT_NAME})
# 코드에서 찾기
from ament_index_python.packages import get_package_share_directory
config = os.path.join(get_package_share_directory('my_pkg'), 'config', 'params.yaml')
#include "ament_index_cpp/get_package_share_directory.hpp"
auto share = ament_index_cpp::get_package_share_directory("my_pkg");

함정: "소스에 있으니 되겠지"가 안 통한다. 소스 디렉터리에서 실행할 때는 우연히 동작할 수 있지만, 설치본에서는 파일이 없다. 설치 규칙을 안 쓴 launch·config 파일은 배포 시점에 사라진다 — 개발 중에는 드러나지 않다가 로봇에 배포하고 발견하는 대표적 문제다.

검증 습관:

# 설치본에 실제로 무엇이 들어갔는가
find install/my_pkg -type f | sort
# 소스에서 나와 설치본만으로 실행해 본다
cd /tmp && source ~/ws/install/setup.bash && ros2 launch my_pkg bringup.launch.py

4.1.5 환경 훅과 setup.bash 생성

source install/setup.bash 한 줄이 무엇을 하는가.

ament_package() 패키지별 환경 훅 생성 share/pkg/hook/*.sh 패키지별 setup 스크립트 share/pkg/package.sh 워크스페이스 setup.bash 모든 패키지 훅을 순회 AMENT_PREFIX_PATH CMAKE_PREFIX_PATH PATH · LD_LIBRARY_PATH PYTHONPATH …

패키지가 자기 훅을 선언할 수 있다.

# 예: 커스텀 환경변수를 설치 시 등록
ament_environment_hooks("${CMAKE_CURRENT_SOURCE_DIR}/env-hooks/my_pkg.sh.in")

이것이 예를 들어 GAZEBO_MODEL_PATH(10.2)나 플러그인 경로를 자동으로 설정하는 방식이다.

setup.bash와 local_setup.bash의 차이가 중요하다:

스크립트동작
setup.bash상위 워크스페이스(언더레이)를 먼저 소싱한 뒤 이 워크스페이스 적용
local_setup.bash이 워크스페이스만 적용
# 일반적: 오버레이 체인 전체
source ~/ws/install/setup.bash

# 언더레이를 이미 소싱했고 이것만 추가하고 싶을 때
source /opt/ros/jazzy/setup.bash
source ~/ws/install/local_setup.bash

함정: setup.bash를 여러 워크스페이스에서 반복 소싱하면 경로가 중복·혼재된다(0.5의 셸 오염 함정). 새 터미널에서 시작하는 것이 가장 안전하며, 스크립트에서는 소싱 순서를 명시적으로 관리한다.

4.1.6 빌드 타입

package.xml의 <export>에서 선언한다(4.4).

<export>
  <build_type>ament_cmake</build_type>
</export>
빌드 타입대상빌드 도구
ament_cmakeC++ 패키지, 인터페이스 패키지CMake
ament_python순수 Python 패키지setuptools(4.3)
ament_cmake_python혼합(C++ + Python 모듈)CMake + setuptools
cmakeament 규약을 안 쓰는 순수 CMakeCMake

혼합 패키지가 필요한 경우: C++ 노드와 Python 유틸리티를 한 패키지에 두거나, 인터페이스 패키지에서 Python 헬퍼를 함께 제공할 때.

# ament_cmake_python 사용 시
find_package(ament_cmake_python REQUIRED)
ament_python_install_package(${PROJECT_NAME})
install(PROGRAMS scripts/my_script.py DESTINATION lib/${PROJECT_NAME})

견해: 혼합 패키지는 가능하지만 권하지 않는다. 빌드 규칙이 복잡해지고 오류 원인이 두 배가 된다. C++ 패키지와 Python 패키지를 나누는 편이 대개 낫다 — 인터페이스 패키지를 분리하라는 1.2.5의 조언과 같은 논리다. 단, 컴포넌트와 그 Python 런처처럼 결합이 강한 경우는 예외로 볼 수 있다.

4.1.7 merge install vs isolated install

colcon이 설치본을 배치하는 두 방식이다(4.5).

isolated (기본) merge (--merge-i nstall) install/pkg_a/ install/pkg_b/ install/pkg_c/ install/ lib · share · include 공유
축isolated(기본)merge(--merge-install)
구조패키지마다 접두어 디렉터리하나의 접두어에 통합
AMENT_PREFIX_PATH패키지 수만큼 항목하나
패키지 제거디렉터리 삭제로 가능어려움
환경변수 길이매우 길어질 수 있다짧다
배포디렉터리가 많다단순
디버깅어느 패키지 것인지 명확섞여 있다

AMENT_PREFIX_PATH가 길어지는 문제가 실질적이다. 패키지 200개를 isolated로 빌드하면 경로 항목이 200개가 되고, 환경변수 길이 한계나 셸 성능 문제가 생길 수 있다. 대규모 워크스페이스에서 --merge-install을 쓰는 이유다.

colcon build --merge-install
echo $AMENT_PREFIX_PATH | tr ':' '\n' | wc -l    # 항목 수 확인

함정: merge와 isolated를 섞으면 안 된다. 같은 install 디렉터리에 두 방식이 섞이면 예측 불가능한 상태가 된다. 방식을 바꾸려면 install·build 디렉터리를 지우고 다시 빌드해야 한다.

rm -rf build install log && colcon build --merge-install

견해: 개발 중에는 기본(isolated), 배포·CI에서는 --merge-install 이 흔한 조합이다. 다만 개발과 배포의 설치 레이아웃이 다르면 배포에서만 나타나는 문제가 생길 수 있으므로, CI에서 배포와 같은 방식으로 한 번은 빌드·시험하는 것이 좋다(4.10).

4.1.8 catkin에서 무엇이 바뀌었나

ROS 1 경험자를 위한 대조다.

축catkin(ROS 1)ament(ROS 2)
워크스페이스 빌드catkin_make·catkin buildcolcon(별도 도구)
devel space있음 — 빌드 결과를 소스에서 바로 사용없음
패키지 발견디렉터리 탐색인덱스(4.1.3)
설치 필수 여부devel space로 생략 가능설치가 기본
CMake 매크로catkin_package()ament_package()
의존 전파catkin_INCLUDE_DIRS 등 통합 변수패키지별 타깃(4.2)
메시지 생성add_message_files·generate_messagesrosidl_generate_interfaces(1.2.5)
Pythoncatkin_python_setup()ament_python 또는 ament_cmake_python

"devel space가 없다"가 가장 큰 실무 변화다. ROS 1에서는 빌드 후 소스 트리의 파일을 그대로 참조할 수 있었지만, ROS 2에서는 설치 단계를 거쳐야 한다. 그래서 4.1.4에서 본 "설치 규칙을 안 쓰면 파일이 사라진다" 문제가 ROS 2 특유의 함정이 된다.

--symlink-install이 이 불편을 완화한다(4.3·4.5) — 복사 대신 심볼릭 링크를 만들어 소스 수정이 즉시 반영되게 한다. Python·설정 파일 개발에서 사실상 필수다.

colcon build --symlink-install

함정: --symlink-install도 모든 경우를 해결하지는 못한다. 새 파일 추가·setup.py 변경·CMake 규칙 변경은 재빌드가 필요하다. 그리고 심링크된 워크스페이스를 그대로 배포하면 소스가 없는 곳에서 깨진다 — 배포본은 심링크 없이 빌드해야 한다.

4.1.9 진단 — 빌드는 됐는데 안 될 때

있음 못 찾음 없음 빌드 성공, 실행 실패 ros2 pkg list에 있나? 소싱 확인 → ament_package() 확인 4.1.3 ros2 run이 실행 파일을 찾나? install(TARGETS …) DESTINATION lib/pkg 4.1.4
찾음 찾음 실패 못 찾음 성공 ros2 run이 실행 파일을 찾나? 런타임 파일을 찾나? install(DIRECTORY …) 누락 · 4.1.4 라이브러리 로드되나? LD_LIBRARY_PATH · 의존 전파 4.2 애플리케이션 문제
(계속 — 실행 파일을 찾은 이후)
# 패키지 등록 확인
ros2 pkg list | grep my_pkg
ros2 pkg prefix my_pkg

# 실행 파일 확인
ros2 pkg executables my_pkg
ls install/my_pkg/lib/my_pkg/

# 설치본 내용 전체
find install/my_pkg -type f | sort

# 라이브러리 의존 확인
ldd install/my_pkg/lib/my_pkg/my_node | grep 'not found'

# 환경 확인
echo $AMENT_PREFIX_PATH | tr ':' '\n'
echo $LD_LIBRARY_PATH | tr ':' '\n'

ldd ... | grep 'not found'가 특히 유용하다 — 링크는 됐는데 런타임에 라이브러리를 못 찾는 경우를 즉시 드러낸다.

4.1.10 함정 카탈로그

#함정증상대처
1ament_package() 누락인덱스 미등록CMakeLists 마지막에 호출
2ament_package()를 중간에 호출이후 설치 규칙 무시반드시 마지막
3실행 파일을 bin에 설치ros2 run이 못 찾음lib/${PROJECT_NAME}
4launch·config 설치 규칙 누락배포본에서 파일 없음install(DIRECTORY …)
5소스 디렉터리 기준 경로 하드코딩설치본에서 실패get_package_share_directory
6워크스페이스 미소싱패키지 없음source install/setup.bash
7setup.bash 반복 소싱경로 오염새 터미널(0.5)
8merge·isolated 혼용예측 불가 상태build·install 삭제 후 재빌드
9심링크 워크스페이스 배포대상에서 깨짐배포본은 심링크 없이
10개발과 배포 레이아웃 상이배포에서만 실패CI에서 배포 방식 검증
11혼합 빌드 타입 남용빌드 복잡·오류 증가패키지 분리
12빌드 실패를 못 알아챔"패키지가 없다"빌드 로그 확인(4.5)

2번을 강조한다. ament_package()는 CMakeLists.txt의 마지막 호출이어야 한다. 그 뒤에 오는 install() 등은 무시되며, 오류 메시지가 나지 않아 원인을 찾기 어렵다. (→ 145차 정정: "무시되고 조용하다"는 서술은 부정확하다 — 실제로는 매크로별로 갈린다. 아래 실행 확인 참고.)

실행 확인(145차, Humble·Jazzy 양쪽) — "무시되며 조용히 실패한다"는 서술을 정정한다. ament_package()를 CMakeLists.txt 중간에 두고 그 뒤에 두 종류의 코드를 각각 시험했다.

  • install(TARGETS mynode DESTINATION lib/${PROJECT_NAME})를 ament_package() 뒤에 두었더니 — 무시되지 않았다. 빌드가 정상 완료됐고, ros2 pkg executables에도 나왔고, ros2 run도 정상 동작했다(MYNODE_RAN, Humble·Jazzy 둘 다 동일). 이 경우엔 순서가 문제가 되지 않는다.
  • ament_export_targets(...)·ament_export_dependencies(...)를 ament_package() 뒤에 두었더니 — 조용히 무시되지 않고, 그 자리에서 명확한 CMake 오류로 빌드가 즉시 실패했다: ament_export_targets() must be called before ament_package(), ament_export_dependencies() must be called before ament_package(). 원인이 오류 메시지에 정확히 적혀 있어 진단이 어렵지 않다.

결론: 이 함정의 실제 위험은 "조용한 무시"가 아니라 "매크로마다 다른 결과"다 — 어떤 것(install)은 순서와 무관하게 별도 처리 없이 동작하고, 어떤 것(ament_export_*)은 순서를 어기면 명확한 오류로 즉시 막힌다. "무시되고 오류 메시지가 안 난다"는 서술은 시험한 두 경우 모두에 해당하지 않았다 — 그래도 "ament_package()는 마지막에"라는 실무 조언 자체는 여전히 옳다(export 계열 매크로에서 확실히 걸리므로). 목차 3, tools/gap_harness/gap145.

4.1.11 체크포인트

  • ament가 하는 일 네 가지와 하지 않는 일을 구분할 수 있는가. "빌드는 됐는데 런타임에 실패"가 왜 ROS 2에서 흔한가.
  • ament 인덱스가 무엇이며, 빈 파일의 존재가 등록인 이유는? ros2 pkg list의 출처는?
  • 실행 파일을 lib/<pkg>/에 설치해야 하는 이유와, bin에 설치했을 때의 증상은?
  • launch·config 파일이 배포본에서 사라지는 이유와, 그것을 개발 중에 발견하는 방법은?
  • setup.bash와 local_setup.bash의 차이를 말할 수 있는가.
  • 네 가지 빌드 타입과 각각의 대상은? 혼합 패키지를 권하지 않는 이유는?
  • merge install과 isolated install의 차이 여섯 가지를 들 수 있는가. 대규모 워크스페이스에서 merge를 쓰는 실질적 이유는?
  • catkin의 devel space가 없어진 것이 실무에 어떤 변화를 가져왔는가. --symlink-install이 무엇을 완화하고 무엇을 못 하는가.
  • "빌드는 됐는데 실행이 안 된다"를 진단하는 순서와 각 단계의 명령을 말할 수 있는가.
  • ament_package()의 호출 위치가 왜 중요한가.

다음 장 4.2는 ament_cmake의 내부로 들어간다 — 의존 전파, 라이브러리 내보내기, 심볼 가시성, 그리고 현대 CMake 방식으로의 전환.