Windows 交叉编译 Linux 程序

最近碰到一个需求是要在 Windows 上交叉编译 Linux 程序,用于做 Windows 代码的跨平台编译检查,发现里面弯弯绕绕还挺多的(主要是 Windows 和 Linux 系统层面的一些差异需要注意),就通过一篇博客记录一下整个交叉编译的流程,也加深对交叉编译流程的理解。

整个交叉编译包含2个主要步骤:sysroot 准备和交叉编译工具链的构建。

  • sysroot 在 WSL 上使用 debootstrap 创建

  • 交叉编译工具链可通过两种方式生成:

    • 通过 Cygwin 和 crosstool-ng 生成
    • 通过 Linux 和 crosstool-ng 以 canadian cross 方式生成

安装 WSL

使用 WSL(Windows Subsystem for Linux)主要是为了以下2件事:

  1. 创建交叉编译的 sysroot

  2. 验证交叉编译产物结果

通过 WSL 可以方便地与 Windows 文件系统进行交互

导入 WSL 镜像

可从清华源获取 Ubuntu 的 WSL 镜像(本次使用的是 Ubuntu 20.04.6):

使用如下命令导入 WSL 镜像:

1
wsl --import <发行版名称> <本机目录> <镜像路径>

完整命令示例如下:

1
wsl --import Ubuntu-20.04 D:\VM\WSL\Ubuntu-20.04 D:\Download\ubuntu-20.04.6-wsl-amd64.wsl

导入完成后可通过如下命令启动指定 Linux 发行版:

1
wsl -d <发行版名称>
Tip

直接 --import 的实例默认只有 root,与商店安装的「自动创建用户」不同,需要手动建普通用户并设为默认登录用户。

(可选)创建普通用户

在 WSL 内执行如下命令创建用户

1
2
adduser <user-name>
usermod -aG sudo <user-name>

编辑 /etc/wsl.conf 设置默认用户:

1
2
3
4
5
[boot]
systemd=true

[user]
default=<user-name>

在 Windows 侧执行 wsl --shutdown 后重新进入 WSL,确认已以普通用户登录。

(可选)更换软件源

参照阿里云 Ubuntu 软件源说明,根据实际使用的 Ubuntu 版本替换 /etc/apt/sources.list 中地址即可

替换之后记得执行 update 和 upgrade

1
2
sudo apt update
sudo apt upgrade

准备 sysroot

Note

sysroot(system root) 是用于编译/交叉编译的“目标系统根目录视图”,仅包含编译时需要的部分(如头文件、库文件、动态链接器等),无法通过 chroot 方式执行程序

rootfs(root filesystem)则是用于运行阶段的完整系统根目录,包含完整的 Linux 系统结构/bin/sbin/etc/usr/var 等),可以通过 chroot 方式进入并执行程序

对 rootfs 裁剪掉不必要的文件后可得到 sysroot。

安装依赖

1
sudo apt install debootstrap systemd-container qemu-user-static binfmt-support
  • debootstrap:从镜像站拉取包并初始化 rootfs。
  • systemd-container:以容器方式进入 rootfs,并自动挂载目录。
  • qemu-user-static:用于支持在非本机 ISA(Instruction Set Architecture,指令集架构) 的 rootfs 里执行目标程序(例如做 aarch64 的 sysroot 时很有用)
  • binfmt-support:让 Linux 支持运行非本机 ISA 的 ELF 程序,可以自动调用 qemu 执行 arm 应用。

初始化 rootfs

通过如下命令初始化 rootfs

1
2
3
4
5
6
sudo debootstrap \
--arch=amd64 \
--variant=minbase \
focal \
/opt/rootfs-focal-amd64 \
https://mirrors.aliyun.com/ubuntu/

执行完成后会提示 “Base system installed successfully”(基础系统成功安装)

初始化 aarch64 rootfs

