Qmake 重要指南

已发表: 2022-03-11

介绍

qmake是 Qt 库附带的构建系统工具,可简化跨平台的构建过程。 与CMakeQbs不同,qmake 从一开始就是 Qt 的一部分,应被视为“原生”工具。 不用说,Qt 的默认 IDE—— Qt Creator——对 qmake 开箱即用提供了最好的支持。 是的,您还可以为那里的新项目选择 CMake 和 Qbs 构建系统,但这些系统并没有很好地集成。 随着时间的推移,Qt Creator 中的 CMake 支持可能会得到改进,这将是发布本指南第二版的一个很好的理由,该指南专门针对 CMake。 即使您不打算使用 Qt Creator,您可能仍希望将 qmake 作为第二个构建系统,以防您构建公共库或插件。 几乎所有第三方基于 Qt 的库或插件都提供用于无缝集成到基于 qmake 的项目中的 qmake 文件。 只有少数提供双重配置,例如 qmake 和 CMake。 如果以下适用于您,您可能更喜欢使用 qmake:

  • 您正在构建一个基于 Qt 的跨平台项目
  • 您正在使用 Qt Creator IDE 及其大部分功能
  • 您正在构建一个独立的库/插件以供其他 qmake 项目使用

本指南描述了最有用的 qmake 功能,并为每个功能提供了真实世界的示例。 不熟悉 Qt 的读者可以使用本指南作为 Qt 构建系统的教程。 Qt 开发人员可以在开始一个新项目时将其视为一本食谱,或者可以选择性地将某些功能应用到任何影响较小的现有项目中。

qmake 构建过程示意图

基本的 Qmake 用法

qmake 规范写在.pro (“项目”)文件中。 这是最简单的.pro文件的示例:

 SOURCES = hello.cpp

默认情况下,这将创建一个Makefile ,该文件将从单个源代码文件hello.cpp构建可执行文件。

要构建二进制文件(在这种情况下是可执行的),您需要先运行 qmake 以生成 Makefile,然后make (或nmakemingw32-make取决于您的工具链)来构建目标。

简而言之,一个 qmake 规范只不过是一个混合了可选控制流语句的变量定义列表。 通常,每个变量都包含一个字符串列表。 控制流语句允许您包含其他 qmake 规范文件、控制条件部分,甚至调用函数。

理解变量的语法

在学习现有的 qmake 项目时,您可能会惊讶于如何引用不同的变量: \(VAR,\){VAR} 或 $$(VAR) ...

在采用规则时使用此迷你备忘单:

  • VAR = value value 给 VAR 赋值
  • VAR += value将值附加到 VAR 列表
  • VAR -= value从 VAR 列表中删除值
  • $$VAR$${VAR}在 qmake 运行时获取 VAR 的值
  • $(VAR) Makefile(不是 qmake)运行时环境 VAR 的内容
  • $$(VAR) qmake(不是 Makefile)运行时环境 VAR 的内容

常用模板

qmake 变量的完整列表可以在规范中找到:http://doc.qt.io/qt-5/qmake-variable-reference.html

让我们回顾一下项目的一些常见模板:

 # Windows application TEMPLATE = app CONFIG += windows # Shared library (.so or .dll) TEMPLATE = lib CONFIG += shared # Static library (.a or .lib) TEMPLATE = lib CONFIG += static # Console application TEMPLATE = app CONFIG += console

只需添加SOURCES += ...HEADERS += ...以列出所有源代码文件,就完成了。

到目前为止,我们已经回顾了非常基本的模板。 更复杂的项目通常包括几个相互依赖的子项目。 让我们看看如何使用 qmake 来管理它。

子项目

最常见的用例是附带一个或多个库和测试项目的应用程序。 考虑以下结构:

 /project ../library ..../include ../library-tests ../application

显然,我们希望能够一次构建所有内容,如下所示:

 cd project qmake && make

为了实现这个目标,我们需要在/project文件夹下有一个 qmake 项目文件:

 TEMPLATE = subdirs SUBDIRS = library library-tests application library-tests.depends = library application.depends = library

注意:使用CONFIG += ordered被认为是一种不好的做法——最好使用.depends代替。

该规范指示 qmake 首先构建一个库子项目,因为其他目标依赖于它。 然后它可以以任意顺序构建library-tests和应用程序,因为这两者是依赖的。

