qmake转CMake:用q2c自动迁移Qt构建脚本

发布时间:2026/9/2 2:52:27
qmake转CMake:用q2c自动迁移Qt构建脚本 简介q2c 是一款面向 Qt 开发者的实用命令行转换工具专注于在 qmake 的 .pro 工程文件与 cmake 的 CMakeLists.txt 之间建立双向映射解决两种构建系统迁移时的重复劳动问题适合需要切换构建体系或学习构建原理的 Qt C 开发者尤其适合跨平台工程维护场景。压缩包为 zip 格式体积仅 14KB共 13 个文件包括 6 个 cpp 源码文件、5 个头文件、1 个 pro 工程文件和 1 个 README 说明文档结构精简便于直接阅读和编译也可作为学习构建系统解析逻辑的参考样本。目前已有三千二百零四人学习下载热度反映出它在 Qt 开发者中的实用价值。通过源码可以理解工程文件解析、配置项提取和构建规则生成的实现思路既有助于对比 qmake 与 cmake 的语法差异和边界情况也能在实际项目中快速生成另一套构建文件再针对特性差异做少量手动调整从而提高工程迁移、项目维护和团队协作效率。1. 为什么做 q2cqmake 和 CMake 之间的真实鸿沟1.1 Qt 构建系统的版本断层做 Qt 项目的人应该都有这种感觉项目一老整个构建体系就像个陈年旧箱子打开一个.pro文件满眼是SOURCES 、HEADERS 、QT widgets看起来很简单真要往 CMake 迁移的时候才发现qmake 那种隐式推断满天飞的写法根本没有一行对应的 CMake 命令。q2c 这个转换工具就是干这个用的把 qmake 的.pro/.pri工程文件转换成 CMake 的CMakeLists.txt减少手工重写构建脚本的工作量。我在维护一个 Qt5 老旧项目时被逼着写了它的第一版后来发现团队里其他人也在反复做同样的事就干脆整理成独立工具。为什么这事这么普遍因为 Qt 6 已经全面转向 CMake新项目几乎默认就是 CMake而还有大量存活了十年以上的 Qt4/Qt5 项目停留在 qmake。结果是同一家公司里新模块用find_package(Qt6)老模块还在qmake里加.pro文件两边的人互相看不懂对方的构建脚本。老项目只要不重构就得有人继续维护 qmake 那套体系但只要有一点重构需求迁移到 CMake 就变成绕不开的工程。而手工重写一个中型项目的 CMakeLists少说也要半天里面还全是容易遗漏的细节。1.2 迁移痛点为什么不能直接手动重写有人会说一个.pro文件不就几十行吗手动翻译一下不就完了这话只对“玩具项目”成立。真实项目的.pro往往是多层嵌套的主工程文件引用一堆.pri子模块每个子模块又有自己的INCLUDEPATH、DEFINES、CONFIG条件编译项。再加上win32:{}、unix:!macx:{}这类作用域以及contains()、count()函数手动转写成 CMake 的时候最容易被漏掉的不是那些显式赋值而是隐藏在 qmake 默认行为里的东西。我自己踩过的最典型一个坑qmake 里只要把头文件写在HEADERS里MOC 就会自动处理带Q_OBJECT的类但到了 CMake你必须明确打开CMAKE_AUTOMOC并且把头文件也列进target_sources。很多人手工迁移时忘了这一步结果编译报一堆“未定义的 vtable”错误排查半小时才发现是 MOC 没跑。这类“qmake 帮我做了但我不知道”的隐式规则才是迁移的真正成本所在。q2c 的定位就是把这些机械性工作先做完让人把精力集中在真正需要判断的语义差异上。2. 核心设计思路从 qmake 到 CMake 的映射逻辑2.1 解析文件的什么、跳过什么设计 q2c 的第一步是划定解析边界。qmake 的语法看起来简单实际上是一种自带函数式特性的脚本语言有变量、有作用域、有函数调用。如果什么都想完整解析等于做一个 qmake 解释器工作量一下就失控了。所以我采取的策略是只处理构建声明不处理控制流逻辑。具体来说q2c 的词法分析器只关心几类内容变量赋值、、-、*、常见变量名SOURCES、HEADERS、FORMS、RESOURCES、QT、CONFIG、DEFINES、INCLUDEPATH等、以及顶层的作用域块。凡是遇到contains()、自定义函数、复杂的嵌套条件先尝试按字面逻辑翻译翻译不了就留下一行# TODO(q2c): 无法自动转换的 qmake 表达式中止转换并提示用户手工处理。这个“不完美但透明”的设计很重要。用户在拿到转换结果时能清楚看到哪些地方是工具拿不准的而不是工具静默地给出一个错误结果。我见过一些自动转换工具为了追求通过率高遇到不认识的行就悄悄跳过结果生成的 CMakeLists 表面能看一编译全是错。q2c 的原则是宁可多留 TODO也不要把错误当成成功。2.2 核心变量的映射规则下面这张表是我最开始写 q2c 时整理的映射关系也是整个工具的骨架qmake 写法CMake 对应备注QT widgetsfind_package(Qt6 REQUIRED COMPONENTS Widgets)target_link_libraries(... Qt::Widgets)Qt5 时则找Qt5::Widgets取决于检测到的版本CONFIG c17set(CMAKE_CXX_STANDARD 17)多个标准同时出现时取最后一个TARGET app_nameproject(app_name LANGUAGES CXX)或add_executable(app_name ...)需要结合TEMPLATE判断是 app 还是 libTEMPLATE appadd_executable对应TEMPLATE lib则生成add_librarySOURCES a.cpp直接列入add_executable/add_library的源文件列表如果 q2c 拆分 source 操作则用target_sourcesHEADERS a.h同样列入源文件列表主要是为了配合CMAKE_AUTOMOC捕获头文件里的Q_OBJECTFORMS a.ui列入源文件列表 开启CMAKE_AUTOUICqmake 会自动跑 uicCMake 也交给 AUTOUICRESOURCES a.qrc列入源文件列表 开启CMAKE_AUTORCC也可以改用qt6_add_resources但 AUTORCC 最简单INCLUDEPATH srctarget_include_directories(... PRIVATE src)老项目经常有全局路径这里需要注意 PUBLIC/PRIVATE 的粒度DEFINES FOO1target_compile_definitions(... FOO1)也兼容没有1的纯宏定义LIBS -lfootarget_link_libraries(... foo)这里不够智能只能做简单转换复杂-L/-Wl参数必须人工检查这些映射本身不复杂难的是确认 qmake 的默认行为。比如QT 和QT -的顺序会影响最终 Qt 模块集合q2c 会先收集所有和-操作最后一次性生成find_package如果QT - core这种极端写法出现工具会直接警告因为 CMake 里几乎没有“去掉 QtCore”这种用法。2.3 隐式规则转为显式配置qmake 最让人头疼的地方是大量行为是“约定优于配置”。比如你在HEADERS里写了一个带Q_OBJECT的类moc 就会自动生成对应文件你在CONFIG console里声明了控制台程序Windows 下就会自动链接控制台子系统。这些事在 CMake 里都必须显式声明。所以 q2c 在处理FORMS、RESOURCES、HEADERS时会额外检查是否应该开启CMAKE_AUTOMOC、CMAKE_AUTOUIC、CMAKE_AUTORCC。这里的小细节是CONFIG no_autogen在 qmake 里可以关闭自动生成转换时就必须把对应变量设为OFF。另外CONFIG console在 qmake 里会影响 Windows 子系统的链接选项CMake 的等价物是设置WIN32_EXECUTABLE属性为OFF如果原来没有consoleQt 的窗口程序需要设WIN32_EXECUTABLE ON。这些都是“看一眼.pro猜不出来必须跑一遍转换结果对比才知道”的经验。3. 实操过程把一个真实 .pro 转成 CMakeLists.txt3.1 运行 q2c 并查看初始输出q2c 是 Python 写的拿到一个.pro文件后直接命令行操作就行python q2c.py app.pro -o CMakeLists.txt我这里准备了一个典型的小例子用来演示转换过程QT widgets TARGET demo_app TEMPLATE app CONFIG c17 SOURCES main.cpp \ mainwindow.cpp HEADERS mainwindow.h FORMS mainwindow.ui RESOURCES assets.qrc这是最理想的情况没有作用域没有.pri嵌套变量名都是 qmake 标准名称。q2c 跑完生成的CMakeLists.txt大概是这样的cmake_minimum_required(VERSION 3.16) project(demo_app LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) add_executable(demo_app main.cpp mainwindow.cpp mainwindow.h mainwindow.ui assets.qrc ) target_link_libraries(demo_app PRIVATE Qt::Widgets)第一次看到这个输出很多人会问为什么头文件、.ui、.qrc要一股脑列进add_executable这不是多余吗原因很简单CMake 的生成器需要知道这些文件存在才能触发对应步骤。.ui文件列进去配合CMAKE_AUTOUIC才自动生成 UI 头文件.qrc列进去CMAKE_AUTORCC才会去编译资源。在 qmake 里这些都是自动关联的CMake 就必须显式写出来所以转换工具直接给你列全省得你漏。3.2 带条件作用域的转换解析上面的例子太顺利了实际项目必然有平台判断。qmake 里最常见的是win32:{}、unix:!macx:{}这种。q2c 遇到这些会尝试翻译成 CMake 的if(WIN32)/if(UNIX AND NOT APPLE)条件块。例如这样一个片段win32: { SOURCES windows_specific.cpp DEFINES USE_WINDOWS } unix:!macx { SOURCES linux_specific.cpp }q2c 生成的 CMake 是这样的if(WIN32) target_sources(demo_app PRIVATE windows_specific.cpp) target_compile_definitions(demo_app PRIVATE USE_WINDOWS) endif() if(UNIX AND NOT APPLE) target_sources(demo_app PRIVATE linux_specific.cpp) endif()这里有个容易出问题的地方unix:!macx在 qmake 里表示“Unix 但不是 macOS”CMake 对应的逻辑是if(UNIX AND NOT APPLE)但APPLE这个变量在 macOS 上才为真所以这个组合写到 Linux 上也是安全的。q2c 会做常见的操作符转换!变成NOT:变成AND逗号分隔则拆成多个条件。但这套逻辑并不完整比如 qmake 里还有else分支以及equals()、contains()函数调用遇到这些q2c 会退化为“输出注释 保留原文”让用户自己补全。这个设计我给很多人解释过自动转换的收益在于处理 80% 的常规模式剩下 20% 的复杂逻辑如果硬转反而会生成一个你以为正确、实际语义不对的 CMake 脚本。3.3 构建验证与补齐步骤拿到CMakeLists.txt后真正的挑战才开始跑构建看输出然后逐条处理编译错误。按照我的经验一套标准流程是cmake -S . -B build cmake --build build第一次跑大概率会有几个问题。最常见的是#include mainwindow.moc这种写法。qmake 时代的人喜欢在.cpp文件末尾手动#include对应的.moc文件这样做可以减少重复编译。CMake 的 AUTOMOC 也能处理这种情况但前提是它得知道这个.moc文件对应的头文件在哪个工程里。q2c 生成的target_sources包含了头文件所以一般没问题。如果遇到mainwindow.moc找不到多半是因为生成时顺序问题项目里有多个 targetAUTOMOC 不知道这个.moc属于谁。这时候最直接的修法是把对应头文件所属的源文件明确加进它的 target 源文件列表或者干脆把.moc包含改掉这不影响功能。另一个高频问题是 Qt 模块缺失。.pro里有QT charts但 q2c 的find_package(Qt6 COMPONENTS Widgets)里没有 Charts因为转换时模块列表是从QT变量收集的如果你项目是通过变量间接赋值的比如QT $$MY_MODULES工具根本不知道MY_MODULES是什么生成的 CMake 自然就缺了。这种问题我的建议是先把 qmake 的QT变量手动跑一遍qmake -query或者直接看.pro把最终模块列表补充到 CMake 里再继续编译。4. 常见问题与排查技巧实录4.1 高频问题速查表把这段时间收集到的问题整理成了表格方便对照排查现象可能原因处理办法CMake 报错找不到Qt5::Widgetsq2c 默认生成find_package(Qt6)但项目用的还是 Qt5手动把Qt6替换成Qt5组件名不变编译报“未定义的 vtable / vptr”错误头文件没进target_sourcesAUTOMOC 没扫描到确认所有带Q_OBJECT的头文件都列在add_executable/add_library里.ui文件生成的头文件找不到CMAKE_AUTOUIC没有开启或者 UIT 没把 ui 文件列进 target检查 CMakeLists 是否设置CMAKE_AUTOUIC ON资源文件.qrc里的图片加载不出来没有开启CMAKE_AUTORCC或者.qrc路径写错设置CMAKE_AUTORCC ON并确认相对路径基于.qrc所在目录链接时出现WIN32相关错误窗口没显示WIN32_EXECUTABLE属性未设置在 CMake 里设置set_target_properties(demo_app PROPERTIES WIN32_EXECUTABLE ON)路径中有空格导致生成脚本错乱qmake 对路径空格的处理比 CMake 宽松用双引号把路径包起来或者使用/代替\转换后目标名称变了原.pro里TARGET和工程文件名不一致注意project()名称不一定要等于 target 名称用add_executable里的名称覆盖大量缺失源文件报错.pro中用SOURCES $$files(...)或 glob 动态拼接文件手动展开这些文件或改造 CMake 用file(GLOB CONFIGURE_DEPENDS ...)4.2 我自己踩过几次比较深的坑第一个坑是.pri子文件里的相对路径。qmake 里INCLUDEPATH ../common这个路径是相对于.pri文件所在目录的而不是相对于主.pro文件。q2c 最早版本没考虑到这个区别直接把路径原样搬进 CMake结果在根目录运行 cmake 时路径全偏了。后来我加了一个逻辑遇到.pri中声明的路径自动拼接上.pri所在目录的相对前缀。这里也想提醒手动转换的人这个细节特别容易被忽略你看到../common觉得没问题实际上路径基准点不对CMake 会给你报“找不到头文件”。第二个坑是CONFIG release和CONFIG debug的处理。qmake 里 release/debug 会影响编译优化级别和是否生成调试信息但 CMake 通常由CMAKE_BUILD_TYPE控制。q2c 不能替用户决定构建类型所以只能把这类配置标记成注释提醒用户在生成 CMake 工程时指定-DCMAKE_BUILD_TYPEDebug/Release。一开始很多人觉得这是工具偷懒但仔细想就明白qmake 的 release/debug 是写在工程文件里的CMake 的理念却是“构建类型由配置阶段决定”两者哲学就不一样硬转反而会出错。第三个坑比较刁钻DESTDIR变量。有些项目会设置DESTDIR $$PWD/bin来指定输出目录qmake 会自动创建目录并把可执行文件放进去。CMake 里等价物是RUNTIME_OUTPUT_DIRECTORY和LIBRARY_OUTPUT_DIRECTORY但这两个属性只影响 target 输出不会自动创建目录。q2c 转完后我第一次在 Windows 上构建发现生成的 exe 没进 bin 目录也没报错只是安静地放在了默认目录。这不算是 q2c 的 bug但确实是使用者最容易忽略的“静默差异”。后来我建议所有转完的项目都顺手加一句set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)这样输出行为才和 qmake 时代一致。5. 工具边界和后续可以扩展的方向5.1 不建议自动转换的场景用了这么久q2c 帮我解决了不少问题但我也必须承认它不是一个万能工具。有些场景我甚至不建议你用自动转换比如深度依赖.pri嵌套的项目q2c 支持把.pri里的变量合并到主流程但如果.pri之间还有相互赋值、条件判断依赖转出来的结果很难读后期维护成本比手动重写还高。大量使用自定义函数和defineReplace()的项目qmake 的函数返回值没法静态解析工具只能留 TODO等于把最难啃的骨头还给你。还在用TEMPLATE subdirs的多目录工程这种项目的核心价值在目录结构和依赖关系q2c 能把每个子目录的.pro分别转成 CMakeLists但父子项目的add_subdirectory()关系需要你手工搭工具目前不擅长。跨平台代码里写满了win32-msvc*、linux-g*这类 qmake spec 条件的工程这些值依赖 qmake 的 mkspecCMake 里找不出天然对应的等价物只能靠人肉看懂逻辑后用MSVC、CMAKE_CXX_COMPILER_ID等变量重写。遇到这些情况我的建议是把 q2c 当成“草稿生成器”让它先跑一遍你拿到一个不完美的 CMake 基线在这个基线上改通常比你从零开始写要快。不要期望工具有时候比人更懂你的代码。5.2 后续可以扩展的方向q2c 目前还比较粗糙但后续的想象空间很大。我整理了几个自己很想做的方向第一Qt5 / Qt6 自动探测。现在工具默认生成Qt6的find_package但很多老项目还在用 Qt5。如果能从.pro里的greaterThan(QT_MAJOR_VERSION, 5)这类表达式推断出版本或者在生成的 CMakeLists 里写一个if兼容块就会更实用。第二对.pri资源的智能化合并。现在的合并只是简单拷贝如果能追踪变量的赋值起点识别出某个.pri对应的库模块就可以直接生成一个 CMake INTERFACE library让主项目用target_link_libraries关联结构清晰很多。第三转换前 diff 和验证机制。最理想的状态是工具能先把 qmake 的“实际生效配置”跑出来比如用qmake -o Makefile检查最终编译器参数然后对照转换后的 CMake 编译命令给出差异报告。这个工程量不小但做好之后能大幅降低人工审查成本。另外如果未来能支持把转换逻辑做成 IDE 插件在 Qt Creator 里点一下就把.pro变成 CMakeLists那对老项目维护者的帮助就更直接了。说实话这类转换工具永远不会做到 100% 完美但它能让你在一个下午把一天的工作量干完就已经值回票价了。我自己在用 q2c 的过程中最大的体会是自动转换不是“替代人”而是“减少重复劳动”。真正复杂的语义判断最终还是得由懂 qmake 也懂 CMake 的人来做。所以每次跑完工具我都会把生成的 CMakeLists 当成一份“初稿”逐行过一遍确认每一个条件分支都符合原始意图。最后再分享一个小技巧不管项目多小转换完一定第一时间跑一次cmake --build全量编译不要跳过。只有编译通过你才真正拥有了一份可以用的 CMake 工程。本文还有配套的精品资源点击获取