在 x86_64 机器上也可以直接创建 arm 的 rootfs,只不过中间步骤略微繁琐一点,需要通过 qemu 进行转义,同时 arm 的 ubuntu 软件源地址也和 x86_64 的不一致(https://mirrors.aliyun.com/ubuntu-ports/),需要额外注意。

操作步骤如下:

  1. 初始化 rootfs(注意--arch--foreign 参数,为了区分,这里创建的是 Ubuntu 18 的 rootfs)
1
2
3
4
5
6
sudo debootstrap \
--arch=arm64 \
--foreign \
bionic \
/opt/rootfs-bionic-arm64 \
https://mirrors.aliyun.com/ubuntu-ports/
  1. 拷贝 qemu-aarch64-static,便于 x86_64 上进入 aarch64 rootfs 执行程序
1
sudo cp /usr/bin/qemu-aarch64-static /opt/rootfs-bionic-arm64/
  1. 进入 rootfs 完成 debootstrap 后续操作
1
2
3
sudo systemd-nspawn \
-D /opt/rootfs-bionic-arm64 \
/debootstrap/debootstrap --second-stage

完成后输出如下:

进入 rootfs

直接通过 chroot 可以进入 rootfs 安装程序,但是通常运行时需要手动挂载 dev、run、proc 等目录,为简化操作,通过 systemd-nspawn 以容器方式进入 rootfs,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
set -euo pipefail

SYSROOT=/opt/rootfs-focal-amd64
SRC=/home/xiao/devenv

sudo systemd-nspawn \
-D "$SYSROOT" \
--bind="$SRC:/mnt/project" \
--bind=/etc/resolv.conf \
--setenv=HOME=/root \
--setenv=TERM="$TERM" \
--hostname=sysroot-env \
--console=interactive \
/bin/bash

将脚本保存到本地执行即可,成功进入 rootfs 后输出如下所示:

进入 aarch64 rootfs

启动脚本和 x86-64 类似,只需要 rootfs 内包含用于转译的 qemu-aarch64-static 程序即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
set -euo pipefail

SYSROOT=/opt/rootfs-bionic-arm64
SRC=/home/xiao/devenv

sudo systemd-nspawn \
-D "$SYSROOT" \
--bind="$SRC:/mnt/project" \
--bind=/etc/resolv.conf \
--setenv=HOME=/root \
--setenv=TERM="$TERM" \
--hostname=sysroot-env \
--console=interactive \
/bin/bash

启动后效果如下:

安装依赖

由于 debootstrap 创建 rootfs 时写入的默认软件源往往不完整,在安装依赖前需要重新调整软件源,命令如下所示:

1
2
3
4
5
tee /etc/apt/sources.list > /dev/null <<'EOF'
deb https://mirrors.aliyun.com/ubuntu/ focal main universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ focal-updates main universe multiverse
deb https://mirrors.aliyun.com/ubuntu/ focal-security main universe multiverse
EOF

然后通过如下命令更新软件源和软件。

1
2
apt update
apt upgrade -y

为满足后续验证需要,我们在 rootfs 内安装下列依赖:

1
2
3
4
5
6
7
8
9
10
apt install -y \
build-essential \
pkg-config \
ca-certificates \
tzdata \
locales \
git \
python3 \
python3-pip \
python3-venv

同时配置 pip 镜像并安装 conan、cmake 和 ninja(通过 pip 安装的 cmake 和 ninja 版本较新,便于使用):

1
2
pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/
pip install conan cmake ninja

若后续要在 sysroot 内编译或验证 GLFW 等依赖 X11/Wayland 的库,可额外安装开发包(体积较大,按需装):

1
apt install -y libwayland-dev libxkbcommon-dev xorg-dev

配置完成后使用 exit 命令退出 rootfs。

创建 sysroot

rootfs 创建完成后,可拷贝 lib、include 和 pkgconfig 目录以生成 sysroot,具体命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/env bash
set -e

SRC=$1
DST=$2

if [ -z "$SRC" ] || [ -z "$DST" ]; then
echo "Usage: $0 <rootfs> <sysroot>"
exit 1
fi

mkdir -p "$DST"

echo "[1/3] Copy essential directories..."

rsync -aHAXm --numeric-ids \
--include="/usr/***" \
--include="/lib" \
--include="/lib64" \
--include="/lib32" \
--include="/libx32" \
--include="*/" \
--exclude="*" \
"$SRC"/ "$DST"/

fix_absolute_symlinks_for_sysroot() {
local SYSROOT="$1"
while IFS= read -r -d '' link; do
tgt=$(readlink "$link")
case "$tgt" in
/lib/*-linux-gnu/*)
base="${tgt##*/}"
tri="${tgt#/lib/}"
tri="${tri%%/*}"
ln -sf "../../../lib/$tri/$base" "$link"
;;
/usr/lib/*-linux-gnu/*)
base="${tgt##*/}"
ln -sf "$base" "$link"
;;
esac
done < <(find "$SYSROOT/usr/lib" -type l -print0 2>/dev/null)

