一次惨绝人寰的编译(下)

接着上一篇,来说说这次在 Linux 上编译时踩的各种坑。

因为之前已经在 Windows 和 Mac 上搞过一轮,加上有 AI 老师傅指点,我以为这次只是跑两个脚本那么简单。没想到一开始就翻了车。

首先是确定使用哪个 Linux 版本。按照 GPT 老师的建议,最好用 Electron 1.8.0 发布时的官方环境 —— Ubuntu 16.04。可惜我手头那台 Linux 机器早几年就升到了 20.04,不太想为了这个重装系统。而且 Electron 编译起来非常庞大,放 VM 里跑也不现实。

这时 GPT 老师突然灵光一闪:为什么不用 Docker?
我以前没怎么碰过 Docker,以为它只是 Linux 的进程隔离工具。结果发现连 Mac 上也能跑,于是就装了一下。很顺利地创建了一个 Ubuntu 16.04 的环境——这算是 VM 吗?还是进程隔离?搞不清楚,但总之新买的 M2 机器终于有了用武之地。

接下来运行 scripts/bootstrap.py,结果立刻报错:文件下载失败。查了一圈才知道,Electron 1.8.0 已经是上古版本,当年开发者把一些关键文件放在 Amazon S3 上。可惜这些 S3 容器现在都挂了。

换句话说,当年 Electron 相较于 nw.js 的一个“先进设计” —— 不需要自己编译 libchromiumcontent,而是用官方共享的预编译版本来加快流程 —— 现在彻底失效了。

没办法,我只能亲自编译这个庞然大物的 libchromiumcontent。先是在 Docker 里尝试拉取指定版本的代码,然后添加 Electron 的 patch,准备编译。可惜这只是我的美好想象,第一步拉取代码就失败了。

原因是 Google 的代码实在太大,他们还特地设计了一个工具来同时sync几十个仓库。Docker 给的内存又太小,每次clone到 80% 左右就崩了。

找到了问题,就把 Docker 的内存和硬盘配额都拉大。总算把所有代码clone下来了。好在 Google 还算厚道,老版本的代码都还在,接下来的编译过程还算顺利。

编译完成后,把生成的 lib 文件重命名为指定的 commit id,丢回 Electron 的目录,bootstrap 就能跳过失败的下载步骤了。

当然,问题还没完。很快又出现了依赖包下载失败的问题,这次是某个 sysroots 压缩包。问了 GPT,才知道 Linux 编译为了避免头文件版本不一致,引入了类似 Windows SDK 的概念,把一整套头文件、库文件打包起来统一管理。

好在这些文件还能在 GitHub 上搜到,我重新创建了一份,填上去就行了。

终于,bootstrap 跑通了,可以开始编译 Electron 本体了。

意外的是,编译和链接过程都异常顺利,顺利得让我有点害怕。果不其然,最后生成的 binary 文件不太对劲,有整整 2G —— 显然还没 strip。Electron 提供了一个 create-dist.py 脚本来处理发布包,但它默认还要执行一些文档生成的流程。只好小改一下脚本,去掉这些额外步骤,然后就得到了大小正常的发布包。

下一步是实机测试,我拿到 Ubuntu 20.04 环境中一测,果然又报错,提示缺少各种动态链接库。

于是我开始一一排查,把缺失的库都摘出来,统一放到发布目录的 lib 文件夹中,然后套一层启动脚本,把 LD_LIBRARY_PATH 指向自己的 lib 目录。终于,程序能正常运行了。

然后是重头戏:部署到 Steam Deck 上看看吧。果然还是报动态库缺失的错误。继续手动补,直到 Electron 成功打开窗口,游戏主界面也能正常显示。

成功了吗?我正想叉会儿腰,结果一进游戏场景直接报错……

这才发现,Steam Deck 的桌面模式和 Steam 启动环境之间还有些差异。折腾半天,终于意识到可能是编码的问题。错误日志提示是某个汉字命名的文件找不到。

我打印了两种环境下的变量,果然,在 Steam 的启动环境中,LC_ALLLANG 被设为了中文 GBK,而我整个程序用的都是 UTF-8。

