Qt Creator 的源码树里藏着不少可以单独拎出来用的好东西,linguist模块就是其中一个。很多人第一次接触它,是因为项目要做多语言界面,发现 Qt 自带的翻译工具链其实就在 Creator 的源码仓库里,而不是单独发一个包。这时候问题就来了:直接拿官方预编译的二进制当然省事,但版本对不上、想改点行为、或者纯粹想搞清楚这套工具是怎么构建出来的,就得自己动手编译。这篇内容就是围绕“从 Qt Creator 源码里把 linguist 模块单独编译出来”这件事展开的,我会把构建体系的选择、依赖关系的梳理、编译过程中的实际卡点,以及编译完之后怎么验证和落地使用,按我自己的操作顺序讲清楚。适合已经会用 Qt、但没怎么碰过 Qt 自身构建体系的中级开发者,也适合想给团队做一套可控翻译工具链的工程负责人。整篇不涉及任何具体机构,纯技术路径分享。
1. 先搞清楚 linguist 在源码树里的真实位置
动手之前最忌讳的就是上来就qmake && make,因为 Qt Creator 的源码组织方式和普通的 Qt 应用完全不是一回事。你得先明白自己要编译的东西到底挂在哪棵子树上,否则后面找.pro文件、找依赖、找输出目录都会绕远路。
1.1 linguist 不是一个独立仓库,而是 qtbase 体系下的工具集
这里有个特别容易混淆的点,我必须先讲明白。linguist这个名字在不同语境下指的东西不一样。在 Qt 的发布包里,linguist通常指那一整套翻译工具,包括lupdate、lrelease、lconvert以及带图形界面的Linguist本体。而在 Qt Creator 的源码仓库里,这些工具的源码并不在 Creator 自己的src/目录下,Creator 只是通过qtbase这个子模块间接依赖它们。
所以你在 Creator 源码根目录下直接搜linguist,大概率只能搜到一些.ts翻译文件和引用配置,真正的工具源码在qtbase/src/linguist/或者qtbase/src/tools/下面。我一开始就是没搞清这层关系,在 Creator 根目录翻来翻去,浪费了不少时间。正确的认知是:你要编译的是 qtbase 里的 linguist 子模块,Creator 只是把它作为依赖拉进来的宿主工程。
这也解释了一个常见疑问:为什么有些人的 Creator 源码里根本没有qtbase目录?因为子模块没初始化。Creator 用 git submodule 管理依赖,克隆完主仓库后必须执行子模块更新,qtbase、qttools这些才会被拉下来。如果你只 clone 了主仓库就去找 linguist,那当然是找不到的。
1.2 用目录结构反推构建入口
搞清楚归属之后,下一步是定位构建入口。qtbase/src/linguist/下面通常会有几个子目录,比如linguist/(图形工具本体)、lupdate/、lrelease/、lconvert/,每个目录里都有对应的.pro文件。这些.pro就是 qmake 体系的构建描述,也是你单独编译的切入点。
我习惯的做法是先ls一遍确认结构,再决定编哪个。如果你只需要命令行工具做 CI 里的翻译提取和发布,那lupdate和lrelease就够了,图形界面完全可以不编,能省一大截编译时间和依赖。如果你需要人工校对翻译,那Linguist本体也得编。这个取舍很关键,因为图形工具会引入 widgets 相关的一大堆依赖,编译体积和时间都不是一个量级。
提示:在动手前先用
find . -name "*.pro" | grep -i linguist把相关工程文件列出来,比盲目翻目录高效得多。这一步花两分钟,后面能省半小时。
1.3 版本匹配是隐藏的前提条件
还有一个容易被忽略的前提:你用来编译的 Qt 版本,必须和 qtbase 源码的版本对得上。qtbase 源码里 linguist 模块用的 API 是跟它自身版本绑定的,如果你拿一个版本差异较大的已安装 Qt 去编,轻则警告一堆,重则直接编译失败。
我的经验是,先看 qtbase 根目录下的.qmake.conf或者版本文件,确认这套源码对应的 Qt 大版本,然后用同版本的 qmake 去驱动。跨大版本编译(比如拿 Qt5 的 qmake 去编 Qt6 的源码)基本是自找麻烦。这一点在后面讲环境准备时还会展开,因为它是很多“莫名其妙编译不过”的根源。
2. 构建体系选型:qmake 还是 CMake
Qt Creator 的源码在不同版本里构建体系是有变化的,早期以 qmake 为主,后来逐步向 CMake 迁移。linguist 模块作为 qtbase 的一部分,同样受这个趋势影响。选错构建体系,你会在一堆找不到的变量和失效的配置里挣扎。
2.1 为什么 linguist 子模块更适合走 qmake 单编
虽然现在 CMake 是大势,但如果你只是想单独把 linguist 这几个工具编出来,qmake 反而是更省心的路径。原因很实际:qtbase/src/linguist/下的.pro文件是自包含的,它声明了自己需要的模块依赖(core、widgets 等),你只要用对应版本的 qmake 进去执行,它就能自己解析出编译规则,不需要你先跑一遍顶层 CMake 配置。
而走 CMake 的话,你通常得从 qtbase 顶层开始配置,触发一大堆你根本不需要的模块检测,配置阶段就可能因为缺这个缺那个而失败。我试过一次从顶层 CMake 配置只为编 linguist,结果卡在某个平台相关的检测上,折腾半天。后来老老实实进子目录用 qmake,几分钟就出结果了。所以这里的结论很明确:单编 linguist,优先 qmake 子目录构建。
2.2 两种路径的对比与适用场景
为了让你一眼看清取舍,我把两条路径的关键差异列成表:
| 对比维度 | qmake 子目录单编 | CMake 顶层配置 |
|---|---|---|
| 配置复杂度 | 低,进目录直接 qmake | 高,需完整工具链检测 |
| 编译范围 | 仅目标工具 | 容易牵连其他模块 |
| 依赖处理 | .pro 自声明 | 依赖顶层配置结果 |
| 适合场景 | 只要几个命令行工具 | 需要整套 qtbase |
| 出错排查 | 范围小,好定位 | 范围大,噪音多 |
从表里能看出来,如果你的目标就是“拿到能用的 lupdate/lrelease/Linguist”,qmake 单编是明显更优的。CMake 路径适合那种“我本来就要完整构建 qtbase”的场景,那时候顺手把 linguist 带上就行,不必单独折腾。
2.3 影子构建:别在源码目录里直接编
不管你选哪条路,有一条铁律:不要在源码目录里直接执行构建。qmake 体系支持影子构建(shadow build),也就是在源码目录之外建一个独立的构建目录,进去执行 qmake 指向源码的.pro。这样做的好处是源码目录保持干净,编译产物、中间文件、Makefile 全在构建目录里,出问题直接删掉构建目录重来,不会污染源码。
我见过有人直接在qtbase/src/linguist/lupdate/里跑 qmake,结果生成一堆.o和 Makefile,后来想切分支或者对比源码状态时被这些文件干扰得很难受。养成影子构建的习惯,是长期做这类源码编译的基本素养。具体操作就是mkdir build-lupdate && cd build-lupdate && qmake ../qtbase/src/linguist/lupdate/lupdate.pro,路径按你的实际结构调。
3. 环境准备里那些真正会咬人的细节
环境准备看起来是最没技术含量的一步,但实际踩坑最多的恰恰是这里。编译器、qmake、依赖库、环境变量,任何一环不对,编译就会以各种奇怪的方式失败,而且报错信息往往不直接指向根因。
3.1 qmake 的来源必须和源码版本一致
前面提过版本匹配,这里展开讲怎么确认。执行qmake -v会输出 qmake 的版本和它对应的 Qt 安装路径。你要拿这个版本号去和 qtbase 源码的版本比对。源码版本一般在qtbase/.qmake.conf里能看到MODULE_VERSION之类的字段,或者看qtbase的 git tag。
如果版本对不上,最稳妥的做法是找一台装了对应版本 Qt 的环境,或者用源码自己先构建一套基础 Qt。后者成本高,一般不建议只为编 linguist 就这么干。实际项目里更常见的是:团队统一了一个 Qt 版本,源码也锁定在对应 tag,这样版本天然一致,省去很多麻烦。这也是我在团队里推的做法——源码版本和工具链版本一起锁,别让它们各自漂移。
3.2 编译器与 C++ 标准的隐性要求
linguist 模块的源码对 C++ 标准是有要求的,尤其是较新版本的 qtbase,可能要求 C++11 甚至更高。如果你的编译器太老,会在编译阶段报一堆语法相关的错误,比如auto推导失败、nullptr不认识之类。这些错误看起来吓人,但根因往往就是编译器版本不够。
我的检查清单是这样的:先g++ --version或clang++ --version确认编译器版本,再确认 qmake 生成的 Makefile 里带的-std=参数是否符合源码要求。如果 Makefile 里的标准偏低,可以在 qmake 时通过QMAKE_CXXFLAGS追加,但更推荐的做法是让 qmake 自己根据.pro里的CONFIG += c++11之类声明去处理,不要手动硬塞,否则容易和源码预期冲突。
3.3 依赖模块的可见性:core 与 widgets
lupdate 和 lrelease 主要依赖 QtCore,而 Linguist 图形工具还依赖 QtWidgets。这些依赖在.pro里通过QT += core widgets之类声明,qmake 会据此去已安装的 Qt 里找对应的库和头文件。如果你的 Qt 安装不完整(比如只装了 core 没装 widgets 开发包),编译图形工具时就会在链接阶段报找不到符号。
排查这类问题的思路很直接:看报错是编译期还是链接期。编译期找不到头文件,说明 include 路径不对,多半是 qmake 指向的 Qt 安装有问题;链接期找不到符号,说明库没链上或者库版本不匹配。分清楚这两类,定位效率会高很多。我一般会先跑一次只编 lupdate 的构建,因为它依赖最少,能跑通说明基础环境没问题,再去编图形工具,把问题范围缩小。
4. 分模块编译的实操顺序与命令
环境确认无误后,就进入真正的编译环节。我推荐的顺序是“先命令行后图形”,因为命令行工具依赖少、成功率高,能快速给你正反馈,也能提前暴露基础环境问题。
4.1 先编 lupdate:最小依赖验证
lupdate 负责从源码里提取待翻译字符串,生成.ts文件,是翻译流程的起点。它的依赖主要是 QtCore,相对干净。操作步骤:
- 建独立构建目录,比如
build-lupdate; - 进入目录,执行
qmake <源码路径>/lupdate.pro; - 执行
make -j$(nproc)并行编译; - 检查生成的二进制是否在预期位置。
这里-j的并行度建议设成 CPU 核心数,能明显缩短编译时间。但如果你机器内存偏小,并行度太高可能触发 OOM,那就适当降到核心数的一半。这个取舍没有绝对标准,看机器实际情况。
编译成功后,先别急着庆祝,跑一下./lupdate -version确认它能正常启动并输出版本。能启动说明动态库链接没问题,这一步验证很有价值,因为有些环境编译能过但运行时找不到库,早发现早处理。
4.2 再编 lrelease:验证资源处理链路
lrelease 负责把.ts编译成运行时可用的.qm二进制翻译文件。它同样以 QtCore 为主,但会涉及一些 XML 解析相关的处理。构建方式和 lupdate 一致,换个.pro路径即可。
我把它放在第二步,是因为它和 lupdate 构成完整的“提取—发布”链路,两个都编通了,说明你的翻译工具链核心已经可用。这时候可以拿一个小型示例项目做端到端验证:用 lupdate 生成.ts,手工填几条翻译,再用 lrelease 生成.qm,最后在程序里加载验证。这个闭环跑通,比任何单元测试都让人安心。
4.3 最后编 Linguist 本体:处理 widgets 依赖
图形工具是重头戏,依赖 QtWidgets,编译时间和体积都上一个台阶。构建命令还是那套,但要有心理准备:如果前面环境有问题,这里会集中爆发。常见的失败点包括 widgets 开发包缺失、图形相关的平台插件依赖没配好等。
我的建议是,如果团队只需要自动化流程,图形工具可以暂时不编,等确有校对需求再补。这样能把首次跑通的周期压到最短。真要编的时候,确保你的 Qt 安装包含 widgets 模块的完整开发文件,别只装了运行时。
4.4 编译产物的组织与安装
编译出来的二进制默认在构建目录里,散落在各子目录。如果你想让它们像正式安装一样集中到某个bin目录,可以在 qmake 时指定INSTALL_ROOT或者直接用make install配合前缀。我一般会设一个自定义前缀,把 lupdate、lrelease、linguist 三个二进制集中放一起,方便后续打包进团队的工具镜像。
注意:
make install的行为受.pro里target.path等变量控制,不同版本可能把东西装到不同位置。装完一定要find一下确认实际落点,别想当然以为在bin下。
5. 编译过程中最常撞上的几类报错
这一节我把实际编译里反复出现的报错归了类,每一类都给出根因判断和处置思路。这些不是从文档里抄的,是踩出来的。
5.1 “找不到头文件”类报错
典型表现是编译某个源文件时提示QSomething: No such file or directory。根因几乎总是 qmake 指向的 Qt 安装不完整,或者 include 路径没包含对应模块。处置顺序:先确认qmake -query QT_INSTALL_HEADERS指向的目录里确实有那个头文件;再确认.pro里是否声明了对应模块;最后确认是不是版本差异导致该头文件在旧版本里不存在。
我遇到过一次,报错说找不到某个 widgets 相关头文件,查下来是那台机器只装了 QtCore 的开发包。补装 widgets 开发包后一次通过。所以这类报错别急着改代码,先查环境。
5.2 链接期“未定义符号”类报错
编译都过了,链接时提示某个符号 undefined。这通常意味着库没链上,或者链的库版本和头文件版本不一致。前者检查.pro的LIBS和QT +=声明,后者检查是不是混用了两套 Qt 的头文件和库。
混用是很隐蔽的坑:比如头文件来自 A 版本,库来自 B 版本,编译期因为头文件里有声明所以能过,链接期找不到对应实现就炸了。排查方法是看编译命令里的-I和-L路径是否指向同一套 Qt。不一致就统一掉。
5.3 平台相关的构建脚本失败
有些平台下.pro会引入平台专属的配置,比如 Windows 下的资源文件、macOS 下的 bundle 配置。这些配置依赖平台工具链,缺了就会在构建脚本阶段失败。这类问题的特点是报错信息里带明显的平台路径或工具名,按提示补对应工具即可。
跨平台团队尤其要注意:同一份源码在不同系统上编,失败点可能完全不同。我的做法是把每个平台的构建命令和已知坑点记在一个共享文档里,新人上手直接照做,避免重复踩。
6. 编完之后:验证、打包与落地使用
编译成功只是拿到二进制,真正让它产生价值的是验证和集成。这一节讲怎么确认这套工具真的能用,以及怎么把它塞进团队的日常流程。
6.1 用最小示例做端到端验证
验证不要贪大,找一个小型 Qt 项目,源码里放几个带tr()的字符串,然后:
- 用编出来的 lupdate 扫描源码,生成
.ts; - 打开
.ts确认字符串被正确提取; - 手工填一两条翻译;
- 用 lrelease 生成
.qm; - 在程序里用
QTranslator加载.qm,切换语言看界面是否变化。
这个闭环里任何一步失败,都能快速定位到是哪个工具的问题。我特别看重第 5 步,因为它是唯一能证明“整条链路真的通”的环节,前面几步都只是中间产物正确。
6.2 版本锁定与团队分发
自己编的工具最大的价值是可控。建议把编译产物和对应的源码版本号绑定,打成一个内部工具包,包里附上版本说明和构建命令。团队里谁需要就用这个包,不要各自去编,否则版本一乱,翻译文件格式对不上,排查起来非常痛苦。
分发时注意二进制依赖的动态库。如果目标机器没装对应 Qt 运行时,工具跑不起来。要么把依赖库一起打包,要么确保目标环境有匹配的 Qt。这一点在 CI 环境里尤其重要,因为 CI 机器往往是干净环境。
6.3 在自动化流程里接入 lupdate 与 lrelease
真正体现价值的是把这两个命令行工具接进构建流水线。典型做法是:代码合并到主分支后,自动跑 lupdate 更新.ts,检测是否有新增未翻译字符串;发布构建前跑 lrelease 生成.qm并打进资源。这样翻译状态始终和代码同步,不会出现“代码改了但翻译文件还是旧的”这种经典问题。
接入时有个细节要注意:lupdate 对源码目录的扫描范围要配好,别把第三方代码或生成代码也扫进去,否则.ts里会混入大量噪音字符串。用-locations和排除规则控制好范围,是长期维护的关键。
7. 几个值得提前知道的经验判断
最后分享几条我在多次编译和使用里沉淀下来的判断,都是那种“早知道能少走弯路”的类型。
第一,别为了编 linguist 去完整构建 qtbase。除非你本来就需要整套 Qt,否则单编子模块是性价比最高的路径,时间成本差一个数量级。
第二,命令行工具优先于图形工具。绝大多数自动化场景只需要 lupdate 和 lrelease,图形工具按需再补,能显著降低首次跑通的门槛。
第三,版本一致性比什么都重要。qmake 版本、源码版本、依赖库版本三者对齐,能消灭掉八成以上的诡异报错。团队里把这条作为硬规矩,比事后排查划算得多。
第四,影子构建和产物集中管理要养成习惯。源码目录保持干净,产物集中到固定位置,后续升级、回滚、分发都会轻松很多。这些看起来是小事,但在长期维护里省下的时间非常可观。
这套流程我在不同环境里跑过多次,从最初的到处碰壁到后来能稳定产出,核心就是把“归属搞清楚、版本对齐、依赖最小化、验证闭环”这四件事做扎实。剩下的就是按部就班执行,遇到报错按编译期和链接期分类定位,基本不会卡太久。