Rust交叉编译和静态链接
项目不包含C/C++依赖的Crate
如果依赖不包含C/C++依赖, Rust的交叉编译非常简单, 例如需要编译aarch64平台的静态链接产物, 只需要:
rustup target add aarch64-unknown-linux-musl, 添加相关工具链 (*-musl为静态链接版本, 而aarch64-unknown-linux-gnu一般为编译动态链接的目标, 具体差异可以查询"gcc vs musl")cargo build --release --target aarch64-unknown-linux-musl, 编译产物- 传输到目标平台后, 使用
ldd <filename>即可检查是否有动态依赖, 输出statically linked即为成功静态链接
可以指定不使用C/C++依赖的Crate
例如Rust中比较流行的HTTP客户端reqwest, 可以指定替换底层的OpenSSL为rustls-tls, 规避C代码依赖, 在Cargo.toml中添加如下features:
reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false }即可指定使用rustls-tls, 从而顺利编译和静态链接, 一般比较流行的Crate都支持替换rustls-tls, 可以自行去文档或者项目代码Readme查看.
项目包含有C/C++依赖的Crate
包含C/C++依赖的项目交叉编译和静态链接较为复杂, 一般分为本机编译和容器编译两种方案, 下面以ffmpeg-next为例, 分别讲讲两种编译方法.
首先在Cargo.toml中添加如下:
ffmpeg-next = {version = "7.1.0", features = ["build"]}ffmpeg-next默认动态链接到本机的FFmpeg相关Lib, 开启features = ["build"]特性, 即可克隆代码编译并静态依赖.
本机编译
安装软件仓库中的musl工具链
首先需要在本机安装musl交叉编译工具链, 以ArchLinux为例, AUR中有用户上传的aarch64-linux-musl平台工具链, 使用命令安装:
yay -S aur/aarch64-linux-musl-cross-bin不同平台名称不一致, 需要自行查询.
自行编译musl工具链
如果对应平台没有编译好的相关工具链, 则需要自行编译, 首先克隆代码:
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_CONFIG和GCC_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流程中, 比较推荐这种方式, 目前有查到比较流行的有两个项目, cross 和 rust-musl-cross, 以下分别介绍这两种方案.
rust-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 方案
项目主要提供命令行工具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 nasm的image lable.
如果环境没有问题, 再次运行编译命令即可正常构建.
cross封装程度较高, 使用相比rust-musl-cross更加丝滑, 但是封装程度过高可能会带来一些奇怪的问题, 尤其是这个工具本身不那么流行, 网络资料很少, 文档中没有的话, 只能靠阅读源码来定位问题.
HTTPS_PROXY 代理问题
在我本机环境中就遇到一个很诡异的问题, 容器中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或者临时取消这些环境变量来正常编译.