简介:q2c是一款面向Qt开发者的qmake与CMake互转命令行工具,主要解决在.pro与CMakeLists.txt之间切换构建体系时的重复手工改写问题。资源包共13个文件,核心代码由6个C++源文件、5个头文件及1个qmake工程文件构成,附带README说明文档,压缩包约14KB,适合已在Qt项目中掌握基本构建流程、希望迁移到CMake或需要对比两种体系的中级开发者。资源描述中清晰梳理了qmake与CMake在语法、跨平台能力、灵活性和社区支持上的差异,并结合q2c的实际转换逻辑,整理了常见转换边界与手动调整注意事项。已有3200余人学习下载,说明这类构建工具转换需求在Qt开发者群体中具有一定关注度。通过学习包内源码与说明,读者可以快速理解q2c的设计思路,并直接编译安装该工具,用于简化自身项目的构建系统迁移过程。 这阵子整理手上的 Qt 老项目,几十个 .pro 文件翻到头皮发麻。好不容易把 qmake 的构建逻辑理顺,又冒出一堆新需求:第三方库只提供 CMake 的 find_package 配置、CI 环境只认 CMake、同事的新代码一上来就用 Qt6 的 qt_add_executable 写法。被夹在 qmake 和 CMake 之间的感觉,做过 Qt 的人都懂。
我也想过直接手工重写,毕竟网上教程一搜一大把,可真对着 .pro 逐行翻的时候才发现,机械重复的映射实在太多,几十个项目靠手敲不仅累,还容易漏。后来我决定用 q2c 这个 qmake 到 CMake 的转换工具打头阵,先把每个 .pro 自动翻译成 CMakeLists.txt,再手工修细节。这套流程走下来,比最初预期省了至少三分之二的时间。这篇就把 q2c 能干什么、不能干什么、转换之后最常踩的坑一次说清楚,给同样在迁移边缘挣扎的人一个参考。
1. qmake 老项目为什么拖不得
1.1 qmake 的便利停留在“Qt 自留地”
qmake 最大的优点就是简单。对一个普通 GUI 项目来说,几行配置就能跑:
QT += widgets TARGET = myapp TEMPLATE = app SOURCES += main.cpp mainwindow.cpp HEADERS += mainwindow.h之所以这么省事,是因为 qmake 和 Qt 工具链深度绑定,moc、uic、rcc 这些步骤都是默认行为,你不用告诉它“这个头文件里有 Q_OBJECT,去跑 moc”,它自己就知道。这种“开箱即用”的体验在 Qt 4、Qt 5 时代非常舒服,尤其是只维护一两个小工具的时候。
但舒服是有代价的。qmake 本质上不是通用构建系统,它的条件表达式、作用域规则和目录组织能力都比较弱,一旦项目膨胀成几十个 .pro 互相 include,或者要对接一大堆非 Qt 的第三方库,维护难度会直线上升。我自己经历过最痛苦的事,就是在一个 multi-project 的工程里追一个 include 路径是怎么串起来的,最后发现是某个 .pri 在好几个 .pro 里被重复引入,导致宏定义打架。这种问题在 qmake 的体系里非常难排查。
1.2 CMake 已经接住了 Qt 官方的主推位置
Qt 5 时代 CMake 支持还属于“能用但不主流”,到了 Qt 6,官方直接把 CMake 扶正,所有新模块都优先提供 CMake config 包,qmake 基本上进入维护状态。第三方 C++ 库、各家 CI 模板、IDE 的默认工程配置,现在清一色围绕 CMake 写。
热搜词里大量出现“qt6 cmake语法”、“vs上如何打开cmake项目”、“cmake教程”,说明大家的迁移需求已经非常集中。说白了,现在做 Qt 开发,再不熟悉 CMake,接新库、进新团队、用新工具链都会非常被动。老项目拖着不迁,越到后面越难受,因为周围生态已经变了,就你手里的构建脚本还停留在上一个时代。
1.3 手写转换不如工具辅助,但工具不是魔法
手工重写 CMakeLists.txt 当然可行,我早期也这么干过。但一个复杂的 .pro 文件动辄几百行,里面全是条件分支、自定义变量、各种路径拼接,靠人逐行翻译,眼睛一花就漏掉一两个关键配置。更麻烦的是,不同项目的 .pro 写法还五花八门,有的把配置堆在 .pro 里,有的拆到 .pri 里,还有一堆自定义函数。
q2c 这类工具的价值,就是把“变量映射”这种机械劳动全部扛下来,让你只盯那些机器处理不了的地方。不过我也先说清楚:q2c 不是点一下就能收工的完整方案,它解决的是“从无到有”的问题,而“从有到好”还是得靠人。接下来重点讲它到底能转什么、不能转什么。
2. q2c 的定位和实际用法
2.1 工具能处理的语法范围和转不动的部分
以我使用 q2c 的经验,它擅长的部分是常规变量赋值和符号拼接,比如:
QT += core gui widgets这类模块声明CONFIG += c++17、TEMPLATE = app这类目标属性SOURCES、HEADERS、FORMS、RESOURCES这些文件列表INCLUDEPATH、LIBS、DEFINES这种路径和宏配置- 基本的
win32、unix、debug、release条件分支
但转不动的地方也很明显:自定义 qmake 函数、复杂的contains()/message()流程控制、system()调用,以及多个 .pro 之间互相依赖的复杂关系。遇到这些,q2c 要么忽略,要么生成一段需要你手工处理的占位内容。
我的建议是,在跑 q2c 之前,先把 .pro 里复杂的条件分支手动展开成几个简单版本,让工具分别转换,最后再用 CMake 的if(WIN32)/if(UNIX)逻辑合并回去。这样既发挥了工具的自动化优势,又避开了它的盲区。
2.2 安装方式和基本命令
q2c 的安装有两种常见路径:一种是从源码编译,一种是用包管理器直接装。不管哪种方式,核心命令基本是传入 .pro 文件、输出 CMakeLists.txt,典型的用法是这样:
q2c -i myapp.pro -o CMakeLists.txt有的版本参数可能略有差异,比如支持q2c myapp.pro > CMakeLists.txt这种重定向写法,具体看q2c --help的输出。我一般习惯先在干净目录里跑,生成后再对比原 .pro 检查一遍,避免工具直接覆盖已经手工优化过的 CMakeLists.txt。
2.3 一个真实转换示例
拿一个典型的 Qt Widgets 项目举例。原始 .pro 文件长这样:
QT += core gui widgets TARGET = demo TEMPLATE = app CONFIG += c++17 SOURCES += main.cpp mainwindow.cpp HEADERS += mainwindow.h FORMS += mainwindow.ui RESOURCES += resources.qrc INCLUDEPATH += third_party/include LIBS += -Lthird_party/lib -lmylib DEFINES += APP_VERSION=\\\"1.2.0\\\"经过 q2c 转换后,生成的 CMakeLists.txt 大致是这个样子:
cmake_minimum_required(VERSION 3.16) project(demo VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) qt_standard_project_setup() qt_add_executable(demo main.cpp mainwindow.cpp mainwindow.h mainwindow.ui resources.qrc ) target_include_directories(demo PRIVATE third_party/include) target_link_directories(demo PRIVATE third_party/lib) target_link_libraries(demo PRIVATE Qt6::Widgets mylib) target_compile_definitions(demo PRIVATE APP_VERSION=\"1.2.0\")可以看到,q2c 已经把文件列表、模块引用、include 路径、链接库、宏定义都转出来了,方向和基本结构是对的。但这里有个细节值得注意:FORMS和RESOURCES被直接列进了qt_add_executable,和 .pro 里的写法不是一一对应的。要理解为什么这样没问题,就得知道 Qt 的 CMake 前置处理机制,这个我放到第 4 章细说。
3. 手工校对真正要用的映射清单
3.1 模块、目标、源文件的映射
q2c 转换完成后,我建议不要直接拿去编译,先对照下面这张表逐项检查一遍,这比盲测报错再回头改高效得多:
| qmake 写法 | CMake 对应写法 | 注意事项 |
|---|---|---|
QT += core gui widgets | find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) | Qt5 用find_package(Qt5 ...),两者目标名不同 |
TARGET = demo | add_executable(demo ...)或qt_add_executable(demo ...) | target 名要和 project 名区分开,避免混淆 |
TEMPLATE = app | qt_add_executable | 用qt_add_executable会自动处理 Qt 入口点 |
TEMPLATE = lib | add_library | 需要手动指定 SHARED 还是 STATIC |
SOURCES += main.cpp | 列进target_sources | 注意路径是相对CMAKE_CURRENT_SOURCE_DIR的 |
HEADERS += mainwindow.h | 建议同样列进target_sources | 为了 AUTOMOC 能扫描到 |
FORMS += mainwindow.ui | 列进target_sources | 依赖 AUTOUIC 处理 |
RESOURCES += resources.qrc | 列进target_sources | 依赖 AUTORCC 处理 |
CONFIG += c++17 | CMAKE_CXX_STANDARD 17 | 也可以按 target 单独设置 |
CONFIG += console | 控制台程序无需特殊处理 | Windows 下WIN32_EXECUTABLE属性决定有无控制台窗口 |
这里最容易翻车的是TEMPLATE = lib的情况。qmake 里写TEMPLATE = lib还不一定就是普通库,有可能是插件,CONFIG += plugin一加,生成的库文件后缀和链接方式都会变化。转成 CMake 后,插件项目通常要设置add_library(mylib MODULE ...),还要在 Qt 的插件目录里做安装配置,这些 q2c 不可能自动猜出来,需要手工补。
3.2 路径、宏、链接库的映射
编译相关的配置是手工校对的重头戏,稍有不慎整个构建就废了:
| qmake 写法 | CMake 对应写法 | 注意事项 |
|---|---|---|
INCLUDEPATH += path | target_include_directories(demo PRIVATE path) | $$PWD要替换成${CMAKE_CURRENT_SOURCE_DIR} |
LIBS += -Lxxx -lyyy | target_link_directories+target_link_libraries | -l前缀要去掉,库名拆出来单独写 |
LIBS += /full/path/libfoo.a | target_link_libraries(demo PRIVATE /full/path/libfoo.a) | 绝对路径直接给 CMake 也能识别 |
DEFINES += FOO=bar | target_compile_definitions(demo PRIVATE FOO="bar") | 字符串值要保留引号,注意转义 |
RC_FILE = app.rc | 添加到源文件列表 | Windows 下资源文件,CMake 会自动处理 |
ICON = app.icns | set_target_properties(... MACOSX_BUNDLE_BUNDLE_NAME ...) | macOS 专属,图标配置需要额外处理 |
DEPENDPATH += path | target_include_directories或 target 间依赖 | 这个变量经常被忽略,漏掉会导致头文件解析顺序问题 |
一个比较隐蔽的坑是 qmake 的LIBS支持-L和-l连写,比如LIBS += -L/opt/lib -lmylib -lz。转成 CMake 的时候,-lmylib要拆成mylib,-lz拆成z,不能直接把整个-lmylib字符串塞进target_link_libraries,否则链接器会报找不到库。q2c 对简单情况能处理好,但路径里带空格或者有多个-L参数时,最好手工确认一遍target_link_directories的顺序。
3.3 平台分支映射
qmake 的条件分支写起来非常随意,比如:
win32: { LIBS += -lws2_32 } unix:!macx: { DEFINES += LINUX_BUILD }CMake 里对应的写法是:
if(WIN32) target_link_libraries(demo PRIVATE ws2_32) endif() if(UNIX AND NOT APPLE) target_compile_definitions(demo PRIVATE LINUX_BUILD) endif()q2c 对这类分支的处理能力有限,尤其是unix:!macx:这种带取反的复合条件,很容易被搞错。我实际用的方法是:先手动把.pro里的分支拆开,生成几份不带条件的简化版配置,让 q2c 分别转换,然后再把 CMake 的条件判断结构手工补回来。其实这条方法也适用于所有很难自动化的部分,本质上就是把 qmake 的“花活”先消解掉,再交给工具去处理那些根本不存在争议的映射。
4. 转换完成后最容易翻车的构建细节
4.1 忘开 AUTOMOC,Q_OBJECT 类直接 vtable 报错
这是刚转 CMake 的头号翻车点。qmake 时代你根本不用关心 moc,但 CMake 里 Qt 的元对象编译是通过 AUTOMOC 机制完成的。qt_add_executable在 Qt6 的 CMake 模块里默认会开启相关设置,但前提是你调用了qt_standard_project_setup(),或者在文件头部显式写了:
set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON)如果没开 AUTOMOC,编译时会出现一堆“undefined reference to vtable”或者qt_metacast相关错误,本质就是 Q_OBJECT 类缺少生成的 moc 文件。
另一个容易忽略的点是:含 Q_OBJECT 的头文件必须出现在某个 target 的源文件列表里,AUTOMOC 才会扫描到它。所以 q2c 把 HEADERS 列进qt_add_executable不是多余的,这正是为了让 AUTOMOC 能找到这些头文件。如果头文件只被 include 但没列进 target,AUTOMOC 默认情况下会跳过,编译照样报 vtable 错。
4.2 cmake_minimum_required 和系统 CMake 版本过旧
热搜词里有条很典型的报错:cmake 3.13 or higher is required. you are running version 3.10.2,这基本是 Ubuntu 18.04 老环境 + 新版 CMake 项目的经典组合。q2c 生成的文件里写的cmake_minimum_required(VERSION 3.16),在系统默认 CMake 3.10 的环境下直接拒绝执行。
解决办法优先级从高到低:
# 用 pip 装新版 CMake,最省事 python3 -m pip install --user cmake # 或者从官方下载二进制替换 wget https://github.com/Kitware/CMake/releases/download/v3.27.9/cmake-3.27.9-linux-x86_64.tar.gz这里我特别提醒一句:不要为了迁就老版本 CMake 而把cmake_minimum_required改成 3.10,因为你用的 Qt6 相关功能(尤其是qt_standard_project_setup)确实要求 3.16 以上,强行降级只会换来后面更诡异的报错。正确做法是让工具链适应当前项目,而不是让项目去迁就过时的系统环境。
4.3 VS 里“编译成功却没有 exe”的排查链路
在 Windows + Visual Studio 环境下,很多人会碰到一个很迷惑的现象:CMake 配置成功,编译也显示成功,但找不到输出 exe。结合我自己踩过的坑,排查顺序应该是这样的。
先看TEMPLATE是不是 app。q2c 转换时如果 .pro 里写的是TEMPLATE = lib,生成的就是add_library,输出是 dll/lib 而不是 exe,这是最直接的原因。再看 target 名和 project 名是否一致,VS 打开 CMake 工程时,启动项显示的名称取决于project()和 target 名,如果两个名字对不上,你 build 了 A 目标,VS 还当你在跑 B 项目,自然找不到 exe。
还要检查输出目录。CMake 的默认输出和 qmake 不一样,qmake 习惯把产物放在当前目录或指定目录,CMake 默认放在 build 目录下的Debug/或Release/子文件夹里。最后,在 VS 的“CMake 目标”窗口里确认你选的是不是 demo 这个 target,有时候你点的是 INSTALL 或者其他子项目,当然看不到预期的 exe。
4.4 main 函数链接不到,多半不是 main 的问题
“cmake main函数链接不到”这类报错,尤其是 MSVC 环境下,十有八九不是真的缺 main 函数,而是 Qt 的入口点处理没接上。qmake 里TEMPLATE = app隐含了 Qt 提供的 WinMain 封装逻辑,但在 CMake 里,这个处理由qt_add_executable接管。
如果你把 q2c 生成的qt_add_executable手滑改成了普通的add_executable,Windows 下的 GUI 程序就会尝试链接到 WinMain,但你的源码里只写了标准 main 函数,于是链接器报错。另外,如果target_link_libraries漏掉了Qt6::Widgets,也会出现符号找不到的问题,因为 Qt 的库没有真正进到链接阶段。还有一个常见情况是 CMakeLists 里有多个 target,比如同时建了库和可执行文件,而你实际链接的时候链到了库 target 上,库里面当然没有 main。
4.5 环境符号冲突和动态库路径污染
还有一类开箱报错,比如cmake: symbol lookup error: cmake: undefined symbol: _zn4json5valueixerknst7...,这种属于 CMake 本身运行时的问题,和项目配置无关。常见原因是系统中存在多个版本的第三方动态库,LD_LIBRARY_PATH 指向了旧库,或者 conda/Python 环境里装了另一个 cmake 和系统库混用。
排查思路很直接:
# 看 cmake 实际链接了哪些动态库 ldd $(which cmake) # 检查环境变量里有没有可疑路径 echo $LD_LIBRARY_PATH把那些指向 conda 目录或旧版本库的路径清掉再跑 cmake,一般就正常了。这个坑在搜索词里也有反映,尤其是 conda + Python + CMake 混装的机器上非常常见,属于典型的“环境问题大于项目问题”。
5. 把 q2c 的产物落进真实项目的实战策略
5.1 先整体转换再拆目标,别一上来就求完美
大项目迁移最容易犯的错,是想一步到位把整个 CMakeLists 组织得非常优雅。我自己第一次就这么干过,结果一个星期都在重构目录结构,项目根本没跑起来。后来学乖了:先用 q2c 把整个项目转成一个可编译的整体,确认能出 exe 之后,再逐步把公共代码抽成add_library,把业务代码改成依赖这些库的目标。
这个顺序很重要。先保证“能编译”,再追求“结构好”,每一步都有验证节点,出问题能快速定位。如果一开始就拆几十个 target,编译报错后你都不知道是转换问题还是依赖顺序问题,调试成本会翻好几倍。抽公共模块的时候,每抽一个就编译一次,别攒着一起改。
5.2 双构建系统并存时的保护措施
过渡期大概率要 qmake 和 CMake 两套构建系统同时存在,方便随时回退。但这里有个要命的坑:q2c 重新生成 CMakeLists.txt 时会覆盖你手工修补过的内容。所以一旦决定开始用 CMake 作为主要维护对象,就别再拿 q2c 回炉,生成的 CMakeLists 只是第一次的底稿,之后的修改全走手工。
另一个建议是,在 .pro 的备份之外,单独保存一份“q2c 转换后我手动改了什么”的笔记。不要觉得这是多此一举,等你在 CMakeLists 里改了十几处细节之后,回头再对比 q2c 的输出,你会发现大部分改动其实是通用规律,完全可以沉淀成自己团队内部的转换规范。
5.3 一套可复用的验证清单
迁移完一个项目,我会按下面这个清单逐项验证:
cmake -S . -B build配置无报错,没有 warning 级别的未定义变量cmake --build build编译无报错,产物路径和文件名正确- 运行程序,确认 UI 资源、翻译文件、第三方库都能正常加载
- 在 Windows + MSVC 和 Linux + GCC 各构建一次,排除平台分支遗漏
- 如果项目依赖 Qt 插件,确认插件目录和库搜索路径设置正确
这套验证最重要的是最后两条,因为 qmake 里很多平台相关的隐含行为在 CMake 里不会自动生效,必须靠实际构建去暴露。
5.4 迁移完成后 CMakeLists 的进一步优化
当你已经稳定跑通第一版 CMake 构建,就可以考虑进一步优化了。比如把裸的 include 路径替换成find_package,让第三方库由 CMake 自行定位;比如用FetchContent管理那些需要跟主项目一起编译的子项目;再比如大资源文件用 Qt6 的qt_add_resources而不是简单列个 .qrc 了事。
还有一个小细节:把 CMake 的临时生成文件加入版本库忽略列表,这些文件一旦被提交进 git,后面会惹出大量无意义的版本冲突。
build*/ CMakeCache.txt CMakeFiles/ cmake_install.cmake CTestTestfile.cmake Makefile最后按我自己的习惯收个尾。q2c 这类转换工具,我的定位始终是“翻译器”而不是“优化器”。它可以把你从 qmake 语法里批量捞出来,但 CMake 的组织方式、依赖管理、模块划分,还得靠转换之后手动打磨。我建议迁移顺序别反:先小项目练手,把一个能正常构建的 CMakeLists 攒成模板,再回去处理老项目的复杂条件分支。等到你把第一版 CMake 构建跑通,回头看那些 .pro,会发现它其实也不算难,只是我们太习惯旧写法而已。
本文还有配套的精品资源,点击获取