在 macOS 上交叉编译 Rust 到树莓派

背景

虽然我还一直没写过 Rust,但是最近使用的 Rust 编写的工具越来越多了,我平时常用的设备包括 macOS 和树莓派,所以经常会遇到不同平台编译的问题。

最开始我选择从源码安装,在每个平台上都下载源码然后编译。然而,Rust 的编译是一个非常漫长而且占用 CPU 的过程,在树莓派上尤为明显,经常遇到因为 CPU 资源不够而失联,或者卡顿很长时间也不知道发展到什么地步了。

所以我就想到了用交叉编译的方式来编译 arm64 的版本给树莓派使用。

之前编译过一些 Go 的代码,超级简单,基本都是很快就编译完成,然后就可以到树莓派上愉快地使用了。

但是 Rust 的编译似乎更加偏底层,每一个依赖都要调用 compiler、linker 等,然后就听到电脑风扇呼呼呼的声音。而且这种编译方式带来了一个很大的问题,这些之类的工具通常都是依赖平台的,不同平台差异还挺大,所以会遇到一些困难。

cross

经过坚持不懈的查找,我看到一个号称是零配置傻瓜式的交叉编译工具 cross,是基于 Docker 来编译其他平台的代码的,不会对现有系统造成影响,而 Docker 的镜像中又包括了所需的其他工具,不用自己操心,听起来很靠谱。

首先要安装 Docker,这里就不多说了。

安装 cross

看上去很简单,也就一行命令的事情:

$ cargo install cross

接下来,交叉编译,只需把原来的 cargo 替换成 cross 即可:

$ cross build --target aarch64-unknown-linux-gnu

意料之中,情理之外的事情发生了,果然出错了。说好的零配置傻瓜式交叉编译呢?

这里会提示缺少 x86_64-unknown-linux-gnu 的工具链。当时的我是懵逼的,因为我用的是 macOS,为啥会需要一个 Linux 的工具链,而且也不是我的 target 工具链。

但是我别无选择,只好尝试按照提示安装:

$ rustup toolchain install x86_64-unknown-linux-gnu

结果又在意料之中,安装不上。果然事情不会这么简单。

Google 之后发现这可能是 rustup 的一个 bug,通过指定 minimal profile 可以解决:

$ rustup toolchain install --profile minimal stable-x86_64-unknown-linux-gnu

终于,工具链装上了。

再次编译,经过漫长的等待,编译成功了,复制到树莓派上亲测可用。

cross 的问题

不得不承认,cross 还是挺傻瓜的,除了安装必须的 x86_64-unknown-linux-gnu 遇到了一点波折,真正的编译过程都不需要我们操心。

但是由于它是基于 Docker 的,当依赖很多的时候可能需要不断地把资源送到 Docker 中去执行编译(我的猜测,可能是错的),每一个依赖的编译都相当地慢,差别非常明显。

当然,如果不缺时间也不在乎电脑资源消耗的话,这也就不是一个问题了。

原生跨平台编译

这个概念是我造的,就是指用一个在当前平台上可以正常运行的(原生)程序,去生成另一个平台的(跨平台)二进制可执行文件。这种方式因为是原生执行编译操作,而不是先启动一个 Docker,所以理论上在编译过程中会节约不少资源和时间。

musl-cross 就是一个这样的工具。

安装 musl-cross

macOS 上安装 musl-cross 看似是一件很简单的事情,因为作者已经提供了对应的 brew 命令:

$ brew install FiloSottile/musl-cross/musl-cross

还贴心地提供了支持 arm64 的参数 --with-aarch64

结果因为新版 brew 去掉了对安装参数的支持,导致我们需要用一些奇怪的小技巧

# 先不带参数安装
$ brew install FiloSottile/musl-cross/musl-cross
# 然后加参数重新安装
$ brew reinstall FiloSottile/musl-cross/musl-cross --without-x86_64 --with-aarch64

总算安装上了,经过这一步,我们拥有了 musl 的编译工具。

然后我们要给 Rust 添加 aarch64-unknown-linux-musl 的 target,因为我们即将用 musl 编译器来编译代码到 aarch64,也就是 arm64。

$ rustup target add aarch64-unknown-linux-musl

尝试编译:

$ cargo build --target aarch64-unknown-linux-musl

结果最后一步总是失败,错误内容是 ld: unknown option: --as-needed。对我来说这就是一条奇怪而没用的信息,但是总算找到了类似问题的 issue,大概就是工具链不兼容的问题,官方给出的方案是换一个 linker,评论里有说换成 lld 是可行的。

然而我并不知道 lld 是什么东西。

经过一番学习,我终于认识到 lldllvm 里面的一个 linker 工具,所以我可能需要安装 llvm

$ brew install llvm

然后,果然发现了 lld 的存在 —— /usr/local/opt/llvm/bin/ld.lld,虽然名字有些出入,但是官方文档里提到了,它就是传说中的 lld

于是在 .cargo/config 中修改一下 linker:

[target.aarch64-unknown-linux-musl]
linker = "/usr/local/opt/llvm/bin/ld.lld"

再次编译,终于成功了!速度比 cross 的方案快了不少。

musl-cross 的问题

上面的安装过程已经可以看出这个方案有些不成熟,各种问题需要自己想办法解决,但好在最终都可以解决。

可能跟 musl 编译的方式有关,最终产生的二进制文件比 gnu 的产物要大不少。

此外,musl 和常用的 glibc 存在一些实现上的差异,可能会导致某些情况下程序的行为不一致,甚至异常。我目前并没有遇到此类问题。

总结

最后,虽然经历了一些波折,但是两个交叉编译的方案我都跑通了。

他们各自有一些利弊,我们可以根据需要来取舍:

  • cross 方案更简单易用,但是依赖 Docker,速度很慢;
  • musl-cross 方案更节约资源,编译速度更快,但是产物更大,而且可能会有一些不符合预期的结果。

© 2021