首页 Qt Creator 源码学习 Qt Creator 源码学习 04:qtcreator.pri

Qt Creator 源码学习 04:qtcreator.pri

7 2K

上一章我们已经分析过项目文件 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 定义的变量,当templateapp时,用于指定应用程序的版本号;当templatelib时,用于指定库的版本号。在 Windows 平台,如果没有指定RC_FILERES_FILE变量,则会自动生成一个 .rc 文件。该文件包含FILEVERSIONPRODUCTVERSION两个字段。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 DEBUG MODE)
}

也可以使用CONFIG测试函数。CONFIG测试函数的优势在于,可以使用第二个参数来从一组值中选取一个需要的。要理解这一点,先看如下语句:

CONFIG = debug
CONFIG += release

上面语句中,CONFIG首先被设置为 debug,其后又被设置为 release。CONFIG的赋值顺序非常重要,在一组互斥的值之间(例如 debug 和 release),最后面的会被认为是激活的。正如上面语句,CONFIG会被认为是 release 的。CONFIG测试函数的第二个参数,正是用于指定这一组值。例如,

CONFIG = debug
CONFIG += release
CONFIG(release, debug|release):message(Release build!) #will print
CONFIG(debug, debug|release):message(Debug build!) #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)语句,将RETVERSION_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_CREATOR QT_NO_CAST_TO_ASCII QT_RESTRICTED_CAST_FROM_ASCII
!macx:DEFINES += QT_USE_FAST_OPERATOR_PLUS QT_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的处理与此类似。

7 评论

彩阳 2016年8月24日 - 18:39

注意,Windows下可能不容易编译通过,因为默认情况下,QVector没有打开接收initialize-list参数的构造函数。需要在合适的pri文件或者$$QTCREATOR_SOURCES/qtcreator.pri中添加这几行:
```
windows {
DEFINES *= Q_COMPILER_INITIALIZER_LISTS
}
```
一些成熟的插件,可以去
http://qtdream.com/topic/687
这里体验一下Qt Creator增强套装

回复
豆子 2016年8月29日 - 09:18

这有可能是 VS2013 的一个 bug,不清楚后续版本是否还有这个问题:https://bugreports.qt.io/browse/QTBUG-39142

回复
gruLewis 2016年9月12日 - 22:39

vs2013版本问题。低版本的不支持c++11的initialize-list语法。

回复
hli 2016年10月24日 - 09:28

安装VS2013 Update5

回复
潮汐 2016年8月25日 - 21:06

写的真不错,让我对pro有了更深的了解

回复
maqidi 2016年8月26日 - 14:04

豆子大神, 你对qbs 了解吗?QtCreator中使用 pro 和 qbs 管理文件。 看上去貌似很好用, 但是文档比较少。。。。。

回复
豆子 2016年8月26日 - 17:46

qbs 目前还不大了解,主要是在看 qmake,也就是用 pro 的部分

回复

回复 豆子 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2