while IFS= read -r -d '' link; do
tgt=$(readlink "$link")
case "$tgt" in
/lib/*-linux-gnu/*)
base="${tgt##*/}"
ln -sf "$base" "$link"
;;
esac
done < <(find "$SYSROOT/lib" -type l -print0 2>/dev/null)
}

ensure_lib64_ld_linux_symlink() {
local SYSROOT="$1"
mkdir -p "$SYSROOT/lib64"
if [[ -f "$SYSROOT/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" ]]; then
ln -sf ../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 \
"$SYSROOT/lib64/ld-linux-x86-64.so.2"
fi
}

ensure_lib64_ld_linux_symlink "$DST"
fix_absolute_symlinks_for_sysroot "$DST"

echo "[2/3] Ensure loader exists..."

find "$DST" -name "ld-linux*" || {
echo "ERROR: dynamic loader not found"
exit 1
}

echo "[3/3] Basic validation..."

echo "- libc:"
find "$DST" -name "libc.so*"

echo "- interpreter candidates:"
find "$DST" -name "ld-linux*"

echo "Done."

其中 ensure_lib64_ld_linux_symlinkfix_absolute_symlinks_for_sysroot 是必要的,因为 rootfs 通过 chroot 执行,其软链接指向根路径的地址是有效的,而在交叉编译中,不可能通过 chroot 方式执行,因此需要手动修正这些软链接的地址,将其转换为相对路径。

打包和解压

打包命令(在 WSL 中执行)

1
2
3
4
5
tar --numeric-owner \
--xattrs \
--acls \
-czf sysroot-focal.tar.gz \
-C "<sysroot 在 WSL 中的位置,例如 /opt/sysroot-focal>" .

解压命令(在 Cygwin 中执行)

1
2
3
4
5
6
sysroot_dir="<sysroot 在 Cygwin 中的位置,例如 /cygdrive/d/sysroot-focal>"
mkdir -p $sysroot_dir
tar -xzf sysroot-focal.tar.gz \
--no-same-owner \
--no-same-permissions \
-C "$sysroot_dir"

Cygwin 构建交叉编译工具链

安装 Cygwin

为在 Windows 上编译 Linux 的可执行程序,需要模拟出 Linux 运行环境(基本上就是模拟出 POSIX 接口),由于 MSYS2 的工具链更新太过激进,编译低版本的工具链可能出现各种各样的问题,这里选择 Cygwin 作为交叉编译的基础环境。

安装依赖

从官网下载 setup-x86_64.exe,在包列表中勾选下列依赖(具体版本参考下文的完整安装列表):

  • autoconf, automake, bison, flex, gawk,
  • help2man, texinfo, diffutils, patch, make,
  • cmake, ninja, gcc-g++, git, wget, xz, zip, unzip,
  • libtool, gperf, libncurses-devel, python312-devel

完整安装列表见:cygcheck-c.txt(通过 cygcheck -c 输出)

Tip

建议将 setup-x86_64.exe 放在 Cygwin 的安装目录下面,因为该程序将作为 cygwin 的包管理器,有可能需要频繁使用该程序安装包。

设置环境变量

Cygwin 的 shell 配置文件(如 ~/.bashrc)末尾中加入下列内容:

1
2
3
4
5
6
7
8
export CYGWIN=winsymlinks:sys
export PATH="/usr/local/bin:/usr/bin"
export LANG=C.UTF-8
export LC_ALL=C.UTF-8
export http_proxy="http://127.0.0.1:7890"
export https_proxy="http://127.0.0.1:7890"
# 可选:少数工具只认 ALL_PROXY
# export ALL_PROXY="$https_proxy"

其中 代理地址 用于 crosstool-ng 编译时下载源码。

(可选)卸载 Cygwin

仅删除安装目录时可能残留注册表项;可用下列 PowerShell 脚本(需 PowerShell 7+,删 HKLM 需管理员权限)。

保存为 uninstall_cygwin.ps1 后执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#requires -Version 7.0
# uninstall_cygwin.ps1 — 仅删除安装目录 + 清理 Cygwin 相关注册表项
# 请用 PowerShell 7 运行: pwsh -NoProfile -File .\uninstall_cygwin.ps1
# 运行前请手动关闭所有 Cygwin / mintty 窗口,否则删除目录可能失败。
# 删除 HKLM 下项需要管理员权限。

