ament_cmake 用户文档

ament_cmake 是ROS 2中基于CMake的软件包构建系统(特别是大多数C/C++项目将使用它)。它是一组增强了CMake并为软件包作者添加便利功能的脚本。在使用 ament_cmake 之前,了解 CMake 的基础知识非常有帮助。您可以在 此处 找到官方教程。

基础知识

可以使用命令行上的 ros2 pkg create <package_name> 创建基本的CMake大纲。然后,构建信息会收集在两个文件中:package.xmlCMakeLists.txt,它们必须位于同一目录中。package.xml 必须包含所有依赖项和一些元数据,以允许 colcon 找到正确的构建顺序,以在CI中安装所需的依赖项,并为使用 bloom 进行发布提供信息。CMakeLists.txt 包含了构建和打包可执行文件和库的命令,将是本文档的主要关注点。

基本项目概述

一个 ament 包的 CMakeLists.txt 的基本概述包括:

cmake_minimum_required(VERSION 3.8)
project(my_project)

ament_package()

``project``的参数将是包名,并且必须与``package.xml``中的包名完全相同。

项目设置由 ament_package() 完成,每个软件包必须恰好调用一次。 ament_package() 安装了 package.xml,在ament索引中注册了软件包,并安装了CMake的配置(和可能的目标)文件,以便其他使用 find_package 的软件包可以找到它。由于 ament_package()CMakeLists.txt 收集了大量信息,因此它应该是您的 CMakeLists.txt 中的最后一个调用。

ament_package 可以接受额外的参数:

  • CONFIG_EXTRAS: 一个CMake文件列表(通过 configure_file() 展开的 .cmake.cmake.in 模板),应该对软件包的客户端可用。有关何时使用这些参数的示例,请参阅 添加资源 中的讨论。有关如何使用模板文件的更多信息,请参阅 官方文档

  • CONFIG_EXTRAS_POST:与 CONFIG_EXTRAS 相同,但添加文件的顺序不同。CONFIG_EXTRAS 文件在为 ament_export_* 调用生成文件之前包含,而 CONFIG_EXTRAS_POST 的文件在之后包含。

除了向 ament_package 添加外,您还可以添加到变量 ${PROJECT_NAME}_CONFIG_EXTRAS${PROJECT_NAME}_CONFIG_EXTRAS_POST,效果相同。唯一的区别是再次添加文件的顺序,具体顺序如下:

  • 由``CONFIG_EXTRAS``添加的文件

  • 通过附加到``${PROJECT_NAME}_CONFIG_EXTRAS``添加的文件

  • 通过附加到``${PROJECT_NAME}_CONFIG_EXTRAS_POST``添加的文件

  • 由``CONFIG_EXTRAS_POST``添加的文件

编译器和链接器选项

ROS 2针对符合C++17和C99标准的编译器。未来可能会针对更新的版本,可以在 这里 找到相关信息。因此,习惯性地设置相应的CMake标志:

if(NOT CMAKE_C_STANDARD)
  set(CMAKE_C_STANDARD 99)
endif()
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 17)
endif()

为了保持代码的清洁,编译器应该对可疑的代码发出警告,并且这些警告应该被修复。

建议至少包括以下警告级别:

  • 对于Visual Studio:默认的 W1 警告级别

  • 对于GCC和Clang:强烈建议使用 -Wall -Wextra -Wpedantic,并建议使用 -Wshadow"

目前建议使用 add_compile_options 来为所有目标添加这些选项。这样可以避免在所有可执行文件、库和测试的目标编译选项中引入杂乱的代码:

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

查找依赖项

大多数 ament_cmake 项目会依赖其他软件包。在CMake中,可以通过调用 find_package 来实现这一点。例如,如果您的软件包依赖于 rclcpp,则 CMakeLists.txt 文件应包含以下内容:

find_package(rclcpp REQUIRED)

注解

通常情况下不需要对不明确需要但是另一个明确需要的依赖项的依赖项进行 find_package。如果出现这种情况,请向相应的软件包提交错误报告。

添加目标

在CMake术语中,目标 是此项目将创建的工件。可以创建库或可执行文件,一个项目可以包含零个或多个这些工件。

使用 add_library 调用创建这些库,该调用应包含目标名称和应编译以创建库的源文件。

在C/C++中,由于头文件和实现分离,通常不需要将头文件作为 add_library 的参数。

建议采用以下最佳实践:

  • 将所有应该供库的客户端使用的头文件(因此必须安装)放入名为软件包名称的 include 文件夹的子文件夹中,而其他所有文件(.c/.cpp 和不应导出的头文件)都在 src 文件夹中。

  • add_library 的调用中,仅显式引用了 .c/.cpp 文件。

  • 通过以下方式查找库 my_library 的头文件

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

