记录一下在 Linux (x86_64) 上用 cargo-zigbuild 交叉编译 Rust 项目到 FreeBSD x86_64 的踩坑过程。项目是一个 Discord bot,依赖了 sysinfo、tikv-jemallocator、sea-orm(SQLite) 等 crate。
背景
Zig 自带了多平台的 libc 和交叉编译工具链,cargo-zigbuild 利用 Zig 作为 linker 来实现 Rust 的交叉编译,通常比自己配置交叉工具链方便很多。但 FreeBSD 作为目标平台时会遇到一些特殊问题。
准备
# 安装 FreeBSD 目标
rustup target add x86_64-unknown-freebsd
# 安装 cargo-zigbuild(如果还没有)
cargo install cargo-zigbuild
问题一:缺少 FreeBSD 系统库
编译本身顺利通过,但链接阶段报错:
error: unable to find dynamic system library 'geom' using strategy 'no_fallback'
sysinfo crate 在 FreeBSD 上需要链接 libkvm、libprocstat、libgeom、libdevstat 等 FreeBSD base system 的库。Zig 的工具链只提供 libc,不包含这些系统库。
解决方案:创建 stub 共享库
这些库只在运行时需要真正的实现,链接时只需要满足符号解析即可。用 Zig 创建最小的 stub .so 文件:
mkdir -p freebsd-stubs && cd freebsd-stubs
# 需要真实符号的库(sysinfo 实际调用了这些函数)
cat > stub_kvm.c << 'EOF'
void *kvm_openfiles(void) { return 0; }
int kvm_close(void) { return 0; }
void *kvm_getprocs(void) { return 0; }
void *kvm_getargv(void) { return 0; }
void *kvm_getenvv(void) { return 0; }
int kvm_getswapinfo(void) { return 0; }
EOF
cat > stub_procstat.c << 'EOF'
void *procstat_open_sysctl(void) { return 0; }
void procstat_close(void) {}
void *procstat_getfiles(void) { return 0; }
void procstat_freefiles(void) {}
EOF
# 空 stub(只需要让链接器找到 .so 文件)
cat > stub_empty.c << 'EOF'
void __stub(void) {}
EOF
编译时用 -nostdlib -ffreestanding 避免引入对 FreeBSD libc 的依赖,同时设置正确的 soname:
# 有实际符号的库——设置 soname 匹配 FreeBSD 真实库名
zig cc -target x86_64-freebsd -shared -nostdlib -ffreestanding \
-Wl,-soname,libkvm.so.7 -o libkvm.so stub_kvm.c
zig cc -target x86_64-freebsd -shared -nostdlib -ffreestanding \
-Wl,-soname,libprocstat.so.1 -o libprocstat.so stub_procstat.c
# 只需要存在的库
for lib in geom rt util execinfo memstat devstat; do
zig cc -target x86_64-freebsd -shared -nostdlib -ffreestanding \
-o lib${lib}.so stub_empty.c
done
为什么需要 soname?
如果直接把 .so 文件的完整路径传给链接器,生成的二进制会把编译机的绝对路径写进 NEEDED 段:
NEEDED: /home/user/project/freebsd-stubs/libkvm.so # 在目标机上找不到
设置了 soname 之后,NEEDED 段会记录 soname 而非文件路径:
NEEDED: libkvm.so.7 # 在 FreeBSD 上正常解析
NEEDED: libprocstat.so.1
问题二:Zig 链接器的 --as-needed 行为
配置 .cargo/config.toml 指定 stub 库的搜索路径后,大部分符号都能解析,但 kvm_close 和 procstat_close 始终报 undefined symbol。
原因是 Zig 的链接器启用了 --as-needed,在处理 -lkvm 时,如果当时没有看到对该库的未解析引用,就会跳过它。由于 kvm_close 在 Drop impl 中被调用,它的引用出现在链接过程的较晚阶段,此时 -lkvm 已经被跳过了。
解决方案:直接传递 .so 文件路径
把 .so 文件作为显式输入传给链接器(而非通过 -l 搜索),可以绕过 --as-needed 的过滤:
# .cargo/config.toml
[target.x86_64-unknown-freebsd]
rustflags = [
"-L", "native=/path/to/freebsd-stubs",
"-C", "link-arg=/path/to/freebsd-stubs/libkvm.so",
"-C", "link-arg=/path/to/freebsd-stubs/libprocstat.so",
]
注意这里只需要显式传 libkvm.so 和 libprocstat.so——其他库通过 -L 就能正常解析。这也是为什么 soname 很重要:link-arg 传的是完整路径,但 soname 确保二进制里记录的是正确的库名。
最终效果
$ cargo zigbuild --target x86_64-unknown-freebsd --release
Finished `release` profile [optimized] target(s)
$ file target/x86_64-unknown-freebsd/release/dc-bot
ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /libexec/ld-elf.so.1, FreeBSD-style
$ readelf -d target/x86_64-unknown-freebsd/release/dc-bot | grep NEEDED
NEEDED: libkvm.so.7
NEEDED: libprocstat.so.1
NEEDED: libm.so.5
NEEDED: libthr.so.3
NEEDED: libc.so.7
所有动态链接的库都是 FreeBSD base system 的一部分,目标机上不需要额外安装任何依赖。
其他方案探讨
Stub 库方案虽然能用,但毕竟是个 hack。这里讨论几种替代思路:
1. 从 FreeBSD 提取真实的库文件
最"正确"的方案——直接拿到 FreeBSD 的真实 .so 文件作为 sysroot:
# 从 FreeBSD release 镜像中提取 base.txz
fetch https://download.freebsd.org/releases/amd64/15.0-RELEASE/base.txz
mkdir -p freebsd-sysroot
tar -xf base.txz -C freebsd-sysroot ./usr/lib/
# 指向提取出来的库
export LIBRARY_PATH=/path/to/freebsd-sysroot/usr/lib
优点是不需要手写任何 stub,符号和 soname 都是真实的。缺点是要下载约 200MB 的 base.txz,而且需要跟踪目标机的 FreeBSD 版本。如果团队有 CI/CD 需求或者目标机跨多个 FreeBSD 版本,这是最稳妥的选择。
2. 在 FreeBSD 上原生编译
最简单的"不交叉"方案——直接在目标机上编译:
# 在 FreeBSD 上安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 直接 cargo build
cargo build --release
没有任何交叉编译问题。但 Oracle Cloud 免费 FreeBSD 实例只有 1GB 内存,编译 Rust 项目时大概率 OOM。可以开启 swap 或者 CARGO_BUILD_JOBS=1 限制并发,但编译速度会非常慢。如果目标机资源充足(比如 4GB+),这其实是最省心的方案。
3. FreeBSD jail / VM 交叉编译
用 FreeBSD VM 或 jail 做编译环境,但编译在本地(或 CI)的 FreeBSD 虚拟机里完成:
- bhyve / VirtualBox / QEMU:在开发机上跑一个 FreeBSD VM,编译完 scp 到目标机。
- CI 方案:GitHub Actions 有
cross-platform-actions/action可以跑 FreeBSD VM,适合自动化。
这样既享受原生编译的无痛体验,又不受目标机资源限制。代价是维护一个 FreeBSD VM 环境。
4. cross-rs
cross 是 Rust 生态比较成熟的交叉编译工具,基于 Docker 容器。但截至目前,cross 官方不支持 FreeBSD target——FreeBSD 的 license 不允许在 Linux 容器中分发其 sysroot。社区有一些非官方的 FreeBSD Docker 镜像方案,但维护状态参差不齐。
5. 避免需要系统库的 crate
从源头解决问题——如果项目对 sysinfo 的依赖不是核心功能,可以用 cfg 条件编译在 FreeBSD 上禁用它:
#[cfg(not(target_os = "freebsd"))]
use sysinfo::System;
或者用 feature flag 控制。这样 FreeBSD 构建不需要链接那些系统库,交叉编译就和 Linux 一样简单。代价是 FreeBSD 上缺少对应的功能。
方案对比
| 方案 | 复杂度 | 可靠性 | 维护成本 |
|---|---|---|---|
| Stub 库 | 中 | 中 | 低,但版本升级可能需要更新符号 |
| 提取真实 sysroot | 低 | 高 | 中,需跟踪 FreeBSD 版本 |
| 目标机原生编译 | 低 | 高 | 低,但受限于目标机资源 |
| FreeBSD VM/CI | 中 | 高 | 高,需维护 VM 环境 |
| cross-rs | - | - | 官方不支持 FreeBSD |
| 条件编译避开 | 低 | 高 | 低,但功能有缺失 |
对于个人项目,stub 方案够用了。对于生产环境或团队协作,提取 sysroot 或 CI 方案更靠谱。
部署:FreeBSD rc.d 服务
FreeBSD 没有 systemd,用的是传统的 rc.d 系统。写个 /usr/local/etc/rc.d/dc_bot:
#!/bin/sh
# PROVIDE: dc_bot
# REQUIRE: LOGIN NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="dc_bot"
rcvar="dc_bot_enable"
: ${dc_bot_enable:="NO"}
pidfile="/home/freebsd/bot/${name}.pid"
command="/usr/sbin/daemon"
command_args="-f -r -R 10 -P ${pidfile} -u freebsd \
-o /var/log/${name}.log \
/usr/bin/env RUST_LOG=dc_bot=debug \
/home/freebsd/bot/dc-bot -c /home/freebsd/bot/config.json \
-d /home/freebsd/bot/sqlite.db"
load_rc_config $name
run_rc_command "$1"
几个踩坑点:
-f必须加:关闭 stdin/stdout/stderr,否则service start会因为 stdout 未关闭而一直挂起。-r -R 10:进程退出后自动重启,等待 10 秒。相当于 systemd 的Restart=always+RestartSec=10。-P而非-p:-r模式下 daemon 有 supervisor 和 child 两个进程,-P记录 supervisor 的 PID,-p记录 child 的。用-P才能正确 stop。- pidfile 放用户目录:
-u降权后写不了/var/run/。 - 用
/usr/bin/env设置环境变量:rc.d 不像 systemd 有Environment=指令。
启用:
sudo sysrc dc_bot_enable=YES
sudo touch /var/log/dc_bot.log && sudo chown freebsd:freebsd /var/log/dc_bot.log
sudo service dc_bot start
对比:systemd vs rc.d
| systemd | rc.d | |
|---|---|---|
| 服务定义 | 声明式 INI 文件 | Shell 脚本 |
| 守护进程化 | 内置 | 需要 daemon(8) |
| 自动重启 | Restart=always | daemon -r |
| 日志 | journald,结构化查询 | 文件 (/var/log/) |
| 用户服务 | systemctl --user,无需 sudo | 不支持,需要 sudo |
| 环境变量 | Environment= | 需要 /usr/bin/env 或 wrapper 脚本 |
systemd 开箱即用的东西更多,rc.d 更透明但需要自己处理更多细节。