目录

Rust交叉编译和静态链接

如果依赖不包含C/C++依赖, Rust的交叉编译非常简单, 例如需要编译aarch64平台的静态链接产物, 只需要:

  1. rustup target add aarch64-unknown-linux-musl, 添加相关工具链 (*-musl为静态链接版本, 而aarch64-unknown-linux-gnu一般为编译动态链接的目标, 具体差异可以查询"gcc vs musl")
  2. cargo build --release --target aarch64-unknown-linux-musl, 编译产物
  3. 传输到目标平台后, 使用ldd <filename>即可检查是否有动态依赖, 输出statically linked即为成功静态链接

例如Rust中比较流行的HTTP客户端reqwest, 可以指定替换底层的OpenSSLrustls-tls, 规避C代码依赖, 在Cargo.toml中添加如下features:

reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false }

即可指定使用rustls-tls, 从而顺利编译和静态链接, 一般比较流行的Crate都支持替换rustls-tls, 可以自行去文档或者项目代码Readme查看.

包含C/C++依赖的项目交叉编译和静态链接较为复杂, 一般分为本机编译和容器编译两种方案, 下面以ffmpeg-next为例, 分别讲讲两种编译方法.

首先在Cargo.toml中添加如下:

ffmpeg-next = {version = "7.1.0", features = ["build"]}

ffmpeg-next默认动态链接到本机的FFmpeg相关Lib, 开启features = ["build"]特性, 即可克隆代码编译并静态依赖.

首先需要在本机安装musl交叉编译工具链, 以ArchLinux为例, AUR中有用户上传的aarch64-linux-musl平台工具链, 使用命令安装:

yay -S aur/aarch64-linux-musl-cross-bin

不同平台名称不一致, 需要自行查询.

如果对应平台没有编译好的相关工具链, 则需要自行编译, 首先克隆代码:

git clone https://github.com/richfelker/musl-cross-make.git
cd musl-cross-make

然后编写编译配置, 在根目录创建config.mak, 写入:

TARGET = x86_64-linux-musl
OUTPUT = /usr/bin/
COMMON_CONFIG += CFLAGS="-g0 -Os" CXXFLAGS="-g0 -Os" LDFLAGS="-s"
GCC_CONFIG += --enable-default-pie --enable-static-pie

其中TARGET是目标平台, OUTPUT是产物输出目录, 根据自身需求修改; COMMON_CONFIGGCC_CONFIG是编译配置, 含义可以自行查询. 更多信息在根目录config.mak.dist记载.

然后make编译, 编译时间可能会很长, 等待编译完成后, make install会把产物输出到配置的目录中.

之后把产物的bin目录添加到环境变量中, 即可完成工具链配置.

如果工具配置没有问题, 以x86_64-unknown-linux-musl目标为例, 运行以下命令即可编译完成:

cargo build --release --target x86_64-unknown-linux-musl

但是FFmpeg有一部分内联汇编优化, 所以ffmpeg-next编译时, 缺少汇编器会报错, 还需要安装汇编器, 使用以下命令安装:

# Arch
yay -S yasm nasm

# Debian/Ubuntu
sudo apt install yasm nasm

其它平台自行查询, 安装完成后, 运行编译命令, 即可顺利编译.

容器编译是使用Docker或者Podman容器来进行交叉编译, 相比在本机配置编译工具链, 更加方便快捷, 且更加容易集成到CI/CD流程中, 比较推荐这种方式, 目前有查到比较流行的有两个项目, crossrust-musl-cross, 以下分别介绍这两种方案.

项目主要是提供一些预构建的编译环境, 来实现Rust项目的交叉编译和静态链接.

首先进入项目目录, 确保启动Docker, 或者配置好Podman, 然后拉取对应目标镜像:

docker pull messense/rust-musl-cross:x86_64-musl

这里拉取了x86_64-musl目标, 更多的目标平台可以参考项目GitHub主页Readme.

然后运行命令编译:

docker run --rm -it -v "$(pwd)":/home/rust/src messense/rust-musl-cross:x86_64-musl cargo build --release

如果需要安装yasm/nasm等外部依赖, 可以自行编写Dockerfile, 构建自己的编译镜像.

这种方案使用简单, 贴近原生容器, 可以自行构建容器细节, 但长期使用不太方便.

项目主要提供命令行工具cross, 来进行"零配置"的交叉编译和交叉测试.

首先安装此工具:

# Cargo安装
cargo install cross

# Arch包管理器安装
sudo pacman -S extra/cross

然后保证Docker/Podman正常配置, cross会自动检测, 优先使用Docker, 普通项目就可以直接运行编译命令:

