Qt项目中的qmake install:INSTALLS变量与安装打包实战
先讲一个我在实际项目里经常遇到的开场开发机上的Qt程序已经能在bin目录里跑起来团队要出发布包突然发现这一步不知道该怎么写于是去翻.pro文件里面没有任何和install沾边的内容只好手动把可执行文件、动态库、头文件一份一份拷到目标目录。这样的拷贝流程用一次两次还行一旦要交付到别的机器、打成deb或rpm、或者要在干净环境里验证“只装运行时能不能跑”手动拷贝的后遗症就会一个一个冒出来——漏文件、权限不对、路径错位、卸载不干净。其实qmake从一开始就提供了整套install机制只不过入口藏在INSTALLS变量和Makefile的install目标里不专门梳理一遍确实容易被绕晕。这篇文章就把Qt项目里围绕qmake install的内容从头到尾讲一遍覆盖install目标是什么、怎么用INSTALLS定义安装规则、临时安装与打包怎么配合、以及那些文档里不写但实际会踩到的坑。1. 先把概念梳理清楚qmake install的完整链路1.1 qmake不执行安装install目标是Makefile的“代言人”很多第一次接触Qt构建的人会误以为存在一个“qmake install”命令输入之后qmake就直接把文件装到系统里了。实际不是这样。qmake的本质是一个生成器它读取.pro/.pri项目文件经过变量展开和规则匹配之后产出适合当前平台和编译套件的Makefile。至于安装动作是在生成的Makefile里被表达成一个名叫install的目标。你真正执行的是make install、nmake install或者jom installqmake并不负责搬文件。但这不代表qmake在install这件事上没作用。恰恰相反install目标的内容完全由qmake根据.pro文件生成。如果你在.pro里什么都没声明Makefile里通常只会出现一个空壳install目标跑起来要么报Nothing to be done要么根本没有对应规则。让install目标真正干活的第一步就是让qmake知道你有哪些东西需要被安装、装到哪里、以什么方式装。换句话说qmake负责把安装意图“翻译”成make工具能理解的动作序列真正的执行者是系统里的install、mkdir、strip、ln等命令。1.2 INSTALLS变量给安装规则打开大门INSTALLS是qmake项目文件里最核心的安装配置变量它本质上是一个列表列出这个项目希望安装的条目名称。你看官方示例或者其他开源项目的Makefile时经常会看到这样的组合target.path /usr/local/bin INSTALLS target这里的target并不是qmake的关键字它只是一个条目名。你完全可以叫bin、app、runtime甚至可以叫whatever。关键是你在后面用“条目名.属性”的写法定义这条规则最后用INSTALLS 把条目名加进列表。一个.pro里可以同时定义很多条qmake会为每一条生成对应的安装子目标然后把它们串到总体的install目标里。每个条目最常用的属性有三个。第一个是path代表安装目的地必须给值否则qmake会告警并跳过这条规则。第二个是files表示要安装的文件列表可以写相对路径、绝对路径、甚至带通配符的路径如果你不写filesqmake会默认安装TARGET也就是编译出来的目标产物。第三个是extra允许你写一段额外的shell命令qmake生成的Makefile会在安装该条目的前后去执行这段命令。还有CONFIG属性可以附加一些修饰项比如no_check_exist代表安装时如果源文件不存在也不报错directory和recursive组合起来用来整目录递归安装。这里就出现了一个关键分水岭如果项目里任何一条INSTALLS都没声明qmake生成的Makefile里虽然可能有install目标但它的行为是空的所谓“安装”也就无从谈起。很多新手把目标文件编译出来了然后满怀期待地敲make install结果发现系统目录里什么都没有多半就是因为.pro文件里压根没写INSTALLS。反过来只要写了一条整个安装链条就被激活了后续所有细节都从这一条规则派生出来。2. 最小可行安装把二进制装进/usr/local/bin2.1 三行配置就能跑通整个安装从一个最简单、每个人都能立刻验证的场景开始。假设你有一个普通的Qt命令行程序项目文件大概长这样QT core TEMPLATE app TARGET mytool SOURCES main.cpp现在要让make install把编译出来的mytool装到/usr/local/bin只需要加三行target.path /usr/local/bin INSTALLS target重新运行qmake生成Makefile然后依次执行qmake make sudo make install整个过程里发生的事情大致是这样的qmake编译和链接工具链生成平台相关的Makefilemake把源码编译成二进制make install触发了Makefile里的install目标qmake会在里面生成目录创建、文件复制、权限设置、符号表剥离等一系列子命令。我第一次这么配的时候以为qmake只是简单地把TARGET对应的文件复制过去。实际看了一下执行过程发现它远比想象中严谨。对于这个最小示例qmake生成的安装命令通常等价于install -m 755 -p mytool /usr/local/bin/mytool-m 755是在设置可执行权限-p是保留修改时间和文件属性这两个细节对手动复制场景有本质区别。手动cp出来的文件经常权限不对而且时间戳被重置排查问题时很不直观。install命令还会在目标目录不存在时自动创建这也是为什么很多项目从来没写mkdir但make install依然能成功。2.2 DESTDIR和target.path到底什么关系一个很容易混淆的概念是DESTDIR。有些开发者会在.pro里写DESTDIR bin然后打算配合make install用。DESTDIR的作用对象是构建过程它控制的是编译出来的目标文件放在哪个目录例如DESTDIR /tmp/mybuild会让生成的mytool出现在/tmp/mybuild/mytool。target.path控制的则是安装阶段的目标位置两者并不直接叠加。不直接叠加但会造成认知混乱。比如你设置了DESTDIR /usr/local/bin然后target.path也写了/usr/local/binmake install执行时会发现源文件路径已经是/usr/local/bin/mytool目标目录也是/usr/local/bin于是复制变得没有意义甚至可能因为文件路径冲突而出现诡异的行为。至少在写安装规则时不要指望靠DESTDIR来“提前安装”DESTDIR的正确用途是构建输出目录而安装规则应该围绕target.path来设计。还有一点要提醒如果你的.pro里同时出现DESTDIR和target.path并且安装后文件路径看起来多了一层那八成是你把两者混着用了。最稳妥的约定是构建阶段产物进DESTDIR发布阶段文件进target.path两者分开维护互不干扰。2.3 前缀变量和Qt安装路径查询直接在.pro里写死/usr/local/bin并不是好做法因为这个项目换一台机器别人可能希望装到/opt/xxx或者家目录你要让写死路径的人改源代码体验非常糟糕。通常的做法是引入PREFIX变量并把所有安装路径都建立在它之上isEmpty(PREFIX) { PREFIX /usr/local } target.path $$PREFIX/bin INSTALLS target这里isEmpty(PREFIX)是qmake里常见的保护性写法意思是如果外部没有传PREFIX就用默认值/usr/local。用户在构建时可以通过qmake PREFIX/opt/mytool来覆盖后面所有安装路径都会跟着变。这一招在打包脚本里尤其常用因为不同发行版的安装根目录规则不一样用变量包装一层生成安装包时改一个参数就行。另外qmake提供了一组$$[...]形式的路径查询变量用来获取Qt本身安装时使用的标准目录。比如$$[QT_INSTALL_LIBS]返回Qt动态库所在目录$$[QT_INSTALL_PREFIX]返回Qt安装根目录$$[QT_INSTALL_HEADERS]返回Qt头文件所在目录。如果想让程序自动装到系统Qt同级的lib或include目录直接用这些变量就能避免硬编码路径。想知道当前机器上qmake查到的具体值在终端跑一句qmake -query就会列出所有可用的安装路径。这个命令在排查路径问题时非常好用因为你给.pro里的路径变量赋值时最终都要落到这些真实路径上。3. 不止一个target库、头文件、文档和配置怎么一起装3.1 库的安装从lib到include的全套安排单个可执行文件的安装只是热身真实项目往往同时包含动态库、头文件、资源文件、帮助文档。把它们全部用INSTALLS组织起来并不复杂但有一个关键点要理解每个INSTALLS条目都是独立的你可以为库、头文件、文档分别定义不同的path和files。假设正在做一个名叫mytoolkit的共享库项目文件大概这样QT - gui TEMPLATE lib TARGET mytoolkit CONFIG shared VERSION 1.2.0 SOURCES toolkit.cpp HEADERS toolkit.h安装规则可以这样写target.path $$PREFIX/lib INSTALLS target headers.path $$PREFIX/include/mytoolkit headers.files toolkit.h INSTALLS headers运行make install后编译产生的libmytoolkit.so会被装到$$PREFIX/lib头文件toolkit.h会被装到$$PREFIX/include/mytoolkit。很多开源项目就是这么布局的头文件单独放一层目录用的时候#include mytoolkit/toolkit.h这样不容易和其他库的同名头文件打架。如果你还希望安装的时候顺带维护版本号链接比如创建libmytoolkit.so.1指向libmytoolkit.so.1.2.0用target.extra可以做到。qmake的extra命令在文件复制完成之后执行写的时候需要注意平台命令差异。Linux下可以这么写target.extra -ln -sf libmytoolkit.so.1.2.0 $$PREFIX/lib/libmytoolkit.so.1前面的短横线意思是告诉make“这条命令如果失败先别把整个安装流程停掉”你一定要想清楚自己是不是真的希望忽略错误。不要为了图省事一律加短横线否则符号链接创建失败时安装过程照样显示成功后面程序一运行就会因为找不到库而出错。3.2 头文件、文档和配置数据的安装姿势文档安装和库安装没有本质区别无非是定义一个新的规则条目docs.path $$PREFIX/share/doc/mytoolkit docs.files README.md CHANGELOG.md INSTALLS docs这里docs.files支持直接罗列多个文件也支持通配符。但有一个经验教训使用通配符时如果匹配到的文件很多qmake会按顺序展开目录混在一起时容易复制出意想不到的结构。对于多目录资源比如一套完整的资源目录最好用整目录方式。语法上只要把目录名放进files再在CONFIG里加上directory和recursivedata.path $$PREFIX/share/mytoolkit/res data.files resources data.CONFIG directory recursive INSTALLS datadirectory告诉qmake这个源文件是一个目录recursive让qmake递归复制目录内的所有子目录和文件。如果没有这两个CONFIG修饰qmake可能只复制目录本身或者只复制一层内容最终出来的文件树残缺不全。这个问题非常隐蔽因为在小项目里目录只有一层时看不出区别一旦resources下面再套两层子目录缺失问题就暴露出来了。配置文件也是一样套路config.path /etc/mytoolkit config.files mytoolkit.conf config.CONFIG no_check_exist INSTALLS configno_check_exist的意思是就算源文件不存在也先不报错。这经常用于“某些机器上配置文件由其他包生成”的场景但使用前要确认目标机器上真的会有对应文件否则你等于把一个残缺安装标记成了成功。3.3 安装后自动执行清理或初始化动作有时候安装不只是复制文件那么简单。比如程序装好之后需要创建运行时缓存目录、需要对配置文件做权限调整、需要更新动态库缓存。这些动作都可以塞进某一条INSTALLS的extra里。举个例子安装完库之后希望自动调用ldconfig刷新链接器缓存可以这样写ldconfig_item.path $$PREFIX/lib ldconfig_item.files libmytoolkit.so ldconfig_item.extra ldconfig -n $$PREFIX/lib INSTALLS ldconfig_item这一条会把库文件复制到目标目录然后执行ldconfig。不过在项目里这种动作最好谨慎使用因为ldconfig会影响整个系统的动态链接状态安装脚本执行时如果权限不够ldconfig会直接返回非零导致make install中断。更稳妥的做法是把这类后置动作留给安装包脚本去处理而不是嵌在qmake安装目标里。还有一个小技巧如果只想在安装完成后打印一条提示信息比如告诉用户需要把某个目录加入PATH可以给一个空files的规则塞extra。但我不推荐在正式项目里这么做因为qmake安装目标会为每条规则生成一个子目标空files规则不仅看起来怪还会在某些make实现下产生多余输出不如直接在构建脚本里用echo。4. 实操现场INSTALL_ROOT与打包暂存安装4.1 make install背后到底跑了几条命令在继续往下之前先把执行的细节看清楚。很多人对make install的印象就是一行安装命令但展开来看它是一组有依赖关系的子目标。qmake在生成Makefile时会为每个INSTALLS条目生成一个install_条目名 的子目标然后按照它们在.pro里出现的顺序把这些子目标挂到总install目标下面。可以用下面这个命令查看真实动作不用真的执行make -n install-n参数让make只打印命令、不实际运行。我调试安装规则时最常用的就是这个命令配合肉眼核对路径能快速发现目录少一层、源文件写错、命令引用了不存在的变量这类问题。有一次我发现安装库时总是报错却看不到任何复制动作用make -n install查了一下才发现源文件的路径写错了qmake找不到文件后面所有步骤都没执行。正常安装一个包含库和文档的项目make install的输出会依次展示类似这样的动作创建目标目录、复制动态库、复制头文件、复制文档目录、为二进制执行strip。顺序基本和INSTALLS列表顺序一致所以你把安装规则写在一起时先后关系其实是可预期的。4.2 INSTALL_ROOT一些“先装到一个临时目录”的场景做安装包的人都会需要一套“暂存安装”方案也就是别把文件直接装进系统而是先装到一个临时空目录然后把临时目录里的内容打包成deb、rpm或者其他格式。这在qmake里有专门的变量INSTALL_ROOT。用法非常简单make install INSTALL_ROOT/tmp/package如果你前面把target.path配置成了/usr/local/bin那么执行之后文件并不会出现在/usr/local/bin而是出现在/tmp/package/usr/local/bin。INSTALL_ROOT就像一个透明前缀它会插到所有安装路径的最前面但不会改变每个规则本身的相对结构。这个特性对打包流程极其重要因为打包工具一般不允许构建过程直接写系统目录而是通过这样一个暂存根目录收集要打包的完整文件树。我实际在项目里配合打包时通常会写一个简单脚本完成全套动作#!/bin/sh set -e rm -rf staging mkdir -p staging qmake PREFIX/usr make -j$(getconf _NPROCESSORS_ONLN) make install INSTALL_ROOT$(pwd)/staging find staging -type f -o -type l | sort这里的关键是把PREFIX设置成/usr而不是默认的/usr/local这样暂存目录里会得到usr/bin、usr/lib、usr/include这样的标准布局。随后用find列出所有文件逐个检查有没有装漏再交给打包工具。如果不加这一步清点等打包完成之后才发现缺了一个头文件排查成本会高很多。4.3 INSTALL_ROOT和DESTDIR别再互相绕了打包场景中你还会遇到另一个变量DESTDIR很多人会把INSTALL_ROOT和它搞混因为它们表面上看都是给安装路径加前缀。DESTDIR在GNU Make的生态里确实常被用作暂存根目录但qmake生成的Makefile里DESTDIR的语义偏向构建输出目录安装规则对它的处理并不统一。如果项目里同时用这两种变量很容易出现文件被装到了意想不到的地方。我建议明确分工构建时如果需要把编译产物放到统一输出目录用DESTDIR安装阶段做暂存打包用INSTALL_ROOT。不要试图让两者互相覆盖也不要在.pro里让DESTDIR参与target.path的拼接。与其冒着路径加倍的风险不如把构建输出和安装暂存分开管理临时目录的清理逻辑也更清楚。还有一点INSTALL_ROOT可以接在make命令后面也可以写进.pro里但写在.pro里会让不同用户构建时难以覆盖反而失去了灵活性。更加合理的方式是命令行传入脚本里甚至可以根据当前打包目录动态拼一个值出来。5. 常见问题与排查见闻5.1 为什么我的Makefile里没有install目标这是我在群聊里见过最多的问题之一明明.pro里写了INSTALLS打开Makefile搜install却发现一片空白或者make install直接提示Nothing to be done。这种情况有几种典型原因。最常见的是没有重新运行qmake。修改.pro之后如果你用的是命令行构建必须重新执行qmake让Makefile重新生成。如果你是在IDE里构建也要触发“运行qmake”这一步只按构建按钮并不会自动重新解析.pro里新加的安装规则。很多时候大家觉得“我改了.pro也重新点过构建了”实际IDE编译时只是调用make继续增量编译根本没有重新跑qmake安装目标自然没有被刷新。另一个原因是把安装规则写错了文件位置。举个例子你有一个subdirs类型的大项目顶层.pro只是用来罗列SUBDIRS真正负责编译和安装的子项目.pro在子目录里。这时如果在顶层.pro写INSTALLS由于顶层本身不产生target安装规则虽然生成出来了实际却不知道要装什么文件。反过来如果你在子项目.pro里写了安装规则却一直用顶层的Makefile执行make install虽然qmake通常会传递安装目标给子目录但前提是子目录的Makefile正确生成了对应的install子目标。排查时先用make -n install看看行为再进到真正有内容的子目录里清理Makefile重新生成基本都能定位。5.2 文件装到了奇怪的多层目录装完文件打开目标目录一看路径变成了/usr/local/usr/local/bin这种多层结构或者/opt/opt/xxx这种问题几乎总是路径拼接重复导致的。常见写法是在target.path里直接写了完整路径然后又往前面接了一个同样包含前缀的变量。比如PREFIX /usr/local target.path $$PREFIX/usr/local/bin你会得到/usr/local/usr/local/bin因为PREFIX本身已经包含了/usr/local后面又写了一遍。类似的情况还有在INSTALL_ROOT已经存在于Makefile规则的前提下又手动在target.path里加了INSTALL_ROOT之类的变量占位符结果生成了拖沓的路径。我的建议是安装路径只遵循一套规则target.path内部要么只用绝对路径要么只用PREFIX作为基座拼接不要同时混用多种前缀来源。临时安装时统一交给make命令里的INSTALL_ROOT变量不要试图在.pro里预先把暂存路径也算进去。5.3 strip掉了调试信息调试时又头大qmake生成的安装目标里默认会对接了release构建的二进制执行strip操作把符号表去掉以减小安装体积。大多数时候这是好事但如果你需要现场调试一个已经从系统安装好的程序发现断点总是提示没有符号那多半就是安装时被strip过了。解决办法分两种。一种是在.pro里把QMAKE_STRIP变量置空比如isEmpty(QMAKE_STRIP) { QMAKE_STRIP true }另一种是干脆不执行strip把QMAKE_STRIP定义成不产生副作用但返回成功的命令。这里要说清楚strip这件事不是坏而是要在不同阶段做不同选择。给用户的发布包和给开发者的debug包应该走两套安装脚本而不是靠一个旮旯里的开关去应急。我自己的习惯是发布包保留默认strip开发专用包引入一个单独的.pro片段关掉strip。5.4 Windows平台下的路径和命令差异Windows上跑qmake安装通常会遇到两类细节。第一是命令差异MSVC环境下qmake生成Makefile时会把install动作映射成copy或xcopy而MinGW环境下则可能沿用类似POSIX的cp组合但两者对路径分隔符的处理要求很严格。我见过最典型的报错是路径里混用了反斜杠和正斜杠导致copy命令把文件名解析成了多个段最终在目标目录里生成一堆奇怪的文件名。第二是环境变量展开方式不同。Makefile里$(INSTALL_ROOT)这种语法在不同make实现下的表现有一定差异如果你的项目要在Windows和Linux上同时使用建议统一用$$target.path这种qmake层面的变量拼接避免直接在extra字段里手写$(DESTDIR)之类的原生Make语法。Windows下还有一个容易踩的坑install目标执行后如果需要重启终端才能让新增的bin目录生效那多半是当前会话的PATH没有刷新和项目本身无关换一个终端再验证即可。5.5 uninstall并不是万能后悔药make uninstall可以从Makefile里调出来删除已安装文件qmake会生成一条uninstall规则并在安装时记录文件路径。但它不是后悔药至少有两个限制。第一uninstall只删除文件不会删除安装时自动创建的空目录目录是install命令顺带建出来的make uninstall并不知道要不要回收。第二如果某条安装规则使用了extra命令创建了符号链接或其他产物uninstall默认不会对这些额外产物负责extra命令产生的副作用得你自己清理。所以在正式发布包流程里不把uninstall当成卸载机制来依赖它更多是开发阶段“装错了改一下重新装”的辅助工具。清理系统环境时还是交给对应操作系统的包管理器或你的卸载脚本。6. 一套可以直接套用的安装模板6.1 .pro侧的统一配置模板把上面的经验汇总成一个相对通用的.pro安装模板覆盖可执行程序、动态库、头文件、文档四种类型。这套配置在实际项目里可以直接改项目名之后拿来用isEmpty(PREFIX) { PREFIX /usr/local } # 如果安装了Qt库也可以让include和lib跟随Qt目录 isEmpty(QT_INSTALL_LIBS) { QT_INSTALL_LIBS $$PREFIX/lib } isEmpty(QT_INSTALL_HEADERS) { QT_INSTALL_HEADERS $$PREFIX/include } target.path $$PREFIX/bin INSTALLS target headers.path $$QT_INSTALL_HEADERS/$$TARGET headers.files $$PWD/include/*.h headers.CONFIG no_check_exist INSTALLS headers docs.path $$PREFIX/share/doc/$$TARGET docs.files README.md CHANGELOG.md INSTALLS docs profiles.path /etc/$$TARGET profiles.files $$PWD/config/$$TARGET.conf profiles.CONFIG no_check_exist INSTALLS profiles这套模板在子目录或单目录项目里都能用唯一的注意点是headers.files的源路径。如果你用shadow build源文件目录和构建目录不一定相同所以用$$PWD把.pro文件所在目录固化成绝对路径比只写include/*.h更稳妥。$$PWD是qmake内置变量代表当前.pro文件所在目录不用手动拼接路径。6.2 打包配合脚本模板配合一个简单可靠的安装打包脚本我建议固定做三件事清空临时目录、重建、生成Makefile后再安装。类似这样#!/bin/bash set -e PREFIX_ARG${PREFIX:-/usr} STAGING$(pwd)/staging rm -rf $STAGING mkdir -p $STAGING qmake PREFIX$PREFIX_ARG make -j$(nproc) make install INSTALL_ROOT$STAGING echo Files staged under: $STAGING find $STAGING -type f -o -type l | sed s|$STAGING|| manifest.txt cat manifest.txt这个脚本最后生成的manifest.txt非常有用打包时要包含哪些文件直接看它就行同时也能在打包前快速核对是否有多余文件被塞进了系统目录。打包完成之后我把manifest.txt和安装包一起归档出问题时对照清单能省不少排查时间。最后再分享一点个人习惯我自己的经验是qmake的install机制并不复杂但非常容易被误用因为它隐藏在生成的Makefile之后不像CMake那样直白地暴露install命令。刚上手时总是习惯写完INSTALLS就去执行make install却忽略了qmake重新执行这一步结果改了N次.pro都不生效最后浪费一下午才发现Makefile根本没更新。后来我形成了一套固定的调试顺序先qmake再make然后make -n install看命令确认路径正确之后再真正执行。这套顺序看着平淡但真的没有一次落空。如果你也在被qmake install这个问题卡住不妨把前因后果再捋一遍INSTALLS定义了你装什么target.path定义了装到哪INSTALL_ROOT定义了从哪条根目录开始积累暂存文件而把这一切从.pro翻译到Makefile的那一步永远是重跑qmake。理清了这条线你离一个干净、稳定、可复现的安装流程就不远了。