3、qmake构建系统:从原理到实战

qmake,说实话,是Qt生态里最老牌的构建工具。我刚开始接触Qt时,用的就是它。那时候CMake还没在Qt圈子里流行起来。这么多年过去了,qmake依然在很多项目里坚挺地运行着。今天我就带你把它彻底搞明白。

3.1 qmake的工作原理

qmake本质上是一个代码生成器。它读取你的.pro文件,然后生成Makefile。就这么简单,但背后有几个关键步骤。

核心流程:

  1. 读取.pro文件,解析所有变量和配置
  2. 根据平台和编译器,生成对应的Makefile
  3. 调用make工具,执行编译和链接

我个人习惯把qmake的工作分成两个阶段:解析阶段和生成阶段。解析阶段处理所有变量、条件判断、函数调用。生成阶段则根据解析结果,输出平台相关的Makefile。

为什么会设计成两步?你想想看,如果每次改个源文件都要重新解析.pro,那效率得多低。qmake只在.pro文件变化时才重新解析,平时只调用make增量编译。这个设计,嗯,很聪明。

qmake 构建流程 .pro 文件 变量 + 配置 + 源文件列表 qmake 解析 条件判断 + 变量展开 Makefile 平台相关构建指令 make 编译 编译 + 链接 目标产物 .exe / .so / .a / .dll 修改 .pro 后重新 qmake

3.2 .pro文件详解

.pro文件是qmake的灵魂。它看起来像脚本,但其实是声明式的。你告诉qmake「我要什么」,而不是「怎么做」。

一个典型的.pro文件长这样:

QT       += core gui widgets
TARGET    = MyApp
TEMPLATE  = app
CONFIG   += c++17 debug

SOURCES += \
    main.cpp \
    mainwindow.cpp \
    dialog.cpp

HEADERS += \
    mainwindow.h \
    dialog.h

FORMS   += \
    mainwindow.ui \
    dialog.ui

RESOURCES += \
    resources.qrc

这里每个变量都有它的使命。我挑几个重点说说。

3.3 TEMPLATE变量

TEMPLATE决定了qmake生成什么类型的项目。说白了,就是告诉qmake「你要造个什么东西」。

模板值 用途 生成产物
app 可执行程序 .exe / 可执行文件
lib 库文件 .so / .dll / .a / .lib
subdirs 多子项目 递归构建子目录
aux 辅助项目 不编译,只处理其他文件

我的经验:用subdirs模板时,记得在每个子目录的.pro里也设置好TEMPLATE。我曾经犯过这个错——父项目设了subdirs,子项目忘了设TEMPLATE,结果qmake报了一堆莫名其妙的错。

3.4 CONFIG配置

CONFIG变量是qmake的开关面板。你往里面加一个值,就打开一个功能。我把它分成几类:

编译选项类:

  • c++11、c++14、c++17、c++20 — 指定C++标准
  • debug、release — 调试或发布模式
  • optimize、optimize_full — 优化级别

平台相关类:

  • qt — 启用Qt支持(默认就开着)
  • shared、static — 库的链接方式
  • plugin — 构建Qt插件

工具类:

  • warn_on、warn_off — 警告级别
  • exceptions、no_exceptions — 异常处理
  • rtti、no_rtti — RTTI开关

注意:CONFIG里的值是有顺序的。后面的值可能会覆盖前面的。比如你先写了CONFIG += debug,后面又写了CONFIG += release,那最终生效的是release。我踩过这个坑,排查了半天才发现是顺序问题。

3.5 自定义构建步骤

有时候qmake内置的功能不够用。比如你想在编译前自动生成代码,或者编译后把产物拷贝到指定目录。这时候就需要自定义构建步骤。

qmake提供了几个钩子:

# 编译前执行
QMAKE_PRE_BUILD = my_prebuild_script.sh

# 编译后执行
QMAKE_POST_BUILD = copy /Y $(DESTDIR_TARGET) C:\deploy\

# 链接前执行
QMAKE_PRE_LINK = generate_version_info.exe

# 自定义编译规则
custom_target.target = generated_code.cpp
custom_target.depends = template.txt
custom_target.commands = python generate.py template.txt generated_code.cpp
QMAKE_EXTRA_TARGETS += custom_target