Write-Host "== Cygwin 卸载(目录 + 注册表)=="

# -------- 可配置路径(若安装在其他盘符,请在此添加)--------
$cygwinPaths = @(
"C:\cygwin64",
"C:\cygwin"
)

$regPaths = @(
"HKCU:\Software\Cygwin",
"HKLM:\SOFTWARE\Cygwin",
"HKLM:\SOFTWARE\WOW6432Node\Cygwin"
)

Write-Host "[1/2] 删除安装目录..."
foreach ($path in $cygwinPaths) {
if (Test-Path -LiteralPath $path) {
Write-Host " 正在删除: $path"
Remove-Item -LiteralPath $path -Recurse -Force -ErrorAction Stop
}
else {
Write-Host " 跳过(不存在): $path"
}
}

Write-Host "[2/2] 清理注册表..."
foreach ($reg in $regPaths) {
if (Test-Path -LiteralPath $reg) {
Write-Host " 正在删除: $reg"
Remove-Item -LiteralPath $reg -Recurse -Force -ErrorAction Stop
}
}

Write-Host "== 完成 =="
Write-Host "若曾把 Cygwin 加入系统 PATH,请自行在「环境变量」中删掉含 cygwin 的条目。"

配置工作目录

执行下列命令设置文件夹路径大小写敏感(需要管理员权限,Win11 下使用开发者模式也可以设置)

1
fsutil.exe file SetCaseSensitiveInfo "D:\workspace" enable
Tip

如果觉得路径太长,可以直接将该路径链接到 ~

1
ln -s /cygdrive/d/workspace ~/workspace

安装 crosstool-ng

基于版本 1.28.0

解压后在源码目录外建 build

1
2
3
4
mkdir build && cd build
../configure --prefix=<ct-ng 安装目录,例如 /cygdrive/d/ctng-build/install>
make -j
make install

bin 目录加入 PATH

1
export PATH="<ct-ng 安装目录>/bin:/usr/local/bin:/usr/bin"

例如:

1
export PATH="/cygdrive/d/ctng-build/install/bin:/usr/local/bin:/usr/bin"

编译并安装完成后通过如下命令检查是否安装成功:

1
ct-ng version

配置工具链

使用菜单进行配置或直接使用已经配置好的 config.HOST-x86_64-cygwin-gnu-x86_64-pc-linux-gnu.txt

1
ct-ng menuconfig

配置时的一些注意事项:

  1. Linux 头文件版本:在满足程序需求的前提下尽量别追新,保持兼容性(本次选择的 Linux 内核版本为 4.4.302)。
  2. glibc / gcc / binutils 组合要与目标环境匹配;一般直接对齐某个发行版即可(例如 Ubuntu 20.04:glibc 2.31 + gcc 9 + binutils 2.34 一类组合,具体以 sysroot 中 glibc 和 binutils 版本为准)。
  3. 下载 tarball 慢时,可在配置里改为国内镜像(例如清华源)。

构建工具链

通过下列命令执行构建

1
ct-ng build

构建时间较长(1h~2h),日志里若出现下载失败,多半是网络或镜像问题。

构建完成后日志输出如下:

如果编译时碰到 internal compiler error 且位置随机,多半是 cygwin 并行执行时出现问题,建议将并行数降低或改为串行执行。可通过如下命令进行调整。

1
ct-ng build.1

基本功能验证

确认编译器版本(可先将 toolchain 的 bin 目录加入 Path 中):

1
x86_64-linux-gnu-gcc --version

输出如下:

然后通过一个简单的 C 程序(hello.c)验证编译效果

1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf("hello from Cygwin!\n");
return 0;
}

通过 gcc 进行编译,会默认查找头文件并链接到 glibc

1
x86_64-linux-gnu-gcc hello.c -o hello

并通过 file 命令查看二进制情况

1
file hello

输出如下:

将 hello 程序拷贝至 WSL 中,通过 ldd 检查 libc 链接情况

最后执行 hello,预期应该会观察到有 hello from Cygwin! 输出,同时返回值为 0

Linux 构建交叉编译工具链