项目目录结构

链接库

在上面的示例中,我们有一个需要链接到应用程序的库。 在 C/C++ 中,这意味着我们需要配置更多的东西:

  1. 指定-I以提供#include 指令的搜索路径。
  2. 指定-L以提供链接器的搜索路径。
  3. 指定-l以提​​供需要链接的库。

因为我们希望所有子项目都是可移动的,所以我们不能使用绝对或相对路径。 例如,我们不应该这样做: INCLUDEPATH += ../library/include ,当然我们不能从临时构建文件夹中引用库二进制文件(.a 文件)。 遵循“关注点分离”的原则,我们可以很快意识到应用项目文件应该从库细节中抽象出来。 相反,库有责任告诉在哪里可以找到头文件等。

让我们利用 qmake 的include()指令来解决这个问题。 在库项目中,我们将在扩展名为.pri的新文件中添加另一个 qmake 规范(扩展名可以是任何东西,但这里i代表包含)。 因此,该库将有两个规范: library.prolibrary.pri 。 第一个用于构建库,第二个用于提供消费项目所需的所有详细信息。

library.pri 文件的内容如下:

 LIBTARGET = library BASEDIR = $${PWD} INCLUDEPATH *= $${BASEDIR}/include LIBS += -L$${DESTDIR} -llibrary

BASEDIR指定库项目的文件夹(准确地说,当前 qmake 规范文件的位置,在我们的例子中是library.pri )。 正如您可能猜到的, INCLUDEPATH将被评估为/project/library/includeDESTDIR是构建系统放置输出工件的目录,例如(.o .a .so .dll 或 .exe 文件)。 这通常在您的 IDE 中配置,因此您永远不应该对输出文件的位置做出任何假设。

application.pro文件中添加include(../library/library.pri)就完成了。

让我们回顾一下在这种情况下如何构建应用程序项目:

  1. 最顶层的project.pro是一个子目录项目。 它告诉我们需要先构建库项目。 所以 qmake 进入库的文件夹并使用library.pro构建它。 在这个阶段, library.a被生成并放入DESTDIR文件夹。
  2. 然后qmake进入application子文件夹,解析application.pro文件。 它找到了include(../library/library.pri)指令,该指令指示 qmake 立即读取和解释它。 这为INCLUDEPATHLIBS变量添加了新定义,因此现在编译器和链接器知道在哪里搜索包含文件、库二进制文件以及要链接的库。

我们跳过了 library-tests 项目的构建,但它与应用程序项目相同。 显然,我们的测试项目还需要链接它应该测试的库。

通过此设置,您可以轻松地将库项目移动到另一个 qmake 项目并包含它,从而引用.pri文件。 这正是社区分发第三方库的方式。

config.pri

对于一个复杂的项目来说,拥有一些被许多子项目使用的共享配置参数是很常见的。 为避免重复,您可以再次利用include()指令并在顶级文件夹中创建config.pri 。 您可能还会将常见的 qmake “实用程序”共享给您的子项目,类似于我们在本指南中接下来讨论的内容。

将工件复制到 DESTDIR

