ROS로 살펴보는 CMake 작성법 2편

ROS로 살펴보는 CMake 작성법 2편

ROS 패키지 빌드, CMake 두 번째입니다. 첫 번째 글을 이어 이제 빌드 패키지, colcon build / catkin_make를 이용해 빌드할 때 사용하는 CMakeLists.txt 내부 내용을 자세히 살펴보도록 하겠습니다.

우선 RPLiDAR의 ROS1 & 2 버전의 CMakeLists.txt를 예시로 해보겠습니다.

그림1

CMakeLists의 빌드 흐름

1. CMake 최소 요구 버전과 project 이름 설정

2. 패키지 확인

3. include 포함 / link(라이브러리 연결 및 실행파일 생성)

4. 설치(경로 설정)


ROS1에서는 catkin이라는 빌드 툴, ROS2에서는 ament_cmake라는 빌드 툴의 차이로 CMakeLists.txt의 내용에서 관련 패키지가 추가된다는 점이 다를 뿐 설치의 큰 흐름은 위의 4가지를 따릅니다.

1. CMake의 최소 요구 버전과 project 이름 설정

ROS1

cmake_minimum_required(VERSION 2.8.3)
project(rplidar_ros)

ROS2

cmake_minimum_required(VERSION 3.5)
project(rplidar_ros2)

cmake의 시작과 함께 기본적으로 작성되는 2가지입니다.

project의 이름을 작성하면 이후 ${PROJECT_NAME}라고 작성하여, 다른 곳에서 project 이름을 이용하기도 합니다. ROS1에서는 쓰이지 않았지만, ROS2에는 사용했군요.

아래의 작성 내용처럼 사용할 수 있는 것이죠.

install(
    TARGETS rplidar_scan_publisher
    RUNTIME DESTINATION lib/${PROJECT_NAME}
)

그리고 가장 **주의해야할 점은 package.xml에 설정한 태그와 CMakeLists.txt에서 설정한 project 이름은 같도록** 해야 합니다.

그림2

1-1. 컴파일 설정 및 경로 설정

ROS1

set(PRLIDAR_SDK_PATH "./sdk/")
set(CMAKE_BUILD_TYPE "Release")
set(CMAKE_CXX_FLAGS "-std=c+11 ${CMAKE_CXX_FLAGS}")

FILE(GLOB RPLIDAR_SDK_SRC
    "${RPLIDAR_SDK_PATH}/src/arch/linux/*.cpp"
    "${RPLIDAR_SDK_PATH}/src/hal/*.cpp"
    "${RPLIDAR_SDK_PATH}/src/*.cpp"
)

ROS2

if(NOT CMAKE_CXX_STANDARD)
    set(CMAKE_CXX_STANDARD 14)    
endif()

