上一章我们已经分析过项目文件 qtcreator.pro。我们看到,qtcreator.pro 中很多重要的功能都使用了来自 qtcreator.pri 中定义的函数或者变量。本章我们就来看看 qtcreator.pri 是怎么写的。
!isEmpty(QTCREATOR_PRI_INCLUDED):error("qtcreator.pri already included") QTCREATOR_PRI_INCLUDED = 1
第一行,如果存在 QTCREATOR_PRI_INCLUDED
,则抛出错误。下面一行则设置了 QTCREATOR_PRI_INCLUDED
。这两行类防止将 qtcreator.pri 引入多次。
QTCREATOR_VERSION = 4.1.82 QTCREATOR_COMPAT_VERSION = 4.1.82 VERSION = $$QTCREATOR_VERSION BINARY_ARTIFACTS_BRANCH = master
接下来定义了几个变量: QTCREATOR_VERSION
即 Qt Creator 的版本; QTCREATOR_COMPAT_VERSION
是插件所兼容的 Qt Creator 版本; VERSION
同样被赋值为 Qt Creator 的版本; BINARY_ARTIFACTS_BRANCH
指定的是 git 的分支。值得说明的是 VERSION
。这其实是 qmake 定义的变量,当 template
是 app
时,用于指定应用程序的版本号;当 template
是 lib
时,用于指定库的版本号。在 Windows 平台,如果没有指定 RC_FILE
和 RES_FILE
变量,则会自动生成一个 .rc 文件。该文件包含 FILEVERSION
和 PRODUCTVERSION
两个字段。 VERSION
应该由主版本号、次版本号、补丁号和构建版本号等组成。每一项都是 0 到 65535 之间的整型。例如:
win32:VERSION = 1.2.3.4 # major.minor.patch.build else:VERSION = 1.2.3 # major.minor.patch
下面是两个重要的函数定义。这两个函数都是用于生成库名字的。
defineReplace(qtLibraryTargetName) { unset(LIBRARY_NAME) LIBRARY_NAME = $$1 CONFIG(debug, debug|release) { !debug_and_release|build_pass { mac:RET = $$member(LIBRARY_NAME, 0)_debug else:win32:RET = $$member(LIBRARY_NAME, 0)d } } isEmpty(RET):RET = $$LIBRARY_NAME return($$RET) } defineReplace(qtLibraryName) { RET = $$qtLibraryTargetName($$1) win32 { VERSION_LIST = $$split(QTCREATOR_VERSION, .) RET = $$RET$$first(VERSION_LIST) } return($$RET) }
说重要,是因为这两个函数在 Qt Creator 中使用了多次,并且完全可以拷贝复制到其它项目继续使用。
前面我们说过,qmake 提供了替换函数和测试函数。这里就是自定义替换函数。定义替换函数使用的语句是 defineReplace
,其参数是函数名字。例如,上面我们定义了 qtLibraryTargetName
这个函数,那么就可以在后面直接使用:
message($$qtLibraryTargetName(LIB_NAME))
注意,我们说过,qmake 是按照从上到下顺序解析,所以在使用必须在定义之后才能正常执行。
下面来看 qtLibraryTargetName
是如何定义的。首先,取消 LIBRARY_NAME
的定义,然后使用语句 LIBRARY_NAME = $$1
赋值。定义函数时可以有参数,使用 $$1
即获取该参数。这里,我们将 $$1
,也就是第一个参数,赋值给变量 LIBRARY_NAME
。下面是 CONFIG
测试函数,该函数可以用于检测 CONFIG
变量中的值。 CONFIG
是一个重要变量,用于指定编译参数,例如我们可以在这里设置 debug 或 release;其可选值可以查阅相关文档。如下面语句:
CONFIG = debug
CONFIG
可以使用 scope 语法来判断,例如
debug { message(IN DEBUGMODE) }
也可以使用 CONFIG
测试函数。 CONFIG
测试函数的优势在于,可以使用第二个参数来从一组值中选取一个需要的。要理解这一点,先看如下语句:
CONFIG = debug CONFIG += release
上面语句中, CONFIG
首先被设置为 debug,其后又被设置为 release。 CONFIG
的赋值顺序非常重要,在一组互斥的值之间(例如 debug 和 release),最后面的会被认为是激活的。正如上面语句, CONFIG
会被认为是 release 的。 CONFIG
测试函数的第二个参数,正是用于指定这一组值。例如,
CONFIG = debug CONFIG += release CONFIG(release, debug|release):message(Releasebuild!) #will print CONFIG(debug, debug|release):message(Debugbuild!) #no print
CONFIG
函数指定仅测试 debug 和 release 两个值,而 release 是激活的,因此会输出“Release build!”。通常情况下,第二个参数不大需要,但是如果需要针对某些特殊互斥值进行测试,就可以使用这个参数。
回过头来看 qtcreator.pri 中的语句。 CONFIG
函数测试如果是 debug,则执行后面的语句。 !debug_and_release|build_pass
是 scope 语法,即当不是 release_and_release 或者 build_pass 时,对于 mac,将 RET
赋值为 $$member(LIBRARY_NAME, 0)_debug
,win 则是 RET = $$member(LIBRARY_NAME, 0)d
。release_and_release 意味着同时编译 debug 和 release 两个版本;build_pass 即构建过程。 member(variablename, position)
是一个替换函数,返回 variablename
中第 position
个元素;没有找到的话则返回空串。 variablename
是必须的, position
默认是 0,也就是会返回第一个元素。仔细研究 $$member(LIBRARY_NAME, 0)_debug
语句,如果 LIBRARY_NAME
为 core 的话,那么,语句的返回值将是 core_debug
。接下来,如果 RET
为空,也就是不是 debug 的情况下,直接将其赋值为 LIBRARY_NAME
;这是为了防止得到一个空的 RET
。最后将 RET
返回。
综上所述, qtLibraryTargetName
函数实现了这样一种功能:当使用 debug 环境编译时,在 mac 下生成的库名称将被重命名追加 _debug
,win 下则追加 d
。因此,当我们使用如下语句时,
message($$qtLibraryTargetName(core))
在 debug 环境下,结果是 core_debug 或者 cored;否则是 core。值得说明的是,这并不是唯一的实现方法。另外的版本中往往使用下面语句:
CONFIG(debug, debug|release) { mac: TARGET = $$join(TARGET,,,_debug) else: TARGET = $$join(TARGET,,,d) }
该实现与上面结果一致,只是使用了 join
函数。函数原型为 join(variablename, glue, before, after)
,会将 variablename
使用 glue
连接起来,同时在连接之后的字符串前面添加 before
,后面添加 after
。注意看这里的使用, $$join(TARGET,,,_debug)
,第二个参数为空,因此将 variablename
与空串相连,由于这是传入的是 TARGET
,是一个字符串而不是列表,与空串相连结果还是其本身; before
为空,即前面不增加内容; after
为 _debug
,即最后增加 _debug
后缀。由此看出,使用这种语句也可以达到相同的目的。
qtLibraryName
替换函数与此类似。首先,它会获得 qtLibraryTargetName
的返回值。如果 win32 环境下,使用 split
将前面定义的 QTCREATOR_VERSION
以 .
为分隔符分成列表,然后使用 $$RET$$first(VERSION_LIST)
语句,将 RET
与 VERSION_LIST
的第一个元素,也就是 major 值,拼接后返回。这是因为,在 Windows 平台,为了避免出现 dll hell,Qt 会自动为生成的 dll 增加版本号。而我们一般只需要主版本一致即可,所以会重新生成新的名字。dll hell 只发生在 Windows 平台,因此这里只需要判断 win32 即可。
接下来定义了一个测试函数:
defineTest(minQtVersion) { maj = $$1 min = $$2 patch = $$3 isEqual(QT_MAJOR_VERSION, $$maj) { isEqual(QT_MINOR_VERSION, $$min) { isEqual(QT_PATCH_VERSION, $$patch) { return(true) } greaterThan(QT_PATCH_VERSION, $$patch) { return(true) } } greaterThan(QT_MINOR_VERSION, $$min) { return(true) } } greaterThan(QT_MAJOR_VERSION, $$maj) { return(true) } return(false) }
与替换函数类似,定义测试函数使用 defineTest
语句。 defineTest(minQtVersion)
定义了一个名为 minQtVersion
的测试函数。该函数有三个参数,这可以从下面的三行看出来。这个函数的定义很简单,只不过使用了几个 qmake 预定义的宏进行判断。不过这些也可以从名字看出其实际含义。
下面又定义了一个替换函数:
# For use in custom compilers which just copy files defineReplace(stripSrcDir) { return($$relative_path($$absolute_path($$1, $$OUT_PWD), $$_PRO_FILE_PWD_)) }
按照注释的说明,这个函数用于自定义编译器复制文件。函数 absolute_path(path[, base])
返回参数 path
的绝对路径;如果没有传入 base
,则将当前目录作为 path
的 base。与此类似,函数 relative_path(filePath[, base])
返回参数 path
的相对路径。
有关“PWD”的几个内置变量是非常常用的。很多时候,我们希望在构建时自动复制一些文件到目标路径,往往需要使用这些变量。这些变量有:
PWD
|
使用该变量 PWD
的文件(.pro 文件或者 .pri 文件)所在目录。 |
_PRO_FILE_PWD_
|
.pro 文件所在目录;即使该变量出现在 .pri 文件,也是指包含该 .pri 文件的 .pro 文件所在目录。 |
_PRO_FILE_
|
.pro 文件完整路径。 |
OUT_PWD
|
生成的 makefile 所在目录。 |
注意,由于 qmake 无法使用只读变量,因此必须时刻警惕不要覆盖这些内置变量的值,否则会发生未知的错误。
下面几行,
QTC_BUILD_TESTS = $$(QTC_BUILD_TESTS) !isEmpty(QTC_BUILD_TESTS):TEST = $$QTC_BUILD_TESTS ...
即使用用户传入值,如果没有,则需要设置一个默认值。这些我们前面已经提到过,这里不再赘述。
下面开始重要的构建路径部分。说重要,是因为 Qt Creator 编译之后生成的文件应该保存在哪里,都是在这里定义的。
IDE_SOURCE_TREE = $$PWD isEmpty(IDE_BUILD_TREE) { sub_dir = $$_PRO_FILE_PWD_ sub_dir ~= s,^$$re_escape($$PWD),, IDE_BUILD_TREE = $$clean_path($$OUT_PWD) IDE_BUILD_TREE ~= s,$$re_escape($$sub_dir)$,, }
IDE_SOURCE_TREE
即源代码所在目录。注意我们使用了 $$PWD
变量直接赋值:这个值会随着 .pro 文件或 .pri 文件的不同位置而有所不同,但都应由此找到源代码树。这是目录组织中需要注意的问题。
函数 re_escape(string)
将参数 string
中出现的所有正则表达式中的保留字进行转义。例如, ()
是正则表达式的保留字,那么, $$re_escape(f(x))
的返回值将是 f//(x//)
。这一函数的目的是保证获得一个合法的正则表达式。函数 re_escape(path)
将参数 path
中的 .
以及 ..
等占位符移除,获得一个明确的路径。我们用下面的实验来证明这一点。目录结构如下:
/test |-application.pro(TEMPLATE=subdirs; SUBDIRS=src) |-application.pri |-src |-src.pro(TEMPLATE=subdirs; SUBDIRS=app) |-app |-app.pro(TEMPLATE=app; include(../../application.pri))
其中,application.pri 内容如下:
isEmpty(IDE_BUILD_TREE) { sub_dir = $$_PRO_FILE_PWD_ message($$_PRO_FILE_PWD_) # 输出=E:/Workspace/test/src/app message($$PWD) # 输出=E:/Workspace/test sub_dir ~= s,^$$re_escape($$PWD),, message($$sub_dir) # 输出=/src/app IDE_BUILD_TREE = $$clean_path($$OUT_PWD) IDE_BUILD_TREE ~= s,$$re_escape($$sub_dir)$,, message($$OUT_PWD) # 输出=E:/Workspace/build-test-Desktop_Qt_5_7_0_MSVC2015_64bit-Debug/src/app message($$IDE_BUILD_TREE) # 输出=E:/Workspace/build-test-Desktop_Qt_5_7_0_MSVC2015_64bit-Debug }
仔细观察以上输出,可以看到, IDE_BUILD_TREE
最终得到的是输出根目录。不管是否启用了 shadow build,这一段代码都能够适用。因此,我们也不妨将其添加到我们自己的工具库中,以便在未来项目中使用。
下面一行
IDE_APP_PATH = $$IDE_BUILD_TREE/bin
设置了最终输出的二进制文件的位置,也就是在根目录下的 bin 目录中。接下来很多行都是设置目录位置,因为语法都很简单,这里不再详细介绍。可以看出,Qt Creator 编译过程中所有的输出位置,都是基于 IDE_BUILD_TREE
这个变量。由此顺利组织我们所需要的输出文件夹树,是很有用的。
INCLUDEPATH += / $$IDE_BUILD_TREE/src / # for <app/app_version.h> in case of actual build directory $$IDE_SOURCE_TREE/src / # for <app/app_version.h> in case of binary package with dev package $$IDE_SOURCE_TREE/src/libs / $$IDE_SOURCE_TREE/tools
INCLUDEPATH
给出了头文件检索目录。这有利于我们 #include
头文件。如果不是设置该值,在 #include
时需要给出全路径。例如目录结构如下:
/ |-core | |-include | |-global.h |-lib |-library.cpp
如果 library.cpp 中需要 #include
core 的 global.h,需要写作
#include "../core/include/global.h"
如果添加
INCLUDEPATH += core
那么只需要写作
#include "include/global.h"
即可。
下面来看 Qt Creator 是怎么做的。由于 Qt Creator 会在每次编译时自动生成一个 app_version.h,包含 Qt Creator 的版本信息(每次自动更新构建版本号,具体生成过程会在后文详细介绍),所有自动生成的代码文件都会出现在 $$IDE_BUILD_TREE/src
,因此,Qt Creator 首先将 $$IDE_BUILD_TREE/src
添加到了 INCLUDEPATH
。然后,Qt Creator 中的 libs 会被各个插件使用,这是库文件而不属于插件,为了能够使用类似
#include "extensionsystem/pluginmanager.h"
这样的语句,而不是一连串的“../../../libs/extensionsystem/pluginmanager.h”,Qt Creator 会将 $$IDE_SOURCE_TREE/src/libs
添加到 INCLUDEPATH
。
下面的语句
QTC_PLUGIN_DIRS_FROM_ENVIRONMENT = $$(QTC_PLUGIN_DIRS) QTC_PLUGIN_DIRS += $$split(QTC_PLUGIN_DIRS_FROM_ENVIRONMENT, $$QMAKE_DIRLIST_SEP) QTC_PLUGIN_DIRS += $$IDE_SOURCE_TREE/src/plugins for(dir, QTC_PLUGIN_DIRS) { INCLUDEPATH += $$dir }
展示了 for
语法的使用,这非常类似于 C++ 的 for_each
循环。
LIBS *= -L$$LINK_LIBRARY_PATH # Qt Creator libraries exists($$IDE_LIBRARY_PATH): LIBS *= -L$$IDE_LIBRARY_PATH # library path from output path
LIBS
是 qmake 连接第三方库的配置。 -L
指定了第三方库所在的目录; -l
指定了第三方库的名字。如果没有 -l
,则会连接 -L
指定的目录中所有的库。这正是 Qt Creator 的用法。Qt Creator 需要使用 LINK_LIBRARY_PATH
中所有库。注意,因为我们使用按顺序编译的方式,所以 qmake 能够保证先生成再链接这些库文件。
DEFINES += QT_CREATORQT_NO_CAST_TO_ASCIIQT_RESTRICTED_CAST_FROM_ASCII !macx:DEFINES += QT_USE_FAST_OPERATOR_PLUSQT_USE_FAST_CONCATENATION
DEFINES
类似于宏定义,相当于使用类似
gcc -DQT_CREATOR
这样的语句。因此,我们可以在源代码中使用 $ifdef
条件编译。
注意在使用中,有的是 +=
运算符,有的是 *=
运算符。前者是追加;后者是没有则追加,有则不作操作,也就是保持存在且唯一。
最后我们来看一段复杂的代码:
# recursively resolve plugin deps done_plugins = for(ever) { isEmpty(QTC_PLUGIN_DEPENDS): / break() done_plugins += $$QTC_PLUGIN_DEPENDS for(dep, QTC_PLUGIN_DEPENDS) { dependencies_file = for(dir, QTC_PLUGIN_DIRS) { exists($$dir/$$dep/$${dep}_dependencies.pri) { dependencies_file = $$dir/$$dep/$${dep}_dependencies.pri break() } } isEmpty(dependencies_file): / error("Plugin dependency $$dep not found") include($$dependencies_file) LIBS += -l$$qtLibraryName($$QTC_PLUGIN_NAME) } QTC_PLUGIN_DEPENDS = $$unique(QTC_PLUGIN_DEPENDS) QTC_PLUGIN_DEPENDS -= $$unique(done_plugins) }
按照注释,这是递归处理插件依赖。 ever
是一个常量,永远不会为 false
或空值,因此 for(ever)
是无限循环,直到使用 break()
跳出。如果 QTC_PLUGIN_DEPENDS
为空,则直接退出循环。事实上, QTC_PLUGIN_DEPENDS
默认没有设置,所以这段代码其实并没有使用。这段代码的目的是,允许用户在编译时直接通过 QTC_PLUGIN_DEPENDS
指定插件依赖。如果没有,则根据每个插件自己的依赖处理。后面的 for
循环遍历 QTC_PLUGIN_DEPENDS
中指定的每一个依赖,然后用另外一个嵌套的循环遍历 $$QTC_PLUGIN_DIRS
指定的插件目录中的每一个目录,找出对应的插件,取其对应的 .pri 文件,即 dependencies_file
。注意循环的最后,使用 -=
运算符移除每次处理的依赖,直到最后 QTC_PLUGIN_DEPENDS
为空,退出循环。
最末的 done_libs
的处理与此类似。