Windows 下交叉编译 Rust 为 Linux 可执行文件

前言

日常在 Windows 上写 Rust,上线环境却是 x86_64 Linux 服务器,这是不少 Web 服务与 CLI 工具的常见组合。
若每次改代码都进 Linux 虚拟机或 CI 里编译,反馈链路会明显变长。
本文介绍一种在 Windows 本地完成 Linux 交叉编译 的实用路径:以 x86_64-unknown-linux-musl 为目标,配合 cargo-zigbuild 与 Zig 链接器,产出静态链接的可执行文件,再打成可直接 scp 到服务器的目录包。
下文不展开 Docker 与 WSL 方案,只聚焦「本机一条命令出 Linux 包」。
示例以带静态前端资源的 Axum 服务为场景,纯 CLI 项目可省略前端相关步骤。

依赖

交叉编译链路需要 Rust 工具链、musl 目标、Zig 与 cargo-zigbuild
下面列出各组件的作用,便于按需安装。

组件 作用
Rust + rustup 编译器与 target 管理
x86_64-unknown-linux-musl musl 静态链接目标 triple
Zig 提供跨平台 C 链接器,替代手写 .cargo/config.toml
cargo-zigbuild 调用 Zig 完成 cargo build 的交叉链接

在 PowerShell 中依次执行以下命令,安装 musl 目标与构建插件。

1
2
rustup target add x86_64-unknown-linux-musl
cargo install cargo-zigbuild

Zig 可通过 pip 安装 ziglang 包,或从 ziglang.org 下载解压后把 zig 加入 PATH。

1
pip install ziglang

安装完成后,用下面命令确认三者可用。

1
2
3
rustup target list --installed
cargo-zigbuild --version
zig version

实现

依赖选型

交叉编译的难点往往不在 Rust 本身,而在 C 系原生库OpenSSL 的链接。
Cargo.toml 里提前选对 feature,可以少踩「Windows 编过、Linux 缺 .so」的坑。

HTTP 客户端优先用 rustls 而不是 OpenSSL,避免在 Windows 上配置 Linux 版 OpenSSL 交叉链接。

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

SQLite 等带 C 代码的 crate,优先启用 bundled,让依赖随源码一起编译,而不是链接目标机上的系统库。

1
rusqlite = { version = "0.31", features = ["bundled"] }

Release 配置可按需开启体积优化:strip 符号、LTO、偏向体积的优化等级。

1
2
3
4
[profile.release]
strip = true
lto = true
opt-level = "s"

若项目还依赖 OpenSSL、系统 libpq 等无法 bundled 的库,musl 交叉编译会明显变复杂,需要单独评估是否改用 gnu 目标或容器构建。

核心编译命令

工具链就绪且依赖选型合理后,一条命令即可在 Windows 上产出 Linux 二进制。

1
cargo zigbuild --release --target x86_64-unknown-linux-musl

产物位于 target/x86_64-unknown-linux-musl/release/ 下,文件名与 Cargo.toml[package] name 一致。
musl 目标默认静态链接 C 运行时,拷贝到常见 x86_64 Linux 发行版上通常可直接运行,无需额外安装 glibc 兼容包。

组装部署目录

Web 类项目往往除二进制外,还需要静态资源、配置文件与启停脚本。
推荐约定一个输出目录(例如 dist-linux),每次构建后把运行所需文件集中放入,整目录上传到服务器。

典型目录结构如下。

1
2
3
4
5
6
dist-linux/
├── my-app # Linux 可执行文件(无 .exe 后缀)
├── web/ # 前端 build 产物(若有)
├── config.json # 运行时配置(首次复制,后续保留)
├── start.sh # 后台启动
└── stop.sh # 优雅停止

PowerShell 脚本可以按「检查工具链 → 构建前端 → 交叉编译 → 复制文件 → 生成 shell 脚本」分步执行。
下面给出精简版示例,项目名请替换为自己的 crate 名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ErrorActionPreference = 'Stop'
$Target = 'x86_64-unknown-linux-musl'
$DistDir = 'dist-linux'
$BinName = 'my-app'

