简介:q2c是一款面向Qt开发者的构建系统转换工具,能够在qmake的.pro项目文件与cmake的CMakeLists.txt之间进行双向转换,有效解决工程体系切换时反复编写构建配置的痛点,特别适合需要维护多构建系统的中高级开发者。资源包共收录13个文件,以C++源码为主体,包含6个实现文件、5个头文件,另有1个Qt工程文件与1份README说明,整体压缩后仅14KB,代码量精简,便于快速阅读与二次开发。目前该资源已有3204人学习下载,覆盖从qmake迁移到cmake、或从cmake回迁qmake的常见场景。通过研读源码,可以了解项目文件解析流程、关键字映射逻辑、配置生成策略以及日志输出等模块设计,也能直观对比qmake与cmake在语法简洁性、跨平台扩展性和社区生态上的差异,为自研构建辅助工具或工程自动化迁移提供有价值的参考。
1. q2c 是什么:给每天改 .pro 的 Qt 开发者一颗后悔药
当你接手一份五年没人动的 Qt 代码,打开目录发现里面躺着一堆 .pro / .pri,而 CI 和新同事只认 CMake 时,第一个念头绝对是:全网找一个能把 .pro 直接翻译成 CMakeLists.txt 的工具。q2c 就是干这件事的,它读 qmake 项目,按变量展开成 CMake target,再补上 find_package 和 qt_standard_project_setup 这类样板,能省掉大半手动重写。反向它也能做,把 CMake 目标还原成 qmake 变量,适合需要同时维护两套构建的过渡期。这篇笔记适合已经把 Qt 工程跑起来、但没系统看过 CMake 写法的人,也适合准备批量迁移多目录工程的人。
2. 先把 CMake 和 qmake 的差异讲明白:q2c 的地基在哪
很多人拿 q2c 转换完发现“编译不过”,不是工具不行,而是根本没搞清楚 .pro 和 CMakeLists.txt 是两套不同的心智模型。qmake 靠全局变量带条件分支来拼 Makefile,CMake 靠 target 和属性来组织构建系统。q2c 夹在中间,并不是逐行翻译,而是先理解 .pro 里每个变量最后会作用到哪个目标,再重新生成 CMake 语法。所以你得先看明白 qmake 和 CMake 各自在描述什么,不然连报错都不知道去哪找。
2.1 qmake 的 .pro 在描述什么:变量和时间点
一个典型 Qt Widgets 工程的 .pro 只有十几行,但每一行都有特殊的时间点语义。下面是我做迁移常会看到的单文件项目:
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TEMPLATE = app TARGET = myapp CONFIG += c++17 SOURCES += main.cpp mainwindow.cpp HEADERS += mainwindow.h INCLUDEPATH += $$PWD/3rdparty/include LIBS += -L$$PWD/3rdparty/lib -lmy3rd RESOURCES += app.qrcqmake 的处理逻辑是从上往下跑脚本:QT变量控制调用 Qt 的哪个模块;TEMPLATE决定生成 app 还是 lib;CONFIG里面塞编译选项;INCLUDEPATH和LIBS会在生成编译器命令行时被展开。这里最容易被 q2c 搞混的是greaterThan这一行,它是条件赋值,q2c 不能像读普通赋值一样直接忽略。
qmake 里的$$PWD会在解析期被替换成当前 .pro 所在目录,所以INCLUDEPATH拿到的是绝对路径;而 CMake 里更推荐用${CMAKE_CURRENT_SOURCE_DIR}这种.source 目录变量。q2c 干的第一个重要工作就是把$$PWD、$$_PRO_FILE_PWD_这类内置变量全部映射到 CMake 的目录变量,否则转出来的 CMakeLists.txt 在别的机器上就是一堆坏路径。
2.2 CMake 的 target 与作用域:q2c 真正要生成的东西
CMakeLists.txt 和 .pro 最本质的区别是:CMake 以 target 为中心,一个 target 拥有自己的源文件、头文件目录、编译选项和链接库;而 qmake 中这些信息大多是全局的,写在一个变量里就所有源文件都吃到。q2c 要做的不是机械地s/INCLUDEPATH/target_include_directories/g,而是把全局变量归拢到某一个 target 名下。
| qmake 概念 | 变量/写法 | CMake 对应物 |
|---|---|---|
| 产物类型 | TEMPLATE = app | add_executable / add_library |
| Qt 模块 | QT += core gui | find_package + target_link_libraries |
| 头文件路径 | INCLUDEPATH | target_include_directories |
| 宏定义 | DEFINES | target_compile_definitions |
| 编译选项 | QMAKE_CXXFLAGS | target_compile_options |
| 资源 | RESOURCES | qt_add_resources |
| 子项目 | SUBDIRS | add_subdirectory |
理解这张表后,你会明白 q2c 转换出来的 CMakeLists.txt 里,为什么大量的target_link_libraries后面跟着PRIVATE。因为 qmake 的LIBS在传统写法中是全局终端链接选项,但 CMake 中最好放到 target 的私有关联,否则写进接口会让所有依赖这个库的子项目也继承无意义的链接项。
另一个容易踩的点是作用域。qmake 里INCLUDEPATH += xxx写在子目录的 .pro 里只影响当前子目录,而 CMake 中如果写成include_directories(xxx)会影响之后所有 target。q2c 一般会尽量写成target_include_directories,但在处理缺失 target 的边界情况时,也会退化成目录级命令,这就是后续需要人工检查的地方。
2.3 q2c 的转换模型:它做的是“语义映射”,不是文本翻译
如果 q2c 只是把 .pro 的每一行翻译成 CMake 语法,那它根本没法用,因为 qmake 的QT += widgets在 CMake 里要变成两步:先find_package(Qt6 COMPONENTS Widgets REQUIRED),再target_link_libraries(... Qt6::Widgets)。这种一对多映射只有解析了变量语义才做得到。
更极端的例子是CONFIG += console。在 qmake 中它表示 Windows 下生成的 exe 要带控制台子系统,但 CMake 里对应的是set_target_properties(... PROPERTIES WIN32_EXECUTABLE FALSE),如果手写,这行几乎不会有人记得。q2c 会把这类常见配置归类成一张小表,转换时直接替换成目标属性。
但 q2c 也不是万灵药。它看到system()、message()、custom target这类带副作用的 qmake 函数时,只能原样输出为注释或警告。所以你转换出来的 CMakeLists.txt 第一眼往往很干净,但那些被工具“吞掉”的条件脚本才是后面翻车的根源。把 q2c 当成帮你搭主框架的脚手架,而不是替你写全部构建逻辑的 AI,使用的心态才正确。
3. 用 q2c 把 .pro 转成 CMakeLists.txt:最小可复现流程
单看项目介绍容易陷入黑匣子焦虑,直接把一个 .pro 丢进去看输出才是最直接的。这章从安装到跑通,给一条可以照着敲的命令链路。我用的是 q2c 最普通的命令行入口,如果你手上的版本带了图形界面,命令名以q2c --help里写的为准。
3.1 拿 q2c 可执行文件:先解决“没有包”的问题
q2c 不是 Qt 官方开箱随附的工具,常见发行形态是源码仓库加一个 pip 入口。我在新机器上一般先试 pip:
pip install q2c q2c --help如果 pip 源里没有,就拉源码自己跑:
git clone <仓库地址> q2c cd q2c python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txt python -m q2c --help这里的关键是不要直接用系统 Python 硬装依赖。q2c 的依赖里常见有lxml或pyyaml这类底层库,在系统环境里装容易和别的工具冲突。我第一次就是图省事直接pip install --user,结果跑起来报依赖找不到,后来老老实实开 venv 就好了。
参数方面,--help主要看入口是q2c还是python -m q2c,以及输入输出参数名。我会顺手跑一遍q2c --version确认安装目录,避免后面在 CI 里调了个空壳命令。拿到 help 后,下一步就可以拿一个小 .pro 做冒烟测试。
3.2 从单 .pro 生成 CMakeLists:命令、参数和生成结果
单文件工程是最好验证路径的。我建议先用一个只有两三个源文件的小工程试,而不要一上来就整个 SUBDIRS 工程,不然输出几百行 CMake,你根本看不出问题在哪。
q2c convert --pro app.pro --cmake CMakeLists.txt这条命令的意思是:读取app.pro,把解析出来的语义映射成CMakeLists.txt并写到当前目录。如果输出文件已存在,有些版本会直接覆盖,所以建议提前备份原文件;也可以看--help里有无--preserve-existing或--diff这类开关。
落地后的 CMakeLists.txt 大概长这样:
cmake_minimum_required(VERSION 3.16) project(myapp VERSION 1.0 LANGUAGES CXX) qt_standard_project_setup() find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED) qt_add_executable(myapp main.cpp mainwindow.cpp mainwindow.h ) target_include_directories(myapp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/include ) target_link_libraries(myapp PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets ) qt_add_resources(myapp "app" PREFIX "/" FILES app.qrc )逻辑说明:qt_standard_project_setup()会设置 Qt 项目常见默认属性,比如自动处理 moc、uic 和 rcc;find_package负责定位 Qt 模块;qt_add_executable创建的 target 名来自TARGET = myapp,那个myapp就是后续 include/link 都绑定的目标名;qt_add_resources后面的"app"是资源集合名,取自 .pro 的同名资源文件。字段参数里有个坑:q2c 会把RESOURCES += app.qrc展开成里面所有文件,如果你原来在 .pro 里用通配符形式写资源,输出会变成一大串文件列表,这不算错,但会导致 CMake 部分改动后需要重新放进 qrc 文件的新资源。
3.3 转完立刻要做的三个检查点:target 名称、Qt 组件、路径变量
第一步先查 target 名。qmake 的TARGET经常写小写字母,而 CMake 的 target 名对大小写敏感,但这只是风格问题,真正要命的是 target 名里带空格或.,在target_link_libraries里需要加引号。我看到输出里的 target 名和 exe 文件名不一致时,会加一行:
set_target_properties(myapp PROPERTIES OUTPUT_NAME "MyApp")第二步查 Qt 组件。很多老项目的 .pro 里写的QT += qml quick实际用了 Quick、Qml 两个模块,q2c 如果只按空格拆,有时会漏掉widgets的补全条件。检查方法是看find_package行和项目里 include 的 Qt 头文件是否对应,宁可多列一个组件,也不要少列导致链接期翻车。
第三步查路径变量。重点看INCLUDEPATH、DEPENDPATH、LIBS里的$$PWD和$$OUT_PWD是否被替换成了 CMake 变量。有些版本在遇到绝对路径时会保留原样,这没问题;真正危险的是 qmake 里LIBS += -L$$PWD/lib被转成target_link_directories但路径少了一层,或者更糟,路径直接丢掉了。我把这一步戏称“路径三查”,查完再交给 CMake,后面就能少看一半报错。
如果你在 VSCode 里用 CMake Tools,转完记得重跑一次CMake: Delete Cache and Reconfigure。q2c 生成的 CMakeLists.txt 和原来手写的缓存不兼容,底部状态栏的 Configure 按钮如果在切换 kit 后一直不出现,多半是CMakePresets.json里的名称和当前工具链对不上,不是 q2c 生成错了。
4. 反过来走:CMake 工程转回 qmake 的边界与偏方
q2c 的标题里有个双向箭头,所以反向转换不能当作赠品功能看待。实际场景里,往往是老库还维护在 CMake,但某个新分支需要塞进一个只能编译 .pro 的陈旧构建环境里,这时候反向生成的 .pro 至少能给你一个能改的起点。
4.1 反向模式能复刻什么:target、sources、include 映射
反向转换的命令和正向类似,只是输入输出交换:
q2c convert --cmake CMakeLists.txt --pro migrated.pro对一个简单的 CMake 目标,比如:
add_executable(console_app main.cpp) target_compile_features(console_app PRIVATE cxx_std_17) target_include_directories(console_app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib)q2c 生成出的 .pro 会近似这样:
TEMPLATE = app TARGET = console_app CONFIG += cxx17 SOURCES += main.cpp INCLUDEPATH += $${PWD}/lib逻辑说明:add_executable映射到TEMPLATE = app;target_compile_features里cxx_std_17映射成CONFIG += cxx17,这是 qmake 比较新的写法,老版本 Qt 可能需要翻译成QMAKE_CXXFLAGS += -std=c++17;target_include_directories里的${CMAKE_CURRENT_SOURCE_DIR}被还原成 qmake 的$$PWD。
如果你手写的 CMake 里有target_link_libraries(console_app PRIVATE Qt6::Core),q2c 会试着生成QT += core,但这里常常出偏差:Qt6::Core的模块名在 qmake 里是core,可Qt6::Quick在 qmake 里要写成QT += quick,这个映射表相对可靠。真正不可靠的是Qt6::Widgets在老 Qt5 工程里还要补greaterThan(QT_MAJOR_VERSION, 4): QT += widgets,q2c 不会帮你补这种历史条件。
4.2 CMake 的 add_library 与自定义函数:q2c 处理不了的语法堡垒
CMake 里大量使用add_library(foo STATIC ...),反向生成TEMPLATE = lib和CONFIG += staticlib是常规操作。但如果你碰到 Qt 的插件库,比如add_library(plugin MODULE ...),qmake 里对应的是TEMPLATE = lib加CONFIG += plugin,不少 q2c 版本会只生成TEMPLATE = lib,导致后面加载插件时找不到入口。
自定义函数是更大的坑:
function(qt_auto_moc target) qt6_generate_moc(${target}) endfunction() qt_auto_moc(myapp)q2c 看到function()和自定义函数调用时,一般会直接把整块拷贝进 .pro 旁边的一个注释文件,或者原地输出一个message("WARNING: function qt_auto_moc not translated")。这不是工具偷懒,而是 qmake 根本没有“函数”这个一等公民,它只有defineReplace/defineTest这种宿主脚本式的结构,无法一对一转换。所以反向转换前,你最好先把 CMakeLists.txt 里的自定义函数手动展开成普通命令,再喂给 q2c,输出会干净很多。
4.3 手动补 .pro 的固定套路:条件与 CONFIG
反向生成的 .pro 里最常见的遗漏是条件逻辑,尤其是if(WIN32)和if(APPLE)这类平台判断。qmake 里对应的写法是:
win32: { LIBS += -lws2_32 } macx: { LIBS += -framework Security }除此之外,CMake 里的add_compile_definitions(WIN32_LEAN_AND_MEAN)要手动改成DEFINES += WIN32_LEAN_AND_MEAN;add_compile_options(-Wall)要改到QMAKE_CXXFLAGS += -Wall下;set_target_properties(... OUTPUT_NAME ...)则要改写成TARGET = 目标名再去设置DESTDIR。这些不是我背下来的,而是每次反向生成后直接 Ctrl+F 查add_和set_开头的那几行,一个个手工对应。
我个人的偏方是:反向 .pro 只用来恢复目录结构和源文件清单,编译选项全部重写。因为 qmake 对CONFIG的解析和 CMake 的编译选项本来就差一层,硬去救那几行过渡代码,不如推倒重写来得快。
5. 转换现场避坑指南:我从 .pro 迁到 CMake 的 5 次翻车
q2c 的优势是省事,劣势是你容易把它的输出当成成品。接下来这五条“现象 → 原因 → 解决”是我在真实项目里踩过的坑,每一条都经过了 q2c 转换和后续人工修复,写成血泪经验给你当参考。
5.1 现象:转完编译直接报 Unknown component: qtquick
有一次把 Qt Quick 工程的 .pro 转成 CMakeLists.txt,配置阶段报:
CMake Error at ...: Unknown component: qtquick原因:.pro 里写的是QT += quick qml,但 q2c 的 Qt 组件映射表是按模块名硬编码的,它把quick识别成Quick、把qml识别成Qml后,生成的find_package是COMPONENTS Core Gui Quick Qml,但某个老版本还在用Qt5::Quick这种命名。报错其实来自 CMake 的find_package里塞进了不存在的组件名,而不是 Qt 真没装。
解决:排查find_package行,把组件名修正成当前 Qt 版本的官方写法。Qt6 中是COMPONENTS Core Gui Qml Quick,Qt5 里则通常用find_package(Qt5 COMPONENTS Core Gui Qml Quick)。另外确认 .pro 里QT += widgets在有Q_OBJECT的窗口头文件时没被漏掉,漏掉不会报这个错,但会少链接Qt5::Widgets。
5.2 现象:INCLUDEPATH 变成了相对路径,第三方头文件找不到
转换后 CMake 配置成功,但编译时大量xxxx.h: No such file or directory。打开 CMakeLists.txt 看到:
target_include_directories(myapp PRIVATE 3rdparty/include)原因:qmake 的INCLUDEPATH += $$PWD/3rdparty/include在转换时,$$PWD被替换成${CMAKE_CURRENT_SOURCE_DIR},但生成的路径直接用了3rdparty/include,如果 CMake 运行目录不是 .pro 所在目录,就会变成相对路径出错。这是我在cmake -B build分离构建时最常见的翻车点。
解决:把该行手动改成${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/include,或者用target_include_directories的BEFORE选项确保顺序。更稳妥的做法是在转完后的全文件搜索里搜include_directories和target_include_directories,凡是没有${CMAKE_CURRENT_SOURCE_DIR}开头的相对路径,全部补全。q2c 不是给你背锅的,它只负责映射,路径规范化得人工管。
5.3 现象:SUBDIRS 多目录工程转出来只剩一个 target
多目录工程结构是常见的main.pro加subdir1/subdir1.pro、subdir2/subdir2.pro,用SUBDIRS聚合。q2c 转换后,CMakeLists.txt 里只有一个add_executable,子目录里全是空文件子目录,或者干脆没生成。
原因:q2c 对SUBDIRS的处理一般会要求main.pro里已经写明条件,比如SUBDIRS += subdir1 subdir2,但很多老工程会在main.pro里用.depends或者FORMS +=让子目录交叉引用,q2c 解析不了这种隐式依赖,就只按第一个子目录生成。
解决:先手动把SUBDIRS里的每个子目录拿出来,单独跑一次单项转换,再在顶层 CMakeLists.txt 里用add_subdirectory(subdir1)、add_subdirectory(subdir2)串联。生成后检查每个子目录的 CMakeLists 是否包含自己的target_name,再用add_dependencies(main subdir1)补依赖。这个过程没有捷径,q2c 对“两个子目录互相链接”的场景只能给你留下两个独立 target,依赖图还得手补。
5.4 现象:qrc 被重复 add_executable,链接时报 duplicate symbol
资源文件本来应该只被一个 target 引用,但转换后 CMake 里出现了两次:
qt_add_executable(myapp ...) qt_add_resources(myapp "app" PREFIX "/" FILES main.qml) qt_add_resources(myapp "app" PREFIX "/" FILES main.qml)原因:qmake 的RESOURCES += app.qrc写一遍,q2c 把它解析成文件列表后,可能同时出现在qt_add_executable的源文件列表和qt_add_resources里,等于是同一个 qrc 内容被 rcc 编译了两遍。链接时qrc_app.cpp里的符号重复,CMake 会报重复定义,而且这跟你之前用CONFIG += resources_big还是小资源没关系。
解决:删除qt_add_executable里的.qrc或资源文件条目,只保留qt_add_resources一处。如果 .pro 里做的是RESOURCES += a.qrc b.qrc,转成 CMake 后我习惯合并成一次qt_add_resources,把两个集合用不同PREFIX隔离,避免文件冲突。检查方法是用grep -n "add_resources" CMakeLists.txt,一个 qrc 文件只能出现一次。
5.5 现象:CONFIG += console 被吞掉,Windows 下双击程序弹不出控制台
控制台程序在 qmake 里写CONFIG += console,转换后的 CMakeLists.txt 里找不到任何对应项。生成出来的 exe 在 Windows 下双击不弹控制台窗口,但输出内容会直接消失,其实程序跑了,只是没有 stdout 给你看。
原因:q2c 对CONFIG的处理倾向于忽略“不影响构建正确性”的项目,console在 qmake 里影响的是链接器子系统,而 CMake 里对应的是WIN32_EXECUTABLE属性。q2c 没有把它映射到set_target_properties,所以转换结果就漏了。
解决:手动补一行:
set_target_properties(myapp PROPERTIES WIN32_EXECUTABLE FALSE)其中FALSE表示以控制台子系统方式链接,和 qmake 的CONFIG += console等价。如果你用 MSVC,也可以直接target_link_options(myapp PRIVATE /SUBSYSTEM:CONSOLE),但属性写法更通用。这个坑的难点在于 q2c 不会报任何警告,只能靠最后验证阶段真跑一次 exe 才能发现。
6. 转换后的收尾技巧:用 q2c 做持续迁移的验证脚本
迁移不是把 CMakeLists.txt 生成出来就结束了,真正的工地习惯是每次改动要么对比构建产物、要么对比运行行为。我会在项目根目录放一个migrate_check.sh,把 qmake 构建和 CMake 构建放在隔离目录里各跑一遍,再比较产物差异。
#!/bin/bash set -euo pipefail rm -rf build-qmake build-cmake mkdir build-qmake build-cmake cd build-qmake qmake ../app.pro make -j"$(nproc)" mv myapp ../myapp-qmake.bin 2>/dev/null || true cd .. cd build-cmake cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build . mv myapp ../myapp-cmake.bin 2>/dev/null || true cd .. if cmp -s myapp-qmake.bin myapp-cmake.bin; then echo "binary identical" else echo "binary differs, check the following:" ls -l myapp-qmake.bin myapp-cmake.bin fi逻辑说明:这个脚本会在两个干净目录里分别用 qmake 和 CMake 构建,然后把可执行文件复制到根目录做二进制比对。绝大多数项目不可能二进制完全一致,所以cmp失败是常态;我要看的是“差多少”,如果两个 bin 大小差好几倍,说明 CMake 的 Release 标志没生效或调试信息被带进来了。脚本里的2>/dev/null || true是为了防止某些平台下可执行文件名后缀不同导致脚本直接退出,改成真正的检查逻辑要看自己目标平台。
跑完脚本后,我还会建一张表记录关键差异,作为后续迁移状态卡:
| 检查项 | qmake 构建 | CMake 构建 | 是否通过 |
|---|---|---|---|
| 可执行文件能否启动 | 是 | 是 | 通过 |
--version输出 | v1.2.0 | v1.2.0 | 通过 |
| 加载第三方插件数量 | 3 | 3 | 通过 |
| 链接库依赖 | liba.so, libb.so | liba.so, libb.so | 通过 |
这个脚本最大的价值不在第一天,而在三周后:你改了一个源文件,qmake 构建被历史原因废掉了,只有 CMake 还在跑,这时候你能立刻知道是代码回归还是构建脚本差异。我现在每接一个新项目,都会先把这套验证脚本塞进pre-commit里,哪怕暂时还没切换构建系统,也能通过对比发现 .pro 和 CMakeLists 是不是真的在描述同一个工程。q2c 帮我把重复劳动省了,但最后一道检查还是得人来做;希望这个流程能帮你也少踩几个坑。
本文还有配套的精品资源,点击获取