这将在构建时将``${CMAKE_CURRENT_SOURCE_DIR}/include``文件夹中的所有文件添加到公共接口中,并在安装时将所有文件添加到include文件夹中(相对于``${CMAKE_INSTALL_DIR}``)。

ros2 pkg create 创建的软件包布局遵循这些规则。

注解

由于Windows是官方支持的平台之一,为了最大程度地影响,任何软件包都应在Windows上构建。Windows库格式强制执行符号可见性,即从客户端使用的每个符号都必须由库明确导出(并且需要隐式导入符号)。

由于通常情况下,GCC和Clang构建不会执行这一操作,因此建议使用 GCC维基 中的逻辑。要在名为 my_library 的软件包中使用它:

有关更多详细信息,请参阅:Windows_Tips_and_Tricks 文档中的 Windows 符号可见性

在软件包既有库又有可执行文件的情况下,请确保结合上述 "库" 和 "可执行文件" 部分的建议。

链接到依赖项

有两种将目标链接到依赖项的方式。

第一种并推荐的方式是使用ament宏 ament_target_dependencies。例如,假设我们想要将 my_library 链接到线性代数库Eigen3。

find_package(Eigen3 REQUIRED)
ament_target_dependencies(my_library PUBLIC Eigen3)

它包括了必要的头文件和库以及它们的依赖项,以便项目能够正确找到它们。

第二种方法是使用``target_link_libraries``函数。

现代的 CMake 更倾向于只使用目标(targets),并对其进行导出和链接。CMake 目标可以被命名空间化,类似于 C++。如果有可用的命名空间目标,请优先使用它们。例如,Eigen3 定义了名为 Eigen3::Eigen 的目标。

在 Eigen3 的示例中,调用应该如下所示:

target_link_libraries(my_library PUBLIC Eigen3::Eigen)

这也将包括必要的头文件、库和它们的依赖项。请注意,这个依赖关系必须之前通过调用 find_package 来发现。

安装

在构建可重用库时,需要导出一些信息以便下游软件包能够轻松使用它。

首先,安装应该对客户端可用的头文件。这个包含目录是定制的,以支持 colcon 中的覆盖;有关更多信息,请参阅 https://colcon.readthedocs.io/en/released/user/overriding-packages.html#install-headers-to-a-unique-include-directory

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

接下来,安装目标并创建导出目标(export_${PROJECT_NAME}),其他代码将使用它来找到这个包。请注意,您可以使用单个 install 调用来安装项目中的所有库。

install(
  TARGETS my_library
  EXPORT export_${PROJECT_NAME}
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
)

ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)
ament_export_dependencies(some_dependency)

上面的代码片段中发生了什么,下面是说明:

  • ament_export_targets 宏用于导出 CMake 的目标。这是为了允许您的库的客户端使用 target_link_libraries(client PRIVATE my_library::my_library) 语法而必要的。如果导出集包括一个库,请在 ament_export_targets 中添加选项 HAS_LIBRARY_TARGET,这将将潜在的库添加到环境变量中。

  • ament_export_dependencies 将依赖项导出到下游包。这是必要的,这样库的使用者也不必为这些依赖项调用 find_package

警告

从CMake子目录调用``ament_export_targets``、``ament_export_dependencies``或其他ament命令将不会按预期工作。这是因为CMake子目录无法在调用``ament_package``的父范围中设置必要的变量。

注解

Windows 的 DLL 文件被视为运行时构件,并安装到 RUNTIME DESTINATION 文件夹中。因此,在 Unix 基础系统上开发库时,建议保留 RUNTIME 安装选项。

  • INSTALL 调用的 EXPORT 标记需要额外注意:它安装了用于 my_library 目标的 CMake 文件。它的名称必须与 ament_export_targets 中的参数完全相同。为确保它可以通过 ament_target_dependencies 使用,它不应与库名称完全相同,而应该具有类似 export_ 的前缀(如上所示)。

  • 所有安装路径都相对于 CMAKE_INSTALL_PREFIX,这个路径已经由 colcon/ament 正确设置。

还有两个额外的可用函数,但对于基于目标的安装来说是多余的:

ament_export_include_directories("include/${PROJECT_NAME}")
ament_export_libraries(my_library)

第一个宏标记了导出的包含目录的目录。第二个宏标记了已安装库的位置(这是在调用 ament_export_targets 时使用 HAS_LIBRARY_TARGET 参数完成的)。只有在下游项目无法或不想使用基于 CMake 目标的依赖关系时才应使用它们。