Cygwin 上编译的程序会依赖 Cygwin-1.dll 做 POSIX 接口到 WinAPI 的翻译,导致其性能和稳定性都不如原生 Windows 程序高,做一些简单的验证没问题,但是在复杂场景上会因为各种兼容性问题导致编译的 .o 损坏、编译器 ICE(internal compile error)等,为了更好的适配实际场景,可以通过在 Linux 上编译出 Windows 上运行的交叉编译工具链。

Note

由于编译工具链的平台和工具链运行的平台不一致,这种编译方式就成为 Canadian Cross 编译方式

交叉编译中有3个平台概念(triplet):

build编译工具链 的平台

host运行工具链 的平台

target运行编译产物 的平台

如果 build 平台和 host 平台不一致,就需要2步才能完成交叉编译工具链的构建:

  1. 构建出 (<build>-<build>-<host>) 的工具链,称为前置准备工具链(prerequisite tools)

    或者至少要求能有一个在 build 平台上能编译 host 平台程序的工具,基本也就是一个精简/全量的交叉编译工具了

  2. 利用 step 1 中准备的工具链,构建出最终需要的(<build>-<host>-<target>)交叉编译的工具链

这种方式相当于是用 交叉编译工具 构建 交叉编译工具,有点套娃的意思。

这种方式听起来很复杂,相比 Cygwin 方案需要多准备一些东西,但 crosstool-ng 已经封装好了相关的编译脚本,只需要将依赖的软件包准备好,剩下的操作基本就和 Cygwin 上一致了,而且由于是原生 Linux 系统,相对于 Cygwin 的运行速度更快,且更稳定。

安装依赖

以 Ubuntu 20.04 为例,建议分两组安装依赖。

先安装基础依赖:

1
2
3
4
5
sudo apt update
sudo apt install -y \
build-essential gperf bison flex texinfo help2man gawk libtool-bin \
libncurses5-dev python3-dev automake autoconf unzip rsync curl file \
patch wget xz-utils zip p7zip-full cmake ninja-build pkg-config

再安装交叉编译windows工具链使用的的 mingw 依赖:

1
2
3
sudo apt install -y \
gcc-mingw-w64-x86-64 \
g++-mingw-w64-x86-64

crosstool-ng 的安装方式同 Cygwin 一致,这里不过多赘述。

配置工具链

当前使用的 triplet 为:

  • build: x86_64-pc-linux-gnu(WSL / Ubuntu)
  • host: x86_64-w64-mingw32(工具链运行在 Windows)
  • target: x86_64-pc-linux-gnu(最终程序运行在 Linux x86_64 上)
Tip

如果需要交叉编译 aarch64,只需要将 target 切换为 aarch64-pc-linux-gnu 即可

也就是 x86_64-w64-mingw32,x86_64-pc-linux-gnu(Canadian Cross)。

先查看可用 samples(建议先确认名称再创建):

1
ct-ng list-samples | grep "mingw32.*linux-gnu"

输出结果如下:

创建工作目录并基于 sample 生成 .config

1
2
3
mkdir -p ~/devenv/mingw-cross
cd ~/devenv/mingw-cross
ct-ng x86_64-w64-mingw32,x86_64-pc-linux-gnu

在此基础上可通过 menuconfig 做交互调整:

1
ct-ng menuconfig

修改后建议执行一次默认化补全并核对:

1
2
ct-ng olddefconfig
ct-ng show-config

输出如下

本次实践里比较关键的版本组合(对标 Ubuntu 20.04,如果需要其他的对标版本,例如 Ubuntu 18.04,则根据实际需要调整即可):

  • Linux headers: 4.4.302
  • glibc: 2.31(Ubuntu 18 使用 2.27)
  • binutils: 2.34(Ubuntu 18 使用 2.30)
  • gcc: 9.5.0

为降低复杂度,可先关闭不必要组件(例如 ltrace/strace),gdb 推荐先使用相对稳定版本(如 12.1)。

配置时的几个注意事项:

  1. ct-ng x86_64-w64-mingw32,x86_64-pc-linux-gnu 只负责生成初始 .config,后续改动都要在同一目录内进行。
  2. 直接编辑 .config 后,一定执行 ct-ng olddefconfig,否则部分新旧键可能不一致。
  3. CT_PREFIX_DIR 建议指向独立目录(例如 ${HOME}/x-tools/...),避免覆盖历史构建结果。
  4. CT_LOCAL_TARBALLS_DIR 建议设置缓存目录,重复构建可显著减少下载时间。
  5. 如果在 Ubuntu 20.04 上对齐 sysroot,建议保持 glibc/binutils/linux headers 与目标基线一致,避免链接期和运行期偏差。

