ament_cmake 用户文档

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

基础知识

可以使用命令行上的“ros2 pkg create <package_name>”生成基本的 CMake 大纲。 然后将构建信息收集到两个文件中:“package.xml”和“CMakeLists.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 文件列表(.cmake.cmake.in 模板,由 configure_file() 扩展),这些文件应该可供包的客户端使用。

有关何时使用这些参数的示例,请参阅 添加资源 中的讨论。 有关如何使用模板文件的更多信息,请参阅 ``官方文档 <https://cmake.org/cmake/help/v3.8/command/configure_file.html>`__。

  • 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 标准的编译器。 未来可能会针对较新的版本,请参阅`此处 <https://www.ros.org/reps/rep-2000.html>`__。 因此,通常设置相应的 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)

Note

永远没有必要“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”中的所有文件添加到公共接口,并在安装时将包含文件夹(相对于“${CMAKE_INSTALL_DIR}”)中的所有文件添加到公共接口。

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

Note

由于 Windows 是官方支持的平台之一,为了发挥最大作用,任何软件包也应在 Windows 上构建。 Windows 库格式强制符号可见性;也就是说,客户端应使用的每个符号都必须由库明确导出(并且符号需要隐式导入)。 由于 GCC 和 Clang 构建通常不这样做,建议使用 GCC wiki 中的逻辑。 要将其用于名为 my_library 的软件包:

有关详细信息,请参阅 Windows Symbol Visibility in the Windows Tips and Tricks document.

链接到依赖项

有两种方法可以将目标与依赖项链接起来。

第一种也是推荐的方法是使用 ament 宏“ament_target_dependencies”。 例如,假设我们想将“my_library”与线性代数库 Eigen3 链接起来。

find_package(Eigen3 REQUIRED)
ament_target_dependencies(my_library PUBLIC Eigen3)

它包括必要的头文件和库以及项目可以正确找到的依赖项。

第二种方法是使用“target_link_libraries”。

现代 CMake 倾向于仅使用目标,导出并链接它们。 CMake 目标可能具有命名空间,类似于 C++。 如果可用,则优先使用命名空间目标。 例如,“Eigen3”定义目标“Eigen3::Eigen”。

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

target_link_libraries(my_library PUBLIC Eigen3::Eigen)

这还将包括必要的标头、库及其依赖项。
请注意,必须事先通过调用“find_package”发现此依赖项。

安装

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

首先,安装客户端可用的头文件。 include 目录是自定义的,以支持“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) 语法是必要的。 如果导出集包含库,请将选项 HAS_LIBRARY_TARGET 添加到 ament_export_targets,这会将潜在库添加到环境变量中。

  • ament_export_dependencies 将依赖项导出到下游包。

这样做是必要的,这样库的用户就不必为这些依赖项调用 find_package

从 CMake 子目录调用 ament_export_targetsament_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 正确设置。

有两个附加功能可用,但对于基于目标的安装来说是多余的: .. code-block:: cmake

ament_export_include_directories(“include/${PROJECT_NAME}”) ament_export_libraries(my_library)

第一个宏标记导出的包含目录的目录。

第二个宏标记已安装库的位置(这是通过调用“ament_export_targets”中的“HAS_LIBRARY_TARGET”参数完成的)。 仅当下游项目不能或不想使用基于 CMake 目标的依赖项时才应使用这些宏。

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

安装可执行文件时,*必须严格遵循*以下节,以便其余 ROS 工具找到它:

install(TARGETS my_exe
    DESTINATION lib/${PROJECT_NAME})

如果包中同时包含库和可执行文件,请确保结合上述“库”和“可执行文件”的建议。

代码检查和测试

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

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

Linting

建议使用 ament_lint_auto 的组合调用:

find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()

  这将运行 ``package.xml`` 中定义的 linters。
  建议使用包 ``ament_lint_common`` 定义的 linters 集。
  其中包含的各个 linters 及其功能可在 ``ament_lint_common 文档 <https://github.com/ament/ament_lint/blob/rolling/ament_lint_common/doc/index.rst>`_ 中查看。

  还可以单独添加 ament 提供的 linters,而不是运行 ``ament_lint_auto``。
  有关如何执行此操作的一个示例可在 ``ament_cmake_lint_cmake 文档 <https://github.com/ament/ament_lint/blob/rolling/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 宏可以设置包括 GMock 的 GTest:

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_generator_cpp_generate_interfaces.cmake”注册到扩展点“rosidl_generate_interfaces”。 当执行扩展点时,这将触发此处脚本“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_package”和“rosidl_generate_interfaces”的方式定义自定义扩展点,但这几乎没有必要。

添加扩展点

在极少数情况下,为 ament 定义一个新的扩展点可能会很有趣。

扩展点可以在宏中注册,以便在调用相应的宏时执行所有扩展。 这样做:

  • 定义并记录扩展的名称(例如“my_extension_point”),这是使用扩展点时传递给“ament_register_extension”宏的名称。

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

ament_execute_extensions(my_extension_point)

Ament 扩展通过定义一个包含扩展点名称的变量并用要执行的宏填充它来工作。

调用“ament_execute_extensions”后,变量中定义的脚本将一个接一个地执行。

添加资源

特别是在开发插件或允许插件的包时,通常需要将资源从另一个 ROS 包(例如插件)添加到一个 ROS 包中。

示例可以是使用插件库的工具插件。

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

ament 索引说明

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

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

举个例子,考虑为 RViz 提供显示插件: 当在名为“my_rviz_displays”的项目中提供 RViz 插件时,该插件将由插件库读取,您将提供一个“plugin_description.xml”文件,该文件将由插件库安装和使用以加载插件。 为了实现这一点,plugin_description.xml 通过以下方式在 resource_index 中注册为资源

pluginlib_export_plugin_description_file(rviz_common plugins_description.xml)

运行“colcon build”时,这会将文件“my_rviz_displays”安装到 resource_index 中的子文件夹“rviz_common__pluginlib__plugin”中。 rviz_common 中的插件库工厂将知道从所有名为“rviz_common__pluginlib__plugin”的文件夹中收集导出插件的包的信息。 插件库工厂的标记文件包含指向“plugins_description.xml”文件的安装文件夹相对路径(以及作为标记文件名的库名称)。 有了这些信息,插件库可以加载库并知道从“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 资源的包的名称列表

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

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

添加到 ament 索引

定义资源需要两条信息:

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

  • 标记文件的布局,可以是任何内容,也可以是空的(例如,对于标记 ROS 2 包的“包”资源,情况就是如此)

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

  • rviz_ogre_media_exports 作为资源名称,

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

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

要注册资源,请使用 ament 函数 ament_index_register_resource。 这将在 resource_index 中创建并安装标记文件。 例如,“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_index_register_resource”的真正调用添加到“ament_package”的 ament 扩展中。 由于每个项目只能调用一次“ament_package”,因此资源注册的位置始终只有一个。 对于“rviz_ogre_media_exports”,这相当于以下策略:

  • 宏“register_rviz_ogre_media_exports”获取文件夹列表并将它们附加到名为“OGRE_MEDIA_RESOURCE_FILE”的变量中。

  • 如果“${OGRE_MEDIA_RESOURCE_FILE}”非空,则另一个名为“register_rviz_ogre_media_exports_hook”的宏将调用“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.cmake”和“register_rviz_ogre_media_exports_hook-extra.cmake”通过“ament_package()”注册为“CONFIG_EXTRA”。