# 清理旧二进制与脚本,保留 data/、config.json 等运行时数据
Remove-Item -Force (Join-Path $DistDir $BinName) -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force (Join-Path $DistDir 'web') -ErrorAction SilentlyContinue

# 若有前端:在 frontend 目录执行 pnpm install && pnpm run build
# Push-Location frontend; pnpm install; pnpm run build; Pop-Location

cargo zigbuild --release --target $Target

New-Item -ItemType Directory -Path $DistDir -Force | Out-Null
Copy-Item -Force "target\$Target\release\$BinName" (Join-Path $DistDir $BinName)
# Copy-Item -Recurse -Force 'frontend\dist' (Join-Path $DistDir 'web')

在 Windows 上写出的 start.shstop.sh 必须使用 UTF-8 无 BOM 且换行为 LF,否则 Linux 上可能出现 ^M 或解释器找不到。
下面用 PowerShell 辅助函数保证编码正确。

1
2
3
4
5
function Write-UnixScript($path, $content) {
$text = $content -replace "`r`n", "`n" -replace "`r", "`n"
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllBytes($path, $utf8NoBom.GetBytes($text))
}

start.sh 可用 nohup 后台运行,并把 PID 写入文件,便于 stop.sh 按 PID 停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
cd "$(dirname "$0")"
PID_FILE="my-app.pid"
LOG_FILE="my-app.log"

if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "Already running (PID $(cat "$PID_FILE"))"
exit 0
fi

chmod +x ./my-app
nohup ./my-app >> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "Started (PID $(cat "$PID_FILE"))"

应用侧建议把静态资源、配置、数据库路径都设为相对可执行文件所在目录,这样 dist-linux 拷到任意路径都能直接跑,无需改环境变量。

验证

将整个 dist-linux 目录上传到 Linux 服务器后,先确认二进制架构与是否静态链接。

1
2
3
4
file ./my-app
ldd ./my-app # musl 静态链接时可能显示 not a dynamic executable 或仅 linux-vdso
chmod +x my-app start.sh stop.sh
./start.sh

查看日志与端口是否正常监听。

1
2
3
tail -f my-app.log
curl -s http://127.0.0.1:8080/health # 按实际端口与路由调整
./stop.sh

若出现 Exec format error,说明目标 triple 与服务器 CPU 架构不一致(例如在 ARM 服务器上用了 x86_64 产物)。
若提示缺少 .so,多半是依赖仍链到了动态库,回到 依赖选型 检查是否用了 OpenSSL 或未 bundled 的 C 库。

扩展

gnu 目标x86_64-unknown-linux-gnu 与 glibc 生态兼容更好,但在 Windows 上交叉链接通常比 musl + zigbuild 更折腾;若强依赖 glibc 动态库,可考虑 WSL、Docker 或 Linux CI 构建。

cross 工具cross 基于容器封装交叉编译环境,适合依赖复杂、本机难以配齐 linker 的项目,代价是需安装 Docker 且构建较慢。

环境变量:脚本里可把 TARGETDIST_DIR 做成可覆盖项,便于同一套脚本试不同 triple 或输出路径。

1
2
$Target = if ($env:TARGET) { $env:TARGET } else { 'x86_64-unknown-linux-musl' }
$DistDir = if ($env:DIST_DIR) { $env:DIST_DIR } else { 'dist-linux' }

增量打包:重新构建时只替换二进制与 web/,不要删除服务器上已有的 data/ 与用户改过的 config.json,避免升级丢数据。

总结

  1. 安装 x86_64-unknown-linux-musl target、cargo-zigbuild 与 Zig。
  2. Cargo.toml 中尽量使用 rustls、bundled 等减少系统 C 库依赖。
  3. cargo zigbuild --release --target x86_64-unknown-linux-musl 生成 Linux 二进制。
  4. 将二进制、静态资源、配置与 LF 格式的启停脚本打入 dist-linux,整目录部署。
  5. 在目标 Linux 上用 fileldd 与一次完整启停验证产物是否可用。

musl 静态二进制体积可能略大于动态链接的 gnu 版,但「Windows 开发、Linux 上线、单文件可拷贝」的链路足够简单,适合多数自研后端与内部工具。