一些宏可以接受不同类型的参数用于非目标导出,但由于现代Make的推荐方式是使用目标,我们将不在此处介绍它们。这些选项的文档可以在源代码本身中找到。

在软件包既有库又有可执行文件的情况下,请确保结合上述 "库" 和 "可执行文件" 部分的建议。

代码检查和测试

为了将测试与使用 colcon 构建库分开,将所有对代码检查和测试的调用都包装在条件语句中:

if(BUILD_TESTING)
  find_package(ament_cmake_gtest REQUIRED)
  ament_add_gtest(<tests>)
endif()

代码检查

建议使用`ament_lint_auto <https://github.com/ament/ament_lint/blob/humble/ament_lint_auto/doc/index.rst#ament_lint_auto>`_中的组合调用:

find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()

这将按照``package.xml``中定义的方式运行代码检查工具。建议使用由``ament_lint_common``软件包定义的代码检查工具集。其中包含的各个代码检查工具及其功能可以在`ament_lint_common文档 <https://github.com/ament/ament_lint/blob/humble/ament_lint_common/doc/index.rst>`_中查看。

ament提供的代码检查工具也可以单独添加,而不是运行``ament_lint_auto``。如何添加的一个示例可以在`ament_cmake_lint_cmake文档 <https://github.com/ament/ament_lint/blob/humble/ament_cmake_lint_cmake/doc/index.rst>`_中找到。

测试

Ament包含CMake宏,可以简化设置GTests的过程。调用:

find_package(ament_cmake_gtest)
ament_add_gtest(some_test <test_sources>)

以添加一个GTest。然后,这是一个常规目标,可以与其他库链接(如项目库)。这些宏有额外的参数:

  • APPEND_ENV:追加环境变量。例如,您可以通过调用来添加到ament前缀路径:

find_package(ament_cmake_gtest REQUIRED)
ament_add_gtest(some_test <test_sources>
  APPEND_ENV PATH=some/addtional/path/for/testing/resources)
  • APPEND_LIBRARY_DIRS:追加库文件目录,以便链接器在运行时能够找到它们。这可以通过设置环境变量,如在Windows上设置``PATH``,在Linux上设置``LD_LIBRARY_PATH``来实现,但这样做会使调用与特定平台相关。

  • ENV:设置环境变量(语法与``APPEND_ENV``相同)。

  • TIMEOUT:设置测试的超时时间(以秒为单位)。GTests的默认超时时间为60秒。例如:

ament_add_gtest(some_test <test_sources> TIMEOUT 120)
  • SKIP_TEST:跳过此测试(在控制台输出中将显示为“通过”)。

  • SKIP_LINKING_MAIN_LIBRARIES:不链接GTest。

  • WORKING_DIRECTORY:设置测试的工作目录。

否则,默认工作目录是 CMAKE_CURRENT_BINARY_DIR,在 CMake 文档 中有描述。

类似地,有一个 CMake 宏用于设置 GTest,包括 GMock:

find_package(ament_cmake_gmock REQUIRED)
ament_add_gmock(some_test <test_sources>)

它具有与 ament_add_gtest 相同的附加参数。

扩展 ament

可以使用 ament_cmake 注册附加的宏/函数,并以多种方式进行扩展。

向 ament 添加函数/宏

扩展ament通常意味着您希望其他软件包可以使用某些函数。向客户端软件包提供宏的最佳方法是将其注册到ament中。

可以通过追加``${PROJECT_NAME}_CONFIG_EXTRAS``变量来实现,该变量由``ament_package()``使用,通过以下方式:

list(APPEND ${PROJECT_NAME}_CONFIG_EXTRAS
  path/to/file.cmake"
  other/pathto/file.cmake"
)

或者,您可以直接将文件添加到``ament_package()``调用中:

ament_package(CONFIG_EXTRAS
  path/to/file.cmake
  other/pathto/file.cmake
)

添加到扩展点

除了可以在其他软件包中使用的简单函数文件外,您还可以向ament添加扩展。这些扩展是通过定义扩展点的函数执行的脚本。ament扩展的最常见用例可能是注册rosidl消息生成器:当编写生成器时,通常希望使用生成器生成所有消息和服务,而无需修改消息/服务定义软件包的代码。通过将生成器注册为``rosidl_generate_interfaces``的扩展,可以实现这一点。

例如,参见

ament_register_extension(
  "rosidl_generate_interfaces"
  "rosidl_generator_cpp"
  "rosidl_generator_cpp_generate_interfaces.cmake")

