ament_cmake 用户文档

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

基础知识

可以使用 玫瑰2 创建 <package_name>; 命令行。然后,构建信息会被收集到两个文件中: package.xmlCMakeLists.txt必须在同一目录下。文件名 package.xml 必须包含所有依赖项和一些元数据,以便 colcon 为您的软件包找到正确的构建顺序,在 CI 中安装所需的依赖项,并为使用 盛开.......。 CMakeLists.txt 包含构建和打包可执行文件和库的命令,也是本文档的重点。

项目基本大纲

基本概要 CMakeLists.txt 包中包含的内容:

cmake_minimum_required(版本 3.8)
项目(我的项目)

ament_package()

反对 项目 将是软件包名称,且必须与 package.xml.

项目设置通过以下方式完成 ament_package() 而且每个软件包必须精确调用一次。 ament_package() 安装 package.xml在 ament 索引中注册该软件包,并为 CMake 安装配置文件(可能还有目标文件),以便其他软件包使用 查找软件包.因为 ament_package()CMakeLists.txt 中的最后一次调用。 CMakeLists.txt.

ament_package 可以有额外的参数:

  • CONFIG_EXTRAS:CMake 文件列表 (.cmake.cmake.in 扩展的模板 configure_file()),这些参数应该提供给软件包的客户端。有关何时使用这些参数的示例,请参阅 添加资源.有关如何使用模板文件的更多信息,请参阅 正式文件.

  • 配置临时员额同上 CONFIG_EXTRAS但添加文件的顺序有所不同。虽然 CONFIG_EXTRAS 文件包含在为 ament_export_* 中调用文件 配置临时员额 之后也包括在内。

而不是增加 ament_package还可以在变量 ${project_name}_config_extras${project_name}_config_extras_post 效果相同。唯一的区别还是添加文件的顺序,总顺序如下:

  • CONFIG_EXTRAS

  • 附加到 ${project_name}_config_extras

  • 附加到 ${project_name}_config_extras_post

  • 配置临时员额

编译器和链接器选项

ROS 2 采用符合 C++17 和 C99 标准的编译器。未来可能会有更新的版本,请参考 这里.因此,通常要设置相应的 CMake 标志:

如果(不是 cmake_c_standard)
  设置(cmake_c_standard 99)
endif()
如果(不是 cmake_cxx_standard)
  设置(cmake_cxx_standard 17)
endif()

为了保持代码的整洁,编译器应该对有问题的代码发出警告,并对这些警告进行修正。

建议至少涵盖以下警告级别:

  • 对于 Visual Studio:默认 W1 警告

  • 用于 GCC 和 Clang: -墙 -Wextra -Wpedantic 强烈推荐 -阴影 可取

目前建议使用 添加编译选项 为所有目标添加这些选项。这就避免了所有可执行文件、库和测试中基于目标的编译选项所造成的代码混乱:

如果(cmake_compiler_is_gnucxx  cmake_cxx_compiler_id 比赛 "Clang";)
  添加编译选项(-墙 -Wextra -Wpedantic)
endif()

查找依赖关系

大多数 ament_cmake 项目会依赖其他软件包。在 CMake 中,可以通过调用 查找软件包.例如,如果您的软件包依赖于 rclcppCMakeLists.txt 文件应包含

查找软件包(rclcpp 要求)

备注

永远没有必要 查找软件包 一个库不是明确需要的,但却是另一个明确需要的依赖库的依赖库。如果是这种情况,请针对相应的软件包提交 bug。

添加目标

在 CMake 术语中 目标 是该项目将创建的工件。既可以创建程序库,也可以创建可执行文件,一个项目可以包含零个或多个可执行文件。

通过调用 add_library,其中应包含目标文件和为创建库而应编译的源文件的名称。

在 C/C++ 中,由于头文件和实现是分离的,因此通常无需将头文件作为参数添加到 add_library.

建议采用以下最佳做法:

  • 将本程序库客户端应该使用的所有头文件(因此必须安装)放入 包括 文件夹,而所有其他文件 (.c/.cpp 以及不应导出的头文件)都在 来源 小册子

  • 只有 .c/.cpp 在调用 add_library

  • 为您的资料库查找页眉 我的图书馆 经由

目标包含目录(我的图书馆
  公众
    "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>";
    "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>";)

这会添加文件夹 ${CMAKE_CURRENT_SOURCE_DIR}/include 的所有文件(相对于......)。 ${cmake_install_dir})安装时。

玫瑰2 创建 创建一个遵循这些规则的软件包布局。

