只是为了方便分发。
最近在尝试用Rust写程序。虽然使用Rust编写的程序在运行速度上与C/C++相差无几1,但是Rust程序的编译实在是相当缓慢并且耗费资源。这也就使得对于一些嵌入式平台而言(比如Raspberry Pi), 如果不想芯片冒烟的话,还是要老老实实地交叉编译。之前写一些Go的程序的时候交叉编译就非常方便,通过指定GOOS
以及GOARCH
环境变量即可2。而Rust则显得有些麻烦,这里3有一份很不错的文档可以供参考,并且其中也提到了对于同架构不同操作系统而言,因为缺乏工具链,除了使用一些可以提供不同环境的CI平台,或是虚拟机之类的工具,很难进行这样的编译。总体来说,要实现直接的二进制分发,要达到两个目标。
首先是静态链接(Static linking),在编译过程中编译器与汇编器只会将代码翻译成目标平台机器码,而最终的可执行文件则通过链接器产生。在链接这一步一般情况会使用动态链接,并链接到当前环境下的基础库。这里无意讨论静态链接和动态链接的优劣,但是一些情况下我们确实需要静态链接。比如我自己写个小程序,放到一些老旧的Linux系统上运行,通常就需要想办法避开glibc的版本问题。
接着是交叉编译(Cross compilation),这一步需要一个能在当前平台执行的编译器,其输出则为目标平台的机器码,并且还需要可以生成目标平台可执行文件的链接器。
那么交叉编译和静态链接的关系又在哪里呢?虽然同为x86_64平台的Unix-like系统,macOS和Linux的二进制格式不一样,libc也不一样,因此在二者之间交叉编译对方平台的二进制文件时,可以选择静态链接以避免动态链接库的版本问题。
对于Rust而言,根据之前提到的文档3,要想编译到常见的x86_64架构的Linux平台,静态链接需要将目标平台设置为x86_64-unknown-linux-musl
,也就是静态链接musl libc来去除对glibc动态链接库的依赖,从而达到生成静态链接二进制的目的。因此,首先通过rustup
添加目标工具链
rustup toolchain add x86_64-unknown-linux-musl
然后,在构建的时候使用
cargo build --target x86_64-unknown-linux-musl
就可以编译出一个x86_64架构Linux平台下静态链接的二进制文件。
但是在实际中,这一个方法并不一定能成功。比如,使用常见的数据库ORM框架Diesel4,利用SQLite作为后端,即便使用上面的命令生成的二进制文件也并非是静态链接的。按Diesel的文档,引入依赖后Cargo.toml
为:
[dependencies]
diesel = { version = "1.4.4", features = ["sqlite"] }
通过查询得知,Diesel在使用SQLite后端的时候,其底层依赖libsqlite3-sys
,而libsqlite3-sys
默认情况下是不会静态链接的,根据其文档5,必须指定其feature为「bundled」,即在Cargo.toml
里显式地指定libsqlite3-sys
的features:
[dependencies]
diesel = { version = "1.4.4", features = ["sqlite"] }
libsqlite3-sys = { version = "*", features = ["bundled"] }
这样,再使用cargo build
的时候在理论上就可以成功地生成静态链接的二进制文件了。
但是事情并没有那么简单,这还只是解决了sqlite库的静态链接问题。再次执行cargo build --target x86_64-unknown-linux-musl
,发现这次在libsqlite3-sys
的编译时会有如下报错
error occurred: Failed to find tool. Is `musl-gcc` installed?
所以转了一圈,交叉编译的问题还没有解决。于是,仍然需要目标平台的musl-gcc。此时有两个解决方案。
Ziglang
第一,利用自带多个平台musl工具链的zig。zig是一门新的旨在替代C的系统级编程语言6。其特色在于没有隐式控制流,并可以利用编译期间的代码执行实现元编程。其编译器本身也是一个C编译器,并且可以实现不同平台的交叉编译。Zig语言的作者的博客中也提到了zig cc
可以作为GCC/Clang的原位替代7。因此,可以通过以下的wrap脚本作为代位C编译器
#!/bin/sh
zig cc -target x86_64-linux-musl $@
保存为zcc
,并chmod +x zcc
,此时再设置环境变量export CC=./zcc
,再次执行cargo build --target x86_64-unknown-linux-musl
,会发现libsqlite3-sys
到编译通过了。然而,此时又出现了新的问题
error: linking with `cc` failed: exit status: 1
[...]
note: ld: library not found for -lcrt0.o
由于链接器并没有使用zig cc
,而还是系统带的cc
,因此自然也不能链接。那么需要修改链接器,于是找到链接器参数可以通过.cargo/config.toml
根据target指定,于是有
$ cat .cargo/config.toml
[target.x86_64-unknown-linux-musl]
linker = "zcc-x64-linux"
接着继续使用cargo
构建,这次仍然报错,
ld.lld: error: duplicate symbol: __init_tp
仔细检查错误信息,发现链接的时候,zig cc
选择链接自己的musl-libc,而rust这边的rustlib也同样提供了libc的符号,符号冲突,因此链接失败。根据遇到同样问题的这篇文章8的说法,可以在.cargo/config.toml
中给rustc
加参数使rustc不再链接自带的libc
$ cat .cargo/config.toml
[target.x86_64-unknown-linux-musl]
rustflags = ["-C", "linker-flavor=gcc", "-C", "link-self-contained=no"]
linker = "zcc-x64-linux"
这一方法比较简单,因为zig提供了常见平台的release可以直接下载运行。实际操作中,我并没有成功,链接阶段仍然会报和之前一样的符号冲突的错误,并且尝试了一些方法也没有成功。因此个人使用了第二种方法,这种方法留给读者参考。
musl-gcc
第二种方法就是真正地编译一套目标平台的musl-gcc。单凭自己去弄清楚一堆依赖显然不现实。所幸已经有人提供了简单的编译脚本9。(这一脚本主要面向目标操作系统为Linux的,其他操作系统可以到这里下载预先编译好的编译器。)
$ git clone https://github.com/richfelker/musl-cross-make.git
$ cd musl-cross-make
$ cp config.mak.dist config.mak
此时需要编辑config.mak
。之后就可以开始编译了。
$ make
$ make install
编译完成之后,可以export CC=/full/path/to/musl-cross-make/output/bin/x86_64-linux-musl-cc
,并且设置.cargo/config.toml
为(注意替换路径)
[target.x86_64-unknown-linux-musl]
linker = "/full/path/to/musl-cross-make/output/bin/x86_64-linux-musl-gcc"
即可利用cargo build --target x86_64-unknown-linux-musl
编译出一个静态链接的二进制文件了。
如果不想自行编译must-gcc
,对于macOS,还有Homebrew版本10,使用方法可以参考这篇博客。
个人利用第二个方法成功地编译出了Linux下的静态链接二进制文件。