它为软件包 rosidl_generator_cpp 在扩展点 rosidl_generate_interfaces 上注册了宏 rosidl_generator_cpp_generate_interfaces.cmake 。当扩展点被执行时,它将触发在此处执行脚本 rosidl_generator_cpp_generate_interfaces.cmake 。特别地,这将在每次执行函数 rosidl_generate_interfaces 时调用生成器。

除了``rosidl_generate_interfaces``之外,生成器的最重要的扩展点是``ament_package``,它将通过``ament_package()``调用简单地执行脚本。在注册资源时,此扩展点非常有用(请参见下文)。

``ament_register_extension``是一个接受三个参数的函数:

  • extension_point:扩展点的名称(大多数情况下,这将是 ament_packagerosidl_generate_interfaces 中的一个)

  • package_name:包含CMake文件的包的名称(即文件所在的项目的项目名称)

  • cmake_filename:当运行扩展点时执行的CMake文件

注解

可以以类似于 ament_packagerosidl_generate_interfaces 的方式定义自定义扩展点,但这几乎是不必要的。

添加扩展点

非常罕见的情况下,定义一个新的扩展点到ament可能是有趣的。

扩展点可以在宏中注册,以便在调用相应的宏时执行所有扩展。要实现这一点

  • 为您的扩展定义和记录一个名称(例如``my_extension_point``),这个名称将在使用扩展点时传递给``ament_register_extension``宏。

  • 在应执行扩展调用的宏/函数中:

ament_execute_extensions(my_extension_point)

Ament扩展通过定义一个变量来工作,该变量包含扩展点的名称,并用要执行的宏填充该变量。在调用``ament_execute_extensions``时,然后按顺序执行变量中定义的脚本。

添加资源

特别是在开发插件或允许插件的软件包时,通常需要从另一个ROS软件包(例如插件)中添加资源。示例可以是使用pluginlib的工具的插件。

这可以通过使用ament索引(也称为“资源索引”)来实现。

解释ament索引

有关设计和意图的详细信息,请参阅`此处 <https://github.com/ament/ament_cmake/blob/humble/ament_cmake_core/doc/resource_index.md>`__

原则上,ament索引包含在您的软件包的install/share文件夹中的一个文件夹中。它包含以不同类型资源命名的浅层子文件夹。在子文件夹内,每个提供该资源的软件包都以“标记文件”的形式引用。该文件可以包含获取资源所需的任何内容,例如资源的安装目录的相对路径,也可以是空的。

举个例子,考虑为RViz提供显示插件:在名为``my_rviz_displays``的项目中提供RViz插件,这些插件将被pluginlib读取,您将提供一个``plugin_description.xml``文件,该文件将被安装和用于pluginlib来加载插件。为了实现这一点,通过资源索引将plugin_description.xml注册为资源

pluginlib_export_plugin_description_file(rviz_common plugins_description.xml)

运行``colcon build``时,它会将文件``my_rviz_displays``安装到resource_index的子文件夹``rviz_common__pluginlib__plugin``中。rviz_common内的Pluginlib工厂将从所有名为``rviz_common__pluginlib__plugin``的文件夹中收集来自导出插件的软件包的信息。Pluginlib工厂的标记文件包含到``plugins_description.xml``文件的安装文件夹相对路径(以及库的名称作为标记文件名)。有了这些信息,pluginlib可以加载库并从``plugin_description.xml``文件中知道要加载哪些插件。

作为第二个例子,考虑让您自己的RViz插件使用您自己的自定义网格的可能性。网格在启动时加载,这样插件所有者就不必处理它,但这意味着RViz必须知道这些网格。为了实现这一点,RViz提供了一个函数:

register_rviz_ogre_media_exports(DIRECTORIES <my_dirs>)

这将目录注册为ament索引中的ogre_media资源。简而言之,它会将名为该项目的文件安装到名为“rviz_ogre_media_exports”的子文件夹中,该文件调用该函数。该文件包含安装文件夹相对于宏中列出的目录的路径。在启动时,RViz现在可以搜索所有名为“rviz_ogre_media_exports”的文件夹,并加载所提供文件夹中的资源。这些搜索使用``ament_index_cpp``(对于Python软件包,使用``ament_index_py``)进行。

在接下来的几节中,我们将探讨如何将您自己的资源添加到ament索引中,并提供最佳实践。

查询ament索引

如有必要,可以通过CMake查询ament索引中的资源。为此,有三个函数:

ament_index_has_resource:如果以下参数存在资源,则获取资源的前缀路径:

  • var:输出参数:如果资源不存在,请将此变量填充为FALSE,否则填充为资源的前缀路径

  • resource_type:资源的类型(例如``rviz_common__pluginlib__plugin``)

  • resource_name: 资源的名称通常等于添加资源的包的名称,资源的类型为 resource_type(例如 rviz_default_plugins)。