备注

由于 Windows 是官方支持的平台之一,因此任何软件包都应构建在 Windows 平台上,这样才能发挥最大作用。Windows 库格式强制执行符号可见性;也就是说,每个应从客户端使用的符号都必须由库显式导出(符号需要隐式导入)。

由于 GCC 和 Clang 编译通常不会这样做,因此建议使用 GCC 维基百科.将其用于名为 我的图书馆:

  • 将链接中的逻辑复制到一个名为 visibility_control.hpp.

  • 更换 DLL我的资料库 (的可见性控制为例 rviz_rendering).

  • 对所有需要导出的符号(如类或函数)使用宏 "MY_LIBRARY_PUBLIC"。

  • 在该项目中 CMakeLists.txt 使用:

    目标编译定义(我的图书馆 私人 "MY_LIBRARY_BUILDING_LIBRARY";)
    

更多详情,请参阅 Windows 提示和技巧文档中的 Windows 符号可见性.

如果软件包既有程序库又有可执行文件,请务必将上述 "程序库 "和 "可执行文件 "中的建议结合起来。

链接到依赖项

有两种方法可以将目标与依赖关系联系起来。

第一种也是推荐的方法是使用 ament 宏 ament_target_dependencies.例如,假设我们要将 我的图书馆 针对线性代数库 Eigen3。

查找软件包(Eigen3 要求)
ament_target_dependencies(我的图书馆 公众 Eigen3)

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

第二种方法是使用 目标链接库.

现代 CMake 更倾向于只使用目标,并根据目标进行导出和链接。CMake 目标可能是命名的,类似于 C++。如果有命名间隔的目标,请优先使用它们。例如 Eigen3 定义了目标 Eigen3::Eigen.

以 Eigen3 为例,调用过程如下

目标链接库(我的图书馆 公众 Eigen3::Eigen)

这还将包括必要的头文件、库及其依赖项。请注意,该依赖必须是之前通过调用 查找软件包.

安装

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

首先,安装客户端应该可以使用的头文件。include 目录是自定义的,以支持在 胶管https://colcon.readthedocs.io/en/released/user/overriding-packages.html#install-headers-to-a-unique-include-directory 了解更多信息。

安装(
  目录 包括
  目的地 包括${项目名称}
)

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

安装(
  目标 我的图书馆
  出口 出口_${项目名称}
  图书馆 目的地 lib
  存档 目的地 lib
  运行时间 目的地 箱柜
)

输出目标(出口_${项目名称} has_library_target)
ament_export_dependencies(某种依赖)

下面是上面片段中发生的情况:

  • "(《世界人权宣言》) 输出目标 宏为 CMake 导出目标。这对于允许库的客户端使用 target_link_libraries(client 私人 my_library::my_library) 语法如果导出集包含一个库,则添加选项 has_library_target输出目标,将潜在的库添加到环境变量中。

  • "(《世界人权宣言》) ament_export_dependencies 向下游软件包导出依赖关系。这是必要的,这样库的用户就不必调用 查找软件包 这些依赖项。

警告

呼叫 输出目标, ament_export_dependencies或其他来自 CMake 子目录的命令将无法正常工作。这是因为 CMake 子目录无法在父作用域中设置必要的变量。 ament_package 被称为

备注

Windows 动态链接库被视为运行时工件,并安装到 运行时间 目的地 文件夹。因此,建议将 运行时间 即使在基于 Unix 的系统上开发库时,也可以安装。

  • "(《世界人权宣言》) 出口 安装调用的符号需要额外注意:它为 我的图书馆 目标。它的名称必须与 输出目标.为了确保可以通过 ament_target_dependencies,它的名称不应与库名完全相同,而应有一个前缀,如 出口_ (如上图所示)。

  • 所有安装路径都相对于 cmake_install_prefix已由 colcon/ament 正确设置。

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

ament_export_include_directories("include/${PROJECT_NAME}";)
AEMENT_EXTORT_LIBRARY(我的图书馆)

第一个宏标记了导出的 include 目录。第二个宏标记了已安装库的位置(由 has_library_target 调用 输出目标).只有当下游项目不能或不想使用基于 CMake 目标的依赖关系时,才应使用这些依赖关系。

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

如果软件包既有程序库又有可执行文件,请务必将上述 "程序库 "和 "可执行文件 "中的建议结合起来。

着色和测试

为了将测试与使用 colcon 构建库分开,请将所有对衬垫和测试的调用封装在一个条件中:

如果(构建测试)
  查找软件包(ament_cmake_gtest 要求)
  添加测试(测试)