if(CMAKE_COMPILE_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    add_compile_options(-Wall -Wextra -Wpedantic)
endif()

if(MSVC)
    add_compile_definitions(
        _USE_MATH_DEFINES
    )
endif()

set(RPLIDAR_SDK_PATH "./sdk/")

FILE(GLOB RPLIDAR_SDK_SRC
    "${RPLIDAR_SDK_PATH}/src/arch/linux/*.cpp"
    "${RPLIDAR_SDK_PATH}/src/hal/*.cpp"
    "${RPLIDAR_SDK_PATH}/src/*.cpp"
)

set() 함수를 먼저 살펴보면 디렉토리부터 문자열 등등 사용자가 설정한 값(Value)을 사용할 수 있도록 합니다. 유형을 살펴보면 아래와 같이 정리됩니다.

그림3

보통의 ROS 패키지들을 살펴보면, set을 사용하는 이유는 경로에 대한 내용이나 컴파일의 추가적인 옵션에 대한 STRING으로 사용하는 경우가 많습니다.

그래서 ROS2 작성 내용을 보면 컴파일 옵션에 대한 설정을 필요로 하는데 C언어의 경우 99, C++의 경우 14가 기본 설정입니다. ROS1의 경우 CMake를 실행할 때 2가지 옵션을 추가했습니다. 하나는 빌드 모드, 하나는 C++ 버전(-std=C+11)이군요.

ROS1

set(PRLIDAR_SDK_PATH "./sdk/")
set(CMAKE_BUILD_TYPE "Release")
set(CMAKE_CXX_FLAGS "-std=c+11 ${CMAKE_CXX_FLAGS}")

FILE(GLOB RPLIDAR_SDK_SRC
    "${RPLIDAR_SDK_PATH}/src/arch/linux/*.cpp"
    "${RPLIDAR_SDK_PATH}/src/hal/*.cpp"
    "${RPLIDAR_SDK_PATH}/src/*.cpp"
)

빌드 타입(BUILD_TYPE)의 경우, 이러한 배포의 목적이라면 “Release”로 쓰고 개발자가 특수한 목적으로 빌드 한다면 “Debug”로 옵션을 수정할 수도 있습니다. 그리고 C++ 11버전도 추가로 전달합니다.

ROS2

if(NOT CMAKE_CXX_STANDARD)
    set(CMAKE_CXX_STANDARD 14)    
endif()

if(CMAKE_COMPILE_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    add_compile_options(-Wall -Wextra -Wpedantic)
endif()

if(MSVC)
    add_compile_definitions(
        _USE_MATH_DEFINES
    )
endif()

set(RPLIDAR_SDK_PATH "./sdk/")

FILE(GLOB RPLIDAR_SDK_SRC
    "${RPLIDAR_SDK_PATH}/src/arch/linux/*.cpp"
    "${RPLIDAR_SDK_PATH}/src/hal/*.cpp"
    "${RPLIDAR_SDK_PATH}/src/*.cpp"
)

ROS2의 경우 if문으로 컴파일러 종류에 따라 옵션을 추가하도록 설정해두었습니다. 컴파일러 ID가 GNUCXX 또는 “Clang”이라면 (-Wall -Wextra -Wpendantic) 3가지의 옵션을 추가하게끔 작성하였습니다.

ROS CMake 파일에 자주 작성된 옵션인데 각각의 기능은 아래와 같이 정리할 수 있습니다.

  • Wall : 컴파일 과정에서의 모든 warining 내용을 화면에 출력한다.
  • Wextra : Wall에서 활성화되지 않는 추가적인 경고 표시도 활성화한다.
  • Wpedantic : 이름이 ‘_‘로 시작하고 끝나는 대체 키워드를 사용하는 경우 경고 메시지를 표시하지 않게 한다.

MSVC는 Microsoft Visual Studio에 사용하는 컴파일러로 Rplidar의 경우 윈도우, MAC, 리눅스에서 사용하게끔 SDK를 쓰는지라 이러한 옵션도 추가적으로 작성한 것으로 보입니다.

C++와 관련한 TMI인데 위의 경고 옵션을 설정하면서 생긴 일인데, C++ 함수 중 일부 17버전에 사용하는 코드를 쓰면서 빌드 할 때 경고 설정 없이 빌드 하면 문제없는데 옵션을 추가하여 빌드 하면 에러로 표시되는 경우도 있었습니다. ROS2의 C++ 기본 버전이 아니다 보니 발생한 문제로 예상됩니다. Stack Overflow에서도 C++ 상위 버전 사용시 문제는 없었다는 내용이 있었습니다. 우려하는 바는 사용에 문제는 없겠지만 컴파일, 디버그로 단위별 테스트로 요구되면 알 수 없는 에러를 야기할 수도 있다고 하니 주의가 필요해 보입니다.

2. 패키지 확인

ROS1

find_package(catkin REQUIRED COMPONENTS
    roscpp
    rosconsole
    sensor_msgs
)

ROS2

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(std_srvs REQUIRED)

​위의 find_package() 함수에 들어가는 패키지는 package.xml에서 설정한, 빌드 할 때 요구되는 최소의 패키지들에 맞춰 작성합니다. ROS에서 C++로 코드를 작성할 것이라면, roscpp(ROS1) / rclcpp(ROS2) 라이브러리가 필요하고 라이다센서에 필요한 데이터를 퍼블리시할 수 있도록 LaserScan 메시지를 사용하기 위해 sensor_msgs도 필요합니다.

그 외 패키지를 빌드하기 위해 사용해야 할 구성을 필요에 맞게 작성하시게 될텐데요. ROS1의 경우 find_package에 ‘catkin’을 작성하고 기본 패키지들을 작성하군요. 이런 식으로 작성하면 catkin의 하위 패키지는 아니더라도 include 디렉토리나 라이브러리 같은 환경 변수들을 공유할 수 있어 위와 같이 하나의 함수(find_package())에 작성하기도 합니다.

3. include 경로 설정(헤더파일)

ROS1

include_directories(
    ${RPLIDAR_SDK_PATH}/include
    ${RPLIDAR_SDK_PATH}/src
    ${catkin_INCLUDE_DIRS}
)

ROS2

include_directories(
    ${RPLIDAR_SDK_PATH}/include
    ${RPLIDAR_SDK_PATH}/src
    ${Boost_INCLUDE_DIRS}
)

rplidar의 파일 구성을 보면 sdk 폴더 안에 헤더 파일(.h/.hpp)과 소스파일(.c/.cpp)을 배치해놨습니다. 그렇다 보니 경로 설정의 경우 ROS1과 ROS2 공통적으로 설정하였습니다.

그림4

ROS2에는 Boost가 있군요.(${Boost_INCLUDE_DIRS}) rplidar의 경우 시리얼 통신 때문에 사용하는 라이브러리입니다. Boost 패키지를 사용하는 경우 ROS2의 작성 내용처럼 include 디렉토리를 추가적으로 작성했군요.

Boost 패키지의 include파일의 경우 ${Boost_INCLUDE_DIR}, catkin 패키지의 경우 ${catkin_INCLUDE_DIRS}로 작성하여 패키지 안의 include 폴더를 의미합니다.

4. 설치(경로 설정)

ROS1

그림5

ROS2

그림6

ROS1에서 빌드 할 경우 catkin 패키지로 인해 catkin_package() 함수를, ROS2에서 빌드 할 경우 ament_cmake 패키지로 인해 ament_package() 함수를 사용한다는 차이를 구분하여 보시면 됩니다.

add_excutable()에서는 빌드 할 때 참조할 코드(소스파일)와 실행 파일 이름을 지정합니다. ROS1의 경우 rplidarNode라는 이름으로 실행 파일 이름을 지정하고 참조 코드들은 sdk 폴더에 있는 c, c++ 파일과 src에 있던 node.cpp로 묶습니다.

앞에서 set() 함수를 이용해 경로와 소스 파일을 설정 안 했다면 소스 파일을 하나하나 나열해야하는데 파일이 많아지면 이러한 방법으로 처리하는게 용이합니다.

ROS2는 rplidar_scan_publilsher라는 이름으로 실행 파일 이름을 설정하고 sdk 폴더의 소스파일들과 src 디렉토리에 있는 rplidar_scan_publisher.cpp을 묶어 빌드 합니다.

ROS1에서는 이어서 target_link_libraries를 작성하는군요.

ROS1

add_executable(rplidarNode src/node.cpp ${RPLIDAR_SDK_SRC})
target_link_libraries(rplidarNode ${catkin_LIBRARIES})

add_executable(rplidarNodeClient src/node.cpp ${RPLIDAR_SDK_SRC})
target_link_libraries(rplidarNodeClient ${catkin_LIBRARIES})

실행 파일로 설정한 rplidarNode에 catkin 라이브러리를 링크하도록 하는군요. 실행 파일을 생성하기 앞서 라이브러리와 실행 파일을 링크해주는 옵션입니다. ROS2를 주로 하다 보니 catkin의 설정이 오히려 익숙하지 않군요. ROS1의 상당 부분은 catkin과의 연결이 많은 듯합니다.

아래처럼 launch 파일부터 rviz, config 등등 실행 파일을 빌드 하면서 설치할 목록들을 작성합니다. 상황에 따라서 포함해야 할 폴더가 있다면 설치 옵션에서 추가해 주면 됩니다.

ROS2

install(DIRECTORY launch rviz
    DESTINATION share/${PROJECT_NAME}
)

ROS1에서 target인 rplidarNode와 rplidarNodeClient의 경우 ddevl 폴더 아래에 위치하게 되는데 기본적인 경로 설정은 아래와 같습니다.

ROS1

install(TARGETS rplidarNode rplidarNodeClient
    ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
    LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
    RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

위에서 차례로부터 살펴보면,

  • ARCHIVE(static 라이브러리와 Window DLL, .lib등)
  • LIBRARY(DLL 이외의 공유 라이브러리와 모듈)
  • RUNTIME(실행 파일과 DLL 형식의 공유 라이브러리)​

순으로 설치 경로를 나타냅니다. LIB_DESTINATION과 BIN_DESTINATION은 기본 경로로 사용됩니다.

ROS2는 ROS1 때와 달리 /install 디렉토리에 설치가 이루어지는데 이때 보통 lib/프로젝트이름 경로로 설정하게 됩니다.

ROS2

install(
    TARGET rplidar_scan_publisher
    RUNTIME DESTINATION lib/${PROJECT_NAME}
)

마지막으로 ROS2에서 빌드 패키지인 ament의 경우 ament_package()가 가장 마지막에 배치해있습니다.

그림7

ament_export_dependencies의 경우 의존성 프로젝트를 내보내는 역할인데 ros_core 패키지 중 하나로, 기본적인 패키지들로 작성되는 경우가 많습니다.


뭔가 쓰면 쓸수록 부족한 점이 많아 아쉽습니다. 글을 작성하면서도 CMake가 2편만으로 설명하기에 얼마나 부족한지 알게 됩니다. 단순히 예제 몇 가지만 따라서는 CMake를 원활하게 이해하고 사용하는데 무리가 있을 겁니다.

저도 현장에서 불안정한 임베디드 환경 속에서 직접 구현해야 하는 경우가 많고 다른 플랫폼을 오가야 하는 상황 덕분에 실전적으로 요령을 익히는데, 공식 문서도 분량이 제법 많아 이를 모두 알고 적용하기는 힘들어 자주 마주하는 함수는 옵션들은 하나씩 적어두었다가 쓰고 있습니다.

단순히 ROS에서 사용하는 파일이 아닌 빌드라는 구성 환경에서 얼마나 광범위하게 적용되는지 이해가 필요한 부분인 것 같습니다. 쓰면서도 타깃이 명확하지 않아 글이 좀 중구난방인 듯합니다.

제가 써오면서 공부한 내용들을 작게나마 적어보는 것으로 여기까지 마무리하려고 합니다. 상황에 따라서 조금씩 설명을 보강하면서 내용을 계속 다듬어야겠습니다.

참고하면 도움이 되는 사이트 추천

👉CMake 사용법 정리

👉별준님의 CMake 튜토리얼

👉박동하님의 CMake할때 쪼오오금 도움이 되는 문서