- 嵌入式
- 系统编程
【免费下载链接】fprime
F´ - A flight software and embedded systems framework
F´(F Prime)是一个面向飞行软件与嵌入式系统的开源框架,其 CMake 构建系统在标准 CMake 之上提供了模块注册、自动编码(autocoding)、单元测试与部署构建等一整套机制。然而,任何构建系统都无法覆盖所有项目的全部需求——当你需要为部署追加一个依赖 F´ 代码的辅助工具、注册一个类似make dict的自定义构建目标,或者把使用其他构建系统的外部库纳入构建流程时,就需要对 F´ 的 CMake 系统进行定制。本文以 docs/UsersGuide/cmake/Customization.md 为骨架,结合仓库内 cmake/API.cmake、cmake/target/target.cmake 等源码实现,完整讲解这三类自定义场景的标准做法、hook 函数规范与实际可运行的示例,帮助你掌握在 F´ 构建系统中安全、可维护地"开洞"的能力。
一、自定义的基本思路:CMake 即底层能力
在动手之前需要明确一个前提:F´ 的构建系统归根结底是 CMake。Customization.md 开篇就强调,这套系统赋予了用户很强的能力——只要是标准 CMake 模式能实现的事情,在 F´ 中几乎都能实现;如果本文的示例没有覆盖到你的需求,建议先去系统学习 CMake 本身。
结合源码可以更清楚地看到"标准 CMake 之上的 F´ 封装"是如何落地的:
- F´ 通过
add_fprime_subdirectory替代原生add_subdirectory来组织目录(见 cmake/API.cmake 中该函数的实现),它会自动计算 binary 目录并保持 F´ 的 include 路径结构; - 模块、可执行文件、部署、单元测试分别通过
register_fprime_module、register_fprime_executable、register_fprime_deployment、register_fprime_ut注册,底层最终都会调用add_library/add_executable等原生 CMake 命令(见 cmake/module.cmake 的generate_base_module_properties); - 自定义目标与自定义自动编码器则通过
register_fprime_target、register_fprime_build_autocoder注入(见 cmake/API.cmake)。
因此,所有自定义工作本质上都是在这些注册点与原生 CMake 命令之间"插入自己的代码"。
自定义代码应该放在哪里
部署的顶层CMakeLists.txt是一个关键插入点。从 cmake/deployment-CMakeLists.txt.template 可以看到部署文件的经典三段式结构:
# Section 1: 基础项目设置 project(Ref C CXX) cmake_minimum_required(VERSION 3.15) # Section 2: 引入 F prime 核心构建系统 include("<PATH-TO>/cmake/FPrime.cmake") # NOTE: 自定义 target 在此处注册(两条 include 之间) include("<PATH-TO>/cmake/FPrime-Code.cmake") # Section 3: 引入部署相关的组件与拓扑 add_fprime_subdirectory("<PATH-TO-MODULE>") ... add_fprime_subdirectory("<PATH-TO-TOP>")模板中明确注释:自定义 target 应注册在FPrime.cmake与FPrime-Code.cmake两条 include 之间。真实部署 Ref/CMakeLists.txt 正是这样做的:
include("${CMAKE_CURRENT_LIST_DIR}/../cmake/FPrime.cmake") # NOTE: register custom targets between these two lines include("${CMAKE_CURRENT_LIST_DIR}/../cmake/FPrime-Code.cmake")原因也很直接:FPrime.cmake负责导入整个构建系统 API 与选项,FPrime-Code.cmake(见 cmake/FPrime-Code.cmake)负责把 Fw、Svc、Os、Drv、CFDP、Utils 等框架核心目录加入构建。目标文件必须先于模块被注册,才能让后续每个模块在定义时被挂上对应的模块级目标——如果注册晚了,cmake/target/target.cmake 中的register_fprime_list_helper会直接抛出FATAL_ERROR("Cannot register fprime target after including subdirectories or FPrime-Code.cmake")。
二、构建依赖 F´ 代码的工具可执行文件(Utilities)
如果你要写一个需要链接 F´ 模块、使用 F´ 端口或类型的辅助工具(例如自定义的字典生成器、代码检查工具、日志分析器等),最直接的方式是调用register_fprime_executable注册一个"工具可执行文件"。
2.1 基本用法
register_fprime_executable的定义与完整参数说明在 cmake/API.cmake 中,其要求(或可选)的变量包括:
| 变量 | 必填 | 说明 |
|---|---|---|
EXECUTABLE_NAME | 可选 | 可执行文件的名称;若不设置也未传参,则使用PROJECT_NAME或由模块名推导 |
SOURCE_FILES | 是(与MOD_DEPS至少其一) | 源文件与自动编码输入(*Ai.xml、*.fpp、*.c、*.cpp等)的列表,会被自动拆分为自动编码输入与手写源码两类 |
MOD_DEPS | 可选 | 无法从 include 图自动推断的非标准链接依赖,例如其他模块名或-lpthread这类链接标志 |
Customization.md 特别提醒:调用该函数前必须先把EXECUTABLE_NAME设置好,以指定工具的名称。一个最小示例:
set(EXECUTABLE_NAME "my_tool") set(SOURCE_FILES "${CMAKE_CURRENT_LIST_DIR}/my_tool.cpp" "${FPRIME_FRAMEWORK_PATH}/Svc/CmdDispatcher/CmdDispatcherComponentAi.xml" ) set(MOD_DEPS Fw_Types -lpthread ) register_fprime_executable()从实现看,register_fprime_executable在 cmake/API.cmake 中首先检查SOURCE_FILES或MOD_DEPS是否已定义,否则报错;随后按EXECUTABLE_NAME→ 位置参数 → 模块名的优先级确定可执行文件名,最后交给generate_executable→generate_base_module_properties创建add_executable目标(见 cmake/module.cmake)。
2.2 与部署可执行文件的区别
register_fprime_executable不用于注册部署二进制(如整个 flight software 镜像),部署应使用register_fprime_deployment(见 cmake/API.cmake)。两者的核心区别:
register_fprime_deployment会以PROJECT_NAME作为可执行文件名,并且会自动运行部署级目标(例如生成字典dict),从而保证标准部署产物齐备;register_fprime_executable只生成一个命名的工具二进制,不触发部署级目标;- 工具目标构建后会自动"安装"到由
FPRIME_INSTALL_DEST变量指定的构建产物目录(可执行文件进入${TOOLCHAIN_NAME}/bin,库进入lib与lib/static,参见 cmake/target/install.cmake)。
2.3 依赖工具时的注意事项
cmake/API.cmake 的 Caveats 一节还给出了一条重要约束:不要把可执行目标放进MOD_DEPS(例如传给register_fprime_deployment),否则多个可执行文件在最终链接时可能因为各自的main定义冲突而失败,而且这类错误与 CMake 定义顺序相关、时隐时现。正确做法是只建立 CMake 层面的依赖:
set(SOURCE_FILES "tool.c") register_fprime_executable(TOOL) ... register_fprime_deployment(MY_DEPLOYMENT) add_dependencies(MY_DEPLOYMENT TOOL) # 仅 CMake 依赖,不参与链接2.4 用独立的 tools 部署只构建工具
Customization.md 建议:如果工具较多、又不希望每次都被部署构建连带编译,可以单独建立一个 tools 部署,在其中只注册工具可执行文件,从而仅构建这些工具。这正好利用了 F´ "模块按需构建"的特性——每个工具目标的依赖子图会被单独解析,未被依赖的模块不会真正编译(cmake/API.cmake 中add_fprime_subdirectory的注释说明,CMake 依赖系统会阻止多余的构建)。
三、自定义构建目标(Make Targets):hook 模式
这是 Customization.md 的篇幅核心:为构建系统添加自定义的全局/模块级构建目标(在 GNU Make 生成器下体现为make <target>与make <MODULE>_<target>)。例如你需要一个统计源码行数的目标、一个汇总各模块文件的sloc目标,或者一个在所有模块上运行自定义脚本的lint目标。
3.1 hook 模式的三步流程
按照 Customization.md 与 cmake/target/target.cmake 的规范,注册自定义目标需要三步:
- 创建目标定义文件(
.cmake),在其中定义两个函数:add_global_target:向顶层添加全局目标(对应make dict这样的全局命令);add_module_target:为每个模块添加模块级子目标(对应make <MODULE>_dict这样的按模块命令)。
- 目标文件还必须定义
add_deployment_target(可以为空实现),用于响应register_fprime_deployment的部署级回调——setup_single_target会对每个模块按FP_TYPE属性决定调用add_module_target还是add_deployment_target,两者必须都存在。 - 用
register_fprime_target注册该文件(宏定义见 cmake/API.cmake):
register_fprime_target("${CMAKE_CURRENT_LIST_DIR}/cmake/my_target.cmake")Customization.md 同时指出:如果你的目标不需要"每个模块一份 + 全局一份"的双层结构,也可以直接在目标文件里调用add_custom_target添加单个目标。
3.2 两个 add 函数的签名与职责
cmake/target/target.cmake 对这两个函数给出了严格的参数规范:
add_global_target(TARGET_NAME):添加全局目标。TARGET_NAME必须被传递给一次add_custom_target调用;如果希望该目标随make(all)默认构建,就给add_custom_target传入ALL参数。add_module_target(MODULE_NAME TARGET_NAME AC_INPUTS SOURCE_FILES AC_OUTPUTS MOD_DEPS):为每个模块添加子目标。参数含义为:MODULE_NAME:正在构建的模块名;TARGET_NAME:目标名,模块级目标一般命名为${MODULE_NAME}_${TARGET_NAME};AC_INPUTS:自动编码输入(Ai.xml文件列表);SOURCE_FILES:手写源码(*.cpp、*.hpp);AC_OUTPUTS:自动编码输出(Ac.cpp、Ac.hpp);MOD_DEPS:模块声明的依赖列表。- 若你的目标命令依赖自动编码产物,应通过
DEPENDS依赖AC_OUTPUTS,确保自定义命令在自动编码步骤之后执行。
命名辅助函数get_target_name会把目标文件名(去.cmake后缀)作为全局目标名,并在传入模块名时生成${MODULE}_${TARGET}的模块级名字。也就是说,若目标文件名为dict.cmake,则全局目标叫dict,各模块的子目标叫<MODULE>_dict。
3.3 原文档示例:裸全局目标
Customization.md 给出的完整示例是注册一个直接复制字典产物到源码目录的全局目标:
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_COMMAND -E执行文件系统操作(copy_directory、touch),而不是硬编码 shell 命令,这保证了跨平台可移植性——这也是 F´ 内置目标的一贯风格,例如 cmake/target/check.cmake 中删除.gcda覆盖率数据文件也是用${CMAKE_COMMAND} -E chdir ... find ...实现的。
运行方式(原文示例,仓库内存在可验证的 Ref 部署):
cmake ../Ref make dict即先对Ref部署执行 CMake 配置,再通过make dict触发该自定义目标。
3.4 源码级示例:F´ 内置目标是怎么写的
仓库cmake/target/目录下自带一套按同一 hook 模式实现的内置目标,是最好的学习范本:
dict(cmake/target/dict.cmake)——字典目标。它把全局与部署级函数留空,只实现add_module_target:
function(dict_add_global_target) endfunction() function(dict_add_deployment_target) endfunction() function(dict_add_module_target MODULE TARGET SOURCES DEPENDENCIES) get_target_name(${TARGET} ${MODULE}) run_ac_set("${SOURCES}" autocoder/fpp autocoder/ai_xml) set(DICTIONARY "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}TopologyAppDictionary.xml") foreach(FILE IN LISTS AC_GENERATED) if (FILE STREQUAL DICTIONARY) set_property(GLOBAL PROPERTY DICTIONARY_FILE "${DICTIONARY}") break() endif() endforeach() endfunction(dict_add_module_target)其注释解释了关键设计:字典按部署(per-deployment)生成,全局变体没有意义;实际生成工作由拓扑模块承担,模块级函数只需要确认字典文件出现在自动编码输出中,并把其路径记录到全局属性DICTIONARY_FILE,供后续install目标把字典安装到${TOOLCHAIN_NAME}/dict目录(见 cmake/target/install.cmake)。
impl(cmake/target/impl.cmake)——实现模板生成目标,展示了"只做模块级、不做全局"的模式:
function(impl_add_module_target MODULE TARGET SOURCE_FILES DEPENDENCIES) get_target_name(${TARGET} ${MODULE}) run_ac_set("${SOURCE_FILES}" autocoder/fpp autocoder/ai_impl) add_custom_target("${TARGET_MOD_NAME}" DEPENDS ${AC_GENERATED}) endfunction(impl_add_module_target)它运行 fpp 与 ai_impl 自动编码器产出模板文件,并以DEPENDS ${AC_GENERATED}建立对自动编码输出的依赖——正是 3.2 节规范的直接体现。
version(cmake/target/version.cmake)——演示了带ALL的全局目标(随默认构建执行),并利用add_custom_target的BYPRODUCTS、copy_if_different来生成version.hpp,可视为"自定义目标生成头文件"的参考模板。
check与ut(cmake/target/check.cmake、cmake/target/ut.cmake)——分别演示了全局目标用${CMAKE_CTEST_COMMAND}跑测试、以及模块级目标按FPRIME_UTS属性聚合单元测试依赖的做法。
fpp_locs(cmake/target/fpp_locs.cmake)——一个特殊目标:它不生成普通的 per-module 命令,而是在add_global_target中一次性完成 FPP 位置索引(locs.fpp)与依赖缓存的预生成,用于解决 FPP 依赖分析的性能问题。
这些内置目标的完整清单与说明可参考 docs/UsersGuide/cmake/Targets.md 与 docs/UsersGuide/cmake/cmake-api.md。
3.5 仅测试可用的目标:register_fprime_ut_target
与register_fprime_target配套,cmake/API.cmake 还提供了register_fprime_ut_target:注册的 UT 目标只在BUILD_TESTING=ON时创建。其内部逻辑是:UT 目标是唯一会在单元测试模块上运行的目标(见 cmake/target/target.cmake 中setup_module_targets对FPRIME_UT_TARGET_LIST的处理——单元测试模块只挂 UT 目标,部署模块则额外追加 UT 目标)。覆盖率(<MODULE>_coverage)这类依赖 gcov 与CMAKE_BUILD_TYPE=TESTING的目标即属此类(见 docs/UsersGuide/cmake/Targets.md)。
四、外部库与其他构建系统的集成
当项目需要引入一个使用独立构建系统(如 autotools、自定义 Makefile)的外部库时,Customization.md 给出了两种标准方案。
4.1 方案一:add_subdirectory + 自定义命令
直接用 CMake 的add_subdirectory(F´ 场景下可改用add_fprime_subdirectory)把外部库目录加入构建,然后在该目录的CMakeLists.txt中调用:
add_custom_target:适用于构建产物不直接作为链接输入的场景,只建立"先构建"的顺序依赖(例如库提供的是运行期脚本或数据);add_custom_command:适用于需要访问输出文件的场景(例如库编译出的静态库/头文件要作为后续目标输入),通过OUTPUT/DEPENDS建立真正的文件级依赖。
选择依据一句话概括:系统是否直接依赖该命令产生的文件。需要文件参与编译链接就用add_custom_command,否则用add_custom_target更简单。
4.2 方案二:ExternalProject_Add
当外部库需要下载、版本控制(checkout 特定版本)以及独立的构建步骤时,应改用 CMake 的ExternalProject模块(ExternalProject_Add命令)。它适合真正的"第三方项目":在配置/构建阶段拉取源码、执行其自身的构建流程,最终将产物纳入 F´ 构建。
仓库中其实就有一个"下载外部项目再接入构建"的现实案例:F´ 在启用单元测试时通过 cmake/googletest-download/googletest.cmake 在配置期下载并构建 GoogleTest——先用configure_file生成下载用的CMakeLists.txt,再用execute_process执行 CMake 完成拉取,最后add_subdirectory将 gtest 目标加入构建(EXCLUDE_FROM_ALL避免其进入默认构建)。该文件在 cmake/FPrime-Code.cmake 中被条件包含:
if (BUILD_TESTING AND NOT DEFINED FPRIME_PRESCAN) include("${FPRIME_FRAMEWORK_PATH}/cmake/googletest-download/googletest.cmake") add_subdirectory("${FPRIME_FRAMEWORK_PATH}/STest/" "${CMAKE_BINARY_DIR}/F-Prime/STest") endif()从源码结构看,这正是"ExternalProject 式下载 + add_subdirectory 接入"的组合实践,可作为集成外部依赖时的参考路径。
五、实践清单与注意事项
综合文档与源码,落地自定义 CMake 时建议遵循以下要点:
- 注册时机:自定义 target 文件放在部署
CMakeLists.txt的两条 include(FPrime.cmake与FPrime-Code.cmake)之间注册,见 cmake/deployment-CMakeLists.txt.template; - 三函数齐全:目标文件必须定义
add_global_target、add_module_target、add_deployment_target(后两者可为空实现),否则setup_global_target的plugin_include_helper路由会失败(见 cmake/target/target.cmake); - 全局目标必须过
add_custom_target:TARGET_NAME必须传给add_custom_target;需要随默认构建运行时加ALL参数; - 依赖自动编码产物:模块级命令若需要
Ac.cpp/Ac.hpp,务必通过DEPENDS ${AC_GENERATED}挂依赖(参考 cmake/target/impl.cmake); - 工具目标不放进
MOD_DEPS:可执行文件之间的依赖用add_dependencies建立(见 cmake/API.cmake 的 Caveats); - 优先
CMAKE_COMMAND -E而非裸 shell 命令:保证跨平台(参考 cmake/target/check.cmake); - 文件操作示例:目录复制与占位文件创建可仿照 Customization.md 的
dict示例,用-E copy_directory与-E touch完成; - 外部库二选一:产物不被直接依赖 →
add_custom_target;产物作为输入 →add_custom_command;需要下载/版本控制/独立构建 →ExternalProject_Add。
结语
F´ 的 CMake 系统是一个"标准 CMake 之上的薄封装 + 插件化目标体系":register_fprime_executable让你轻松产出依赖 F´ 代码的工具;register_fprime_target+ hook 文件(add_global_target/add_module_target/add_deployment_target)让你按统一模式接入全局与模块级构建目标,仓库内置的dict、impl、check、install、version、fpp_locs等目标(cmake/target/ 目录)就是最直接的学习范本;而add_custom_command/ExternalProject_Add则覆盖了外部构建系统的集成需求。掌握这些模式之后,任何"标准构建系统没提供"的项目级需求,都可以用标准 CMake 方式在 F´ 中安全地实现。
- 嵌入式
- 系统编程
【免费下载链接】fprime
F´ - A flight software and embedded systems framework
相关推荐
F´ 飞行软件框架 CMake 定制指南:自定义工具、构建目标与外部库集成
F´ 飞行软件框架 CMake 定制指南:自定义工具、构建目标与外部库集成 导读 F´(F Prime)飞行软件框架内置了一套完整且高度自动化的 CMake 构
嵌入式系统编程F´ CMake 定制实战:工具程序、自定义 Make Target 与外部库集成全指南
F´ CMake 定制实战:工具程序、自定义 Make Target 与外部库集成全指南 F´(F Prime)飞行软件与嵌入式系统框架提供了一套基于 CMak
嵌入式系统编程如何用taskt免费RPA工具实现Windows自动化:零代码桌面机器人终极指南
如何用taskt免费RPA工具实现Windows自动化:零代码桌面机器人终极指南 taskt是一款完全免费开源的Windows桌面自动化工具,基于C 和.NET
嵌入式系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考