此时会在当前目录生成 .config。本次实际使用并验证通过的配置如下:

打包工具链和 sysroot

这种方式构建出来的工具链可以直接在 Windows 环境下运行,而不依赖 msys2 / cygwin 等环境,只需要额外处理软链接问题即可

最简单的处理方式就是将软链接全部转换为实际的文件(dereference),虽然会造成空间的浪费,但是这种方案实现起来简单,而且处理之后可以直接在 windows 上使用,但Windows 上仍需要保证文件夹的路径大小写敏感。处理脚本为 dereference_symlink.py

交叉编译验证

最后,我们需要结合 sysroot 和编译好的 toolchain,对 CMake 项目进行编译检查,并通过项目内置的单元测试检查功能是否正常。

对于 CMake 的 toolchain 文件,由于我们切换为 WSL 中生成的 sysroot,需要调整 toolchain.cmake 中部分路径和编译选项 ,完整内容如下(toolchain-focal.cmake):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)

set(_P "${CMAKE_CURRENT_LIST_DIR}")
set(CMAKE_C_COMPILER "${_P}/bin/x86_64-linux-gnu-gcc")
set(CMAKE_CXX_COMPILER "${_P}/bin/x86_64-linux-gnu-g++")

set(_SYS "${_P}/x86_64-linux-gnu/sysroot-focal")
set(_LIB "${_SYS}/usr/lib/x86_64-linux-gnu")
set(CMAKE_SYSROOT "${_SYS}")
set(CMAKE_FIND_ROOT_PATH "${_SYS}")

set(CMAKE_C_FLAGS_INIT "-B${_LIB}")
set(CMAKE_CXX_FLAGS_INIT "-B${_LIB}")
set(CMAKE_EXE_LINKER_FLAGS_INIT "-pthread -Wl,-rpath-link,${_LIB}")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-pthread -Wl,-rpath-link,${_LIB}")

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

相比 ct-ng 自动生成的 sysroot,Ubuntu 下会多一层标识系统架构的三元组目录(<架构-系统-libc 类型>,例如 x86_64-linux-gnu),需要修正库文件的路径:

  • 通过 -B 指定工具链组件位置(例如 crt1.ocrti.o 等),将 /usr/lib 转换为 /usr/lib/x86_64-linux-gnu
  • 通过 -Wl,-rpath-link,<lib-dir> 指定链接时间接依赖搜索的系统库位置,将 /usr/lib 转换为 /usr/lib/x86_64-linux-gnu

在 CMake 配置阶段通过 -DCMAKE_TOOLCHAIN_FILE=... 指定上述 toolchain 文件路径即可。

为方便直接测试,可将 WSL 与 Cygwin 中的 workspace 用软链接对齐为同一路径,从而保证 ctest 记录的可执行文件路径一致(在 WSL 中运行位于 Windows 文件系统上的构建产物时,通常会有一定性能损耗)。

可通过如下命令创建软链接

1
2
cd ~
ln -s <workspace 在 Cygwin 或 WSL 上的实际路径> workspace

{fmt}

源码:https://github.com/fmtlib/fmt/releases/download/12.1.0/fmt-12.1.0.zip

编译命令(Cygwin 中执行):

1
2
3
4
5
6
7
cmake -S . -B build \
-DFMT_DOC=OFF \
-DFMT_INSTALL=OFF \
-DFMT_TEST=ON \
-DCMAKE_TOOLCHAIN_FILE=$toolchain_path \
-G Ninja
cmake --build build

WSL 中 ctest 执行结果:

Catch2

源码:https://github.com/catchorg/Catch2/archive/refs/tags/v3.14.0.tar.gz

编译命令(Cygwin 中执行):

1
2
3
4
5
6
7
8
9
10
11
12
cmake -S . -B build \
-DCATCH_DEVELOPMENT_BUILD=ON\
-DCATCH_INSTALL_DOCS=OFF \
-DCATCH_INSTALL_EXTRAS=OFF \
-DCATCH_ENABLE_REPRODUCIBLE_BUILD=ON \
-DCATCH_BUILD_TESTING=ON \
-DCATCH_BUILD_EXAMPLES=ON \
-DCATCH_BUILD_EXTRA_TESTS=ON \
-DCATCH_BUILD_BENCHMARKS=ON \
-DCMAKE_TOOLCHAIN_FILE=$toolchain_path \
-G Ninja
cmake --build build