endif()

林亭

建议使用来自 自动:

查找软件包(自动 要求)
ament_lint_auto_find_test_dependencies()

将运行 package.xml.建议使用由软件包 ament_lint_common.其中包含的单个衬纸及其功能可参见 ament_lint_common docs.

也可以单独添加由 ament 提供的流化床,而不是运行 自动.其中一个例子是 ament_cmake_lint_cmake 文档.

测试

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

查找软件包(ament_cmake_gtest)
添加测试(some_test <test_sources>;)

添加一个 GTest。这样,它就成为一个常规目标,可以与其他库(如项目库)进行链接。宏有附加参数:

  • APPEND_ENV:附加环境变量。例如,你可以通过调用

查找软件包(ament_cmake_gtest 要求)
添加测试(some_test <test_sources>;
  APPEND_ENV PATH=some/addtional/path/for/testing/resources)
  • 添加库文件在运行时,链接器可以找到这些库。这可以通过设置环境变量来实现,如 路径 在 Windows 和 ld_library_path 但这使得调用具有平台特性。

  • 环境:设置环境变量(语法与 APPEND_ENV).

  • 超时:设置测试超时(秒)。GTests 的默认值为 60 秒。例如

添加测试(some_test <test_sources>; 超时 120)
  • SKIP_TEST:跳过此测试(在控制台输出中将显示为 "通过")。

  • 跳过链接主库:不要链接 GTest。

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

否则,默认工作目录是 cmake_current_binary_dirCMake 文档.

同样,有一个 CMake 宏可以设置 GTest(包括 GMock):

查找软件包(ament_cmake_gmock 要求)
ament_add_gmock(some_test <test_sources>;)

它的附加参数与 添加测试.

扩展活动

可以使用 ament_cmake 并以多种方式加以扩展。

在内容中添加函数/宏

扩展 ament 通常意味着您希望将某些功能提供给其他软件包。向客户端软件包提供宏的最佳方法是向 ament 注册。

可以通过添加 ${project_name}_config_extras 变量,该变量被 ament_package() 经由

清单(附录 ${项目名称}_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 消息生成器:在编写生成器时,通常希望在不修改消息/服务定义包代码的情况下,也能用生成器生成所有消息和服务。这可以通过将生成器注册为 生成接口.

例如

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

注册宏 rosidl_generator_cpp_generate_interfaces.cmake 为软件包 rosidl_generator_cpp 到延伸点 生成接口.当扩展点被执行时,将触发脚本的执行 rosidl_generator_cpp_generate_interfaces.cmake 这里。特别是,每当函数 生成接口 被执行。

发电机最重要的扩展点,除了 生成接口ament_package的脚本,它将简单地执行带有 ament_package() 调用。该扩展点在注册资源时非常有用(见下文)。

ament_register_extension 是一个包含三个参数的函数:

  • 扩展点:扩展点的名称(大多数情况下,它将是 ament_package生成接口)

  • 包名:包含 CMake 文件的软件包名称(即文件被写入的项目名称)

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

备注

可以用类似于 ament_package生成接口但这几乎没有必要。

添加扩展点

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

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

  • 定义并记录扩展名(例如 我的扩展点),它是传递给 ament_register_extension 宏。

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

ament_execute_extensions(我的扩展点)

Ament 扩展的工作方式是定义一个包含扩展点名称的变量,并在其中填入要执行的宏。调用 ament_execute_extensions然后,变量中定义的脚本将被逐个执行。

添加资源

特别是在开发插件或允许使用插件的软件包时,经常需要将资源从另一个 ROS 软件包(如插件)添加到另一个软件包中。使用插件库的工具插件就是一个例子。

这可以通过调整指数(也称为 "资源指数")来实现。

解释了补偿指数

有关设计和意图的详细信息,请参见 这里

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

举个例子,考虑为 RViz 提供显示插件:在一个名为 my_rviz_displays 将被插件库读取。 plugin_description.xml 文件,pluginlib 将安装并使用该文件加载插件。为此,plugin_description.xml 将作为资源注册到 resource_index 中。

pluginlib_export_plugin_description_file(rviz_common plugins_description.xml)

运行时 胶管 构建,这会安装一个文件 my_rviz_displays 子文件夹 rviz_common__pluginlib__plugin 到 resource_index 中。rviz_common 中的 Pluginlib 工厂将知道从所有名为 rviz_common__pluginlib__plugin 用于导出插件的软件包。pluginlib 工厂的标记文件包含一个安装文件夹的相对路径,该路径指向 plugins_description.xml 文件(以及作为标记文件名的程序库名称)。有了这些信息,pluginlib 就能加载程序库,并知道要从 plugin_description.xml 锉刀

作为第二个例子,请考虑让您自己的 RViz 插件使用自定义网格的可能性。网格会在启动时加载,因此插件所有者无需处理,但这意味着 RViz 必须知道网格。为此,RViz 提供了一个函数:

register_rviz_ogre_media_exports(目录 <my_dirs>;)

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

在下面的章节中,我们将探讨如何将自己的资源添加到 ament 索引中,并提供相关的最佳实践。

查询ment索引

如有必要,可通过 CMake 查询资源的 ament 索引。为此,有三种功能:

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

  • 变异:输出参数:如果资源不存在,则在该变量中填入 FALSE,否则填入资源的前缀路径

  • 资源类型:资源类型(例如 rviz_common__pluginlib__plugin)

  • 资源名称:资源的名称,通常相当于添加了 resource_type 类型资源的软件包的名称(例如:"......")。 rviz_default_plugins)

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

  • 变异输出参数:如果存在资源标记文件,则用该文件的内容填充。

  • 资源类型:资源类型(例如 rviz_common__pluginlib__plugin)

  • 资源名称:资源的名称,通常相当于添加了 resource_type 类型资源的软件包的名称(例如:"......")。 rviz_default_plugins)

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

