
qmake 常被误解为“过时的玩具”,但在 Qt 5/6 的庞大生态中,它依然是默认构建工具,且内建了 MOC、UIC、RCC 等代码生成器的无缝集成。本文不介绍“如何写一个 .pro 文件”,而是深入 qmake 的变量解析机制、作用域链、自定义函数与编译步骤,并对比 CMake 的优劣,带你掌握 qmake 作为“领域特定语言(DSL)”的进阶用法,构建真正可维护、跨平台的大型项目。
qmake 是 Trolltech(现 Qt 公司)为 Qt 项目量身打造的构建工具,它生成平台原生的 Makefile(或 Visual Studio 工程)。区别于 CMake 的通用性,qmake 深度绑定 Qt 的元对象编译器(MOC)、UI 编译器和资源编译器。其 .pro 文件本质是一种声明式脚本语言,具备变量、条件、循环和函数,但设计上更接近“宏处理器”。
很多开发者认为 qmake 已死,但 Qt 6 仍然完全支持,且相比 CMake,qmake 对 Qt 模块的依赖解析更简洁。关键在于,它内置了 Qt 的构建生命周期管理,这正是本文探讨的核心价值。
qmake 变量分为三类,理解这点是避免配置混乱的基础:
.pro 有效。cache() 函数存储,跨子项目共享)。$$() 读取系统环境变量,通过 $$ENV() 访问)。# 定义项目变量
SOURCES += main.cpp widget.cpp
# 读取环境变量(构建服务器常用)
QMAKE_CXXFLAGS += $$(CXX_EXTRA_FLAGS)
# 写入缓存(多子项目共享)
cache(CACHE_VAR, add, "shared_value")qmake 的变量展开是惰性(lazy)的,在 Makefile 生成时才展开,这允许循环依赖,但也容易造成难以追踪的 bug。
# 错误示例:条件依赖未定义变量
DEFINES += DEBUG
message("Debug is set: $$DEFINES") # 此处会输出 DEBUG
# 延迟求值:使用 $$eval() 强制展开
VAR = foo
BAR = $$eval($$VAR) # 展开为 "foo"生产经验:使用 $$dirname()、$$basename()、$$join()、$$split() 等内置函数处理路径,避免硬编码分隔符(Windows/Unix 差异)。
qmake 的作用域不是 C++ 的块级作用域,而是条件执行块,基于变量是否非空判断。这常被误解:
win32: CONFIG += console
else: unix: CONFIG += console
# 等价于:
win32 {
CONFIG += console
} else:unix {
CONFIG += console
}注意:else 必须紧贴 :,否则解析失败。
生产级项目需区分 Windows(MinGW/MSVC)、Linux、macOS,以及编译器版本:
win32-msvc* {
QMAKE_CXXFLAGS += /std:c++latest
DEFINES += _CRT_SECURE_NO_WARNINGS
}
win32-g++ {
QMAKE_CXXFLAGS += -std=c++17
}
unix:!macx {
# Linux 特定,包含 X11 库
LIBS += -lX11
}
macx {
# macOS 使用 frameworks
LIBS += -framework CoreFoundation
}进阶技巧:使用 contains() 检测 CONFIG 中的自定义值,实现 feature toggle:
CONFIG += use_openssl
contains(CONFIG, use_openssl) {
DEFINES += HAVE_OPENSSL
LIBS += -lssl -lcrypto
}默认的 SOURCES/HEADERS 只处理 C++ 编译和 MOC。实际项目中需要:
这是 qmake 最强大的功能,允许定义新的输入/输出规则:
# 将 .proto 编译为 .pb.cc 和 .pb.h
PROTO_INPUT = $$files(proto/*.proto)
PROTO_OUTPUT = $$replace(PROTO_INPUT, .proto$, .pb.cc)
protoc.name = protoc
protoc.input = PROTO_INPUT
protoc.output = ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.pb.cc
protoc.commands = protoc -I=$$PWD/proto --cpp_out=$$OUT_PWD/gen $$QMAKE_FILE_IN
protoc.depends = $$PROTO_INPUT
protoc.variable_out = SOURCES HEADERS
protoc.clean = ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.pb.cc ${QMAKE_FILE_PATH}/${QMAKE_FILE_BASE}.pb.h
QMAKE_EXTRA_COMPILERS += protoc关键点:
input 变量名指向包含文件列表的变量。output 支持占位符(${QMAKE_FILE_BASE} 等)。variable_out 将生成文件添加到构建目标,使它们参与编译。若需运行独立命令(如打包、测试),使用 QMAKE_EXTRA_TARGETS:
tests.target = run-tests
tests.commands = cd $$OUT_PWD && ./testrunner
tests.depends = all # 依赖 all 目标,确保构建完成
QMAKE_EXTRA_TARGETS += tests
# 定义 POST_TARGETDEPS 使默认构建依赖此目标
POST_TARGETDEPS += tests常见错误是硬编码路径,导致跨环境失败。利用 $$PWD 和 $$OUT_PWD:
# 相对路径绝对化
THIRD_PARTY_DIR = $$PWD/../third_party
INCLUDEPATH += $$THIRD_PARTY_DIR/include
# 区分 Debug/Release 库版本
CONFIG(debug, debug|release) {
LIBS += -L$$THIRD_PARTY_DIR/lib/debug -lmylib
} else {
LIBS += -L$$THIRD_PARTY_DIR/lib/release -lmylib
}将通用配置抽取为 .pri,供多个项目包含:
# common.pri
COMMON_SOURCES = utils.cpp logger.cpp
COMMON_HEADERS = utils.h logger.h
INCLUDEPATH += $$PWD
DEFINES += ENABLE_LOG
# 在 app.pro 中
include(../common/common.pri)
SOURCES += $$COMMON_SOURCES main.cpp
HEADERS += $$COMMON_HEADERS注意:.pri 文件中的 $$PWD 取决于包含它的项目文件路径,应使用 $$_PRO_FILE_PWD_ 获取包含文件自身的路径(需要 requires() 特性)。
qmake 本身不控制并行度,而是通过 Makefile 的 -j 参数。但可以在 .pro 中设置:
# 设置默认并行数(非强制,make 会覆盖)
QMAKE_MAKEFILE_GENERATOR = UNIX
# 对生成的 Makefile 添加 .NOTPARALLEL? 不,相反
# 更推荐在构建脚本中 export MAKEFLAGS="-j8"分模块设置不同标准:
# 对 core 模块启用 C++20
core {
QMAKE_CXXFLAGS += -std=c++20
}
# 对 gui 模块使用 C++17
gui {
QMAKE_CXXFLAGS += -std=c++17
}警告视为错误(生产推荐):
win32-msvc: QMAKE_CXXFLAGS += /WX
unix: QMAKE_CXXFLAGS += -Werror特性 | qmake | CMake |
|---|---|---|
Qt 集成 | 原生、零配置 MOC/UIC/RCC | 需 find_package(Qt6) 和 qt_add_executable |
自定义步骤 | 声明式 QMAKE_EXTRA_COMPILERS | 命令式 add_custom_command(更复杂) |
IDE 支持 | Qt Creator 深度支持 | 通用,但 Visual Studio 生成略繁琐 |
非 Qt 依赖 | 可用 pkg-config,但不原生 | 内置 find_package,生态更广 |
学习曲线 | 低,但高级功能晦涩 | 陡峭,但更灵活 |
维护现状 | Qt 官方仍支持,但新特性优先 CMake | 主流,Qt 6 推荐 |
结论:
# root.pro
TEMPLATE = subdirs
SUBDIRS = core gui plugins tests
core.file = core/core.pro
gui.depends = core
plugins.depends = core
tests.depends = core gui使用 INSTALLS 目标安装产物:
# 在子项目中定义安装
target.path = /usr/local/bin
INSTALLS += target
# 在根项目中统一执行 install在根 .pro 中定义 CONFIG += debug,子项目继承,但注意子项目可能重新定义。使用 CONFIG(debug, debug|release) 确保作用域生效。
message("Current dir: $$PWD")
message("Sources: $$SOURCES")write_file() 输出调试信息write_file($$OUT_PWD/config.log, "DEFINES = $$DEFINES")# 查看原始 Makefile 中的编译命令
make -n=(赋值)、+=(追加)、-=(移除)、*=(唯一追加)。$$files() 的文件列表顺序不确定。
解决:用 $$sorted() 排序或显式列举。QMAKE_SPEC 未正确识别。
解决:通过 -spec 参数指定 mkspec,并在 .pro 中检测 $QMAKE_SPEC。# 不依赖 Qt Creator
qmake -r CONFIG+=release PREFIX=/opt/myapp
make -j$(nproc)
make installFROM qt:5.15.2-linux
COPY . /src
WORKDIR /src
RUN qmake && make利用 qmake 的 system() 函数获取 Git commit:
GIT_HASH = $$system(git rev-parse --short HEAD)
DEFINES += GIT_COMMIT=\\\"$$GIT_HASH\\\"qmake 不是完美的,它的语法(尤其是作用域和转义)常令人困惑,但在 Qt 生态内,它依然是最可靠、最无痛的工具之一。理解其内部机制——变量求值、编译器封装、依赖图生成——有助于你驾驭它,而非被它驾驭。
如果你正在评估迁移到 CMake,请注意 Qt 官方提供了 qt6_add_executable 等函数,但学习曲线陡峭。若项目不复杂,坚持 qmake 完全可行。
最后建议:
.pro 文件必须经过 qmake -project 生成框架,再手调。qmake -v 检查版本,新版本修复了若干变量展开 bug。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。