제4부 빌드·패키징·배포
4.2 ament_cmake 심층
4.2.1 이 장에서 배우는 것 / 선수 지식
ament_cmake는 ROS 2 C++ 패키지의 뼈대다. 그런데 이 영역은 ROS 2 안에서 관행이 가장 활발하게 바뀌어 온 곳이기도 하다 — 초기의 ament_target_dependencies 중심 스타일에서 현대 CMake 타깃 기반 스타일로 이행 중이며, 인터넷의 예제 상당수가 옛 방식이다.
이 장은 두 가지를 함께 다룬다. ① 실제로 무슨 일이 일어나는가(의존 전파의 메커니즘), ② 지금 어떻게 쓰는 것이 맞는가.
배우는 것:
- 의존 전파의 원리 —
find_package가 무엇을 가져오는가. - 두 가지 스타일 —
ament_target_dependenciesvs 현대 CMake 타깃. - 라이브러리 내보내기 — 다른 패키지가 내 라이브러리를 쓰게 하는 법.
- 심볼 가시성 —
COMPOSITION_BUILDING_DLL의 정체. - pluginlib — 플러그인을 등록·발견하는 구조.
- 테스트·린트 매크로 —
ament_lint_auto·ament_add_gtest. ament_cmake_auto— 보일러플레이트를 줄이는 선택지.
선수 지식: 4.1(ament 구조·설치 레이아웃), 0.6(첫 패키지), CMake 기본(타깃·find_package·target_link_libraries).
정직:
ament_target_dependencies의 권장 여부는 배포판을 거치며 변해 왔다. Jazzy에서 여전히 동작하지만 현대 CMake 스타일이 권장되는 방향이며, 정확한 폐기(deprecation) 상태는ament_cmake문서와 릴리스 노트를 확인해야 한다(확인 필요). 아래 두 스타일을 모두 보이고 판단 근거를 제시한다. 이 환경에서 빌드 검증하지 않았다(목차 3). 145차가ament_package()위치 함정(4.2.10 6번)을 실행 확인해 서술을 정정했다 — 아래 4.2.10을 보라.
4.2.2 find_package가 실제로 하는 일
find_package(rclcpp REQUIRED)
이 한 줄이 무엇을 가져오는가.
| 산출물 | 설명 |
|---|---|
임포트 타깃(rclcpp::rclcpp) | 헤더 경로·라이브러리·컴파일 옵션을 함께 들고 다니는 객체 |
구식 변수(rclcpp_INCLUDE_DIRS 등) | 하위 호환용 |
| 전이 의존 | rclcpp가 의존하는 것들이 자동으로 함께 로드된다 |
임포트 타깃이 현대 CMake의 핵심 개념이다. 타깃에 링크하면 그 타깃이 요구하는 include 디렉터리·컴파일 정의·링크 라이브러리가 자동으로 전파된다. 변수를 수동으로 나열할 필요가 없다.
CMAKE_PREFIX_PATH가 탐색 경로다 — 워크스페이스를 소싱하면 설정된다(0.5·4.1.5).
echo $CMAKE_PREFIX_PATH | tr ':' '\n'
함정:
find_package가 실패하면 대개 소싱 문제다. "패키지를 설치했는데 CMake가 못 찾는다"면CMAKE_PREFIX_PATH를 먼저 본다.package.xml에 의존을 선언했어도 CMake는 그것을 읽지 않는다 —package.xml과CMakeLists.txt에 각각 선언해야 한다(4.4).
4.2.3 두 가지 스타일
4.2.3.1 구식 — ament_target_dependencies
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)
add_executable(my_node src/my_node.cpp)
ament_target_dependencies(my_node rclcpp sensor_msgs)
하는 일: 지정한 패키지들의 include·라이브러리·정의를 타깃에 붙인다. 편리하고 오랫동안 표준이었다.
문제점:
| 문제 | 설명 |
|---|---|
| PUBLIC/PRIVATE 구분 없음 | 모든 의존이 사실상 공개로 전파되어, 내 라이브러리를 쓰는 쪽이 불필요한 의존을 부담한다 |
| 구식 변수 기반 | 임포트 타깃의 이점을 온전히 못 쓴다 |
| 비ROS 라이브러리와 혼용 시 어색 | target_link_libraries와 섞어 쓰게 된다 |
4.2.3.2 현대 CMake — 타깃 링크
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)
add_executable(my_node src/my_node.cpp)
target_link_libraries(my_node PRIVATE
rclcpp::rclcpp
${sensor_msgs_TARGETS} # 인터페이스 패키지는 형태가 다르다
)
이점:
PUBLIC·PRIVATE·INTERFACE로 전파 범위를 통제한다.- 비ROS 라이브러리(Eigen·OpenCV·Boost)와 일관되게 다룬다.
- CMake 생태계의 표준 방식이라 학습·이전이 쉽다.
정직: 인터페이스 패키지(msg·srv)의 타깃 이름은 일반 라이브러리와 다르다.
${sensor_msgs_TARGETS}같은 변수를 쓰거나, 자기 패키지의 타입은rosidl_get_typesupport_target()으로 얻는다(1.2.5). 정확한 형태는 배포판에 따라 다를 수 있다(확인 필요)—find_package후 사용 가능한 타깃을 확인하는 것이 확실하다.
전파 범위 결정:
| 키워드 | 의미 | 언제 |
|---|---|---|
PRIVATE | 이 타깃만 사용. 밖으로 전파 안 됨 | 구현에만 쓰는 의존(기본으로 삼을 것) |
PUBLIC | 이 타깃과 이것을 쓰는 쪽 모두 | 헤더에 등장하는 의존 |
INTERFACE | 이것을 쓰는 쪽만 | 헤더 전용 라이브러리 |
판단 기준은 단순하다: 내 공개 헤더에 그 타입이 나오면 PUBLIC, 아니면 PRIVATE.
// my_lib.hpp — 공개 헤더
#include "rclcpp/rclcpp.hpp" // → rclcpp는 PUBLIC
class MyLib { rclcpp::Node::SharedPtr node_; };
// my_lib.cpp — 구현
#include <opencv2/opencv.hpp> // → OpenCV는 PRIVATE
견해: 새 코드는 현대 CMake 스타일로 쓰라.
ament_target_dependencies는 당분간 동작하겠지만,PUBLIC/PRIVATE구분이 없어 의존 그래프가 실제보다 부풀어 오른다 — 대규모 워크스페이스에서 빌드 시간과 재빌드 범위에 영향을 준다(4.5). 기존 코드를 급히 바꿀 필요는 없지만, 새로 쓰는 것은 새 방식으로.
4.2.4 라이브러리를 내보내기
실행 파일만 만드는 패키지는 간단하지만, 라이브러리를 제공하는 패키지는 규칙이 많다.
cmake_minimum_required(VERSION 3.8)
project(my_lib)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)
# 1. 라이브러리 생성
add_library(my_lib SHARED
src/my_lib.cpp
)
# 2. include 경로 — 빌드 시와 설치 후가 다르다
target_include_directories(my_lib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/${PROJECT_NAME}>
)
target_compile_features(my_lib PUBLIC cxx_std_17)
# 3. 의존 링크
target_link_libraries(my_lib PUBLIC
rclcpp::rclcpp
${sensor_msgs_TARGETS}
)
# 4. 헤더 설치
install(DIRECTORY include/
DESTINATION include/${PROJECT_NAME}
)
# 5. 라이브러리 설치 + 내보내기 세트 등록
install(TARGETS my_lib
EXPORT export_my_lib
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
# 6. 다른 패키지가 쓸 수 있게 내보내기
ament_export_targets(export_my_lib HAS_LIBRARY_TARGET)
ament_export_dependencies(rclcpp sensor_msgs)
ament_package() # 반드시 마지막 (4.1.10)
각 단계가 왜 필요한가:
| # | 없으면 |
|---|---|
2 (BUILD_INTERFACE/INSTALL_INTERFACE) | 설치 후 헤더 경로가 틀려 의존 패키지가 컴파일 실패 |
| 4 | 헤더가 설치되지 않아 의존 패키지가 못 찾음 |
5 EXPORT | 내보내기 세트에 등록되지 않음 |
6 ament_export_targets | 다른 패키지의 find_package가 타깃을 못 봄 |
6 ament_export_dependencies | 의존의 의존이 전파되지 않아 링크 실패 |
ament_export_dependencies가 자주 누락된다. 내 라이브러리가 rclcpp를 PUBLIC으로 쓰면, 내 라이브러리를 쓰는 패키지도 rclcpp가 필요하다. 이것을 선언하지 않으면 그쪽에서 "왜 rclcpp를 못 찾지" 오류가 난다.
함정:
$<BUILD_INTERFACE:...>/$<INSTALL_INTERFACE:...>를 빠뜨리고 절대 경로를 쓰면, 빌드한 머신에서는 되고 설치본을 다른 곳에 옮기면 깨진다. 크로스 컴파일·배포에서 드러난다(4.8).
4.2.5 심볼 가시성
1.13.3에서 본 COMPOSITION_BUILDING_DLL의 정체다.
target_compile_definitions(my_component PRIVATE "MY_PKG_BUILDING_LIBRARY")
// visibility_control.hpp
#ifndef MY_PKG__VISIBILITY_CONTROL_HPP_
#define MY_PKG__VISIBILITY_CONTROL_HPP_
#if defined _WIN32 || defined __CYGWIN__
#ifdef __GNUC__
#define MY_PKG_EXPORT __attribute__ ((dllexport))
#define MY_PKG_IMPORT __attribute__ ((dllimport))
#else
#define MY_PKG_EXPORT __declspec(dllexport)
#define MY_PKG_IMPORT __declspec(dllimport)
#endif
#ifdef MY_PKG_BUILDING_LIBRARY
#define MY_PKG_PUBLIC MY_PKG_EXPORT
#else
#define MY_PKG_PUBLIC MY_PKG_IMPORT
#endif
#else
#define MY_PKG_PUBLIC __attribute__ ((visibility("default")))
#endif
#endif
왜 필요한가:
| 플랫폼 | 문제 |
|---|---|
| Windows | 명시하지 않으면 심볼이 내보내지지 않는다 — dllexport 필수 |
| Linux(gcc) | 기본은 모두 공개. -fvisibility=hidden을 쓰면 명시 필요 |
리눅스만 쓴다면 없어도 동작한다. 그래서 많은 프로젝트가 생략하는데, 크로스 플랫폼 배포나 심볼 가시성 최적화를 고려하면 필요하다.
견해: 심볼 가시성을 좁히는 것은 리눅스에서도 이득이 있다 — 라이브러리 로딩이 빨라지고, 심볼 충돌이 줄고, 바이너리가 작아진다. 다만 보일러플레이트가 늘어나므로, 공개 배포 라이브러리에는 넣고 내부 전용 패키지는 생략하는 절충이 흔하다.
4.2.6 pluginlib
Nav2 플래너(8.5)·ros2_control 컨트롤러(6.4)·RViz2 플러그인(11.4)이 모두 이 구조를 쓴다.
find_package(pluginlib REQUIRED)
add_library(my_plugins SHARED src/my_planner.cpp)
target_link_libraries(my_plugins PUBLIC nav2_core::nav2_core)
pluginlib_export_plugin_description_file(nav2_core plugins.xml)
<!-- plugins.xml -->
<library path="my_plugins">
<class name="my_pkg/MyPlanner"
type="my_pkg::MyPlanner"
base_class_type="nav2_core::GlobalPlanner">
<description>커스텀 전역 플래너</description>
</class>
</library>
#include "pluginlib/class_list_macros.hpp"
PLUGINLIB_EXPORT_CLASS(my_pkg::MyPlanner, nav2_core::GlobalPlanner)
동작 원리: pluginlib_export_plugin_description_file이 ament 인덱스(4.1.3)에 등록하고, 런타임에 pluginlib가 그 인덱스를 읽어 dlopen한다.
# 등록된 플러그인 확인
ros2 pkg xml my_pkg | grep -A3 plugin
find /opt/ros/jazzy/share/ament_index/resource_index -name 'nav2_core' -exec cat {} \;
흔한 실패:
| 증상 | 원인 |
|---|---|
| 플러그인을 못 찾음 | plugins.xml 미설치 또는 인덱스 미등록 |
dlopen 실패 | 라이브러리 이름 불일치(<library path=>) |
| 심볼 없음 | PLUGINLIB_EXPORT_CLASS 누락 |
| 기반 클래스 불일치 | base_class_type 오타 |
함정:
<library path="my_plugins">의 이름은lib접두어와.so확장자를 뺀 것이다.libmy_plugins.so면my_plugins. 이것을 틀리면 런타임에만 드러난다.
4.2.7 테스트와 린트
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
find_package(ament_cmake_gtest REQUIRED)
ament_add_gtest(test_my_lib test/test_my_lib.cpp)
target_link_libraries(test_my_lib my_lib)
# launch 통합 테스트 (14.6)
find_package(launch_testing_ament_cmake REQUIRED)
add_launch_test(test/test_integration.py)
endif()
<!-- package.xml -->
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>ament_cmake_gtest</test_depend>
ament_lint_auto가 자동으로 실행하는 린터들:
| 린터 | 검사 |
|---|---|
ament_cpplint·ament_uncrustify | C++ 스타일 |
ament_cppcheck | 정적 분석 |
ament_flake8·ament_pep257 | Python 스타일 |
ament_copyright | 라이선스 헤더 |
ament_xmllint | XML 문법 |
ament_lint_cmake | CMake 스타일 |
colcon test --packages-select my_pkg
colcon test-result --verbose
견해: 린트를 초기에 켜라. 나중에 켜면 수백 개의 경고가 한꺼번에 대량 출력돼 결국 끄게 된다. 스타일에 이견이 있으면 특정 린터만 비활성화하는 편이 전부 끄는 것보다 낫다.
# 특정 린터만 제외
set(ament_cmake_uncrustify_FOUND TRUE) # uncrustify 건너뛰기
ament_lint_auto_find_test_dependencies()
4.2.8 ament_cmake_auto
보일러플레이트를 줄이는 선택지다.
find_package(ament_cmake_auto REQUIRED)
ament_auto_find_build_dependencies() # package.xml의 의존을 자동 find_package
ament_auto_add_library(my_lib SHARED src/my_lib.cpp)
ament_auto_add_executable(my_node src/my_node.cpp)
ament_auto_package(INSTALL_TO_SHARE launch config)
| 축 | 수동 | ament_cmake_auto |
|---|---|---|
| 코드 길이 | 길다 | 짧다 |
package.xml과 동기화 | 수동(불일치 가능) | 자동 |
| 세밀한 제어 | 완전 | 제한적 |
| PUBLIC/PRIVATE 구분 | 가능 | 어렵다 |
| 학습 곡선 | CMake 지식 필요 | 낮다 |
견해: 소규모·단순 패키지에는 편리하고, 라이브러리를 내보내는 패키지에는 부족하다. 특히
PUBLIC/PRIVATE구분(4.2.3)을 제대로 하려면 수동 방식이 필요하다. 팀 안에서 방식을 통일하는 것이 개별 선택보다 중요하다 — 두 스타일이 섞인 워크스페이스는 읽기 어렵다.
4.2.9 자주 쓰는 CMake 조각
컴파일 옵션:
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# 최적화 (Release가 기본이 아니다)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
함정:
CMAKE_BUILD_TYPE을 지정하지 않으면 최적화가 꺼진 채 빌드된다. "왜 이렇게 느리지"의 원인이 이것인 경우가 있다.
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
컴포넌트 등록(1.13.3):
find_package(rclcpp_components REQUIRED)
add_library(my_component SHARED src/my_component.cpp)
target_link_libraries(my_component PRIVATE rclcpp::rclcpp rclcpp_components::component)
rclcpp_components_register_nodes(my_component "my_pkg::MyComponent")
자기 패키지의 인터페이스 사용(1.2.5):
rosidl_generate_interfaces(${PROJECT_NAME} "msg/MyMsg.msg" DEPENDENCIES std_msgs)
rosidl_get_typesupport_target(cpp_typesupport ${PROJECT_NAME} rosidl_typesupport_cpp)
target_link_libraries(my_node PRIVATE "${cpp_typesupport}")
설치 묶음:
install(TARGETS my_node DESTINATION lib/${PROJECT_NAME})
install(DIRECTORY launch config urdf DESTINATION share/${PROJECT_NAME})
install(PROGRAMS scripts/helper.py DESTINATION lib/${PROJECT_NAME})
install(PROGRAMS ...)는 실행 권한을 유지한다 — Python 스크립트를 ros2 run으로 실행하려면 이것을 써야 한다(FILES는 권한을 잃는다).
4.2.10 함정 카탈로그
| # | 함정 | 증상 | 대처 |
|---|---|---|---|
| 1 | package.xml에만 의존 선언 | CMake가 못 찾음 | find_package도 필요(4.4) |
| 2 | ament_export_dependencies 누락 | 의존 패키지에서 링크 실패 | 공개 의존 선언 |
| 3 | 헤더 설치 누락 | 의존 패키지 컴파일 실패 | install(DIRECTORY include/) |
| 4 | BUILD_INTERFACE/INSTALL_INTERFACE 누락 | 설치본 이동 시 깨짐 | 제너레이터 표현식 사용 |
| 5 | 모든 의존을 PUBLIC | 의존 그래프 부풀림·재빌드 확대 | 헤더 등장 여부로 판단 |
| 6 | ament_package() 위치 오류 | 이후 규칙 무시 | 마지막에(4.1.10) |
| 7 | pluginlib <library path> 오타 | 런타임 로드 실패 | lib·.so 제외한 이름 |
| 8 | PLUGINLIB_EXPORT_CLASS 누락 | 심볼 없음 | 매크로 추가 |
| 9 | CMAKE_BUILD_TYPE 미지정 | 최적화 없이 빌드 | Release 지정 |
| 10 | install(FILES)로 스크립트 설치 | 실행 권한 없음 | install(PROGRAMS) |
| 11 | 린트를 나중에 켬 | 경고 폭증 → 포기 | 초기부터 |
| 12 | 두 스타일 혼용 | 읽기 어려움 | 팀 내 통일 |
6번, 145차 실행 확인. "이후 규칙 무시"는 매크로마다 다르다는 것이 밝혀졌다 — install(TARGETS ...)을 ament_package() 뒤에 둬도 실제로는 무시되지 않고 정상 빌드·실행됐지만(Humble·Jazzy 둘 다), ament_export_targets()·ament_export_dependencies()를 뒤에 두면 조용히 무시되는 대신 그 자리에서 명확한 CMake 오류(... must be called before ament_package())로 즉시 실패했다. "마지막에"라는 대처 자체는 여전히 옳지만, 증상 칸의 "이후 규칙 무시"는 시험한 두 경우 어느 쪽에도 정확히 들어맞지 않았다. 상세는 4.1.10을 보라.
4.2.11 체크포인트
find_package(rclcpp REQUIRED)가 가져오는 것 세 가지를 말할 수 있는가. 탐색 경로는 무엇이 정하는가.ament_target_dependencies와 현대 CMake 스타일의 차이와, 후자를 권하는 이유를 설명할 수 있는가.PUBLIC과PRIVATE을 판단하는 기준을 한 문장으로 말할 수 있는가.- 라이브러리를 내보내는 여섯 단계를 나열하고, 각각이 없을 때의 증상을 댈 수 있는가.
ament_export_dependencies가 누락되면 무슨 일이 생기는가.$<BUILD_INTERFACE:>/$<INSTALL_INTERFACE:>가 없으면 언제 문제가 드러나는가.- 심볼 가시성 매크로가 어느 플랫폼에서 필수이며, 리눅스에서도 쓰면 어떤 이득이 있는가.
- pluginlib의 동작 원리를 ament 인덱스와 연결해 설명할 수 있는가. 흔한 실패 네 가지는?
ament_cmake_auto의 이점과 한계는? 언제 쓰지 않는 편이 나은가.CMAKE_BUILD_TYPE을 지정하지 않으면 무슨 일이 생기는가.install(FILES)와install(PROGRAMS)의 차이가 왜 중요한가.
다음 장 4.3은 Python 쪽 — ament_python의 setup.py·엔트리포인트·리소스 설치, 그리고 --symlink-install이 왜 rclpy 개발에서 사실상 필수인가.