cross build --release --target x86_64-unknown-linux-musl

与cargo的命令格式完全兼容, 实际工作原理就是将后续命令参数传递给容器内部的cargo.

ffmpeg-next这种有外部依赖的, 需要在配置文件中指定外部依赖的安装选项, 在Cargo.toml同级目录创建Cross.toml, 写入内容:

[build]
pre-build = [
    "apt-get update && apt-get -y install yasm nasm",
]

这会在构建镜像时增加一层内容为apt-get update && apt-get -y install yasm nasmimage lable.

如果环境没有问题, 再次运行编译命令即可正常构建.

cross封装程度较高, 使用相比rust-musl-cross更加丝滑, 但是封装程度过高可能会带来一些奇怪的问题, 尤其是这个工具本身不那么流行, 网络资料很少, 文档中没有的话, 只能靠阅读源码来定位问题.

在我本机环境中就遇到一个很诡异的问题, 容器中ffmpeg-next相关依赖编译的时候会尝试git clone https://github.com/FFmpeg/FFmpeg, 但是它总是请求127.0.0.1:7897代理, 宿主机在/etc/environment有这个配置:

HTTPS_PROXY=http://127.0.0.1:7897
HTTP_PROXY=http://127.0.0.1:7897
ALL_PROXY=socks5://127.0.0.1:7897

但是容器中我并没有任何配置, git自身没有配置任何代理, Docker也没有配置任何代理, 换用Podman也是一样的情况, 去掉/etc/environment中的代理内容, 即可正常编译, 在Cross.toml写入内容:

[build.env]
passthrough = [
    "HTTP_PROXY=http://172.17.0.1:7897",
    "HTTPS_PROXY=http://172.17.0.1:7897",
    "http_proxy=http://172.17.0.1:7897",
    "https_proxy=http://172.17.0.1:7897",
]

运行时docker ps检查容器, 找到cross使用的容器id, 执行命令docker inspect fb8911c1cc88 > cross_container_info.json, 查看环境变量, 确实有相关环境变量

// ...
"Env": [
    "HTTPS_PROXY=http://127.0.0.1:7897",
    "XARGO_HOME=/xargo",
    // ...
    "USER=lixp",
    "HTTP_PROXY=http://172.17.0.1:7897",
    "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    // ...
],
// ...

HTTPS_PROXY使用了http://127.0.0.1:7897, 而没有被覆盖, 似乎是在哪一步被更高优先级的操作覆盖了, 查看cross源码, 在src/docker/shared.rs下发现:

impl DockerCommandExt for Command {
    fn add_configuration_envvars(&mut self) {
        let other = &[
            "http_proxy",
            "TERM",
            "RUSTDOCFLAGS",
            "RUSTFLAGS",
            "BROWSER",
            "HTTPS_PROXY",
            "HTTP_TIMEOUT",
            "https_proxy",
            "QEMU_STRACE",
        ];
        let cargo_prefix_skip = &[
            "CARGO_HOME",
            "CARGO_TARGET_DIR",
            "CARGO_BUILD_TARGET_DIR",
            "CARGO_BUILD_RUSTC",
            "CARGO_BUILD_RUSTC_WRAPPER",
            "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER",
            "CARGO_BUILD_RUSTDOC",
        ];
        let cross_prefix_skip = &[
            "CROSS_RUNNER",
            "CROSS_RUSTC_MAJOR_VERSION",
            "CROSS_RUSTC_MINOR_VERSION",
            "CROSS_RUSTC_PATCH_VERSION",
        ];
        let is_passthrough = |key: &str| -> bool {
            other.contains(&key)
                || key.starts_with("CARGO_") && !cargo_prefix_skip.contains(&key)
                || key.starts_with("CROSS_") && !cross_prefix_skip.contains(&key)
        };

        // also need to accept any additional flags used to configure
        // cargo or cross, but only pass what's actually present.
        for (key, _) in env::vars() {
            if is_passthrough(&key) {
                self.args(["-e", &key]);
            }
        }
    }
    // 其它代码...
}

可以在cross的处理逻辑中, http_proxy/https_proxy/HTTPS_PROXY三个变量会被透传到容器中, 但是HTTP_PROXY不会被传递, 非常奇怪的逻辑

临时解决可以让运行容器使用--network=host参数, 让容器使用本机网络, 这样就可以访问127.0.0.1:7897代理, 根据文档查询, 使用如下命令增加环境变量编译:

CROSS_CONTAINER_OPTS=--network=host cross  build --release --target x86_64-unknown-linux-musl

或者临时取消这些环境变量来正常编译.