qmake转CMake:用q2c自动迁移Qt构建脚本
2026/9/2 2:52:25 网站建设 项目流程

简介:q2c 是一款面向 Qt 开发者的实用命令行转换工具,专注于在 qmake 的 .pro 工程文件与 cmake 的 CMakeLists.txt 之间建立双向映射,解决两种构建系统迁移时的重复劳动问题,适合需要切换构建体系或学习构建原理的 Qt C++ 开发者,尤其适合跨平台工程维护场景。压缩包为 zip 格式,体积仅 14KB,共 13 个文件,包括 6 个 cpp 源码文件、5 个头文件、1 个 pro 工程文件和 1 个 README 说明文档,结构精简,便于直接阅读和编译,也可作为学习构建系统解析逻辑的参考样本。目前已有三千二百零四人学习下载,热度反映出它在 Qt 开发者中的实用价值。通过源码可以理解工程文件解析、配置项提取和构建规则生成的实现思路,既有助于对比 qmake 与 cmake 的语法差异和边界情况,也能在实际项目中快速生成另一套构建文件,再针对特性差异做少量手动调整,从而提高工程迁移、项目维护和团队协作效率。

1. 为什么做 q2c:qmake 和 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子模块,每个子模块又有自己的INCLUDEPATHDEFINESCONFIG条件编译项。再加上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 的词法分析器只关心几类内容:变量赋值(=+=-=*=)、常见变量名(SOURCESHEADERSFORMSRESOURCESQTCONFIGDEFINESINCLUDEPATH等)、以及顶层的作用域块。凡是遇到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 += c++17set(CMAKE_CXX_STANDARD 17)多个标准同时出现时,取最后一个
TARGET = app_nameproject(app_name LANGUAGES CXX)add_executable(app_name ...)需要结合TEMPLATE判断是 app 还是 lib
TEMPLATE = appadd_executable对应TEMPLATE = lib则生成add_library
SOURCES += a.cpp直接列入add_executable/add_library的源文件列表如果 q2c 拆分 source 操作,则用target_sources
HEADERS += a.h同样列入源文件列表主要是为了配合CMAKE_AUTOMOC捕获头文件里的Q_OBJECT
FORMS += a.ui列入源文件列表 + 开启CMAKE_AUTOUICqmake 会自动跑 uic,CMake 也交给 AUTOUIC
RESOURCES += a.qrc列入源文件列表 + 开启CMAKE_AUTORCC也可以改用qt6_add_resources,但 AUTORCC 最简单
INCLUDEPATH += srctarget_include_directories(... PRIVATE src)老项目经常有全局路径,这里需要注意 PUBLIC/PRIVATE 的粒度
DEFINES += FOO=1target_compile_definitions(... FOO=1)也兼容没有=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 在处理FORMSRESOURCESHEADERS时,会额外检查是否应该开启CMAKE_AUTOMOCCMAKE_AUTOUICCMAKE_AUTORCC。这里的小细节是:CONFIG += no_autogen在 qmake 里可以关闭自动生成,转换时就必须把对应变量设为OFF。另外,CONFIG += console在 qmake 里会影响 Windows 子系统的链接选项,CMake 的等价物是设置WIN32_EXECUTABLE属性为OFF;如果原来没有console,Qt 的窗口程序需要设WIN32_EXECUTABLE ON。这些都是“看一眼.pro猜不出来,必须跑一遍转换结果对比才知道”的经验。

3. 实操过程:把一个真实 .pro 转成 CMakeLists.txt

3.1 运行 q2c 并查看初始输出

q2c 是 Python 写的,拿到一个.pro文件后,直接命令行操作就行:

python q2c.py app.pro -o CMakeLists.txt

我这里准备了一个典型的小例子,用来演示转换过程:

QT += widgets TARGET = demo_app TEMPLATE = app CONFIG += c++17 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找不到,多半是因为生成时顺序问题,项目里有多个 target,AUTOMOC 不知道这个.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_sources,AUTOMOC 没扫描到确认所有带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 宽松用双引号把路径包起来,或者使用/代替\
转换后目标名称变了.proTARGET和工程文件名不一致注意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 += releaseCONFIG += debug的处理。qmake 里 release/debug 会影响编译优化级别和是否生成调试信息,但 CMake 通常由CMAKE_BUILD_TYPE控制。q2c 不能替用户决定构建类型,所以只能把这类配置标记成注释,提醒用户在生成 CMake 工程时指定-DCMAKE_BUILD_TYPE=Debug/Release。一开始很多人觉得这是工具偷懒,但仔细想就明白:qmake 的 release/debug 是写在工程文件里的,CMake 的理念却是“构建类型由配置阶段决定”,两者哲学就不一样,硬转反而会出错。

第三个坑比较刁钻:DESTDIR变量。有些项目会设置DESTDIR = $$PWD/bin来指定输出目录,qmake 会自动创建目录并把可执行文件放进去。CMake 里等价物是RUNTIME_OUTPUT_DIRECTORYLIBRARY_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 的 mkspec,CMake 里找不出天然对应的等价物,只能靠人肉看懂逻辑后,用MSVCCMAKE_CXX_COMPILER_ID等变量重写。

遇到这些情况,我的建议是把 q2c 当成“草稿生成器”:让它先跑一遍,你拿到一个不完美的 CMake 基线,在这个基线上改,通常比你从零开始写要快。不要期望工具有时候比人更懂你的代码。

5.2 后续可以扩展的方向

q2c 目前还比较粗糙,但后续的想象空间很大。我整理了几个自己很想做的方向:

第一,Qt5 / Qt6 自动探测。现在工具默认生成Qt6find_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 工程。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询