- 嵌入式
- 系统编程
【免费下载链接】fprime
F´ - A flight software and embedded systems framework
F´(F Prime)是一个面向飞行软件与嵌入式系统的开源框架,其构建系统基于 CMake 构建。本文围绕官方用户指南中的 Targets 文档展开,系统讲解 F´ 构建系统中"构建目标(Targets)"子系统的核心机制:它如何以"全局目标 + 模块级目标"双层结构组织构建动作,仓库内置了哪些标准目标,以及如何通过register_fprime_target注册自定义目标。读完本文,你将掌握make <target>与make <MODULE>_<target>两种调用方式的使用场景、内置目标的实现原理,并能独立编写一个可在全局与每个模块上同时生效的自定义构建目标。
一、什么是 F´ 的构建目标子系统
在 F´ 的 CMake 构建体系中,"目标(Targets)"指的是构建系统向用户暴露的可执行动作入口。除了 CMake 本身为每个库(library)、可执行文件(executable)生成的构建产物目标外,F´ 额外提供了一套"自定义动作目标"的注册机制,让构建系统能够承载与编译产物无关的功能性任务。
官方文档(Targets.md)给出了一个典型动机:用户可能希望创建一个统计系统内文件总数的构建目标。这种目标不属于某个可执行文件或库的编译产物,而是独立的功能入口——这正是 Targets 子系统要解决的需求:让用户把任意新功能注册进构建系统,并通过make直接触发。
Targets 子系统在 cmake/target/target.cmake 中被描述为"允许针对模块与顶层目标进行构建的多部分构建(multi-part builds)",典型的例子如sloc(代码行数统计)或dict(字典生成)——它们一部分动作作用于每个模块,最终再汇入某个全局命令。官方文档将这种机制概括为:
Targets are applied both at the global scope and per-module scope. Thus each target can provide a set of build targets (one per registered module) and a global build target.
即每个注册的 target 文件通常产生两类入口:
- 全局目标(global target):例如
count,作用于整个项目; - 模块级目标(module target):例如
<MODULE>_count,作用于某个具体模块。
对于生成 GNU make 文件的工程,执行方式为make <target>与make <MODULE>_<target>。文档给出的具体示例为make Svc_CmdDispatcher_coverage——即对Svc_CmdDispatcher模块执行coverage目标。
二、内置标准目标(Built-In Targets)
F´ 的 CMake 系统内置了若干对所有项目都有用的标准目标,由 cmake/FPrime.cmake 中的fprime_setup_standard_targets宏统一注册。该宏的实现如下:
macro(fprime_setup_standard_targets) if (NOT DEFINED FPRIME_SUB_BUILD_TARGETS) register_fprime_target(target/build) register_fprime_build_autocoder(autocoder/fpp OFF) register_fprime_build_autocoder(autocoder/ai_xml OFF) register_fprime_build_autocoder(autocoder/packets OFF) register_fprime_target(target/version) register_fprime_target(target/install) register_fprime_ut_target(target/ut) if (FPRIME_ENABLE_UTIL_TARGETS) register_fprime_target(target/refresh_cache) register_fprime_ut_target(target/check) endif() endif() endmacro(fprime_setup_standard_targets)每个内置目标对应cmake/target/目录下的一个.cmake文件,见 cmake/target 目录,包括build.cmake、version.cmake、install.cmake、ut.cmake、check.cmake、refresh_cache.cmake、fpp_depend.cmake、fpp_locs.cmake、default.cmake等。下面结合源码逐一说明核心内置目标。
1. build:模块构建目标
build目标是整个系统的基础目标,由 cmake/target/build.cmake 实现。它的职责是为系统中的每个模块完成实际的构建配置:注册目标(库或可执行文件)、设置编译源文件、标记自动代码生成(autocoder)产出的源文件、解析模块与链接依赖、配置头文件包含路径等。
注意其文档中的特别说明:
This target sets up the build for every module in the system. WARNING: it registers a target set to the module name, not including
_build. This is for historical reasons.
也就是说,build目标注册的模块级目标直接使用模块名(不带_build后缀),这是出于历史兼容性的设计。其全局目标build_add_global_target是一个空实现——因为常规 CMake 构建中的all目标已经覆盖了"构建全部"的语义:
function(build_add_global_target TARGET) endfunction(build_add_global_target)模块级实现build_add_module_target则真正完成核心构建配置,包括调用run_ac_set运行自动编码器、通过resolve_dependencies解析依赖、将手写源码与生成源码分类后交给build_setup_build_module处理,并把模块信息写入module-info.txt:
function(build_add_module_target MODULE TARGET SOURCES DEPENDENCIES) get_target_property(MODULE_TYPE "${MODULE}" FP_TYPE) message(STATUS "Adding ${MODULE_TYPE}: ${MODULE}") get_property(CUSTOM_AUTOCODERS GLOBAL PROPERTY FPRIME_AUTOCODER_TARGET_LIST) run_ac_set("${SOURCES}" ${CUSTOM_AUTOCODERS}) resolve_dependencies(RESOLVED ${DEPENDENCIES} ${AC_DEPENDENCIES}) filter_lists("${AC_SOURCES}" SOURCES AC_GENERATED) file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/module-info.txt" "${HEADER_FILES}\n${SOURCES_FILTERED}\n${AC_GENERATED}\n${AC_FILE_DEPENDENCIES}\n${DEPENDENCIES}\n" ) build_setup_build_module("${MODULE}" "${SOURCES_FILTERED}" "${AC_GENERATED_FILTERED}" "${RESOLVED}") ... endfunction(build_add_module_target)2. version:版本信息生成目标
version目标用于生成版本文件,由 cmake/target/version.cmake 实现。其全局目标会生成三个文件到构建目录的versions子目录下:
version.hppversion.cppversion.json
生成过程通过add_custom_command调用 Python 脚本 cmake/target/version/generate_version_info.py,并注入PYTHONPATH、FPRIME_PROJECT_ROOT、FPRIME_FRAMEWORK_PATH、FPRIME_LIBRARY_LOCATIONS等环境变量。当设置FPRIME_CHECK_FRAMEWORK_VERSION为 ON 时,脚本会额外传入--check参数用于版本校验:
function(version_add_global_target TARGET) set(OUTPUT_DIR "${CMAKE_BINARY_DIR}/versions") ... if (FPRIME_CHECK_FRAMEWORK_VERSION) set(OPTIONAL_CHECK_ARG "--check") endif() add_custom_command(OUTPUT "${OUTPUT_HPP}" "${OUTPUT_CPP}" "${OUTPUT_JSON}" COMMAND "${CMAKE_COMMAND}" -E env "PYTHONPATH=${PYTHONPATH}:${FPRIME_FRAMEWORK_PATH}/Autocoders/Python/src" "FPRIME_PROJECT_ROOT=${FPRIME_PROJECT_ROOT}" "FPRIME_FRAMEWORK_PATH=${FPRIME_FRAMEWORK_PATH}" "FPRIME_LIBRARY_LOCATIONS=${FPRIME_LIBRARY_LOCATIONS_CSV}" "${FPRIME_VERSION_INFO_SCRIPT}" "${OUTPUT_DIR}" "${OPTIONAL_CHECK_ARG}" ... ) add_library("${TARGET}" "${OUTPUT_CPP}") endfunction()值得注意:version目标最终通过add_library("${TARGET}" "${OUTPUT_CPP}")生成了一个真正的 CMake 库目标,而不是单纯的add_custom_target——这展示了 target 文件内部既可以使用add_custom_target,也可以使用标准 CMake 目标命令来组织构建。该目标的模块级实现(version_add_module_target)为空函数,因为版本信息属于全局性产物。
3. install:安装目标
install目标将 F´ 构建产物安装到 build-artifacts 目录,由 cmake/target/install.cmake 实现,依赖 CMake 3.13+ 的安装命令。文件开头强制设置:
set(CMAKE_SKIP_INSTALL_ALL_DEPENDENCY TRUE CACHE BOOL "Install all dependency" FORCE)它通过_install_real_helper过滤出真正的可安装目标(可执行文件、库等编译产物,即is_target_real判断为真的目标)再执行安装。由于字典(dictionaries)是每个部署(deployment)级别的产物,全局安装目标为空实现:
# Dictionaries are per-deployment, a global variant does not make sense function(install_add_global_target) endfunction()4. ut 与 check:单元测试相关目标
ut与check是仅当构建单元测试(BUILD_TESTING=ON)时才生效的目标,二者通过register_fprime_ut_target注册(见 cmake/API.cmake 中的说明:这些目标只在系统构建单元测试时创建)。
- ut 目标(cmake/target/ut.cmake):全局目标在
FPRIME_ENABLE_UTIL_TARGETS开启时创建名为ut_exe的目标(UT_TARGET变量,注释标明 "For historical reasons")。该文件还负责在运行测试前清理*.gcda覆盖率文件,通过_ut_setup_clean_file生成clean.cmake脚本并注册到TEST_INCLUDE_FILES。模块级与部署级目标会遍历依赖的FPRIME_UTS属性,把所有单元测试可执行文件挂到对应目标上。 - check 目标(cmake/target/check.cmake):提供一个运行单元测试的入口。全局实现直接调用 CTest:
function(check_add_global_target TARGET_NAME) add_custom_target(${TARGET_NAME} COMMAND ${CMAKE_CTEST_COMMAND}) endfunction(check_add_global_target)模块级实现为每个模块生成<MODULE>_check目标,执行ctest --verbose并依赖对应 UT 可执行文件;部署级实现则把部署内所有 UT 名称拼接为正则,通过ctest -R "<正则>"只运行该部署内的测试。若部署没有任何 UT,则打印 "No unit tests defined for ${MODULE}" 提示。
5. refresh_cache 与其他辅助目标
refresh_cache(cmake/target/refresh_cache.cmake)是一个轻量目标,仅在需要刷新 CMake 缓存时快速执行,模块级与部署级实现均为空。此外,仓库还包含fpp_depend.cmake与fpp_locs.cmake两个目标文件,用于 FPP 模型的依赖信息与位置信息的子构建(sub-build),由fprime_initialize_build_system中的run_sub_build(info-cache target/fpp_locs target/fpp_depend)触发。
全部内置目标的注册与说明,可参见 docs/UsersGuide/cmake/cmake-api.md 中"Target Documentation"一节,其提到"dict 目标可能先构建本地字典片段,再汇总为全局字典"——这正是 Targets 子系统"模块级产出 + 全局汇总"设计思想的典型应用。
三、Target 文件的结构约定:三个必需函数
无论内置目标还是自定义目标,每个 target 文件都必须遵循统一的接口约定。根据 cmake/target/target.cmake 顶部的规范说明,target 文件必须定义三个函数:
| 函数 | 作用域 | 必需性 | 典型行为 |
|---|---|---|---|
${TARGET_NAME}_add_global_target(TARGET_NAME) | 全局 | 必须(可为空实现) | 通过add_custom_target("${TARGET_NAME}" ...)创建顶层目标,如make dict;若传ALL参数则随常规make一并执行 |
${TARGET_NAME}_add_module_target(MODULE_NAME TARGET_NAME SOURCE_FILES DEPENDENCIES) | 每个模块 | 必须(可为空实现) | 为每个模块注册<MODULE>_<target>子目标;可通过DEPENDS依赖自动编码器输出(AC_OUTPUTS)以保证在自动代码生成之后执行 |
${TARGET_NAME}_add_deployment_target(MODULE TARGET SOURCES DEPENDENCIES FULL_DEPENDENCIES) | 每个部署 | 必须(可为空实现) | 针对register_fprime_deployment注册的部署模块执行汇总逻辑,FULL_DEPENDENCIES为递归解析后的完整依赖列表 |
这里需要说明命名细节:target 文件内部定义的函数实际名称由"目标名(文件名)"加函数名后缀构成。例如 cmake/target/version.cmake 定义的是version_add_global_target(文件名version.cmake的 stem 是version),cmake/target/check.cmake 定义的是check_add_global_target、check_add_module_target、check_add_deployment_target。系统通过get_target_name(见 cmake/target/target.cmake)从目标文件路径提取 stem 得到TARGET_NAME,并据此派生出模块级目标名<MODULE>_<target>。
执行顺序与调度机制
setup_global_targets遍历全局属性FPRIME_TARGET_LIST与FPRIME_UT_TARGET_LIST(后者仅在BUILD_TESTING时生效),为每个已注册 target 调用setup_global_target;setup_global_target通过plugin_include_helper(cmake/utilities.cmake)惰性加载 target 文件——仅当以<target名>_add_global_target命名的命令尚未定义时才include该文件,随后用cmake_language(CALL ...)动态调用;setup_module_targets/setup_single_target在注册模块时被调用,根据模块的FP_TYPE属性分发:普通模块调用add_module_target,Deployment类型模块调用add_deployment_target,并传入模块源码、依赖及(对部署)递归依赖列表。recurse_targets负责从FPRIME_TARGET_DEPENDENCIES与FPRIME_MODULES全局属性中递归收集所有 F´ 模块依赖。
这套"惰性加载 + 命名约定 + 动态调用"的机制(见 cmake/utilities.cmake 的注释,dispatch_<function>(PLUGIN_NAME ...)分发模式)使得多个 target 文件之间互不干扰,且可以在不加载全部文件的情况下按需初始化。
四、注册目标:register_fprime_target 与 register_fprime_ut_target
自定义 target 文件的注册入口是register_fprime_target宏,定义于 cmake/API.cmake:
macro(register_fprime_target TARGET_FILE_PATH) if (CMAKE_DEBUG_OUTPUT) message(STATUS "[target] Registering custom target: ${TARGET_FILE_PATH}") endif() register_fprime_list_helper("${TARGET_FILE_PATH}" FPRIME_TARGET_LIST OFF) endmacro(register_fprime_target)其参数TARGET_FILE_PATH可以是:
- 文件路径:直接指向定义目标的 CMake 文件;
- 短 include 路径:要求该文件所在目录已加入 CMake 的 include path(例如内置目标直接写
target/build)。
被注册的文件应定义前述三个函数:add_global_target、add_module_target、add_deployment_target(API.cmake 的注释中明确列出)。注册后目标会进入全局属性FPRIME_TARGET_LIST,由构建系统在各模块设置阶段统一展开。
配套宏register_fprime_ut_target(cmake/API.cmake)与前者完全一致,但仅当BUILD_TESTING=ON时才注册——这是ut、check等测试类目标使用的注册通道,保证生产构建不会携带测试目标。
此外,cmake/FPrime.cmake 中的fprime_setup_override_targets展示了目标的覆盖机制:当用户设置FPRIME_SUB_BUILD_TARGETS变量(一个目标文件列表)时,系统跳过标准目标注册,仅注册用户指定的目标,从而实现"仅子构建"或"自定义目标集合"的构建模式。在cmake/test/data/TestDeployment/CMakeLists.txt(cmake/test/data/TestDeployment/CMakeLists.txt)中可以看到真实调用示例:
register_fprime_target("target/test") # Test target and autocoder supporting it五、编写自定义目标:从钩子模式到完整示例
Customization 指南 将自定义目标的实现方式总结为钩子模式(hook pattern):
- 创建一个 CMake 文件,在其中定义
add_global_target与add_module_target两个函数(按前述命名约定,实际函数名前缀为目标名); - 通过
register_fprime_target注册该文件; - 在两个 add 函数内部,使用 CMake 原生的
add_custom_target来组装目标本身。
该指南特别指出:如果目标不需要同时具备"每模块"与"全局"两个步骤,用户也可以直接调用add_custom_target添加目标(即跳过钩子模式,写一个裸全局目标)。
裸全局目标示例
Customization.md 给出的最小示例——生成 Python 字典目录的dict目标:
add_custom_target( dict COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/dict/serializable ${CMAKE_SOURCE_DIR}/py_dict/serializable COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_SOURCE_DIR}/py_dict/serializable/__init__.py )运行方式:
cmake ../Ref make dict这个例子展示了add_custom_target的典型用法:通过COMMAND指定一个或多个要执行的命令(这里用cmake -E完成目录拷贝与空文件创建,从而得到可导入的 Python 包结构)。
完整钩子模式示例(全局 + 模块级 + 部署级)
参照内置目标的实现风格,一个同时覆盖全局与模块级视图的 target 文件骨架如下:
# count.cmake —— 统计文件数的自定义目标 # 命名约定:函数名前缀 = 目标文件名(count) function(count_add_global_target TARGET) add_custom_target(${TARGET} COMMAND ${CMAKE_COMMAND} -E echo "Counting all files..." # 在这里添加全局统计命令,例如递归统计仓库文件总数 ) endfunction(count_add_global_target) function(count_add_module_target MODULE_NAME TARGET_NAME SOURCE_FILES DEPENDENCIES) add_custom_target("${MODULE_NAME}_${TARGET_NAME}" COMMAND ${CMAKE_COMMAND} -E echo "Counting files in ${MODULE_NAME}: ${SOURCE_FILES}" # 在这里对 SOURCE_FILES 执行按模块统计 DEPENDS ${AC_OUTPUTS} # 需要时依赖自动编码输出 ) endfunction(count_add_module_target) function(count_add_deployment_target MODULE TARGET SOURCES DEPENDENCIES FULL_DEPENDENCIES) add_custom_target("${MODULE}_${TARGET}" COMMAND ${CMAKE_COMMAND} -E echo "Rolling up count for deployment ${MODULE}" # 汇总该部署内所有依赖模块的统计结果 ) endfunction(count_add_deployment_target)注册方式(在部署顶层 CMakeLists.txt 或自定义构建入口中):
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") # 确保 include 路径 register_fprime_target(count) # 使用短路径 # 或 register_fprime_target("${CMAKE_CURRENT_LIST_DIR}/cmake/count.cmake") # 使用文件路径注册后即可执行:
make count # 全局统计 make Ref_count # 按部署汇总 make Svc_TlmChan_count # 按模块统计(针对 Svc/TlmChan 模块)参数说明与实现要点
add_module_target收到的SOURCE_FILES是模块 CMakeLists.txt 中通过set(SOURCE_FILES ...)指定的手写源码;AC_INPUTS/AC_OUTPUTS分别是自动编码器的输入(.xml/.fpp文件)与输出(Ac.cpp/Ac.hpp);MOD_DEPS是set(MOD_DEPS ...)指定的依赖。需要让目标在自动代码生成之后运行,就应把AC_OUTPUTS放入add_custom_target的DEPENDS(见 cmake/target/target.cmake 的规范)。- 若希望目标随常规
make一并构建,在add_custom_target中传ALL参数(见 cmake/target/target.cmake)。 - 模块级与部署级函数必须定义但允许为空实现:
default.cmake(cmake/target/default.cmake)的默认实现中,add_module_target为空、add_deployment_target仅做依赖汇总、add_global_target仅创建空目标;而setup_global_target的注释也强调"所有 target 必须定义<target名>_add_global_target,即使它是空函数"。这保证系统在调度时不会因函数缺失而崩溃。 default.cmake中add_deployment_target的默认行为值得借鉴:它创建${MODULE}_${TARGET}空目标后,遍历依赖列表,把每个存在的${DEPENDENCY}_${TARGET}通过add_dependencies挂到其上——这就是"模块级产物汇总到部署级/全局"的标准模式。
六、常见问题与调试技巧
make <MODULE>_<target>找不到目标:确认该目标文件已通过register_fprime_target注册,且模块确实存在。目标名由文件名 stem 派生,例如注册count.cmake得到count与<MODULE>_count。若使用短路径注册,请确认目录已加入CMAKE_MODULE_PATH。- 区分
register_fprime_target与register_fprime_ut_target:测试相关目标(如check)必须用后者注册,它仅在BUILD_TESTING=ON时生效(cmake/API.cmake)。 - 调试输出:在 CMake 配置时设置
CMAKE_DEBUG_OUTPUT为 ON,可以看到[target] Registering custom target: ...、[target] Setting up '...' on all module ...等日志(见 cmake/API.cmake 与 cmake/target/target.cmake),用于定位注册与分发过程中的问题。 - 模块级与部署级二选一:
setup_single_target只会对某个模块调用两个函数之一(普通模块走add_module_target,Deployment类型走add_deployment_target),不要假设两者都会被执行(cmake/target/target.cmake)。 - 自定义目标的官方范例:仓库
cmake/target/目录下的每个内置文件本身都是 target 文件的最佳范本,编写自定义目标时可直接对照 cmake/target/version.cmake、cmake/target/check.cmake 等实现。
总结
F´ 的 Targets 子系统以"一个 target 文件、三类钩子函数、双层执行入口"的简洁模型,把任意自定义动作平滑地接入 CMake 构建流程:add_global_target提供全局入口,add_module_target提供逐模块入口,add_deployment_target提供逐部署的汇总入口,三者由构建系统统一调度(cmake/target/target.cmake)。内置的build、version、install、ut、check、refresh_cache等目标(注册逻辑见 cmake/FPrime.cmake)覆盖了编译、版本生成、安装与测试等标准需求,而register_fprime_target与register_fprime_ut_target(cmake/API.cmake)则向开发者开放了同等的扩展能力。无论是要实现"统计文件总数"这类工具型目标,还是要构建"先按模块生成字典片段、再汇总全局字典"的多阶段目标,都可以在本文给出的骨架之上直接落地。
- 嵌入式
- 系统编程
【免费下载链接】fprime
F´ - A flight software and embedded systems framework
相关推荐
F´ 构建系统 Targets 完全指南:内置 Target 解析与自定义 Target 注册
F´ 构建系统 Targets 完全指南:内置 Target 解析与自定义 Target 注册 F´(F Prime)的 CMake 构建系统提供了一套可扩展的
嵌入式系统编程Cytoscape.js 布局(Layout)体系详解:从内置算法到自定义扩展
Cytoscape.js 布局(Layout)体系详解:从内置算法到自定义扩展 布局(Layout)是 Cytoscape.js 中负责 为图中每个节点计算坐标
数据可视化Burn 训练框架指标体系详解:从内置 Metric、Adaptor 适配到自定义指标实现
Burn 训练框架指标体系详解:从内置 Metric、Adaptor 适配到自定义指标实现 Burn 的 Learner 训练框架内置了一套可插拔的指标(Met
UI组件前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考