1. 项目概述为什么我们需要重新审视fmtlib的集成方式如果你是一个C开发者尤其是经常和字符串格式化、日志输出打交道的朋友那么fmtlib这个名字你一定不陌生。它几乎成了现代C项目中替代printf和iostream的首选速度快、类型安全、API现代。我自己在多个生产项目里用它从简单的日志信息拼接到复杂的报表生成它都没掉过链子。但是随着C20标准的普及特别是“模块”这个重磅特性的落地我们过去集成fmtlib的方式——无非就是#include fmt/core.h然后链接库——开始显得有点“老派”了。更头疼的是构建工具虽然CMake是事实标准但用它来优雅地处理C20模块尤其是像fmtlib这样既提供头文件又提供模块接口的项目一度是个让人挠头的难题。网上搜到的方案要么是手动写一堆.ixx文件的编译命令要么就是直接放弃模块化退回到头文件包含的老路。这不fmtlib官方仓库里就有一个开了挺久的Issue#3990核心诉求就是“咱们能不能用上CMake对C模块的原生支持别再用那些手动拼装的土办法了”这个痛点我深有体会。手动管理模块编译命令不仅容易出错而且和CMake的target_link_libraries那种声明式、自动传递依赖的优雅体验格格不入。更重要的是你没法方便地通过target_compile_definitions来传递像FMT_STATIC_THOUSANDS_SEPARATOR这样的编译期定义灵活性大打折扣。所以今天我想和你深入聊聊的就是如何彻底解决这个痛点。我们将基于最新的CMake特性3.28和fmtlib的源码探索一套全新的、干净的、面向未来的集成方案。这套方案不仅能让你在C20模块项目中顺畅使用fmtlib更能让你理解CMake模块支持背后的原理举一反三应用到其他库上。无论你是正在评估向C20模块迁移还是已经被模块的构建问题困扰已久这篇文章都能给你一份可以直接“抄作业”的解决方案。2. 核心思路与方案选型手动编译 vs. 原生CMake模块支持在动手之前我们得先搞清楚现状和可选的道路。fmtlib本身是支持以模块形式使用的它通过一个叫做FMT_MODULE的宏来控制。但正如Issue里提到的现有的支持方式本质是“手动编译”。2.1 传统手动编译模块的弊端过去如果你想将fmtlib作为模块使用大概需要这么干你需要单独编译fmt.cc这个源文件但必须使用特殊的编译器标志告诉编译器这是一个模块接口文件比如对于MSVC是/interface对于GCC/Clang是-fmodules-ts等。生成模块二进制文件.ifc,.gcm等。在你的主项目中手动指定这个模块二进制文件的路径并链接fmt库。这个过程完全绕开了CMake的依赖管理机制。带来的问题非常具体依赖传递失效你的可执行文件target_link_libraries(app fmt)CMake只知道链接fmt的库文件但不知道还需要为app传递模块接口信息。你得手动为app的编译命令添加模块搜索路径。编译定义无法设置像FMT_STATIC_THOUSANDS_SEPARATOR这样的宏定义需要在编译fmt模块接口单元时就确定。手动编译时你可以在命令行加-DFMT_STATIC_THOUSANDS_SEPARATOR。但一旦把它作为子项目通过add_subdirectory或FetchContent集成到你的CMake里你想通过target_compile_definitions(fmt PUBLIC ...)来设置就难了因为手动编译的脚本可能不读取CMake为目标设置的属性。跨平台和编译器适配复杂手动写编译命令意味着你要为MSVC、GCC、Clang分别写一套逻辑维护成本高。2.2 CMake原生模块支持的优势与现状CMake从3.25版本开始实验性支持C模块并在3.28版本中进行了显著改进和稳定。其核心思想是引入了一个新的FILE_SET类型——CXX_MODULES。它的工作原理可以这样通俗理解以前target_sources就是添加.cpp、.h这些文件。现在你可以告诉CMake“我这个目标库或可执行文件有一些文件是C模块接口.cppm,.ixx或模块实现单元。” CMake会识别这些文件并自动为它们生成正确的、针对当前编译器的编译命令。更重要的是当其他目标链接target_link_libraries这个库时CMake会自动将必要的模块依赖信息比如模块接口文件在哪里传递给依赖方完全自动化。这带来的好处是颠覆性的声明式配置你只需要说“这些文件是模块”CMake负责搞定怎么编译。依赖自动传递A库使用了模块B可执行文件链接了A那么B编译时自动能import来自A的模块。无需手动指定路径。完美融合现有CMake生态target_compile_definitions、target_include_directories、target_compile_options等所有属性设置对模块接口单元的编译同样生效。这意味着你可以轻松地通过CMake命令来配置fmtlib的编译选项。目前主流编译器对新CMake模块支持的状态如下MSVC支持最好从Visual Studio 2019 16.10左右开始就提供了较好的支持也是CMake这方面功能推进的主要参考。GCC从GCC 11开始支持C20模块但需要搭配CMake 3.28及以上版本并且可能需要一些额外的标志如-fmodules-ts但CMake通常会帮你管理。Clang对C20模块的支持也在快速完善中Clang 16 版本配合CMake 3.28 通常可以工作。所以我们的方案选型非常明确放弃手动编译的“野路子”拥抱CMake 3.28的原生CXX_MODULES FILE_SET支持。这也是fmtlib那个Issue里提问者尝试并验证可行的方向。接下来我们就来拆解如何具体实现。3. 实战将fmtlib改造为CMake原生模块我们的目标不是修改fmtlib的源码而是通过编写一个“包装”或“补丁”式的CMake脚本让现有的fmtlib源码能够以原生模块的方式被构建和消费。这里假设你通过git submodule、FetchContent或者直接把源码放在项目里来使用fmtlib。3.1 基础项目结构与CMake版本要求首先确保你的CMake版本至少是3.28。在CMakeLists.txt的开头进行断言cmake_minimum_required(VERSION 3.28) project(MyFmtProject LANGUAGES CXX)假设你的项目结构如下my_project/ ├── CMakeLists.txt ├── src/ │ └── main.cpp └── extern/ └── fmt/ # fmtlib 源码目录 ├── include/fmt/ ├── src/format.cc ├── src/os.cc └── ...其他源文件3.2 关键步骤解析重新定义fmt库目标传统的fmtlibCMakeLists会创建一个fmt库目标可能是静态库fmt::fmt或共享库。我们要做的是拦截或重新定义这个目标的源文件设置方式。核心在于target_sources命令与FILE_SET CXX_MODULES的配合。fmtlib的主要模块接口定义在src/fmt.cc注意在模块语境下这个.cc文件实际上扮演了模块接口单元的角色虽然它的扩展名不是.ixx。以下是一个关键的CMake脚本片段你可以将它放在主CMakeLists.txt中在add_subdirectory(extern/fmt)之前或之后执行目的是修改fmt目标的属性# 方法一如果fmt库目标尚未被add_library定义我们可以先定义它。 # 但通常fmtlib自己的CMakeLists.txt已经定义了。所以我们更多用方法二。 # 方法二在add_subdirectory之后修改已存在的fmt目标。 # 假设通过add_subdirectory后我们有了一个叫fmt的目标可能是别名实际是fmt::fmt。 # 首先移除它原有的源文件特别是传统的编译方式。 # 注意直接操作其他目录下的target属性需要谨慎。更推荐的是编写一个补丁文件或使用CMake的覆盖机制。 # 这里展示一种更清晰的方法我们不对原始的fmt目标动手术而是创建一个新的、专门用于模块的target。 # 但为了保持兼容性其他代码可能依赖fmt::fmt我们可以将新目标命名为fmt_module然后让fmt::fmt指向它。 # 1. 首先包含fmt目录但暂时不启用它的库构建如果它有选项控制的话。 # 或者我们直接手动创建目标不调用它的CMakeLists.txt。考虑到对原有项目侵入最小我推荐一种**“包装器”** 方案。我们在主项目中创建一个新的CMake函数或脚本来“按我们的方式”构建fmt。# 定义一个函数用于创建模块化的fmt库 function(add_fmt_module_target) # 创建一个接口库用于承载模块的消费属性。但我们需要一个真正的库来编译源文件。 # 因此创建一个静态库目标。 add_library(fmt_module STATIC) # 关键步骤将fmt.cc声明为C模块接口文件 target_sources(fmt_module PUBLIC FILE_SET CXX_MODULES FILES ${CMAKE_CURRENT_SOURCE_DIR}/extern/fmt/src/fmt.cc PRIVATE # 其他非模块接口的源文件如os.cc, format.cc等仍然作为普通私有源文件添加 ${CMAKE_CURRENT_SOURCE_DIR}/extern/fmt/src/format.cc ${CMAKE_CURRENT_SOURCE_DIR}/extern/fmt/src/os.cc # ... 添加fmtlib需要的所有其他源文件具体列表请参考fmtlib自己的CMakeLists.txt ) # 设置包含目录。模块接口可能需要看到自己的头文件虽然模块不“包含”头文件但编译接口单元时需要。 target_include_directories(fmt_module PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/extern/fmt/include ) # PUBLIC包含目录这样链接fmt_module的目标也能找到头文件用于非模块代码或兼容性。 target_include_directories(fmt_module PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/extern/fmt/include ) # 设置必要的编译定义。例如我们可以在这里定义FMT_STATIC_THOUSANDS_SEPARATOR。 target_compile_definitions(fmt_module PUBLIC FMT_MODULE1 # 告诉fmtlib源码我们正在以模块模式编译它 # FMT_STATIC_THOUSANDS_SEPARATOR1 # 按需开启 ) # 设置C标准 target_compile_features(fmt_module PUBLIC cxx_std_20) # 为这个库目标添加一个别名保持与fmtlib传统命名的一致性 add_library(fmt::fmt_module ALIAS fmt_module) endfunction() # 调用函数创建目标 add_fmt_module_target()重要提示上面的源文件列表format.cc,os.cc等是不完整的。fmtlib有很多源文件你需要根据版本将其全部列出或者用一个file(GLOB ...)来收集虽然通常不推荐GLOB但对于第三方库的包装可以酌情使用。最稳妥的方法是查看你所用fmtlib版本中的CMakeLists.txt看它到底编译了哪些源文件。3.3 处理编译器的差异与特定标志不同编译器对模块的支持细节不同CMake会尽力处理但有时我们需要微调。我们可以根据编译器类型设置一些属性。if(MSVC) # MSVC 编译模块接口单元需要 /interface 标志但CMake的FILE_SET应该会自动添加。 # 我们可以添加一些其他优化或警告抑制。 target_compile_options(fmt_module PRIVATE /W4 /permissive-) elseif(CMAKE_CXX_COMPILER_ID MATCHES GNU|Clang) # GCC/Clang可能需要显式开启C20模块支持尽管CMake设置了cxx_std_20。 # 实际上CMake 3.28 对于 FILE_SET CXX_MODULES 应该会自动添加 -fmodules-ts 或 -stdc20。 # 但我们可以确保标准是C20。 target_compile_options(fmt_module PRIVATE -Wall -Wextra) # 对于GCC如果版本较低可能需要手动添加 -fmodules-ts if(CMAKE_CXX_COMPILER_ID STREQUAL GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 11) message(WARNING GCC 11 may have incomplete C20 modules support.) endif() endif()3.4 在你的主程序中使用模块化的fmt现在假设你的主程序src/main.cpp想要使用模块化的fmt它应该这样写// main.cpp import fmt; // 直接导入模块而不是 #include fmt/core.h int main() { // 现在你可以像往常一样使用fmt的函数 // 但它们是通过模块导入的而不是头文件包含。 auto str fmt::format(The answer is {}., 42); fmt::print({}\n, str); return 0; }在你的主CMakeLists.txt中链接我们新创建的库add_executable(my_app src/main.cpp) target_link_libraries(my_app PRIVATE fmt_module) # 链接我们的模块化fmt库 # 同样你的app也需要C20支持 target_compile_features(my_app PRIVATE cxx_std_20)神奇的事情发生了你不需要手动指定任何模块搜索路径-fmodule-mapper、/reference等。CMake因为target_link_libraries的关系自动知道my_app依赖于fmt_module而fmt_module导出了一个叫fmt的模块。CMake会自动安排编译顺序先编译模块接口fmt.cc并为my_app的编译命令设置好所有必要的依赖信息。4. 深入原理CMake如何管理模块依赖理解了“怎么做”之后我们有必要深入一层看看CMake的“魔法”背后到底做了什么。这能帮助你在遇到问题时自己调试。4.1FILE_SET CXX_MODULES的语义当你使用target_sources(... FILE_SET CXX_MODULES FILES ...)时你是在做两件事声明模块接口你列出的文件如fmt.cc会被CMake识别为“会导出C模块”的源文件。建立依赖图CMake会解析这些源文件实际上是通过编译器或扫描工具找出它们导出的模块名例如fmt和导入的模块名。这个依赖图被存储在CMake的内部数据结构中。4.2 依赖的自动传递当目标B通过target_link_libraries(A B)链接到目标A时CMake不仅会传递传统的链接库信息.lib,.a文件路径还会传递模块依赖信息。对于B消费者CMake会获取A提供者导出的所有模块列表。在生成B的编译命令时比如cl.exe或g的命令行CMake会自动添加必要的标志使得编译器能找到A导出的模块接口文件编译后的.ifc或.gcm文件。例如对于MSVC它可能会添加/reference fmtpath/to/fmt.ifc。4.3 编译顺序的保证C模块要求一个模块的接口单元BMIBinary Module Interface必须在所有导入它的翻译单元之前编译完成。CMake通过分析目标间的依赖关系图自动确定了正确的编译顺序。在我们的例子里fmt_module的fmt.cc生成fmt模块BMI一定会先于my_app的main.cpp导入fmt模块被编译。4.4 查看CMake生成的命令如果你好奇CMake具体生成了什么命令可以在配置完成后使用cmake --build . --verbose或make VERBOSE1来查看详细的编译命令行。你应该能看到在编译main.cpp时命令行里包含了指向fmt.ifc文件的引用参数。5. 常见问题、陷阱与解决方案实录在实际操作中你几乎一定会遇到一些问题。下面是我在尝试这套方案时踩过的坑和解决办法。5.1 问题一编译错误 “找不到模块 ‘fmt’”现象编译主程序时编译器报错说import fmt;声明失败找不到模块fmt。排查思路检查CMake版本确保是3.28或更高。运行cmake --version确认。检查编译器支持确保你的编译器支持C20模块并且CMake能检测到这种支持。可以尝试创建一个简单的模块测试项目。检查FILE_SET设置确认target_sources中FILE_SET CXX_MODULES后面的文件路径是否正确并且该文件确实是模块接口单元对于fmtlib就是src/fmt.cc。检查依赖链接确认你的可执行文件通过target_link_libraries链接了fmt_module目标。模块依赖是通过链接关系传递的不像头文件路径需要target_include_directories。查看详细输出使用cmake --build . --verbose查看编译main.cpp的具体命令。检查命令行中是否包含了模块引用参数如MSVC的/reference fmt...。如果没有说明CMake没有成功建立模块依赖。解决方案最常见的原因是CMake版本过低。升级到3.28以上。其次是编译器版本过低。升级到支持C20模块的版本MSVC 2019 16.10, GCC 11, Clang 16。确保fmt_module目标的CXX_MODULES FILE_SET已正确设置并且该目标成功编译生成了模块接口文件在构建目录下寻找.ifc或.gcm文件。5.2 问题二链接错误找不到fmt库的符号现象模块导入编译通过了但链接阶段报错提示fmt::v10::detail::...等符号未定义。排查思路检查源文件是否完整fmt_module目标是否包含了fmtlib所有的实现源文件如format.cc,os.cc等模块接口单元fmt.cc通常只包含接口声明实现是在其他.cc文件里。如果这些实现文件没有被编译进fmt_module库链接时自然会找不到符号。检查链接顺序确保你的target_link_libraries命令中fmt_module出现在正确的位置。解决方案核对源文件列表这是最容易出错的地方。去fmtlib源码的CMakeLists.txt里找到add_library或target_sources命令把里面所有的.cc源文件除了示例和测试都复制到你的包装函数里。一个更简单但不那么优雅的方法是使用aux_source_directory或file(GLOB ...)但要注意可能包含不需要的文件。一个更可靠的参考是查看fmtlib编译后生成的build.ninja或Makefile看它编译了哪些.o文件。5.3 问题三想自定义编译定义如FMT_STATIC_THOUSANDS_SEPARATOR不生效现象你在target_compile_definitions(fmt_module PUBLIC ...)里添加了FMT_STATIC_THOUSANDS_SEPARATOR1但编译fmt.cc时似乎没被定义或者编译通过了但运行时行为不对。排查思路定义是否传递PUBLIC定义应该对fmt_module目标自身及其所有依赖者都可见。检查编译fmt.cc的命令行详细输出看是否有-DFMT_STATIC_THOUSANDS_SEPARATOR1或/D FMT_STATIC_THOUSANDS_SEPARATOR1标志。宏的生效时机有些宏必须在第一次包含fmt头文件或编译模块接口单元之前就定义好否则可能因为条件编译而失效。FMT_STATIC_THOUSANDS_SEPARATOR很可能就属于这种。确保它在编译fmt.cc时就被定义。解决方案使用target_compile_definitions(fmt_module PUBLIC ...)是正确的。如果怀疑没生效检查CMake生成的编译命令。如果问题依旧可以尝试在add_fmt_module_target函数内部在target_sources命令之前就使用add_definitions但这是全局的不推荐或者更精确地设置目标的COMPILE_DEFINITIONS属性set_target_properties(fmt_module PROPERTIES COMPILE_DEFINITIONS FMT_STATIC_THOUSANDS_SEPARATOR1 )一个关键点fmtlib的src/fmt.cc里面会包含format.h等头文件。这些头文件里可能有基于FMT_MODULE宏的条件编译。因此FMT_MODULE1这个定义必须有并且要在编译接口单元时生效。我们的方案中已经通过target_compile_definitions添加了它。5.4 问题四与fmtlib自身的CMakeLists.txt冲突现象你的项目可能已经通过add_subdirectory(fmt)引入了fmtlib它自己定义了一个fmt目标。你又用我们的方法创建了一个fmt_module目标导致目标名冲突或者链接了错误的库。解决方案方案A推荐不直接使用add_subdirectory。将我们的包装函数放在主项目的CMakeLists中直接管理fmtlib的源码编译。这样完全掌控。方案B利用fmtlib的配置选项。fmtlib的CMakeLists.txt可能有选项来控制是否构建库。你可以先设置一个缓存变量禁止它构建默认的库然后再创建我们的模块化目标。# 在add_subdirectory之前设置 set(FMT_INSTALL OFF CACHE BOOL FORCE) set(FMT_MODULE ON CACHE BOOL FORCE) # 如果fmtlib自己有这个选项的话 add_subdirectory(extern/fmt) # 此时原始的fmt目标可能不会被创建或者被创建但不包含源文件。 # 然后我们再调用自己的add_fmt_module_target函数。这种方法需要深入研究fmtlib的CMake脚本看它是否提供了相关的钩子或选项。方案C重命名目标。确保你的fmt_module目标名唯一并且你的应用链接的是这个新目标。5.5 编译器特定问题备忘录编译器可能遇到的问题解决方案或检查点MSVC错误 C7595: “fmt”: 未找到模块接口单元1. 确认CMake版本 3.28。2. 检查/reference参数是否出现在编译命令中。3. 尝试以“开发者命令提示符”运行CMake确保环境完整。GCC错误模块“fmt”未定义1. 确认GCC版本 11。2. 检查是否使用了-fmodules-ts标志CMake应自动添加。3. 清理构建目录模块缓存gcm.cache可能损坏。Clang模块文件未找到1. 确认Clang版本 16。2. Clang可能需要显式指定模块输出路径(-fmodule-output)。CMake 3.28 应能处理。3. 检查-fimplicit-modules或-fimplicit-module-maps标志。6. 进阶将方案封装为可复用的CMake函数为了在多个项目中方便使用我们可以把整个逻辑封装成一个函数放在cmake/AddFmtModule.cmake这样的文件里。# cmake/AddFmtModule.cmake function(add_fmt_module) set(options) set(oneValueArgs PREFIX) # 可以指定源码路径前缀 set(multiValueArgs DEFINITIONS) # 可以传递自定义编译定义 cmake_parse_arguments(ARG ${options} ${oneValueArgs} ${multiValueArgs} ${ARGN}) if(NOT ARG_PREFIX) set(ARG_PREFIX ${CMAKE_CURRENT_SOURCE_DIR}/extern/fmt) endif() # 查找所有必要的源文件这里需要根据fmt版本调整 file(GLOB FMT_SOURCES ${ARG_PREFIX}/src/*.cc # 排除测试文件如果存在的话 ) # 更精确的做法是手动列出避免包含示例 set(FMT_CORE_SOURCES ${ARG_PREFIX}/src/format.cc ${ARG_PREFIX}/src/os.cc # ... 列出所有核心源文件 ) # 模块接口文件 set(FMT_MODULE_SOURCE ${ARG_PREFIX}/src/fmt.cc) add_library(fmt_module STATIC) target_sources(fmt_module PUBLIC FILE_SET CXX_MODULES FILES ${FMT_MODULE_SOURCE} PRIVATE ${FMT_CORE_SOURCES} ) target_include_directories(fmt_module PUBLIC ${ARG_PREFIX}/include PRIVATE ${ARG_PREFIX}/include ) target_compile_definitions(fmt_module PUBLIC FMT_MODULE1 ${ARG_DEFINITIONS} # 传递用户自定义定义 ) target_compile_features(fmt_module PUBLIC cxx_std_20) # 提供别名 add_library(fmt::fmt ALIAS fmt_module) # 将目标名返回给父作用域可选 set(fmt_module_target fmt_module PARENT_SCOPE) endfunction()在主CMakeLists.txt中list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(AddFmtModule) add_fmt_module( PREFIX ${CMAKE_CURRENT_SOURCE_DIR}/extern/fmt DEFINITIONS FMT_STATIC_THOUSANDS_SEPARATOR1 )7. 性能与兼容性考量7.1 编译性能使用C20模块的一个主要宣称优势是改善编译速度。对于fmtlib这样的常用库将其转换为模块后首次编译需要编译模块接口单元fmt.cc生成BMI这可能比单纯解析头文件稍慢一点。增量编译一旦BMI生成所有导入fmt模块的源文件都不需要再重复解析庞大的fmt头文件编译速度会有显著提升。尤其是在大型项目中fmt被数十上百个文件包含时效果更明显。7.2 二进制兼容性模块接口是ABI应用程序二进制接口的一部分吗目前C标准没有规定BMI的格式各编译器实现不同MSVC的.ifcGCC/Clang的.gcm。因此BMI不能跨编译器甚至跨编译器版本共享。你为MSVC 2022生成的.ifc文件不能给GCC用。甚至不同补丁版本的MSVC之间也可能不兼容。链接库的二进制兼容性保持不变。fmt_module最终生成的静态库.lib/.a或动态库.dll/.so的ABI与用传统头文件方式编译出的库是相同的前提是编译定义一致。模块只影响编译期不影响链接期的符号。7.3 与头文件模式的共存我们的方案创建了一个新的目标fmt_module。你项目中的其他第三方库或者遗留代码可能仍然使用#include fmt/core.h。只要它们链接的是同一个fmt_module库它提供了所有符号并且包含了正确的头文件路径我们通过target_include_directories设置了PUBLIC包含那么混合使用是完全可行的。一部分代码import fmt;另一部分代码#include fmt/core.h它们都能正确链接到同一个fmt库实现。这种灵活性使得向模块的迁移可以逐步进行而不是“一刀切”。8. 总结与最终建议走完这一整套流程我们再回头看那个Issue就能深刻理解提问者的诉求了。手动编译模块的方式确实割裂了CMake的依赖管理模型让开发者回到了“管理编译命令行”的原始时代。而CMake 3.28带来的原生CXX_MODULES支持正是解决这一痛点的官方答案。给想要实践的你几点最终建议环境先行确保你的工具链达标——CMake 3.28编译器MSVC/GCC/Clang使用较新且稳定支持C20模块的版本。这是所有工作的基础。逐步实施不要试图一次性把你整个项目模块化。先从像fmtlib这样的基础、稳定、且你深刻理解的第三方库开始实践。成功集成一个就能积累信心和经验。深入理解原理不要只复制粘贴CMake命令。花点时间理解FILE_SET CXX_MODULES做了什么以及CMake是如何传递模块依赖的。这会在你遇到古怪错误时给你自己调试的能力。拥抱社区动态fmtlib官方Issue #3990的讨论还在继续。很有可能在未来某个版本fmtlib会官方提供对CMake原生模块的支持。届时我们的这些“包装”工作就可以光荣退休了。关注上游进展及时调整你的项目。做好问题记录C20模块和CMake的集成仍然是一个比较前沿的领域不同编译器、不同项目结构可能会遇到独特的问题。把你遇到的问题和解决方案记录下来不仅帮助自己也可能帮助到社区里的其他人。通过这套方案我们不仅解决了fmtlib的集成痛点更重要的是掌握了一套方法论。未来当其他库如range-v3、Catch2等也逐步提供模块支持时你可以用同样的思路去集成它们。C的模块化时代已经到来而CMake正在为我们铺平构建的道路。现在是时候动手让你的项目体验一下编译速度提升的快感了。