ament_cmake user documentation — ROS 2 Documentation: Foxy documentation
在ros1的编译系统为catkin_make;为了在ros2与ros1进行区分,这里用名称为ament_cmake其意义都是编译操作。本文阐述其应用的规则和注意点。
( 了解 CMake 的基础知识将非常有帮助,可以在此处找到官方教程。)
可以在命令行上使用 ros2 pkg create
ament 包的 CMakeLists.txt 的基本大纲包含:
- cmake_minimum_required(VERSION 3.5)
- project(my_project)
-
- ament_package()
project 的参数将是包名,并且必须与 package.xml 中的包名相同。
项目设置由 ament_package() 完成,并且每个包必须只发生一次此调用。 ament_package() 安装 package.xml,使用 ament 索引注册包,并为 CMake 安装配置(可能还有目标)文件,以便其他包可以使用 find_package 找到它。由于 ament_package() 从 CMakeLists.txt 中收集了大量信息,因此它应该是 CMakeLists.txt 中的最后一次调用。虽然可以通过调用 install 复制文件和目录的函数来跟踪对 ament_package() 的调用,但将 ament_package() 保留在最后一次调用更简单。
ament_package 可以被赋予额外的参数:
除了添加到 ament_package 之外,您还可以添加到变量 ${PROJECT_NAME}_CONFIG_EXTRAS 和 ${PROJECT_NAME}_CONFIG_EXTRAS_POST 具有相同的效果。唯一的区别还是添加文件的顺序,总顺序如下:
编译要构建的主要目标有两个:分别由 add_library 连接库和 add_executable 构建的可执行文件。
由于头文件和 C/C++ 中的实现分离,并不总是需要将这两个文件作为参数添加到 add_library/add_executable。
- target_include_directories(my_target
- PUBLIC
- $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
- $<INSTALL_INTERFACE:include>)
这会在构建期间将文件夹 ${CMAKE_CURRENT_SOURCE_DIR}/include 中的所有文件添加到公共界面,并在安装时添加包含文件夹中的所有文件(相对于 ${CMAKE_INSTALL_DIR})。
原则上,如果两个文件夹都被称为 include 并且相对于${CMAKE_CURRENT_SOURCE_DIR} 和
${CMAKE_INSTALL_DIR} 处于顶层,则不需要在此处使用生成器表达式,但它很常见。
有两种方法可以将你的包链接到一个新的依赖项。
第一种也是推荐的方法是使用 ament 宏 ament_target_dependencies。例如,假设我们要将 my_target 与线性代数库 Eigen3 链接起来。
- find_package(Eigen3 REQUIRED)
- ament_target_dependencies(my_target Eigen3)
它包括项目正确找到的必要头文件和库及其依赖项。它还将确保在使用覆盖工作空间时所有依赖项的包含目录都正确排序。
第二种方法是使用 target_link_libraries。
现代 CMake 中推荐的方法是只使用目标,导出和链接它们。 CMake 目标是命名空间的,类似于 C++。例如,Eigen3 定义了目标 Eigen3::Eigen。
至少在 ament_target_dependencies 宏中不支持 Crystal Clemmys 目标名称之前。有时需要调用 target_link_libaries CMake 函数。在 Eigen3 的示例中,调用应如下所示
- find_package(Eigen3 REQUIRED)
- target_link_libraries(my_target Eigen3::Eigen)
这还将包括必要的标头、库及其依赖项,但与 ament_target_dependencies 相比,它在使用覆盖工作区时可能无法正确排序依赖项。
注意:永远不需要 find_package 一个没有显式需要的库,但它是另一个显式需要的依赖项的依赖项。如果是这种情况,将针对相应的包提交错误。(也就是说,本项目显式依赖于A,而A隐式
依赖于B,那么没有必要将B列入)
在构建可重用库时,需要导出一些信息以供下游包轻松使用。
- ament_export_targets(my_libraryTargets HAS_LIBRARY_TARGET)
- ament_export_dependencies(some_dependency)
-
- install(
- DIRECTORY include/
- DESTINATION include
- )
-
- install(
- TARGETS my_library
- EXPORT my_libraryTargets
- LIBRARY DESTINATION lib
- ARCHIVE DESTINATION lib
- RUNTIME DESTINATION bin
- INCLUDES DESTINATION include
- )
在这里,我们假设文件夹 include 包含需要导出的标题。请注意,不必将所有标题放入单独的文件夹中,只需将那些应由客户端包含的标题放入。
这是上面代码段中发生的事情:
从 CMake 子目录调用 ament_export_targets、ament_export_dependencies 或其他 ament 命令将无法按预期工作。这是因为 CMake 子目录无法在调用 ament_package 的父范围内设置必要的变量。这如何办?
最后一个大型安装命令安装库。存档和库文件将导出到 lib 文件夹,运行时二进制文件将安装到 bin 文件夹,安装头文件的路径是 include。
Windows DLL 被视为运行时工件并安装到 RUNTIME DESTINATION 文件夹中。因此建议即使在基于 Unix 的系统上开发库时也不要遗漏 RUNTIME 安装。
有两个附加功能可以使用,但对于基于目标的安装来说是多余的:
- ament_export_include_directories(include)
- ament_export_libraries(my_library)
第一个宏标记导出的包含目录的目录(这是通过目标安装调用中的 INCLUDES DESTINATION 实现的)。第二个宏标记已安装库的位置(这是通过调用 ament_export_targets 中的 HAS_LIBRARY_TARGET 参数完成的)。
一些宏可以为非目标导出采用不同类型的参数,但由于现代 Make 的推荐方法是使用目标,我们不会在这里介绍它们。这些选项的文档可以在源代码本身中找到。
ROS 2 针对至少在 Crystal Clemmys 之前符合 C++14 和 C99 标准的编译器。较新的版本可能会在未来成为目标,并在此处引用。因此习惯上设置相应的 CMake 标志:
- if(NOT CMAKE_C_STANDARD)
- set(CMAKE_C_STANDARD 99)
- endif()
- if(NOT CMAKE_CXX_STANDARD)
- set(CMAKE_CXX_STANDARD 14)
- endif()
为了保持代码干净,编译器应该对有问题的代码发出警告,并且应该修复这些警告。
建议至少覆盖以下警告级别:
尽管现代 CMake 建议在目标基础上添加编译器标志,即调用
target_compile_options(my_target PRIVATE -Wall)
目前建议使用目录级函数 add_compile_options(-Wall),以免所有可执行文件和测试的基于目标的编译选项使代码混乱。
由于 Linux、Mac 和 Windows 都是官方支持的平台,因此任何软件包都应该在 Windows 上构建以产生最大影响。 Windows 库格式强制符号可见性:应该从客户端使用的每个符号都必须由库显式导出(并且数据符号需要隐式导入)。
为了保持它与 Clang 和 GCC 构建兼容,建议使用 GCC wiki 中的逻辑。要将它用于名为 my_library 的包:
target_compile_definitions(my_library PRIVATE "MY_LIBRARY_BUILDING_LIBRARY")
详细文档: Windows Symbol Visibility in the Windows Tips and Tricks document.
为了将测试与使用 colcon 构建库分开,请将所有对 linter 和测试的调用包装在一个条件中:
- if(BUILD_TESTING)
- find_package(ament_cmake_gtest REQUIRED)
- ament_add_gtest(
) - endif()
1)语义检测Linting
Linting 代码的本意就是找出程序中的错误,这些错误包括潜在的语法错误,编译错误,拼写错误等。
- find_package(ament_lint_auto REQUIRED)
- ament_lint_auto_find_test_dependencies()
这将运行 package.xml 中定义的 linter。建议使用包 ament_lint_common 定义的 linter 集。其中包含的各个 linter 及其功能可以在 ament_lint_common 文档中看到。
ament 提供的 Linter 也可以单独添加,而不是运行 ament_lint_auto。可以在 ament_cmake_lint_cmake 文档中找到如何执行此操作的一个示例。
2 测试(testing)
Ament 包含 CMake 宏以简化 GTest 的设置。称呼:
- find_package(ament_cmake_gtest)
- ament_add_gtest(some_test
)
添加 GTest。这是一个可以链接到其他库(例如项目库)的常规目标。宏具有附加参数:
APPEND_ENV:附加环境变量。例如,您可以通过调用添加到 ament 前缀路径:
- find_package(ament_cmake_gtest REQUIRED)
- ament_add_gtest(some_test
- APPEND_ENV PATH=some/addtional/path/for/testing/resources)
ament_add_gtest(some_test <test_sources> TIMEOUT 120)
否则,默认工作目录是 CMAKE_SOURCE_DIR,它将被评估为顶级 CMakeLists.txt 的目录。
同样,有一个 CMake 宏来设置 GTest,包括 GMock:
- find_package(ament_cmake_gmock REQUIRED)
- ament_add_gmock(some_test
)
它具有与 ament_add_gtest 相同的附加参数。
可以使用 ament_cmake 注册其他宏/函数并以多种方式对其进行扩展。
1)将function/macro添加到 ament编译工程
扩展 ment 通常意味着您希望某些功能可用于其他包。将宏提供给客户端包的最佳方式是将其注册到 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
- )
2) 添加到扩展点
除了具有可以在其他包中使用的功能的简单文件之外,您还可以在 ament.xml 中添加扩展名。这些扩展是使用定义扩展点的函数执行的脚本。 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_package 或 rosidl_generate_interfaces 之一)
package_name:包含 CMake 文件的包的名称(即文件写入的项目的项目名称)
cmake_filename:运行扩展点时执行的 CMake 文件
| 注意 |
|---|
可以以与 ament_package 和 rosidl_generate_interfaces 类似的方式定义自定义扩展点,但这几乎没有必要。 |
2)添加扩展点
在极少数情况下,为ment 定义一个新的扩展点可能会很有趣。
可以在宏中注册扩展点,以便在调用相应的宏时执行所有扩展。为此:
ament_execute_extensions(my_extension_point)
Ament 扩展通过定义一个包含扩展点名称的变量并用要执行的宏填充它来工作。在调用 ament_execute_extensions 时,变量中定义的脚本会一个接一个地执行。
特别是在开发插件或允许插件的包时,通常必须从另一个 ROS 包(例如插件)中添加资源。示例可以是使用 pluginlib 的工具的插件。
这可以使用ment 索引(也称为“资源索引”)来实现。
1)Ament 指数说明
有关设计和意图的详细信息,请参见此处 原则上,ment 索引包含在包的 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 的文件夹中为导出插件的包收集信息。 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)完成的。
在以下部分中,我们将探讨如何将您自己的资源添加到 amment 索引中,并提供这样做的最佳实践。
2)查询ment索引
如有必要,可以通过 CMake 查询资源索引。为此,有以下三个功能:
ament_index_has_resource:如果资源存在,则使用以下参数获取资源的前缀路径:
var:输出参数:如果资源不存在,则使用 FALSE 填充此变量,否则使用资源的前缀路径
resource_type:资源的类型(例如 rviz_common__pluginlib__plugin)
resource_name:资源的名称,通常相当于添加了resource_type类型资源的包的名称(例如rviz_default_plugins)
ament_index_get_resource:获取特定资源的内容,即amet索引中标记文件的内容。
请注意,如果资源不存在,ament_index_get_resource 将引发错误,因此可能需要使用 ament_index_has_resource 进行检查。
ament_index_get_resources:从索引中获取所有注册了特定类型资源的包
3)添加到ment索引
定义一个资源需要两个信息:
对于 RViz 网格资源,相应的选择是:
为了让用户可以轻松地为您的包注册资源,您还应该提供宏或函数,例如 pluginlib 函数或 rviz_ogre_media_exports 函数。 要注册资源,请使用 amment 函数 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 到 resource_index 中,其内容由变量 ${OGRE_MEDIA_RESOURCE_FILE} 给出。该宏有许多有用的参数:
由于每个包只存在一个标记文件,如果 CMake 函数/宏被同一个项目调用两次,通常会出现问题。但是,对于大型项目,最好将调用注册资源分开。
因此,最佳实践是让注册资源的宏(例如 register_rviz_ogre_media_exports.cmake)只填充一些变量。然后可以在 ament_package 的 ament 扩展中添加对 ament_index_register_resource 的真正调用。由于每个项目只能调用一次 ament_package,因此资源注册的位置总是只有一个。在 rviz_ogre_media_exports 的情况下,这相当于以下策略:
- ament_register_extension("ament_package" "rviz_rendering"
- "register_rviz_ogre_media_exports_hook.cmake")