请注意 ament_index_get_resource 如果资源不存在,就会出错,因此可能需要使用 ament_index_has_resource.

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

  • 变异:输出参数:填写所有注册了 resource_type 资源的软件包名称列表

  • 资源类型:资源类型(例如 rviz_common__pluginlib__plugin)

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

增加娱乐指数

定义一个资源需要两个信息位:

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

  • 标记文件的布局,可以是任何内容,也可以是空(例如,标记 ROS 2 软件包的 "软件包 "资源就是如此)。

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

  • rviz_ogre_media_exports 作为资源名称、

  • install path 包含资源的所有文件夹的相对路径。这样,您就可以在软件包中编写使用相应资源的逻辑。

为方便用户为软件包注册资源,您应进一步提供宏或函数,如 pluginlib 函数或 rviz_ogre_media_exports 功能。

要注册资源,请使用 ament 功能 ament_index_register_resource.这将创建并安装 resource_index 中的标记文件。例如,调用 rviz_ogre_media_exports 具体如下

ament_index_register_resource(rviz_ogre_media_exports 内容 ${媒体资源文件})

这会安装一个名为 ${project_name} 项目名称 文件夹中 rviz_ogre_media_exports 到 resource_index 中,其内容由变量 ${ogre_media_resource_file}(媒体资源文件.该宏有许多有用的参数:

  • 第一个(未命名)参数是资源名称,相当于 resource_index 中的文件夹名称

  • 内容:标记文件的内容字符串。可以是相对路径列表等。 内容 不能与 内容文件.

  • 内容文件:用于创建标记文件的文件路径。该文件可以是普通文件,也可以是用 configure_file(). 内容文件 不能与 内容.

  • 软件包名称:导出资源的软件包/库的名称,相当于标记文件的名称。默认为 ${project_name} 项目名称.

  • ament_index_binary_dir:生成索引的基本路径。除非确有必要,否则始终使用默认的 ${CMAKE_BINARY_DIR}/ament_cmake_index.

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

由于每个软件包只存在一个标记文件,如果 CMake 函数/宏被同一项目调用两次,通常不会有问题。不过,对于大型项目来说,最好还是将注册资源的调用拆分开来。

因此,最佳做法是让宏注册一个资源,如 register_rviz_ogre_media_exports.cmake 只填充一些变量。真正调用 ament_index_register_resource 然后可以在扩展名中添加 ament_package.由于只能调用一次 ament_package 在每个项目中,只有一个地方可以注册资源。在 rviz_ogre_media_exports 这相当于以下战略:

  • 宏观 register_rviz_ogre_media_exports 将文件夹列表追加到一个名为 媒体资源文件.

  • 另一个宏名为 register_rviz_ogre_media_exports_hook 电话 ament_index_register_resource 如果 ${ogre_media_resource_file}(媒体资源文件 是非空的。

  • "(《世界人权宣言》) register_rviz_ogre_media_exports_hook.cmake 文件在第三个文件中注册为扩展名 register_rviz_ogre_media_exports_hook-extras.cmake 通过呼叫

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.cmake 登记为 CONFIG_EXTRAament_package().