ament_index_get_resource: 获取特定资源的内容,即 ament 索引中标记文件的内容。

  • var: 输出参数,如果存在,将填充资源标记文件的内容。

  • resource_type:资源的类型(例如``rviz_common__pluginlib__plugin``)

  • resource_name: 资源的名称通常等于添加资源的包的名称,资源的类型为 resource_type(例如 rviz_default_plugins)。

  • PREFIX_PATH: 要搜索的前缀路径(通常,默认的 ament_index_get_prefix_path() 就足够了)。

请注意,如果资源不存在,ament_index_get_resource 将抛出错误,因此可能需要使用 ament_index_has_resource 进行检查。

ament_index_get_resources:从索引中获取所有注册了特定类型资源的软件包

  • var:输出参数,包含所有注册了指定资源类型的软件包名称列表

  • resource_type:资源的类型(例如``rviz_common__pluginlib__plugin``)

  • PREFIX_PATH: 要搜索的前缀路径(通常,默认的 ament_index_get_prefix_path() 就足够了)。

添加到 ament 索引

定义资源需要两个信息:

  • 资源的名称必须是唯一的,

  • 标记文件的布局可以是任意的,也可以是空的(例如,用于标记ROS 2包的“package”资源)

对于RViz网格资源,对应的选择是:

  • rviz_ogre_media_exports 作为资源的名称,

  • 安装路径相对于包含资源的所有文件夹的路径。这将使您能够编写使用包中相应资源的逻辑。

为了让用户能够轻松地为您的软件包注册资源,您还应该提供宏或函数,例如 pluginlib 函数或 rviz_ogre_media_exports 函数。

要注册一个资源,请使用 ament 函数 ament_index_register_resource。这将在资源索引中创建和安装标记文件。例如,对于 rviz_ogre_media_exports 的相应调用如下所示:

ament_index_register_resource(rviz_ogre_media_exports CONTENT ${OGRE_MEDIA_RESOURCE_FILE})

这将安装一个名为 ${PROJECT_NAME} 的文件到资源索引中的文件夹 rviz_ogre_media_exports 中,其内容由变量 ${OGRE_MEDIA_RESOURCE_FILE} 提供。该宏有一些可能有用的参数:

  • 第一个(无名称)参数是资源的名称,对应于资源索引中文件夹的名称

  • CONTENT: 标记文件的内容作为字符串。这可以是一个相对路径列表等。CONTENT 不能与 CONTENT_FILE 一起使用。

  • CONTENT_FILE: 用于创建标记文件的文件的路径。该文件可以是一个普通文件或使用 configure_file() 扩展的模板文件。CONTENT_FILE 不能与 CONTENT 一起使用。

  • PACKAGE_NAME:导出资源的软件包/库的名称,即标记文件的名称。默认为 ${PROJECT_NAME}

  • AMENT_INDEX_BINARY_DIR:生成的ament索引的基路径。除非真的有必要,否则始终使用默认值 ${CMAKE_BINARY_DIR}/ament_cmake_index

  • SKIP_INSTALL:跳过安装标记文件。

由于每个软件包只存在一个标记文件,如果同一项目调用了两次CMake函数/宏,则通常会出现问题。然而,对于大型项目,最好将调用注册资源的操作拆分为多次。

因此,最佳实践是让一个注册资源的宏,例如 register_rviz_ogre_media_exports.cmake,仅填充一些变量。然后,在 ament_package 的扩展中添加对 ament_index_register_resource 的真实调用。由于每个项目只能有一次对 ament_package 的调用,因此资源的注册始终只有一个位置。对于 rviz_ogre_media_exports,这就是以下策略:

  • register_rviz_ogre_media_exports 接受一个文件夹列表,并将它们附加到名为 OGRE_MEDIA_RESOURCE_FILE 的变量中。

  • 另一个名为 register_rviz_ogre_media_exports_hook 的宏在 ${OGRE_MEDIA_RESOURCE_FILE} 非空时调用 ament_index_register_resource

  • 通过调用,将 register_rviz_ogre_media_exports_hook.cmake 文件在第三个文件 register_rviz_ogre_media_exports_hook-extras.cmake 中注册为 ament 扩展

ament_register_extension("ament_package" "rviz_rendering"
  "register_rviz_ogre_media_exports_hook.cmake")
  • 文件 register_rviz_ogre_media_exports.cmakeregister_rviz_ogre_media_exports_hook-extra.cmakeament_package() 中被注册为 CONFIG_EXTRA