How to use modern CMake best practices in ROS 1?

asked 2021-11-03 17:04:55 -0500

Pinknoise2077 gravatar image

Hi,

I am trying to improve my CMakeLists.txt by using modern CMake best practices (i.e. using target_include_directories instead of include_directories) for a ROS 1 package. I'd like to create a package that works in the devel and install workspace, passes roslint successfully, and of course, can be used with the find_package command just like any other ROS package.

Can someone provide a toy example to start from that satisfies the requirements above? I've been trying this so far, but I can't get past roslint and I fail to understand what I'm doing wrong.

cmake_minimum_required(VERSION 3.5)

project(my_ros_project)

add_compile_options(
  -std=c++14
)

find_package(catkin REQUIRED COMPONENTS
   another_ros_package
   rosdoc_lite
   roslint
)

# A system package
find_package(Eigen3 REQUIRED)

catkin_package(
  INCLUDE_DIRS
    include
  LIBRARIES
    ${PROJECT_NAME}
  CATKIN_DEPENDS
    another_ros_package
  DEPENDS
    EIGEN3
)

# This package
target_include_directories(${PROJECT_NAME} PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE:include>")

# ROS
target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${catkin_INCLUDE_DIRS})
target_link_libraries( ${PROJECT_NAME} PUBLIC ${catkin_LIBRARIES})
add_dependencies(${PROJECT_NAME} ${catkin_EXPORTED_TARGETS})

# Eigen
target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC
  "${EIGEN3_INCLUDE_DIR}")

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

install(
  DIRECTORY include/${PROJECT_NAME}/
  DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
)

I get the following linter errors and I don't understand what is the issue.

First error:

error: include path 'include' is exported but not used for the build
* You have listed an include path in the INCLUDE_DIRS stanza of
* the catkin_package() command, but that path is not mentioned in
* any include_directories() call.
* You can ignore this problem with --ignore unused_include_path

I can simply remove the include from INCLUDE_DIRS to make that linter error disappear, but that seems wrong.

Second error:

CMakeLists.txt(39): error: target_include_directories() exports external PUBLIC path '${catkin_INCLUDE_DIRS}'
* You have exported an include path as part of your interface for
* dependent targets, but that path does not belong to your
* package. You probably meant to use the PRIVATE scope if you
* merely need that include path for your build. If you actually
* wanted to export the path to your dependees, you hould be aware
* that target properties will be exported as fixed strings,
* meaning your installed package will break if that location
* changes for any reason. A more robust way would be to use
* target_link_libraries(PUBLIC) with the proper imported target
* instead.
* You can ignore this problem with --ignore external_interface_path
edit retag flag offensive close merge delete

Comments

While using modern CMake is certainly a good idea, I'm wondering whether it makes sense in this specific context. Catkin was mostly written when there was only "old CMake", meaning it does a couple of things behind-the-scenes which CMake these days does itself (processing and gathering all the include and link libraries, exporting them, passing them on to dependees, etc).

Managing the difference between devel and install spaces is also partly taken care of for you among other things (via the extras-files fi).

Using target_link_libraries(..) and target_include_directories(..) with PUBLIC and PRIVATE is of course nice, but Catkin will not export your CMake targets, nor will CMake (as you don't have an EXPORT), so that information doesn't get to downstream consumers (unless something has recently changed).

Mixing modern CMake with this is possible, but seems 'unnecessary' and makes your CMakeLists.txt harder to read, while the gains seem minimal.

gvdhoorn gravatar image gvdhoorn  ( 2021-11-04 01:33:38 -0500 )edit

The primary motivation for me to use modern CMake is the greater isolation and the fact we get relocatable packages. Building with catkin_tools or catkin_make_isolated also results in isolated builds. Relocatable packages are sort-of achieved by Catkin already (although you'll find lots of hard-coded absolute paths everywhere), and using modern CMake doesn't solve the remaining problems.

Again: I'm not saying you shouldn't do this -- after all, that's your decision. I just wanted to take a step back and evaluate the pros-cons.

re: roslint: do you mean catkin_lint? Because the errors you show are from the latter I believe.

From the looks of it, catkin_lint is just confused by your use of "non standard" things like target_include_directories(..), and the second error seems to reinforce what I wrote in my first comment: mixing Catkin-CMake and modern CMake is non-trivial, especially when using CMake targets (and INTERFACE libraries, etc).

gvdhoorn gravatar image gvdhoorn  ( 2021-11-04 01:39:35 -0500 )edit

I guess that's what I thought, but I still wondered if there was a better way of packaging ROS 1 packages than using old CMake. Thanks for the feedback.

Pinknoise2077 gravatar image Pinknoise2077  ( 2021-11-04 14:41:52 -0500 )edit

Well realistically, what would you gain by using modern CMake? In this specific context I mean.

ROS 1 packages primarily use Catkin, and as long as you stay "inside" that environment, things "just work" (and if they don't, there are probably known work-arounds).

Catkin is in maintenance mode, so I don't believe it will see any major improvements which could make it more amenable to modern CMake any more.

On the ROS 2 side things are different, and Ament is much more recent, and there's work to slowly migrate away from customisations and rely on standard CMake as much as possible. It's not going to go fast though.

I'm not trying to gold plate the situation btw. I'm mostly curious as to your motivations. If your motivation was "I'd like to see what's possible", that would be a perfectly fine one of course.

gvdhoorn gravatar image gvdhoorn  ( 2021-11-04 14:44:41 -0500 )edit

I think what I would gain is to be able to reduce the scope of what is included per target instead of everything inside the project (i.e. target vs directory scope) and specify PRIVATE/PUBLIC/INTERFACE keywords.

Pinknoise2077 gravatar image Pinknoise2077  ( 2021-11-04 14:49:32 -0500 )edit

If you're ok with ignoring catkin_lint warnings/errors about this, then that should be perfectly possible.

Catkin will not interfere with any of that.

gvdhoorn gravatar image gvdhoorn  ( 2021-11-04 14:51:23 -0500 )edit