磨了大半天,前四篇把环境、源码获取这些前置事项都梳理完了,这篇终于要进入真正烧 CPU 的环节——编译。很多人在这一步翻车,不是没把前面的准备做完,就是卡在一些看起来很诡异、实际上原因很朴素的错误里。我这篇就把自己反复踩出来的流程完整写清楚,从编译前检查、GN 参数怎么写,到真正跑 ninja、处理常见编译错误、最后验证产物,一条龙讲完。不绕弯子,直接开写。1. 编译前的最后“体检”:环境变量与磁盘规划1.1 先花两分钟确认之前的准备没白做写编译流程前,默认你已经把前四篇的内容都走完了。但我在不同机器上重装过好几次环境,发现一个规律:只要中间隔了几天再回来继续,就有人漏配置。与其等到 ninja 报错再回去查,不如开编之前顺手验证几个基础变量。在继续之前打开终端,跑下面几行确认 depot_tools 在 PATH 里、gn/ninja 这些工具能直接找到:echo $PATH | grep depot_tools which gn which ninja which autoninja正常情况下应该能看到 depot_tools 路径,并且 gn、ninja、autoninja 都有返回结果。如果哪条是空的,说明上一轮配好的 PATH 在新开的终端里没有生效,这时候优先检查 shell 配置文件,比如.zshrc里的 export 语句。顺手检查一下 Xcode 命令行工具状态:xcode-select -p返回的路径类似/Applications/Xcode.app/Contents/Developer。如果提示找不到,说明 Xcode 的 command line tools 没装好。这一步漏了会导致编译时各种 SDK 头文件缺失,后面怎么折腾都没用。1.2 磁盘空间是最大的隐藏炸弹Chromium 这项目的体量说出来挺吓人,源码目录加构建产物会吃掉非常大的空间。我自己的经验是,源码目录本身大约二三十个 GB,第一次全量构建的中间产物和最终 out 目录加起来又是几十 GB。不同平台、不同版本差异不小,但至少给自己留出 100 GB 以上的余量比较稳妥。快速查看磁盘余量:df -h /Users/你的用户名另外确认一下编译用的临时目录也不紧张。macOS 的 TMPDIR 默认指向/var/folders/...,理论上系统会自动清理,但如果是旧机器剩余空间本就紧张,链接阶段很容易因为临时文件写不进去而报错。可以直接用下面的命令看到当前 shell 的临时目录路径:echo $TMPDIR磁盘剩余空间真的是我踩过最多次的坑。曾经有一次编译到 90% 的时候突然报No space left on device,当时整个人是崩溃的。查了半天发现是 out 目录和系统缓存盘在同一块分区,空间耗尽后 ninja 直接中断,而且这种中断不是重跑一遍就能恢复的,还得先清理空间。所以我在开始构建前都会养成习惯:先清理旧的 out 目录或者把不用的模拟器缓存、旧版本 App 缓存清一遍。1.3 让 ccache 帮你把重复编译的时间省下来如果你计划不止编一次,比如后面还要调整参数重新编,强烈建议在这种版本把 ccache 配上。macOS 下 Chromium 官方脚本对 ccache 的支持不算新特性了,depot_tools 里甚至有配套的包装脚本。不过在 Chromium 的构建系统里启用 ccache,并不需要改动构建脚本本身,只需要通过 GN 参数把它挂进去。我习惯在环境变量里先指定缓存目录:export CCACHE_DIR="$HOME/.ccache" export CCACHE_SLOPPINESS="time_macros"然后进入源码目录,执行:gn gen out/Default --args='is_debug=false cc_wrapper="ccache"'里面cc_wrapper="ccache"就是让 gn 生成的构建命令经过 ccache 包装一层。这样同一份源码、同一套参数重新编译时,很多编译单元不会真的重新编译,而是直接命中缓存。第一次开 vivavis 构建可能感觉不到差别,但在你改一个配置文件就要重新链接的场景里,差价非常明显。还会有人把ccache和sccache做对比,后者适合分布式场景。单机编译用 ccache 就够,没什么必要引入更多东西。2. GN 参数到底该怎么选:从 Debug 到 Release 的取舍2.1 GN 是什么,为什么不能直接跑 ninja很多第一次编 Chromium 的人会困惑:为什么不能像普通开源项目那样./configure && make?Chromium 的构建系统是两层结构:GN 负责生成构建描述文件,ninja 才是真正干活的构建器。GN 会读取args.gn里的参数,结合源码目录里的 BUILD.gn 文件生成一套build.ninja文件。后面每次改动参数,都得重新跑一次gn gen,ninja 才会用新参数干活。可以这样理解:GN 是决策层,决定编哪些目标、用什么编译选项;ninja 是执行层,只负责按照生成好的规则把一个个编译任务调度并执行。因此,编译之前最重要的事就是把args.gn里的参数想清楚。2.2 一套稳妥的基线参数下面这组参数是我在 macOS 上编 Chromium 144 时实际在用的,适合希望得到一个可日常跑、体积适中、编译时间不夸张的产物的场景:# out/Default/args.gn is_debug = false is_official_build = true symbol_level = 0 blink_symbol_level = 0 enable_nacl = false target_cpu = "arm64"逐条解释一下,方便你根据自己的需求调整:is_debug = false:关闭调试信息,开启优化,产物体积和运行速度都更接近正式版。is_official_build = true:这个开关会启用更贴近官方发布版的优化等级,比如更高的优化级别以及部分功能裁剪。但它也会让编译时间明显变长,CPU 占用拉满的情况更常见。如果你只是想要一个快速验证用的浏览器,可以把这个参数改成false,能得到更快的构建速度,但产物性能和体积会有差距。symbol_level = 0:这个最关键。Chromium 链接时最怕的就是符号表太大,尤其 macOS 链接器处理巨大符号表的时候,内存占用和耗时都会暴涨。不调试的话,直接设成 0。blink_symbol_level = 0:和上一条类似,针对渲染引擎部分再补一刀。enable_nacl = false:Native Client 很早就没有多少应用场景了,关掉能省一截不必要的编译任务。target_cpu = "arm64":在 Apple Silicon 上必须明确指定。如果是在 Intel 的 Mac 上编译,改成"x64"。有一个地方要注意,你要是之前用is_debug = true编过,后面切到 release 参数,建议把out目录整个删掉重新gn gen。原因很简单:GN 生成 ninja 文件时虽然会清掉很多旧目标,但某些生成的中间产物(尤其是一些带模式后缀的文件)可能会残留,导致链接时出现 ODR 或符号重复这类让人摸不着头脑的错误。与其排查半天,不如重新来一次全量构建,至少干净省心。2.3 自定义 component 模式:给开发调试留一个后门如果你不是想要完整的浏览器,而是打算基于 Chromium 做二次开发,比如改渲染层代码、研究浏览器内部实现,那is_component_build = true会更有用。组件构建模式会把庞大的静态库拆成多个动态库,编译单元更小、链接更快,而且增量重编时一次改动只需要重编对应模块。代价是产物变成一堆 dylib,日常使用没有单个 App 方便,启动也可能比静态链接慢。这个模式适合“频繁改代码”的阶段,不适合“最终验证整个浏览器”的场景。所以我的建议是:如果你只是想要一个能打开的 Chromium 浏览器产物,用上面那组参数;你要是想改源码、做研究,再考虑切到组件模式。不要一开始就 two 者兼顾,两头都占最浪费编译时间。2.4 改参数的正确姿势改args.gn有现实中的“正确姿势”:gn args out/Default这个命令会打开一个文本编辑器让你直接改参数,保存退出后自动重新生成 ninja 文件。比起手动写文件再去gn gen,它能帮你做参数校验,写错的布尔值、非法组合都会在保存时报警。如果只改了一个参数,但没重新gn gen,就直接跑autoninja,经常会出现“改了等于没改”的困惑。GN 的生成结果并不会监控args.gn的修改时间,ninja 只认生成后的 ninja 文件内容。所以记住这个顺序:改参数 ->gn gen->autoninja。3. 正式开编:autoninja 的正确打开方式3.1 生成构建文件后再动手先确保构建文件是最新的,再开始编译。以 out/Default 为例,最直接的做法是:gn gen out/Default如果你在args.gn里已经写好参数,这步会自动读取并生成构建脚本。如果之前没生成过,它会创建目录并写入完整的build.ninja。生成完构建文件后,不要直接从 ninja 开始,用 depot_tools 提供的autoninja替代裸ninja。它的作用是自动根据 CPU 核心数和内存压力调整并发任务数。裸ninja -j 32在 8 核 Mac 上可能会直接吃到内存上限,而 autoninja 会相对保守地控制并行度。虽然有时候显得“不够激进”,但胜在不容易把机器直接跑死。3.2 编译目标怎么选进入源码目录后,正式构建命令很简单:autoninja -C out/Default chrome-C指定构建目录,chrome是目标名。这里有必要解释一下“目标”的概念:Chromium 源码里有很多可执行文件和库,像content_shell、chrome、blink_tests这些都是独立目标。只编译chrome意味着只构建浏览器主体及其依赖,不包括测试相关的一堆东西。如果你不确定某个目标叫什么名字,可以在源码目录下搜索:gn ls out/Default | grep 关键字比如想看有哪些测试目标,可以gn ls out/Default | grep test | head。为什么不建议直接编all?因为 Chromium 的all目标覆盖面极广,会带上一大堆测试程序、性能工具、Demo 示例,编译时间和磁盘占用都是chrome目标的数倍。很多人第一次编译失败,不是参数问题,而是目标选太大了。第一次全量编译时,终端会长时间滚动输出编译日志。看到一堆.cc文件在编译说明一切正常。中间如果出现某个子命令失败,不要慌,先记住失败信息,后面单独处理。编译过程中可以偶尔按一下Ctrl+T看下负载情况,如果 load average 一直是几十,说明编译器还在全力输出。3.3 中断和恢复:增量编译并没有想象中脆弱这是很多人第一次遇到时的心理坎:编到一半觉得太久,直接 Ctrl+C 停了,然后担心“是不是要从头再来”。实际上完全不需要。Chromium 的 ninja 构建系统天然支持增量编译。中断后再跑同样的命令:autoninja -C out/Default chrome它会跳过已经完成且没有变化的部分,直接从上次中断的位置继续。增量能力来自每个目标文件的依赖记录,ninja 会精确到每个文件、每条编译命令来判断是否需要重跑。不过有一种情况接近“必须从头再来”,就是刚才提到的改参数后没清理旧构建目录。如果is_debug、is_component_build这类会结构性改变对象文件格式的参数被改动,旧对象文件和新参数不兼容,ninja 可能不会自动清理全部过期文件。稳妥做法是删掉 out 目录里的配置缓存,或者干脆整个 out 目录重建。我第一次意识到增量编译的重要,是在调一个渲染相关参数时。那会儿已经完成过全量编译,后来每次只改一两处源码,重新跑 autoninja 只需几分钟到十几分钟。这种体验比“每次都要等一两个小时”舒服太多,也更愿意频繁尝试不同参数组合。3.4 第一次全量编译大概要多久这个问题没有标准答案,因为差别非常大。芯片代际、内存大小、是否开is_official_build、目标是否包含调试符号,都会让编译时间从天到小时之间浮动。能提供的一个粗略参考是:在 Apple Silicon 中端机型上,用is_debug=false + is_official_build=true + symbol_level=0编 chrome 目标,大约两到四小时。如果是is_official_build=false,时间可能降到一小时多。Intel 老款 Mac 上,五六小时甚至过夜都很正常。编译时长有波动,但有一个经验可以分享:不要开着视频会议、渲染软件去编译,尤其是链接阶段,内存和磁盘 IO 会顶到峰值,轻则变慢,重则直接把编译进程杀死。固态硬盘的剩余空间和写入温度也会影响整体速度,尽量保持磁盘凉爽通风。4. 编译途中最容易踩的坑:错误信息背后的真实原因4.1 “build stopped: subcommand failed” 与内存不足遇到ninja: build stopped: subcommand failed.这句话时,真正的错误信息其实在这行上面。往回翻终端日志,找到第一个报红或者带FAILED字样的行,那才是根因。下面是一种常见情况:FAILED: obj/xxx/base/xxx.o clang++: error: unable to execute command: Killed clang++: error: clang frontend command failed due to signal (use -v to see invocation)Killed这个单词出现,绝大多数时候是内存不足导致的编译进程被系统强杀。Chromium 里有些大编译单元非常吃内存,比如某些 V8 和渲染相关的大文件,单线程编译一个文件就能吃到几个 GB。并行度太高内存耗尽,编译器进程就会被内核直接干掉。排查方法很简单:grep -c "clang" out/Default/build.ninja但这只能说明编译任务总数,查并发度更直接。你可以在环境变量里限制并行度,比如:NINJA_JOBS=8 autoninja -C out/Default chrome或者直接用裸 ninja:ninja -C out/Default -j 4 chrome把并发数调到适当值,通常能绕开内存瓶颈。如果还是被 Killed,考虑临时关闭一些重度依赖符号的模块,或者给编译过程的临时目录换到更大的磁盘空间。4.2 Xcode 版本与系统 SDK 不匹配macOS 上编译 Chromium 离不开系统 SDK,但 Chromium 构建并不会直接用系统的默认clang,它倾向使用随源码一起拉取的工具链版本。这样带来一个典型问题:工具链版本要求的最低 macOS SDK 版本和当前系统不匹配,编译时会出现类似:xcrun: error: SDK "macosx" cannot be located或者各种'X' file not found,指向的路径在 SDK 目录下找不到。这类问题的核心是:构建脚本找不到一个合适的 Xcode SDK 路径。最容易生效的动作是先检查 Xcode 命令行工具和完整 Xcode 是否都装了:sudo xcode-select --reset xcodebuild -version xcodebuild -showsdks如果-showsdks里看不到任何 macosx SDK,说明 Xcode 安装本身出了问题,重新安装 Xcode 或者命令行工具解。另一个常见问题是同时装了多个 Xcode 版本,系统默认的 command line tools 指向了一个旧版本。用:sudo xcode-select -s /Applications/Xcode.app/Contents/Developer切换到最新路径后,再跑xcodebuild -showsdks确认。这步做完再重新gn gen一次,因为 SDK 路径会被记录在构建参数里,光改系统默认还不够,必须让 GN 重新探测。4.3 链接阶段的 ld 失败:符号缺失还是参数冲突链接阶段的报错往往比编译阶段更吓人,因为日志长、提示含糊。我遇到过最典型的一种:ld: warning: object file ... was built for newer macOS version (14.0) than being linked (13.0) ld: error: undefined symbol: _dlopen后面那个undefined symbol是最核心的线索。它通常意味着某个库没有正确链接进来,或某个编译目标缺了依赖项。遇到它先别去改源码,优先怀疑构建配置问题。如果之前编过其它版本、换过target_cpu,那么可能是某个 submodule 里的旧对象文件残留,解决办法是清掉相关 target 的临时输出重新编译。Linux 用户习惯用make clean,Chromium 里对应的就是删掉out/Default/obj/xxx对应目录,但不建议手动删单个目录,容易删不干净。直接全量重编一次是更可靠的方案。还有一条链接期经验:macOS 上链接 Chromium 最大的性能瓶颈在符号表。之前提过symbol_level=0就是为了避免这里爆内存。要是真的需要调试符号,也不要symbol_level=2直接注入到所有模块,可以只给指定的测试目标开局部符号,实现用 GDB 或 LLDB 逐步查看一遍。把symbol_level设在 0 和 2 之间反复横跳,每次链接都像坐过山车一样漫长。4.4 “文件太大无法扫描”的误导性报错还有一种不常见但碰到就很头疼的情况,报错类似:error: unable to open output file ... 'File too big'明明磁盘还有几十 GB,为什么会说 file too big?这其实是文件系统格式的限制。某些外置盘或网络盘使用 FAT32 或旧式文件系统,单个文件大小上限是 4GB。Chromium 的链接阶段会产生超大文件,如果 out 目录建到了这种分区上,铁定报错。解决办法是把整个源码和 out 目录挪到本机 APFS 分区,不要放在外置 FAT32 盘上。这也提醒了开头说的磁盘规划:不只看剩余空间,还要看文件系统类型。5. 编译产物的解析与验证:到底得到了什么5.1 产出目录里都有什么编译成功跑完,终端会回到提示符,最后一段日志一般是ninja: no work to do或显示完成时间。此时输出目录下会生成非常多文件,以 out/Default 为例,最重要的几个包括:Chromium.app chrome_crashpad_handler icudtl.dat resources.pak chrome_100_percent.pak chrome_200_percent.pakChromium.app是最终的浏览器应用,里面嵌套着完整可执行文件。icudtl.dat是 ICU 数据文件,负责国际化字符处理,不能被误删。各种.pak是资源文件,存放界面字符串和图片资源。想快速确认产物是否可用,可以直接打开它:open out/Default/Chromium.app --args --user-data-dir=/tmp/chromium-profile --no-first-run首次启动可能比预想慢一点,因为各种缓存还没有生成。看到默认起始页正常渲染,基本就能说明这个产物是能用的。5.2 用内置测试做一次冒烟验证如果你不只是想要“能打开”的浏览器,还想验证构建质量,可以用 Chromium 自带的测试集跑一次冒烟测试。先从最基础的开始:autoninja -C out/Default blink_unittests然后运行:out/Default/blink_unittests --gtest_filter=WebView.*这里的WebView.*只是示例,实际你可以根据自己关注的模块来勾选。跑全量测试会非常久,但挑一组跟自己改动相关的测试跑完,至少能确认编译过程没有引入基础性错误。注意,这些测试程序本身并没有包含在chrome目标里,所以你得先单独编译它们:autoninja -C out/Default blink_unittests这个过程会额外花一点时间,但比起重新全量编译,仍然划算得多。5.3 签名与运行权限:本地用 ad-hoc 就够了如果你只是自己在本地跑,不做分发,最省事的做法是直接用 ad-hoc 签名。每次编译完成后,产物默认可能没有有效签名,macOS 上直接打开会提示“已损坏”或被 Gatekeeper 拦截。处理方式很简单:codesign --force --deep -s - out/Default/Chromium.app-s -表示使用 ad-hoc 签名,不给应用分配任何身份,纯粹为了让系统认为它是“被签名”的。这个操作在每次重新编译后可能需要重复,因为链接生成的新二进制文件会覆盖旧的签名状态。从这一步也能看出 Chromium 编译流程里的一个特点:拿到产物之后,离“日常使用”还差一个打包和签名的环节。如果后面想把这个自定义版本长期留在 Mac 上,还可以考虑把它放进/Applications,同时注意不要用--user-data-dir的时候污染默认配置目录。我个人这几年折腾 Chromium 编译最大的体会是:真正花时间的往往不是那一两个小时满负载编译本身,而是编译前后反复确认环境、分析错误信息、决定参数取舍的过程。尤其是第一次全量编译,心态特别重要——不要期待一次成功,更不要每次失败就推倒重来。学会读懂 FAILED 那一行,学会利用增量编译,学会在几个关键参数之间找到平衡点,这个流程其实非常成熟、非常稳。如果你后面打算把 Chromium 换成自己的代号、加自己的功能,那编译流程到这里其实只是起点。还有一层更深的玩法是修改 GN 构建脚本、新增自己的源文件、定制产品名称和图标,这些我在后续合适的时机再单独写一篇。
磨了大半天,前四篇把环境、源码获取这些前置事项都梳理完了,这篇终于要进入真正烧 CPU 的环节——编译。很多人在这一步翻车,不是没把前面的准备做完,就是卡在一些看起来很诡异、实际上原因很朴素的错误里。我这篇就把自己反复踩出来的流程完整写清楚,从编译前检查、GN 参数怎么写,到真正跑 ninja、处理常见编译错误、最后验证产物,一条龙讲完。不绕弯子,直接开写。1. 编译前的最后“体检”:环境变量与磁盘规划1.1 先花两分钟确认之前的准备没白做写编译流程前,默认你已经把前四篇的内容都走完了。但我在不同机器上重装过好几次环境,发现一个规律:只要中间隔了几天再回来继续,就有人漏配置。与其等到 ninja 报错再回去查,不如开编之前顺手验证几个基础变量。在继续之前打开终端,跑下面几行确认 depot_tools 在 PATH 里、gn/ninja 这些工具能直接找到:echo $PATH | grep depot_tools which gn which ninja which autoninja正常情况下应该能看到 depot_tools 路径,并且 gn、ninja、autoninja 都有返回结果。如果哪条是空的,说明上一轮配好的 PATH 在新开的终端里没有生效,这时候优先检查 shell 配置文件,比如.zshrc里的 export 语句。顺手检查一下 Xcode 命令行工具状态:xcode-select -p返回的路径类似/Applications/Xcode.app/Contents/Developer。如果提示找不到,说明 Xcode 的 command line tools 没装好。这一步漏了会导致编译时各种 SDK 头文件缺失,后面怎么折腾都没用。1.2 磁盘空间是最大的隐藏炸弹Chromium 这项目的体量说出来挺吓人,源码目录加构建产物会吃掉非常大的空间。我自己的经验是,源码目录本身大约二三十个 GB,第一次全量构建的中间产物和最终 out 目录加起来又是几十 GB。不同平台、不同版本差异不小,但至少给自己留出 100 GB 以上的余量比较稳妥。快速查看磁盘余量:df -h /Users/你的用户名另外确认一下编译用的临时目录也不紧张。macOS 的 TMPDIR 默认指向/var/folders/...,理论上系统会自动清理,但如果是旧机器剩余空间本就紧张,链接阶段很容易因为临时文件写不进去而报错。可以直接用下面的命令看到当前 shell 的临时目录路径:echo $TMPDIR磁盘剩余空间真的是我踩过最多次的坑。曾经有一次编译到 90% 的时候突然报No space left on device,当时整个人是崩溃的。查了半天发现是 out 目录和系统缓存盘在同一块分区,空间耗尽后 ninja 直接中断,而且这种中断不是重跑一遍就能恢复的,还得先清理空间。所以我在开始构建前都会养成习惯:先清理旧的 out 目录或者把不用的模拟器缓存、旧版本 App 缓存清一遍。1.3 让 ccache 帮你把重复编译的时间省下来如果你计划不止编一次,比如后面还要调整参数重新编,强烈建议在这种版本把 ccache 配上。macOS 下 Chromium 官方脚本对 ccache 的支持不算新特性了,depot_tools 里甚至有配套的包装脚本。不过在 Chromium 的构建系统里启用 ccache,并不需要改动构建脚本本身,只需要通过 GN 参数把它挂进去。我习惯在环境变量里先指定缓存目录:export CCACHE_DIR="$HOME/.ccache" export CCACHE_SLOPPINESS="time_macros"然后进入源码目录,执行:gn gen out/Default --args='is_debug=false cc_wrapper="ccache"'里面cc_wrapper="ccache"就是让 gn 生成的构建命令经过 ccache 包装一层。这样同一份源码、同一套参数重新编译时,很多编译单元不会真的重新编译,而是直接命中缓存。第一次开 vivavis 构建可能感觉不到差别,但在你改一个配置文件就要重新链接的场景里,差价非常明显。还会有人把ccache和sccache做对比,后者适合分布式场景。单机编译用 ccache 就够,没什么必要引入更多东西。2. GN 参数到底该怎么选:从 Debug 到 Release 的取舍2.1 GN 是什么,为什么不能直接跑 ninja很多第一次编 Chromium 的人会困惑:为什么不能像普通开源项目那样./configure && make?Chromium 的构建系统是两层结构:GN 负责生成构建描述文件,ninja 才是真正干活的构建器。GN 会读取args.gn里的参数,结合源码目录里的 BUILD.gn 文件生成一套build.ninja文件。后面每次改动参数,都得重新跑一次gn gen,ninja 才会用新参数干活。可以这样理解:GN 是决策层,决定编哪些目标、用什么编译选项;ninja 是执行层,只负责按照生成好的规则把一个个编译任务调度并执行。因此,编译之前最重要的事就是把args.gn里的参数想清楚。2.2 一套稳妥的基线参数下面这组参数是我在 macOS 上编 Chromium 144 时实际在用的,适合希望得到一个可日常跑、体积适中、编译时间不夸张的产物的场景:# out/Default/args.gn is_debug = false is_official_build = true symbol_level = 0 blink_symbol_level = 0 enable_nacl = false target_cpu = "arm64"逐条解释一下,方便你根据自己的需求调整:is_debug = false:关闭调试信息,开启优化,产物体积和运行速度都更接近正式版。is_official_build = true:这个开关会启用更贴近官方发布版的优化等级,比如更高的优化级别以及部分功能裁剪。但它也会让编译时间明显变长,CPU 占用拉满的情况更常见。如果你只是想要一个快速验证用的浏览器,可以把这个参数改成false,能得到更快的构建速度,但产物性能和体积会有差距。symbol_level = 0:这个最关键。Chromium 链接时最怕的就是符号表太大,尤其 macOS 链接器处理巨大符号表的时候,内存占用和耗时都会暴涨。不调试的话,直接设成 0。blink_symbol_level = 0:和上一条类似,针对渲染引擎部分再补一刀。enable_nacl = false:Native Client 很早就没有多少应用场景了,关掉能省一截不必要的编译任务。target_cpu = "arm64":在 Apple Silicon 上必须明确指定。如果是在 Intel 的 Mac 上编译,改成"x64"。有一个地方要注意,你要是之前用is_debug = true编过,后面切到 release 参数,建议把out目录整个删掉重新gn gen。原因很简单:GN 生成 ninja 文件时虽然会清掉很多旧目标,但某些生成的中间产物(尤其是一些带模式后缀的文件)可能会残留,导致链接时出现 ODR 或符号重复这类让人摸不着头脑的错误。与其排查半天,不如重新来一次全量构建,至少干净省心。2.3 自定义 component 模式:给开发调试留一个后门如果你不是想要完整的浏览器,而是打算基于 Chromium 做二次开发,比如改渲染层代码、研究浏览器内部实现,那is_component_build = true会更有用。组件构建模式会把庞大的静态库拆成多个动态库,编译单元更小、链接更快,而且增量重编时一次改动只需要重编对应模块。代价是产物变成一堆 dylib,日常使用没有单个 App 方便,启动也可能比静态链接慢。这个模式适合“频繁改代码”的阶段,不适合“最终验证整个浏览器”的场景。所以我的建议是:如果你只是想要一个能打开的 Chromium 浏览器产物,用上面那组参数;你要是想改源码、做研究,再考虑切到组件模式。不要一开始就 two 者兼顾,两头都占最浪费编译时间。2.4 改参数的正确姿势改args.gn有现实中的“正确姿势”:gn args out/Default这个命令会打开一个文本编辑器让你直接改参数,保存退出后自动重新生成 ninja 文件。比起手动写文件再去gn gen,它能帮你做参数校验,写错的布尔值、非法组合都会在保存时报警。如果只改了一个参数,但没重新gn gen,就直接跑autoninja,经常会出现“改了等于没改”的困惑。GN 的生成结果并不会监控args.gn的修改时间,ninja 只认生成后的 ninja 文件内容。所以记住这个顺序:改参数 ->gn gen->autoninja。3. 正式开编:autoninja 的正确打开方式3.1 生成构建文件后再动手先确保构建文件是最新的,再开始编译。以 out/Default 为例,最直接的做法是:gn gen out/Default如果你在args.gn里已经写好参数,这步会自动读取并生成构建脚本。如果之前没生成过,它会创建目录并写入完整的build.ninja。生成完构建文件后,不要直接从 ninja 开始,用 depot_tools 提供的autoninja替代裸ninja。它的作用是自动根据 CPU 核心数和内存压力调整并发任务数。裸ninja -j 32在 8 核 Mac 上可能会直接吃到内存上限,而 autoninja 会相对保守地控制并行度。虽然有时候显得“不够激进”,但胜在不容易把机器直接跑死。3.2 编译目标怎么选进入源码目录后,正式构建命令很简单:autoninja -C out/Default chrome-C指定构建目录,chrome是目标名。这里有必要解释一下“目标”的概念:Chromium 源码里有很多可执行文件和库,像content_shell、chrome、blink_tests这些都是独立目标。只编译chrome意味着只构建浏览器主体及其依赖,不包括测试相关的一堆东西。如果你不确定某个目标叫什么名字,可以在源码目录下搜索:gn ls out/Default | grep 关键字比如想看有哪些测试目标,可以gn ls out/Default | grep test | head。为什么不建议直接编all?因为 Chromium 的all目标覆盖面极广,会带上一大堆测试程序、性能工具、Demo 示例,编译时间和磁盘占用都是chrome目标的数倍。很多人第一次编译失败,不是参数问题,而是目标选太大了。第一次全量编译时,终端会长时间滚动输出编译日志。看到一堆.cc文件在编译说明一切正常。中间如果出现某个子命令失败,不要慌,先记住失败信息,后面单独处理。编译过程中可以偶尔按一下Ctrl+T看下负载情况,如果 load average 一直是几十,说明编译器还在全力输出。3.3 中断和恢复:增量编译并没有想象中脆弱这是很多人第一次遇到时的心理坎:编到一半觉得太久,直接 Ctrl+C 停了,然后担心“是不是要从头再来”。实际上完全不需要。Chromium 的 ninja 构建系统天然支持增量编译。中断后再跑同样的命令:autoninja -C out/Default chrome它会跳过已经完成且没有变化的部分,直接从上次中断的位置继续。增量能力来自每个目标文件的依赖记录,ninja 会精确到每个文件、每条编译命令来判断是否需要重跑。不过有一种情况接近“必须从头再来”,就是刚才提到的改参数后没清理旧构建目录。如果is_debug、is_component_build这类会结构性改变对象文件格式的参数被改动,旧对象文件和新参数不兼容,ninja 可能不会自动清理全部过期文件。稳妥做法是删掉 out 目录里的配置缓存,或者干脆整个 out 目录重建。我第一次意识到增量编译的重要,是在调一个渲染相关参数时。那会儿已经完成过全量编译,后来每次只改一两处源码,重新跑 autoninja 只需几分钟到十几分钟。这种体验比“每次都要等一两个小时”舒服太多,也更愿意频繁尝试不同参数组合。3.4 第一次全量编译大概要多久这个问题没有标准答案,因为差别非常大。芯片代际、内存大小、是否开is_official_build、目标是否包含调试符号,都会让编译时间从天到小时之间浮动。能提供的一个粗略参考是:在 Apple Silicon 中端机型上,用is_debug=false + is_official_build=true + symbol_level=0编 chrome 目标,大约两到四小时。如果是is_official_build=false,时间可能降到一小时多。Intel 老款 Mac 上,五六小时甚至过夜都很正常。编译时长有波动,但有一个经验可以分享:不要开着视频会议、渲染软件去编译,尤其是链接阶段,内存和磁盘 IO 会顶到峰值,轻则变慢,重则直接把编译进程杀死。固态硬盘的剩余空间和写入温度也会影响整体速度,尽量保持磁盘凉爽通风。4. 编译途中最容易踩的坑:错误信息背后的真实原因4.1 “build stopped: subcommand failed” 与内存不足遇到ninja: build stopped: subcommand failed.这句话时,真正的错误信息其实在这行上面。往回翻终端日志,找到第一个报红或者带FAILED字样的行,那才是根因。下面是一种常见情况:FAILED: obj/xxx/base/xxx.o clang++: error: unable to execute command: Killed clang++: error: clang frontend command failed due to signal (use -v to see invocation)Killed这个单词出现,绝大多数时候是内存不足导致的编译进程被系统强杀。Chromium 里有些大编译单元非常吃内存,比如某些 V8 和渲染相关的大文件,单线程编译一个文件就能吃到几个 GB。并行度太高内存耗尽,编译器进程就会被内核直接干掉。排查方法很简单:grep -c "clang" out/Default/build.ninja但这只能说明编译任务总数,查并发度更直接。你可以在环境变量里限制并行度,比如:NINJA_JOBS=8 autoninja -C out/Default chrome或者直接用裸 ninja:ninja -C out/Default -j 4 chrome把并发数调到适当值,通常能绕开内存瓶颈。如果还是被 Killed,考虑临时关闭一些重度依赖符号的模块,或者给编译过程的临时目录换到更大的磁盘空间。4.2 Xcode 版本与系统 SDK 不匹配macOS 上编译 Chromium 离不开系统 SDK,但 Chromium 构建并不会直接用系统的默认clang,它倾向使用随源码一起拉取的工具链版本。这样带来一个典型问题:工具链版本要求的最低 macOS SDK 版本和当前系统不匹配,编译时会出现类似:xcrun: error: SDK "macosx" cannot be located或者各种'X' file not found,指向的路径在 SDK 目录下找不到。这类问题的核心是:构建脚本找不到一个合适的 Xcode SDK 路径。最容易生效的动作是先检查 Xcode 命令行工具和完整 Xcode 是否都装了:sudo xcode-select --reset xcodebuild -version xcodebuild -showsdks如果-showsdks里看不到任何 macosx SDK,说明 Xcode 安装本身出了问题,重新安装 Xcode 或者命令行工具解。另一个常见问题是同时装了多个 Xcode 版本,系统默认的 command line tools 指向了一个旧版本。用:sudo xcode-select -s /Applications/Xcode.app/Contents/Developer切换到最新路径后,再跑xcodebuild -showsdks确认。这步做完再重新gn gen一次,因为 SDK 路径会被记录在构建参数里,光改系统默认还不够,必须让 GN 重新探测。4.3 链接阶段的 ld 失败:符号缺失还是参数冲突链接阶段的报错往往比编译阶段更吓人,因为日志长、提示含糊。我遇到过最典型的一种:ld: warning: object file ... was built for newer macOS version (14.0) than being linked (13.0) ld: error: undefined symbol: _dlopen后面那个undefined symbol是最核心的线索。它通常意味着某个库没有正确链接进来,或某个编译目标缺了依赖项。遇到它先别去改源码,优先怀疑构建配置问题。如果之前编过其它版本、换过target_cpu,那么可能是某个 submodule 里的旧对象文件残留,解决办法是清掉相关 target 的临时输出重新编译。Linux 用户习惯用make clean,Chromium 里对应的就是删掉out/Default/obj/xxx对应目录,但不建议手动删单个目录,容易删不干净。直接全量重编一次是更可靠的方案。还有一条链接期经验:macOS 上链接 Chromium 最大的性能瓶颈在符号表。之前提过symbol_level=0就是为了避免这里爆内存。要是真的需要调试符号,也不要symbol_level=2直接注入到所有模块,可以只给指定的测试目标开局部符号,实现用 GDB 或 LLDB 逐步查看一遍。把symbol_level设在 0 和 2 之间反复横跳,每次链接都像坐过山车一样漫长。4.4 “文件太大无法扫描”的误导性报错还有一种不常见但碰到就很头疼的情况,报错类似:error: unable to open output file ... 'File too big'明明磁盘还有几十 GB,为什么会说 file too big?这其实是文件系统格式的限制。某些外置盘或网络盘使用 FAT32 或旧式文件系统,单个文件大小上限是 4GB。Chromium 的链接阶段会产生超大文件,如果 out 目录建到了这种分区上,铁定报错。解决办法是把整个源码和 out 目录挪到本机 APFS 分区,不要放在外置 FAT32 盘上。这也提醒了开头说的磁盘规划:不只看剩余空间,还要看文件系统类型。5. 编译产物的解析与验证:到底得到了什么5.1 产出目录里都有什么编译成功跑完,终端会回到提示符,最后一段日志一般是ninja: no work to do或显示完成时间。此时输出目录下会生成非常多文件,以 out/Default 为例,最重要的几个包括:Chromium.app chrome_crashpad_handler icudtl.dat resources.pak chrome_100_percent.pak chrome_200_percent.pakChromium.app是最终的浏览器应用,里面嵌套着完整可执行文件。icudtl.dat是 ICU 数据文件,负责国际化字符处理,不能被误删。各种.pak是资源文件,存放界面字符串和图片资源。想快速确认产物是否可用,可以直接打开它:open out/Default/Chromium.app --args --user-data-dir=/tmp/chromium-profile --no-first-run首次启动可能比预想慢一点,因为各种缓存还没有生成。看到默认起始页正常渲染,基本就能说明这个产物是能用的。5.2 用内置测试做一次冒烟验证如果你不只是想要“能打开”的浏览器,还想验证构建质量,可以用 Chromium 自带的测试集跑一次冒烟测试。先从最基础的开始:autoninja -C out/Default blink_unittests然后运行:out/Default/blink_unittests --gtest_filter=WebView.*这里的WebView.*只是示例,实际你可以根据自己关注的模块来勾选。跑全量测试会非常久,但挑一组跟自己改动相关的测试跑完,至少能确认编译过程没有引入基础性错误。注意,这些测试程序本身并没有包含在chrome目标里,所以你得先单独编译它们:autoninja -C out/Default blink_unittests这个过程会额外花一点时间,但比起重新全量编译,仍然划算得多。5.3 签名与运行权限:本地用 ad-hoc 就够了如果你只是自己在本地跑,不做分发,最省事的做法是直接用 ad-hoc 签名。每次编译完成后,产物默认可能没有有效签名,macOS 上直接打开会提示“已损坏”或被 Gatekeeper 拦截。处理方式很简单:codesign --force --deep -s - out/Default/Chromium.app-s -表示使用 ad-hoc 签名,不给应用分配任何身份,纯粹为了让系统认为它是“被签名”的。这个操作在每次重新编译后可能需要重复,因为链接生成的新二进制文件会覆盖旧的签名状态。从这一步也能看出 Chromium 编译流程里的一个特点:拿到产物之后,离“日常使用”还差一个打包和签名的环节。如果后面想把这个自定义版本长期留在 Mac 上,还可以考虑把它放进/Applications,同时注意不要用--user-data-dir的时候污染默认配置目录。我个人这几年折腾 Chromium 编译最大的体会是:真正花时间的往往不是那一两个小时满负载编译本身,而是编译前后反复确认环境、分析错误信息、决定参数取舍的过程。尤其是第一次全量编译,心态特别重要——不要期待一次成功,更不要每次失败就推倒重来。学会读懂 FAILED 那一行,学会利用增量编译,学会在几个关键参数之间找到平衡点,这个流程其实非常成熟、非常稳。如果你后面打算把 Chromium 换成自己的代号、加自己的功能,那编译流程到这里其实只是起点。还有一层更深的玩法是修改 GN 构建脚本、新增自己的源文件、定制产品名称和图标,这些我在后续合适的时机再单独写一篇。