这里有个细节要注意:QMAKE_PRE_BUILD和QMAKE_POST_BUILD里的命令,是直接传给底层make工具的。Windows上用cmd语法,Linux/macOS上用shell语法。跨平台时得小心。

避坑指南:我曾经在Windows上写QMAKE_POST_BUILD = copy ...,结果Linux上编译时报错。后来改成用$$QMAKE_COPY这个qmake内置变量,才解决了跨平台问题。qmake其实提供了很多平台抽象变量,比如$$QMAKE_COPY、$$QMAKE_MKDIR、$$QMAKE_DEL_FILE,建议优先用它们。

3.6 条件判断与作用域

qmake支持条件编译,这是它最强大的特性之一。语法很简洁:

# 平台判断
win32 {
    SOURCES += windows_specific.cpp
    LIBS += -lws2_32
}
unix:!macx {
    SOURCES += linux_specific.cpp
    LIBS += -lrt
}
macx {
    SOURCES += mac_specific.cpp
    LIBS += -framework Foundation
}

# 配置判断
debug {
    DEFINES += DEBUG_MODE
    QMAKE_CXXFLAGS += -g
}
release {
    DEFINES += NDEBUG
    QMAKE_CXXFLAGS += -O2
}

# 嵌套作用域
win32:debug {
    message("Building debug on Windows")
    LIBS += -lwinmmd
}

作用域可以嵌套,也可以写在一行用冒号连接。我个人习惯把复杂的条件拆开写,可读性更好。你想想看,半年后回来看代码,一长串条件判断谁受得了。

3.7 常用内置函数

qmake提供了一些内置函数,让.pro文件更灵活:

函数 作用 示例
include() 包含其他.pro文件 include(common.pri)
message() 输出信息 message("Building $$TARGET")
error() 终止构建并报错 error("Unsupported platform")
exists() 检查文件是否存在 exists(config.pri)
system() 执行系统命令 system(git rev-parse HEAD)
find() 查找文件 find(SOURCES, "*test*")

实用技巧:用include()可以把公共配置抽成.pri文件。我习惯把编译选项、第三方库路径、通用宏定义都放在一个common.pri里,所有子项目都include它。改一处,处处生效,省心。

3.8 实战:一个完整的.pro文件

最后,给你看一个我实际项目中用过的.pro文件。它综合了上面讲的大部分知识点:

QT       += core gui widgets network
TARGET    = DataCollector
TEMPLATE  = app
CONFIG   += c++17 warn_on

# 公共配置
include(../common.pri)

# 平台相关
win32 {
    LIBS += -lsetupapi -lhid
    RC_FILE = app.rc
}
unix:!macx {
    LIBS += -ludev
    DEFINES += LINUX_BUILD
}
macx {
    LIBS += -framework IOKit
    DEFINES += MAC_BUILD
}

# 调试与发布
debug {
    DEFINES += DEBUG_LOGGING
    DESTDIR = ../build/debug
}
release {
    DEFINES += QT_NO_DEBUG_OUTPUT
    DESTDIR = ../build/release
}

# 源文件
SOURCES += \
    main.cpp \
    collector.cpp \
    parser.cpp \
    exporter.cpp

HEADERS += \
    collector.h \
    parser.h \
    exporter.h

# 自定义构建:编译前生成版本信息
version_target.target = version.h
version_target.depends = ../VERSION
version_target.commands = python $$PWD/gen_version.py ../VERSION $$PWD/version.h
QMAKE_EXTRA_TARGETS += version_target
PRE_TARGETDEPS += version.h

# 编译后拷贝到部署目录
win32 {
    QMAKE_POST_BUILD = copy /Y $$DESTDIR\\$$TARGET.exe C:\\deploy\\
} else {
    QMAKE_POST_BUILD = cp $$DESTDIR/$$TARGET /opt/deploy/
}

这个例子涵盖了模板选择、配置管理、平台判断、自定义步骤、文件包含等核心用法。你照着这个结构写,基本能应对大部分项目需求。

qmake虽然看起来简单,但用好它需要积累。我刚开始用的时候也经常翻车,尤其是跨平台那部分。慢慢来,多写几个项目就熟练了。


可可精品资料库