通常,项目有一些“其他”文件需要与库或应用程序一起分发。 我们只需要能够在构建过程中将所有此类文件复制到DESTDIR中。 考虑以下代码段:

 defineTest(copyToDestDir) { files = $$1 for(FILE, files) { DDIR = $$DESTDIR FILE = $$absolute_path($$FILE) # Replace slashes in paths with backslashes for Windows win32:FILE ~= s,/,\\,g win32:DDIR ~= s,/,\\,g QMAKE_POST_LINK += $$QMAKE_COPY $$quote($$FILE) $$quote($$DDIR) $$escape_expand(\\n\\t) } export(QMAKE_POST_LINK) }

注意:使用此模式,您可以定义自己的可重用函数来处理文件。

将此代码放入/project/copyToDestDir.pri ,以便您可以将其include()到要求苛刻的子项目中,如下所示:

 include(../copyToDestDir.pri) MYFILES += \ parameters.conf \ testdata.db ## this is copying all files listed in MYFILES variable copyToDestDir($$MYFILES) ## this is copying a single file, a required DLL in this example copyToDestDir($${3RDPARTY}/openssl/bin/crypto.dll)

注意: DISTFILES 是出于相同目的而引入的,但它仅适用于 Unix。

代码生成

将代码生成作为预构建步骤的一个很好的例子是当 C++ 项目使用 Google protobuf 时。 让我们看看如何将protoc执行注入到构建过程中。

您可以轻松地在 Google 上找到合适的解决方案,但您需要了解一个重要的极端案例。 假设你有两个合约,其中 A 引用 B。

 A.proto <= B.proto

如果我们首先为A.proto生成代码(生成A.pb.hA.pb.cxx )并将其提供给编译器,它将失败,因为依赖B.pb.h尚不存在。 为了解决这个问题,我们需要在构建生成的源代码之前通过所有原型代码生成阶段。

我在这里找到了这个任务的一个很好的片段:https://github.com/jmesmon/qmake-protobuf-example/blob/master/protobuf.pri

这是一个相当大的脚本,但您应该已经知道如何使用它:

 PROTOS = A.proto B.proto include(protobuf.pri)

查看protobuf.pri时,您可能会注意到可以轻松应用于任何自定义编译或代码生成的通用模式:

 my_custom_compiler.name = my custom compiler name my_custom_compiler.input = input variable (list) my_custom_compiler.output = output file path + pattern my_custom_compiler.commands = custom compilation command my_custom_compiler.variable_out = output variable (list) QMAKE_EXTRA_COMPILERS += my_custom_compiler

范围和条件

通常,我们需要专门为给定平台定义声明,例如 Windows 或 MacOS。 Qmake 提供了三个预定义的平台指标:win32、macx 和 unix。 这是语法:

 win32 { # add Windows application icon, not applicable to unix/macx platform RC_ICONS += icon.ico }

范围可以嵌套,可以使用运算符! , | 甚至通配符:

 macx:debug { # include only on Mac and only for debug build HEADERS += debugging.h } win32|macx { HEADERS += windows_or_macx.h } win32-msvc* { # same as win32-msvc|win32-mscv.net }

注意:Unix 是在 Mac OS 上定义的! 如果要测试 Mac OS(不是通用 Unix),请使用unix:!macx条件。

在 Qt Creator 中, debugrelease的范围条件没有按预期工作。 要使这些正常工作,请使用以下模式:

 CONFIG(debug, debug|release) { LIBS += ... } CONFIG(release, debug|release) { LIBS += ... }

有用的功能

Qmake 有许多嵌入式功能,增加了更多的自动化。

第一个示例是files()函数。 假设您有一个生成可变数量源文件的代码生成步骤。 以下是如何将它们全部包含在SOURCES中:

 SOURCES += $$files(generated/*.c)

这将在generated的子文件夹中找到所有扩展名为.c的文件,并将它们添加到SOURCES变量中。

第二个示例与前一个示例类似,但现在代码生成生成了一个包含输出文件名(文件列表)的文本文件:

 SOURCES += $$cat(generated/filelist, lines)

这只会读取文件内容并将每一行视为SOURCES的条目。

注意:嵌入式函数的完整列表可以在这里找到:http://doc.qt.io/qt-5/qmake-function-reference.html

将警告视为错误

以下代码段使用前面描述的条件范围功能:

 *g++*: QMAKE_CXXFLAGS += -Werror *msvc*: QMAKE_CXXFLAGS += /WX

这种复杂的原因是因为 MSVC 有一个不同的标志来启用这个选项。

生成 Git 版本

当您需要创建包含从 Git 获得的当前 SW 版本的预处理器定义时,以下代码段很有用:

 DEFINES += SW_VERSION=\\\"$$system(git describe --always --abbrev=0)\\\"

只要git命令可用,这适用于任何平台。 如果您使用 Git 标签,那么即使分支继续进行,这也会查看最新的标签。 修改git describe命令以获得您选择的输出。

结论

Qmake 是一款出色的工具,专注于构建基于 Qt 的跨平台项目。 在本指南中,我们回顾了基本工具的使用和最常用的模式,这些模式将使您的项目结构保持灵活,构建规范易于阅读和维护。

想了解如何让您的 Qt 应用程序看起来更好吗? 尝试:如何在 C++ 中使用贝塞尔曲线和 QPainter 获得圆角形状:分步指南