WSL 中 ctest 执行结果(CMake 配置阶段读取到了宿主机器上安装的 Python,但 WSL 内暂未安装 Python 3.12,导致 Catch2 的这几个测试用例无法运行,直接跳过):

sqlite

源码:https://sqlite.org/2026/sqlite-amalgamation-3530100.zip

由于 sqlite 的测试依赖 tcl,且需要编译 sqlite-tcl 插件才行,较为繁琐,这里仅做基本的功能验证。

准备一个 CMakeLists.txt 用于编译 sqlite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
cmake_minimum_required(VERSION 3.16)
project(sqlite3-amalgamation C CXX)

enable_testing()

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

set(SQLITE_DIR ${CMAKE_SOURCE_DIR})

add_compile_definitions(
SQLITE_THREADSAFE=1
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
)

add_library(sqlite3 STATIC
${SQLITE_DIR}/sqlite3.c
)

target_include_directories(sqlite3 PUBLIC
${SQLITE_DIR}
)

target_link_libraries(sqlite3 PUBLIC
Threads::Threads
dl
m
)

add_executable(sqlite3_cli
${SQLITE_DIR}/shell.c
)

target_link_libraries(sqlite3_cli PRIVATE
sqlite3
)

add_test(
NAME sqlite_basic_test
COMMAND sqlite3_cli :memory: ".read ${CMAKE_SOURCE_DIR}/test.sql"
)

set_tests_properties(sqlite_basic_test PROPERTIES
PASS_REGULAR_EXPRESSION "ok"
)

测试的 sql 文件

1
2
3
CREATE TABLE t(x INTEGER);
INSERT INTO t VALUES(42);
SELECT 'ok';

编译命令(Cygwin 中执行):

1
2
3
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE=$toolchain_path
cmake --build build

WSL 中 ctest 执行结果:

WSL 中 sqlite3_cli 调用输出示例:

Abseil

Abseil 的测试依赖 GoogleTest,需要将两个源码都解压出来

编译命令(Cygwin 中执行):

1
2
3
4
5
6
7
8
9
10
cmake -S . -B build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$toolchain_path \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_CXX_STANDARD_REQUIRED=ON \
-DBUILD_TESTING=ON \
-DABSL_BUILD_TESTING=ON \
-DABSL_ENABLE_INSTALL=OFF \
-DABSL_LOCAL_GOOGLETEST_DIR=../googletest-1.17.0
cmake --build build

WSL 中 ctest 执行结果:

GLFW

源码:https://github.com/glfw/glfw/archive/refs/tags/3.4.tar.gz

编译命令(Cygwin 中执行):

1
2
3
4
5
6
7
8
cmake -S . -B build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$toolchain_path \
-DCMAKE_BUILD_TYPE=Release \
-DGLFW_BUILD_EXAMPLES=ON \
-DGLFW_BUILD_TESTS=ON \
-DGLFW_BUILD_DOCS=OFF \
-DGLFW_BUILD_WAYLAND=OFF
cmake --build build

由于 GLFW 不支持 ctest,只能手动执行 build/tests 下的单元测试,而且必须手动关闭弹出的 GUI 窗口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
set -euo pipefail

# 将 BIN 设为测试可执行文件所在目录(请按实际构建输出修改,例如 GLFW 常为 build/tests)
BIN="${BIN:-build/tests}"

uname -a

if [[ ! -d "${BIN}" ]]; then
echo "Not found: ${BIN}" >&2
exit 1
fi

failed=0
n=0
shopt -s nullglob
for path in "${BIN}"/*; do
[[ -f "${path}" && -x "${path}" ]] || continue
echo "==> ${path}"
set +e
"${path}" > /dev/null 2>&1
rc=$?
set -e
if [[ "${rc}" -ne 0 ]]; then
echo "FAILED: ${path} (exit ${rc})" >&2
failed=$((failed + 1))
fi
n=$((n + 1))
done

echo "==> Done: ${n} program(s), ${failed} failed."
[[ "${failed}" -eq 0 ]]

同时下图展示了 WSL 下运行 glfw heightmap 案例的效果: