ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

XMake能否取代CMake?C++构建系统十年演变与实战对比

XMake能否取代CMake?C++构建系统十年演变与实战对比 这几年用CMake用得最深的场景多半是被人拉去修那些层层嵌套的CMakeLists.txt。修着修着我就想如果当初有个能用普通脚本语言描述构建的工具也不至于这么痛苦。直到XMake出现它在C社区里被反复拿来跟CMake对比“XMake能否取代CMake”也就成了每次构建系统讨论绕不开的题。借着复盘C构建系统十年变局这个机会我把这两个工具从语法、包管理、跨平台支持到实际迁移成本彻底掰扯一遍给还在选型或者正准备动手迁移的兄弟们一个真实参考。这篇文章适合三类人看一是新项目刚起步、纠结到底选CMake还是XMake的二是被现有CMake工程折腾得不行、想换工具的三是对构建系统发展趋势感兴趣、想搞清楚XMake到底什么来头的。我不会只罗列官方文档里的概念更多是讲我实际落地时踩过的坑和对比之后的想法。1. 为什么突然聊“取代”C构建系统这十年到底经历了什么聊“XMake能否取代CMake”之前得先把C构建系统这十年的演变捋清楚。很多人没有经历过那段最混乱的时期觉得CMake是理所当然的选择但它今天的位置其实是踩着无数前辈的“尸体”爬上来的。理解了这段历史你才能明白为什么XMake一出现就会有那么多人跑去对比。1.1 从Makefile到CMake一套构建规则统治全平台十年前写C最基本的构建方式是Makefile。Makefile本身不难难的是你要为一个项目维护好几套规则Linux下用gccWindows下用MSVCmacOS下可能还要考虑clang。同一个项目在三个平台上跑的构建脚本几乎完全是三份代码。更要命的是当时的项目还经常用autotools那一套autoheader、autoconf、automake一步步下来生成configure脚本再跟Makefile联动。这一套流程在Linux服务器上还能忍到了Windows上基本就是灾难。我印象最深的一次经历是帮朋友移植一个老项目源文件不多但Makefile里写了一堆平台判断还有一些奇怪的编译参数。我把项目拷到Windows上各种路径分隔符问题、编译器参数不兼容问题整整搞了两天才跑起来。这种体验在当时一点都不罕见跨平台构建的痛是所有C开发者共有的痛。CMake的聪明之处在于它第一次让你用一套描述性的DSL同时覆盖多个平台和编译器。你不用再关心每个平台的具体命令是什么只需要声明“我有一个可执行文件它链接了哪些库”剩下的事交给CMake生成对应平台的构建文件。这种模式在2010年后迅速占据了主流位置尤其随着Vcpkg、Conan这些包管理工具出现加上CMake 3.x版本不断迭代它基本成了C新项目的默认选择。1.2 CMake的舒适区与暗伤为什么有人开始“反CMake”但CMake的舒适区同时也是它的暗伤。随着项目越来越大CMakeLists.txt的复杂度也在指数级上升。最典型的问题有三个第一个是变量作用域。CMake的变量作用域设计得很“灵活”但灵活性太高就容易失控。你在子目录里set一个变量有时候能传到父目录有时候又传不出去取决于你用的是普通变量还是缓存变量取决于你有没有加PARENT_SCOPE参数。大型项目里经常出现“我明明设置了某个变量编译就是不生效”的诡异现象查了半天发现是被某个子目录覆盖了。第二个是字符串列表的处理。CMake的列表是用分号分隔的字符串而这个最简单设计的坑实在太深了。路径里带空格、文件名带分号、从外部传入多个参数稍不注意就会被CMake拆成意想不到的片段。我在一个项目里调试过路径带空格的问题最后发现需要把参数用引号层层包起来那种挫败感是写在代码里的不跑一遍真的体会不到。第三个是语法本身。CMake的命令式语法接近一门定制的编程语言但它没有标准的函数库没有清晰的异常机制错误信息往往含糊其辞。即使官方在3.x时代主推target-oriented的写法依然没办法抹掉历史包袱网上大量的老教程和旧项目代码还在用add_compile_options、include_directories这些全局影响函数新旧写法混在一起阅读体验极差。这些暗伤积累到一定程度自然就有人开始寻找替代品Bazel、Meson、XMake都是在这个背景下出现的。它们各自的路线不同但目标都是解决CMake“用起来太痛苦”的问题。2. XMake是什么凭什么被拿来和CMake比XMake最初出现在开发者视野里很多人第一反应是“又出来一个玩具”。它的确年轻但发展速度并不慢。和Bazel这种技术巨头主导的项目不同XMake从一开始就是靠“好用”两个字来争取用户的它更贴近普通C开发者的日常。2.1 XMake的设计哲学一切皆配置配置即代码XMake最大的特点也是它跟CMake最根本的区别在于它选择用Lua作为配置脚本语言。所有构建描述都写在xmake.lua文件里而这个文件本质上是Lua代码。意味着你可以用循环、分支、函数、自定义类来组织构建逻辑而不只是调用一堆CMake提供的命令。我对比过同一个项目的CMakeLists.txt和xmake.lua那是两种完全不同的编写体验。CMake里你要维护一大堆变量状态xmake.lua里我更像在正常写程序逻辑清晰结构自然想抽公共函数就抽公共函数想写工具函数就写工具函数。这种“配置即代码”的模型对习惯了普通编程语言的开发者来说上手成本比CMake的DSL低很多。举个例子我需要根据操作系统类型决定是否链接某个库。CMake写法要写一堆if(WIN32)判断还要注意变量作用域xmake.lua里直接用Lua的if语句再加上平台内置变量十几行就搞定了逻辑一目了然。这种差异在只有十几个文件的小项目里体现不出来一旦target多、配置复杂优势就非常明显。2.2 一个Lua脚本能做什么用最少代码描述构建直接上代码对比。假设我有一个简单项目包含src目录下的源文件需要链接fmt库。CMakeLists.txt大概是这个样子cmake_minimum_required(VERSION 3.15) project(hello) find_package(fmt REQUIRED) add_executable(hello src/main.cpp src/util.cpp) target_include_directories(hello PRIVATE include) target_link_libraries(hello PRIVATE fmt::fmt)而xmake.lua是这样set_project(hello) set_version(0.1.0) add_rules(mode.debug, mode.release) add_requires(fmt) target(hello) set_kind(binary) add_includedirs(include) add_files(src/*.cpp) add_packages(fmt)一眼看过去xmake.lua更像是在声明一个目标对象它有名字有类型有源文件有依赖。而CMake那条add_executable只是把一堆参数串起来。对于单个target区别不大但如果你有二三十个targetxmake.lua的写法可以直接用循环批量生成CMake则要复制粘贴一堆几乎一样的代码改起来风险也更大。还有一点很关键XMake把“构建模式”做成了内置规则。mode.debug和mode.release是add_rules直接提供的它会自动帮你处理好调试信息、优化等级、宏定义这些事。CMake里这些东西靠CMAKE_CXX_FLAGS_RELEASE等变量控制虽然也标准但新手很难搞清楚要改哪个。2.3 与同代构建系统横评XMake、Meson、Bazel到底差在哪聊XMake很难绕开Meson和Bazel它们都是CMake替代者阵营里的有力竞争者但三条技术路线的侧重完全不同。构建系统配置语言核心后端包管理典型适用场景CMake定制DSL各平台原生构建文件find_package / FetchContent / 第三方集成广泛使用老牌大项目XMakeLua各平台原生构建文件也支持其他后端xrepo内置中小型项目、个人项目、快速原型MesonPython风格DSLNinjawrap文件依赖集成系统级项目GNOME等BazelStarlarkBazel自研Bazel仓库机制大型单仓Google生态内Bazel是最“重型”的它在Google内部能支撑千万行级别的单仓缓存和远程执行能力极强但代价是配置复杂度极高依赖外部Python工具链一点小改动都可能涉及整套规则重写。Meson走了Python风格的语法路线以Ninja作为唯一的后端生成器配置体验比CMake好不少但它在Windows和MSVC生态的支持相对没有CMake和XMake那么顺滑。XMake则是“轻量交付”的路线它不要求你学一套新语言只需要你会一点Lua就能很快写出构建脚本。它不像Bazel那样强势绑定一套整体工程规范也不像Meson那样只认Ninja而是像CMake一样生成各平台原生的构建文件直接在Windows上用Visual Studio工程文件构建在Linux上用Makefile或Ninja构建迁移成本相对较低。3. 硬碰硬XMake和CMake的核心维度对比前面聊了各自的定位和理念这节我们落到实际开发的维度上把XMake和CMake放在一起比一比。我的对比标准非常朴素谁能让项目跑起来更省心谁在复杂场景下更好排查问题谁对生态的兼容性更强。3.1 语法与可维护性复杂项目里谁更省心刚才代码示例里已经体现了语法的基本差异这里再往深处说一点。CMake的问题在于命令式DSL实在太“自由”同一种效果有无数种写法新老风格混用严重。现代CMake官方推荐target-oriented写法但网络上的教程、旧项目的代码还是大量使用全局函数。我见过一个项目里一行add_compile_options影响了所有target后面的人想单独关掉某个警告只能靠add_compile_options的另一些HACK方式绕来绕去。相比之下xmake.lua里的target作用域非常清晰每个target的配置都写在对应的target块内不会全局污染。就算你非要在多个target里共用某些配置也可以用includes引入公共配置或者用函数封装。所有配置都是Lua代码你可以自己写函数来统一管理这对维护大型项目有很大帮助。还有一点是对错误信息的友好度。CMake的报错经常是“无法找到XXX”但不告诉你为什么找不到尤其在find_package的时候环境变量、config文件路径、版本不匹配排查成本非常高。XMake的报错信息一般会带上具体的配置文件名和行号同时在编译失败时会把编译器的输出完整地呈现出来定位问题快很多。3.2 包管理与依赖集成现代C项目的刚需现代C项目已经离不开包管理依赖怎么拉、版本怎么锁、二进制怎么分发直接影响项目的可交付性。CMake生态里最常见的组合是Vcpkg或Conan加find_package靠CMake的包查找机制把第三方库接入工程。这种做法能用但有几个痛点。一是引入新依赖的流程很长你要先在包管理器里装库再在CMakeLists里写find_package再配置target_link_libraries三步缺一不可。二是版本锁定比较麻烦Vcpkg是整体版本基线模式Conan的配置又有点门槛。三是最终用户拿到你的项目时他得先在自己的机器上把这一堆依赖装好缺失一个库就编译不过。XMake内置了xrepo包管理直接在xmake.lua里用add_requires声明依赖首次构建时XMake会自动从远端仓库下载源码并编译集成到target里只需要add_packages一行。它把CMake里“下载依赖、查找依赖、链接依赖”三个步骤简化成了一个。用起来非常像Python的pip思路声明依赖交给工具处理。我拿spdlog举例CMake的FetchContent写法是include(FetchContent) FetchContent_Declare(spdlog GIT_REPOSITORY https://github.com/gabime/spdlog.git GIT_TAG v1.12.0 ) FetchContent_MakeAvailable(spdlog) target_link_libraries(app PRIVATE spdlog::spdlog)xmake.lua则是add_requires(spdlog) target(app) set_kind(binary) add_files(src/*.cpp) add_packages(spdlog)从维护角度说xmake的写法直观不少依赖版本写在同一处升级依赖时只改一行。3.3 跨平台与工具链支持实际场景见真章跨平台可不是“在Windows和Linux上都能编译”这么简单它还包括对不同编译器、不同架构甚至是交叉编译场景的支持。CMake在这方面最大的优势是经过十几年验证的成熟度几乎所有你能想到的编译器场景网上都有CMake的配置例子。XMake同样支持Windows、Linux、macOS三大平台对MSVC、gcc、clang、MinGW这些主流编译器都做了适配。交叉编译场景CMake需要你准备一个toolchain.cmake文件然后在构建时指定CMAKE_TOOLCHAIN_FILE这一步很多新手直接卡住。XMake提供了更简化的方式通过xmake f --toolchainyour-toolchain来切换工具链也可以用--sdk指向交叉编译SDK目录设计上明显更用户友好。在实际项目中我自己遇到比较多的是命令行里的工具链适配和配置状态保持问题。用CMake时CMakeCache.txt里缓存了之前的配置切编译器或者改架构经常需要删缓存重来。XMake也有类似问题但它的配置管理更透明xmake f命令可以直接查看和修改当前配置不用翻CMakeCache。4. 实操把一个真实项目从CMake迁到XMake要几步空谈对比有用但不够真正有说服力的是把项目从CMake迁到XMake跑一遍。这节我用一个贴近实际的项目来做迁移演示项目结构不复杂但包含了头文件目录、第三方依赖、多配置模式这些常见需求拿来当模板参考足够了。4.1 一个单target项目的迁移对照假设项目结构是这样demo/ ├── CMakeLists.txt # 待迁移的CMake脚本 ├── include/ │ └── demo_util.h ├── src/ │ ├── main.cpp │ └── demo_util.cpp └── xmake.lua # 迁移后的XMake配置原CMakeLists.txt可能长这样cmake_minimum_required(VERSION 3.15) project(demo) find_package(spdlog REQUIRED) add_executable(demo src/main.cpp src/demo_util.cpp) target_include_directories(demo PRIVATE include) target_link_libraries(demo PRIVATE spdlog::spdlog)传统构建方式是mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease cmake --build . -j8 ./demo换成xmake.lua之后set_project(demo) set_version(1.0.0) add_rules(mode.debug, mode.release) add_requires(spdlog) target(demo) set_kind(binary) add_includedirs(include) add_files(src/*.cpp) add_packages(spdlog)构建命令变成xmake f -m release xmake -j8 xmake run整个过程干净利落。尤其需要注意的是xmake默认就会把构建目录放在当前项目的build目录下所有编译产物按平台和模式隔离不会像CMake那样需要你手动管理build目录和CMakeCache。4.2 多target工程怎么组织才不失控实际的工程不会只有一个target可能有库、测试、工具好几个二进制目标还会共享一堆头文件和编译选项。在CMake里这种结构最痛苦的就是变量污染全局头文件目录、全局宏定义到处都是。xmake组织多target工程的思路更接近“每个目标都是一等公民”。你可以把多个target写在一个xmake.lua里也可以把不同子项目的配置拆到各自的xmake.lua里再用includes包含进来。比如set_project(demo) includes(src/xmake.lua, tests/xmake.lua) -- 公共执行规则 add_rules(mode.debug, mode.release)然后src/xmake.lua里定义核心库target(demo_core) set_kind(static) add_includedirs(include, {public true}) add_files(demo_core/*.cpp) add_packages(spdlog, {public true})tests/xmake.lua里定义测试程序target(demo_tests) set_kind(binary) add_files(main.cpp) add_deps(demo_core) add_packages(catch2)这里有几个细节值得注意。add_includedirs的public参数我特意标出来了它表示这个头文件目录会传递给依赖该target的其它目标这正是CMake里target_include_directories(... PUBLIC ...)做的事情但xmake的写法更直观直接作为参数修饰符而不是函数重载。add_packages同理public依赖会被子目标继承。这种“目标属性可继承”的写法让大型项目的依赖关系非常清晰每个xmake.lua只管自己的事。4.3 迁移时容易踩的坑变量、循环与配置清理迁移过程不可能毫无障碍我把自己踩过的以及身边朋友踩过的坑总结一下能帮大家省不少时间。第一个坑是变量作用域问题。CMake的全局变量在xmake里没有对应概念如果你原来的CMakeLists里有大量set(VAR ...)然后在子目录里使用迁移时最好重新设计成函数或局部变量。xmake也支持全局变量但滥用全局变量反而会把代码搞乱不如一开始就用return和函数的组合来组织。第二个坑是循环与字符串拼接。CMake的foreach语法非常晦涩很多项目里都有“循环里字符串拼接绕来绕去”的代码。xmake里你可以直接写Lua循环配合跨平台路径函数一行路径拼接就解决了。比如把多个子目录下的源文件加入targetfor _, dir in ipairs({core, utils, io}) do add_files(path.join(dir, *.cpp)) end这在CMake里要做到同等效果代码会绕很多。第三个坑是缓存与配置残留。切换编译模式时CMake偶尔会出现Cache不一致导致编译失败xmake同样可能碰到但解决方式简单。如果你改了依赖版本或者编译器直接删掉build目录重来xmake f -c xmake f -m release xmake这个-c参数就是清理配置比手动找CMakeCache文件方便得多。5. 常见问题与排查技巧实录这一节我把平时在一些技术社区里被反复问到的问题和自己在使用CMake或XMake过程中实际遇到的坑放在一起做个速查。这些问题覆盖了一些搜索热度很高的场景也都是真实影响开发效率的地方值得好好看看。5.1 CMake实际使用中的高频问题速查很多问题其实是CMake使用者的高频困扰不一定跟XMake有关但了解它们的解决思路对两种工具对比也有帮助。问题场景常见做法推荐解决思路生成的VS工程使用相对路径set(CMAKE_USE_RELATIVE_PATHS ON) 老方法用CMAKE_RUNTIME_OUTPUT_DIRECTORY和VS_DEBUGGER_WORKING_DIRECTORY控制别依赖全局开关执行bash命令execute_process(COMMAND bash -c ...)注意跨平台用${CMAKE_COMMAND} -E env不要硬编码bash预编译头文件写法target_precompile_headers老项目用add_library OBJECT库也是常见的过渡方案输出路径去掉debugset(CMAKE_RUNTIME_OUTPUT_DIRECTORY ...)用生成器表达式将其按配置拼接如${CMAKE_BINARY_DIR}/bin/$cmake可以代替keil5吗不是同一个维度的问题CMake是构建系统Keil是IDECMake配合armcc/gcc可以生成Keil工程的底层构建过程但IDE操作还得依赖Keil我挑几个展开说。生成VS工程相对路径的问题我见过不少项目里直接挂CMAKE_USE_RELATIVE_PATHS但这个变量其实早就被官方标记为不推荐使用了。更合理的做法是把输出目录统一设置到项目根下的bin目录这样VS工程里引用的路径就相对稳定了。建议用生成器表达式set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)这样不同配置的输出至少在一个固定的根目录下后面用脚本处理起来也方便。执行bash命令那条很多项目会在测试时跑一段脚本。直接用execute_process调用bash在Linux上没问题但跨平台到Windows就挂了。我建议用CMake自带的${CMAKE_COMMAND} -E env或者cmake -E chdir的方式来做至少保证在不同平台上行为一致。5.2 换到XMake之后这些场景怎么处理换到XMake之后上面这些场景的解决思路有一些是可以用Lua脚本自定义的。比如想在构建前执行一段外部命令用os.run函数after_build(function(target) os.run(python3 scripts/post_build.py) end)这个after_build是XMake提供的构建钩子类似的还有before_build、on_build等它们可以挂载在target上也可以全局定义。如果你想把VS工程生成成相对路径可以直接在配置里对target设置vs_folder等属性或者干脆把路径输出统一用projectdir变量拼接。对于预编译头的需求XMake提供了内置的c_precompile_h规则target(app) set_kind(binary) add_files(src/*.cpp) add_cxxflags(stdc17) add_rules(c.precompile_h, {header include/pch.h})比CMake的target_precompile_headers要简洁一些而且能自动处理MSVC和gcc在pch上的差异省掉了不少平台兼容代码。5.3 嵌入式场景与VS Code远程开发怎么接关于“cmake可以代替keil5吗”这类问题本质上要区分构建系统和IDE。CMake或XMake是构建系统负责把源码变成二进制Keil是IDE包含了编辑器、调试器、烧录工具。如果你只是做裸机或RTOS开发完全可以用CMake或者XMake arm-none-eabi-gcc来写构建脚本然后靠调试器工具来烧录。XMake对ARM交叉编译的支持也挺好通过--toolchain指定arm-none-eabi-gcc即可这和rk3576这类芯片构建系统镜像的思路是相通的。对于很多开发者在VSCode里配置C/C环境的需求主要受益点是得到compile_commands.json文件让clangd或者C/C插件能够索引代码。CMake生成这个文件需要设置CMAKE_EXPORT_COMPILE_COMMANDSXMake则更简单直接xmake f -p linux --export-compile-commands会生成compile_commands.json到build目录VSCode里配置好compile_commands路径之后代码跳转、补全、错误提示都能正常工作体验不输给专业的IDE。如果是在远程虚拟机里跑编译我的习惯是用SSH连接远程服务器在远程执行xmake命令本地VSCode通过Remote-SSH插件直接打开项目文件夹这样代码编辑在本地编译在远程非常顺滑。重点是要把远程机器的build目录和工具链装好剩下的交给xmake自己处理就行。6. 我的判断取代不了但值得认真对待把XMake和CMake从语法、包管理、跨平台配置到实际迁移过程全部过了一遍之后回到最开始的问题XMake能不能取代CMake我的答案很明确短期内不能但它的出现本身已经改变了C构建系统的版图。CMake最大的护城河不是技术而是生态。十几年下来几乎每一个开源C库都提供了CMake的find_package配置嵌入式、HPC、游戏引擎、图形学这些领域的工具链也都默认能用CMake接入。一个项目想换构建系统代价不只是改写脚本本身还有整套周边生态的重建。对于大多数企业和开源项目来说这个switch cost实在太高了不是技术问题是经济问题。但XMake也有它明确的生存空间。个人项目、中小型库、需要快速迭代的原型验证这些场景下XMake的开发效率明显更高包管理的集成让依赖处理省心很多。我现在的日常工作习惯是新开的个人项目和不太合作的小项目直接用XMake维护中的老项目继续用CMake尤其是那些要交付给客户或者要嵌入到别人CI里的项目CMake依然是更稳妥的选择。最后再分享一个小建议。如果你还在纠结选型不要只看功能对比列表拿你手头一个真实项目分别用CMake和XMake搭一遍构建设置好debug和release两种模式再加上一个第三方依赖跑一遍看哪个更顺手。工具选型最终是匹配团队习惯的事没有绝对的统一答案。我的体会是构建系统的价值从来不在于它多炫酷而在于当项目出问题时它能不能帮你快速定位到问题所在。从这个角度看XMake是个相当称职的选手而CMake仍然是个可靠的老伙计。
返回列表