知道原因就好办了:在启动脚本中强制把 LANGLC_ALL 设成 en_US.UTF-8

最后检查了一遍工作路径、存档目录这些,没再出啥问题。

Linux 版本,终于搞定!


总结:

  1. Docker 非常好用,但拉取大仓库还是得调高内存和硬盘。
  2. Linux 的编译环境必须固定头文件版本,打包发布时还得附带所需动态链接库。可以用 Steam Deck 进行真实环境测试,Steam 启动器自带部分常用库,只需补少数缺失项。
  3. 一定要完整测试整套环境。Linux 环境变化太多,Steam 与桌面环境也可能差异极大。

一次惨绝人寰的编译(上)

为了让一个老游戏兼容 Mac 和 Steam Deck,我前阵子动手做了个新版本的构建。游戏是用 JavaScript 写的,跑在 Electron 上。既然用了“跨平台”的技术栈,那它至少应该在这种时候派上点用场吧?

任务目标很明确:编译 Electron 1.8.0 的 macOS 和 Linux 版本。

一开始我以为构建 macOS 版本需要用 Apple Silicon 的设备,于是入手了一台最低配的 Mac Mini M2,8GB 内存、256GB SSD,能省就省。但上手之后才发现,macOS 提供了一个叫 Rosetta 的 Intel 兼容层,大多数老程序在 M2 上运行完全没问题。换句话说,我这台新电脑根本没派上用场。

于是我翻出了多年前的老设备——Mac Mini 2014,发现当年我确实在这台机器上编译过 Electron 1.8.0。稍微调整下配置,理论上是可以继续用的。信心满满地开工。

回顾旧文档、准备构建环境、跑脚本,一切按部就班。但第一轮构建很快就失败了,脚本一运行就报错。问题出在 Python:Electron v1.8.0当年依赖的是 Python 2.7,而我在 2023 年升级了 macOS,系统默认已经切换到了 Python 3。很多老脚本因此不再兼容。

于是退回 Python 2.7,再次运行 build.py,这次终于顺利进入构建流程。前期编译过程很快,毕竟历史中间文件还在。可没想到,等到了 link 阶段,机器突然开始变卡,几分钟后系统直接重启了。

我试了几次,都是在 link 阶段死机。最初还怀疑是老机器硬件出问题了。但联想到死机总发生在同一阶段,我猜可能是资源耗尽。打开任务管理器观察,果然在崩溃前有异常磁盘写入,某个系统进程(kernel什么的)在短时间内写了三十多个 G数据。但编译目录并没有生成相应的大文件,看起来不是 link 阶段输出的问题。

我接着观察内存使用情况,发现一个叫 dsymutil 的进程在这个阶段疯狂占用内存,远超物理内存限制,很可能触发了大量 swap 操作,最终导致系统不堪重负直接重启。macOS 在内存耗尽时没有进行优雅处理,反而选择硬重启,这倒是出乎意料。

dsymutil 是什么我并不清楚,于是请教了 ChatGPT。它告诉我,这个工具是 LLVM 工具链的一部分,用于生成调试符号(debug symbols)。考虑到 Electron 本身体积庞大,符号信息数量可能非常可观。显然,dsymutil 没有能力处理这种规模的符号,最终把系统拖垮。

既然问题出在生成调试信息,而 link 阶段其实并不依赖它,我想到一个简单粗暴但有效的办法:制作一个同名空脚本,替代系统中的 dsymutil,让它直接跳过这一步。

具体做法很简单:写一个只包含 exit 0 的 shell 脚本,命名为 dsymutil,赋予可执行权限,放进 $PATH 的前列。这一改动之后,构建流程终于顺利完成,link 阶段不再崩溃。

剩下的收尾工作就相对平淡:为了发布到 Steam,还需要处理 macOS 应用包中的 symbolic links,把它们转换成实际文件,避免打包时出现兼容性问题。另外也踩了 macOS 的路径处理一个坑:JavaScript 获取到的是真实路径而非 symbolic link,对路径逻辑略作调整后问题解决。

Mac版本处理完之后,我打算着手构建 Linux 版本,以支持 Steam Deck。听起来似乎没什么难度?嗯嗯,我得先写到这儿了,牵扯到electron这坨***肯定简单不了,Linux的事情且听下回分解吧。

简单记录一下

树莓派到手之后,参考网上的资料折腾了几天,都是很基本的东西。

1、SD卡

为了读写速度,专程购买了Kingston的32G Class10高速SD卡,标号SD10V/32G。

虽然官方列表http://elinux.org/RPi_SD_cards 信誓旦旦表示it works,但装好系统镜像之后boot报错。似乎是部分数据不可读,虽然能看到树莓派logo,也似乎开始加载内核,但很快就停滞,一直提示timeout。在网上搜了下,很多人抱怨这个卡没法用,推荐Sandisk的牌子,我手头没有不评价。更换成一张Kingston 8G Class4的卡之后终于可以启动系统镜像,还好新买的卡可以用在相机上,否则就浪费了……

后来找到个很老的Kingston 512m TF卡,加了套子之后也可以正常的使用在树莓派上。因为容量问题,只能加载其他小号系统镜像,因为官方镜像要求2G空间。

2、系统镜像

我安装的是2013-02-09-wheezy-raspbian.zip,Raspbian “wheezy”,应该是个Debian的RPi移植版。

关于如何安装镜像,如何设置,参考了这篇文章:《树莓派Raspberry Pi上手报告

不过我的显示器无法直接全屏显示1080p的画面,手动修改了config.txt中的显示模式才得以全屏。树莓派的官方系统镜像为2G,刷写之后,多于2G的部分没有分区,最好在第一次设置的时候扩展到全盘。

系统设置界面的选项,选定之后项目会立即生效,这有点出乎我的意料。实际上设置界面并不是简单的修改配置文件,而是要运行某些配置程序。

3、系统更新

注意更新之前需要连接网线。刚开始玩的时候我也不知道为啥需要更新,但很多文章都建议首先做如下步骤,我就照做了:

sudo apt-get update

sudo apt-get upgrade

后来才知道这是更新Raspbian的软件包和系统程序。最新的镜像里可能不如网上当前的版本新,更新一下聊胜于无。

4、固件更新

树莓派的固件是放在SD卡的第一个分区上的,这个分区是FAT分区,直接在windows也可以查看。

此分区上保存的几个文件,是在启动时被GPU/CPU自动加载的。因为树莓派没有bios,它的GPU首先会读取SD卡文件,靠文件名或config.txt中的配置,以固定顺序加载几个文件,然后再唤醒CPU。

具体可以参考这里的描述:

http://elinux.org/RPi_Software

http://kariddi.blogspot.sg/2012/08/raspberry-pi-bare-metal-part-1-boot.html

总之,更新固件很简单,拷贝文件到SD卡即可。最新的固件托管在github上,而且有人制作了脚本来自动更新固件。

https://github.com/Hexxeh/rpi-update

参考网站说明即可,相当于自动下载文件然后拷贝到第一个分区/boot/

5、软件安装

最方便的方法是接网线,然后通过网络安装各种软件。基本上两种方法:

a) 用sudu apt-get install命令安装软件包。很方便,不过需要有网络。

b) 用git来安装,很多软件并没有放到系统的软件包列表中,需要用git直接拿到编译好的版本或源码。

已经记不太清楚最初安装了些什么,基本上属于用到啥装啥。但gcc编译器,git,以及chrome都是一定要装的,系统自带的浏览器比较弱……

安装chrome,仍然可以参考雷锋网那篇文章。装好编译器,源码就可以直接在树莓派上编了。在pc上编比较麻烦,得配置交叉编译环境,因为树莓派是arm芯片。

6、总结

到此为止,一个可用的系统差不多就弄好了。不过这样只是换了个地方玩linux,如果想研究linux可以继续折腾。因为树莓派比我想象的慢,在上面玩linux不如在虚拟机上玩。

但我对树莓派本身比较感兴趣,打算抛开linux来折腾它。但接下来先记录一下安装ruby,SDL,web服务,以及我那坑爹的Linksys AE3000无线网卡的安装过程。

To be continued…