记录一下在 Linux (x86_64) 上用 cargo-zigbuild 交叉编译 Rust 项目到 FreeBSD x86_64 的踩坑过程。项目是一个 Discord bot,依赖了 sysinfotikv-jemallocatorsea-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 上需要链接 libkvmlibprocstatlibgeomlibdevstat 等 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_closeprocstat_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.solibprocstat.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

systemdrc.d
服务定义声明式 INI 文件Shell 脚本
守护进程化内置需要 daemon(8)
自动重启Restart=alwaysdaemon -r
日志journald,结构化查询文件 (/var/log/)
用户服务systemctl --user,无需 sudo不支持,需要 sudo
环境变量Environment=需要 /usr/bin/env 或 wrapper 脚本

systemd 开箱即用的东西更多,rc.d 更透明但需要自己处理更多细节。