简介q2c是一款面向Qt开发者的构建系统转换工具专注解决qmake与cmake之间的互转需求。它既能从.pro文件生成CMakeLists.txt也能反向转换适合需要在两种构建体系间迁移、对比或维护多构建链路的C/Qt项目开发者。该压缩包内含13个文件主要包含6个C源码文件与5个头文件另有1个qmake工程文件和1个说明文档可完整支持本地编译与功能了解。整包仅14KB结构精简便于快速获取。目前已有3204人学习下载。通过学习这份源码及配套说明可以掌握q2c的解析与生成逻辑理解qmake与cmake在语法、跨平台能力及灵活性上的核心差异并能在实际项目中借助该工具减少手工改写构建脚本的工作量。文档同时提示了复杂项目转换时可能遇到的特性丢失问题有助于提前规避风险适合对构建系统原理感兴趣的Qt开发者。 前阵子接手一个老项目Qt Widgets写的东西还是qmake .pro的那套工程组织方式。项目里的.pro文件写了快十年维护的人换了好几茬里面的条件分支和自定义函数已经没人敢动。而Qt 6发布之后官方已经把CMake定位为首选构建系统很多第三方库比如一些新版的图像处理库、串口库也都只提供CMake的集成方式。这时候就面临一个很实际的问题业务代码不算复杂但手工把.pro翻译成CMakeLists.txt看着好像就是个文件格式转换实际上坑非常多。我一开始也想纯手工迁移真正做到一半发现不行。qmake和CMake看起来都在描述怎么编译、怎么链接但两者的心智模型完全不一样。qmake更偏向Qt项目的专用构建器CMake是通用的构建系统生成器直接一对一翻译必然会踩到一堆边界情况。后来找到了q2c这个工具专门做qmake到CMake的转换用下来整体思路是对的但也不是双击一下就跑通所有项目。这篇文章就把q2c的用法、原理、还有我实际转换过程中踩过的坑整理出来给同样需要从qmake迁移到CMake的团队参考。1. 为什么需要q2cqmake迁移CMake的真实痛点1.1 Qt版本演进带来的构建系统分水岭qmake存在的时间非常长从Qt 3时代就开始用了。它的特点是约定优于配置——只要你按照Qt的套路写.pro文件QT widgets、TEMPLATE app、SOURCES main.cpp 这几行写下来项目就能编。Qt官方在Qt 5时代同时维护qmake和CMake两套构建方式但到了Qt 6官方文档已经把CMake作为First-class支持而qmake虽然还在定位已经明显变成了兼容旧项目。这个变化对老项目的直接影响是生态层面的。比如你现在想接一个只提供CMake配置的第三方库qmake这边就得自己手写库路径、头文件路径、链接参数而CMake只需要find_package。如果你的项目还要跨平台跑Android、iOS、WebAssembly这种Qt支持的平台CMake在这方面也明显更顺——Android的toolchain文件、iOS的交叉编译配置CMake都有现成的体系qmake在这些平台上配置起来要费劲得多。1.2 手工转换一个.pro文件到底要改多少东西我拿一个非常典型的Qt Widgets项目举例它的.pro文件大概长这样TEMPLATE app QT core gui widgets network CONFIG c17 SOURCES \ main.cpp \ mainwindow.cpp \ networkmanager.cpp HEADERS \ mainwindow.h \ networkmanager.h FORMS \ mainwindow.ui RESOURCES \ resources.qrc TRANSLATIONS \ app_zh_CN.ts \ app_en.ts DEFINES APP_VERSION\\\1.2.3\\\ INCLUDEPATH 3rdparty/include LIBS -L3rdparty/lib -lfoo这段内容看起来不多但逐个翻译成CMake就麻烦了SOURCES/HEADERS要改成target_sources或者直接放进add_executableFORMS要交给AUTOUIC处理RESOURCES要交给AUTORCC处理TRANSLATIONS要处理成qt_add_translations或者add_custom_targetDEFINES要转成target_compile_definitions注意引号转义INCLUDEPATH要转成target_include_directoriesLIBS要转成target_link_libraries这还只是最简单的场景。一旦.pro里出现条件分支比如win32 { ... }、contains()函数、自定义函数、mmaplib之类的扩展手工转换的工作量就直线上升。而q2c做的事情就是把这其中大部分机械翻译的工作自动化。2. q2c的工作原理与工具设计2.1 整体思路不是正则替换是语法级解析q2c的核心思路是把qmake的.pro文件当作一门小语言的脚本来解析然后在内存里构建出一个语义模型最后根据这个模型生成CMakeLists.txt。它背后大致分为两层解析层处理qmake的变量赋值、作用域、函数调用、内置变量名。生成层把解析出的语义映射到CMake的语句。这是它和简单正则替换工具最本质的区别。如果只是把SOURCES 替换成target_sources(那遇到作用域嵌套win32:contains(DEFINES, FOO) { ... }这种qmake特性就完全没法处理。q2c至少做到了语法级别的理解所以它能识别哪些内容是条件分支、哪些是函数调用、哪些是普通的赋值。2.2 核心映射关系表转换过程中最核心的映射关系我整理成了一张表qmake写法CMake对应写法说明TEMPLATE appadd_executable()lib则对应add_library()还有subdirs对应add_subdirectory()QT widgetsfind_package(Qt6 REQUIRED COMPONENTS Widgets)需要根据Qt版本调整CONFIG c17set(CMAKE_CXX_STANDARD 17)也可以写成target_compile_featuresSOURCES ...add_executable里列源码或用target_sources直接平铺即可HEADERS ...如果不影响编译可以省略但建议列进去放到target_sources里能触发AUTOMOC还能出现在IDE里DEFINES FOOtarget_compile_definitions(... PRIVATE FOO)带值的宏需要注意转义INCLUDEPATH ...target_include_directories(... PRIVATE ...)注意相对路径的基准变了LIBS -lfootarget_link_libraries(... PRIVATE foo)静态库路径要写完整FORMS xxx.uiset(CMAKE_AUTOUIC ON)只要UI文件在源文件里就能自动处理RESOURCES xxx.qrcset(CMAKE_AUTORCC ON)同上TRANSLATIONS xxx.tsqt_add_translations()Qt6建议的方式2.3 这个工具有什么边界q2c能处理掉80%的常规qmake语法但它不是银弹。qmake里最麻烦的一类东西是自定义函数defineReplace()、defineTest()这种。工具没法理解你自定义函数里的业务逻辑遇到这种一般会跳过或者生成一个占位注释。另外qmake的$$system()这类执行外部命令的写法q2c同样无法在CMake里复现——CMake当然也能执行外部命令execute_process但自动转换时要判断这条命令在配置期执行还是在构建期执行就复杂了工具不敢乱猜所以这块大概率需要人工处理。我最初用q2c的时候也犯了个错误指望一条命令完成整个迁移。实际跑下来发现工具的好处是能把重复的机械性工作批量干完让人集中精力去处理那些只有人才能判断的部分。3. q2c实操从.pro到CMakeLists.txt的完整过程3.1 准备阶段确认环境与项目结构正式转换之前我的建议是先确认几件事项目要切换到哪个Qt版本Qt 5还是Qt 6。如果目标是Qt 6转换出来的CMakeLists.txt应该用find_package(Qt6 ...)如果暂时还在Qt 5就用find_package(Qt5 ...)。q2c生成的默认值通常是Qt6风格但代码兼容性由你自己控制。项目里有没有第三方库很多老项目直接在.pro里写死相对路径比如LIBS ../3rdparty/lib/foo.a。这种在CMake里建议改成通过变量管理方便后续换成find_package。有没有平台相关的条件分支qmake用win32 {}、unix {}、macx {}做条件编译。CMake里对应的写法是if(WIN32)、if(UNIX)、if(APPLE)。这部分q2c会尝试映射但映射之后你还是得检查一遍因为qmake和CMake对unix的定义不完全一样。3.2 实际操作三种典型项目的转换指令先用一个小型单窗口程序跑通流程。我测试用的项目结构是demo/ ├── demo.pro ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui └── resources.qrc直接用q2c执行q2c demo.pro正常情况下会生成一个CMakeLists.txt。我执行后的输出略作整理大致是这样的cmake_minimum_required(VERSION 3.16) project(demo VERSION 1.0 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 Core Gui Widgets ) add_executable(demo main.cpp mainwindow.cpp mainwindow.h ) target_include_directories(demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ) target_link_libraries(demo PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets )到这一步基本框架已经出来了直接cmake -S . -B build cmake --build build应该能编过。对于简单项目q2c生成的CMakeLists.txt几乎可以直接用。再看一个稍微复杂一点的项目包含多个子目录、每个子目录一个.pro文件、最外层还有管理用的.pro文件。qmake的常规做法是外层用TEMPLATE subdirsq2c对外层会生成一个顶层的CMakeLists.txt用add_subdirectory()把各个子项目串起来逻辑上有点类似。但是这里有个细节要小心qmake的subdirs模板对子目录的依赖关系是用.depends声明的比如core.depends utils而CMake的add_subdirectory()需要配合add_dependencies()或者target之间的链接关系来表达构建顺序。q2c对两层以上的subdirs依赖处理得不算完美转换后建议检查一下每个子目录的target名称是否一致避免出现链接阶段找不到符号的情况。3.3 转换后的手工修正我实际做过的三类改动q2c生成的CMakeLists.txt只是骨架要让项目真正编译过、并且行为跟原来一致下面几类修正是我几乎每次都会碰到的。第一类链接库路径重写原来的.pro里有LIBS -L$$PWD/3rdparty/lib -ljpeg -lpngq2c可能转换成target_link_libraries(demo PRIVATE jpeg png )这显然编译不过。我在实际项目中改成了set(THIRDPARTY_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/lib) target_link_libraries(demo PRIVATE ${THIRDPARTY_LIB_DIR}/libjpeg.a ${THIRDPARTY_LIB_DIR}/libpng.a )如果要接的是动态库还要给target_link_directories指定运行时搜索路径或者在Windows上用$TARGET_FILE_DIR配合拷贝dll。这个阶段不是q2c能完全自动搞定的因为工具没有你本机文件系统的上下文。第二类条件分支和平台差异qmake里的经典写法win32 { SOURCES windows_specific.cpp DEFINES USE_WIN_STUFF } macx { SOURCES mac_specific.mm } unix:!macx { SOURCES linux_specific.cpp }q2c转换后的版本大概率是这种结构if(WIN32) target_sources(demo PRIVATE windows_specific.cpp) target_compile_definitions(demo PRIVATE USE_WIN_STUFF) elseif(APPLE) target_sources(demo PRIVATE mac_specific.mm) else() target_sources(demo PRIVATE linux_specific.cpp) endif()我在转换后发现一个挺容易忽略的坑qmake的unix在macOS上是成立的macOS也是Unix系所以很多老项目会写unix:!macx来表示Linux/FreeBSD。CMake里同样要用if(UNIX AND NOT APPLE)来对应。但如果你项目的条件分支是从!win32角度写的那情况又不一样了。每一处都要在目标平台上跑一遍交叉验证。第三类AUTOMOC的隐藏坑CMake处理Qt的元对象编译器MOC用的是CMAKE_AUTOMOC这个变量设置为ON之后只要源文件里包含了Q_OBJECTCMake就会自动生成moc文件。这比qmake省心不少但也带来一个坑如果一个源文件被多个target共享AUTOMOC可能会产生moc文件冲突。遇到这种情况需要在CMakeLists.txt里用set_source_files_properties(... PROPERTIES SKIP_AUTOMOC TRUE)或者把共享代码抽成单独的子目录。q2c对这类问题不会预判因为它在转换时只看到一份文件清单看不到构建时会不会出现多target引用。3.4 处理Qt6新增模块和不兼容点如果项目要迁到Qt 6q2c转换后的CMakeLists.txt还会遇到一些Qt 6特有的问题。比如Qt6::Widgets、Qt6::Gui这些组件名和Qt 5的Qt5::Widgets相比只是前缀变了但模块内部的分类变了。一个最典型的例子是Qt 6把某些类从QtWidgets挪到了QtGui或者把QRegExp换成了QRegularExpression。这类问题不是构建系统能解决的而是代码层面的兼容性。在CMakeLists.txt层面我的建议是如果项目还在做双版本兼容用类似这样的写法find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets)这样Qt 5和Qt 6都能找到对应的组件。q2c生成的默认写法可能没有这种双版本兼容逻辑需要自己调整。4. 常见问题与排查技巧4.1 转换后编译报错速查表我整理了转换过程中最常遇到的一批编译错误和对应处理方式报错现象常见原因解决方向找不到QWidget头文件find_package少了Widgets组件检查find_package里的COMPONENTS是否齐全找不到ui_mainwindow.hAUTOUIC没生效或.ui文件没进入target_sources确认UI文件在源码列表里并开启CMAKE_AUTOUIC未定义的引用vtable for ...MOC文件没有生成确认头文件里含Q_OBJECT的类在target_sources里并且CMAKE_AUTOMOC是ON未定义的引用qt_metaextract_...一般还是MOC问题比vtable更典型的MOC缺失排查方式和上面一样找不到第三方库的符号LIBS路径转换不完整用绝对路径或生成器表达式方式链接库项目编译过了但运行找不到dll动态库运行时路径没设置Windows上写post-build拷贝Linux上设置LD_LIBRARY_PATH或rpathcmake 3.13 or higher is required类似报错本机CMake版本过低升级CMake或者在CMakeLists.txt里降低cmake_minimum_required编译成功但Visual Studio里没有生成exe在VS里直接改CMakeLists.txt生成目录没刷新VS打开CMake项目后需要等CMake配置完成重新生成或切换配置4.2 依赖处理不是所有qmake变量都有对应项QT 这类常见的变量有明确对应项但qmake的很多内置变量和函数在CMake里根本没有直接等价物TARGET.files、TARGET.path、INSTALLS targetqmake的安装/部署机制。CMake里对应的是install(FILES ...)语义接近但写法差异大。DESTDIRqmake里指定输出目录。CMake里通常用set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ...)或者不要设把构建和安装分开管理更规范。DEPENDPATH、VPATH影响的是qmake对头文件依赖搜索路径的推导。CMake没有直接对应项因为CMake的依赖追踪主要由编译器的depfile机制处理一般不需要手动设置。system()调用qmake可以在解析时执行外部命令比如用$$system(git rev-parse HEAD)生成版本号。CMake里要在config阶段执行命令需要写成execute_process()然后把结果用target_compile_definitions()传给代码。这类转换没有统一规律我的习惯是先把工具生成的CMakeLists.txt跑一遍把编译错误当成引导一个一个解决。这比一开始就通读生成的代码去猜有没有遗漏要高效得多因为编译错误本身就告诉你哪里链不上、哪里还缺依赖。4.3 过渡期qmake和CMake并行使用如果你暂时不想一下子把整个项目从qmake迁到CMake或者第三方库还在用qmake可以考虑最短路径的并行方案。我试过的一个可行方案是在CMakeLists.txt里做一个包装层通过add_custom_target调用qmake来构建旧模块然后把产物用add_library(IMPORTED)导入再接给CMake新模块链接。这个方案不用改动旧模块的.pro文件新模块可以先用CMake开发解决了换构建系统导致全项目阻塞的问题。不过它会让构建流程变得复杂CI里要额外处理qmake和CMake两套环境定位问题也麻烦。所以这个方案只适合过渡期不适合长期运行。另一个过渡期的做法是保留.pro文件作为参考文档先用q2c生成CMakeLists.txt之后所有新增文件只在CMakeLists.txt里添加不反向更新.pro。这样至少保证新的构建逻辑是唯一的避免两套构建系统各写一份、最后文件列表不一致的情况。5. 一点实操体会用q2c做了几个项目的迁移之后我最大的感受是工具的价值不是一键生成一个能编译的CMakeLists.txt而是它把.con的常规语法批量翻译完了省掉大量枯燥的体力活逼着我把精力放在那些真正需要人工判断的地方——条件分支语义、第三方库路径、平台差异、AUTOMOC的边界情况。对一个有历史包袱的Qt项目来说这一步省下的时间非常可观。最后再分享一个小技巧转换前先跑一遍cmake -S . -B build让CMake把报错和警告一次吐出来然后再逐条处理。如果项目里.pro文件很多建议先转一个最小的子项目、跑通整体流程再动手转其余部分。这样不会在迁移过程中攒出一大堆编译错误排查起来也更有条理。如果你手头正好有个老Qt项目要换构建系统不妨先拿q2c跑一遍看看再结合本文提到的修正项做一轮人工审查整个过程会比纯手工翻译轻松一个量级。本文还有配套的精品资源点击获取