☰
Windows 上用 MSYS2 交叉编译 XLSXIO 实战指南
2026/10/12 11:53:11 网站建设 项目流程

1. 为什么要在 Windows 上折腾 MSYS2 而不是直接装个虚拟机

很多人第一次听到“在 Windows 上编译 Linux 程序”这件事,第一反应是开个虚拟机或者用 WSL。虚拟机方案确实省心,但代价是资源占用高、文件系统隔了一层、和宿主机的交互也麻烦。WSL 这几年体验好了不少,可一旦涉及到需要和 Windows 侧的构建产物做交叉验证、或者团队里有人坚持用原生 Windows 工具链时,WSL 的边界就开始显现了。

MSYS2 走的是另一条路。它本质上是把一套 POSIX 兼容层和包管理器搬到了 Windows 上,让你能在原生 Windows 环境里使用类 Unix 的 shell、编译器和构建工具。用它来做交叉编译,核心价值在于:你可以在同一台机器上,既保留 Windows 的开发习惯,又能产出面向其他平台的可执行文件或库。对于 XLSXIO 这类需要处理 Excel 文件格式的库来说,交叉编译的意义在于——你可以在 Windows 上完成开发和初步验证,然后直接产出能在目标平台上跑的二进制,省去来回切换环境的折腾。

这篇文章面向的是有一定命令行基础、但没怎么在 Windows 上做过交叉编译的开发者。我会从 MSYS2 的安装配置讲起,一步步走到用交叉编译工具链编译 XLSXIO,中间会解释每个关键选择背后的理由,也会分享一些我在实际操作中踩过的坑。你不需要事先精通 Makefile 或 CMake,但至少得知道./configure和make大概是在干什么。

提示:本文假设你使用的是 64 位 Windows 10 或 Windows 11,并且有管理员权限来安装软件。如果你用的是公司配发的电脑,安装前最好确认一下安全策略是否允许运行未签名的可执行文件。

2. MSYS2 的安装与基础环境配置

2.1 下载与安装时的路径选择

MSYS2 的安装包可以从其官网获取,安装过程本身是标准的 Windows 安装向导,但有一个细节值得注意:安装路径不要包含空格和中文。默认路径通常是C:\msys64,这个就很好。我见过有人把它装到C:\Program Files\MSYS2下面,结果后续编译时各种路径解析问题层出不穷。原因在于 MSYS2 内部的很多脚本和 Makefile 对空格的处理并不完美,虽然理论上可以用引号包裹,但一旦涉及嵌套调用,转义就会变得非常脆弱。

安装完成后,你会看到开始菜单里多了几个快捷方式,分别是 MSYS2 MSYS、MSYS2 MINGW64、MSYS2 MINGW32、MSYS2 UCRT64 等。这些的区别在于它们启动的 shell 环境不同,预置的编译器工具链也不同。简单来说:

  • MSYS:最接近纯 POSIX 的环境,适合跑 autotools 脚本和 configure。
  • MINGW64:使用 MinGW-w64 工具链,编译出来的是原生 Windows 程序,依赖 msvcrt。
  • UCRT64:同样用 MinGW-w64,但依赖的是 Windows 10 之后的 Universal C Runtime,兼容性更好。

对于交叉编译来说,我们主要会在 MSYS 环境里操作,因为 configure 脚本和 Makefile 在纯 POSIX 环境下跑得最顺。但编译出来的目标文件是给别的平台用的,所以最终链接时用的是交叉工具链,而不是 MINGW64 自带的 gcc。

2.2 包管理器的基本用法与镜像加速

MSYS2 使用pacman作为包管理器,这个和 Arch Linux 上的是同一个。第一次使用前,建议先更新一下包数据库和已安装的包:

pacman -Syu

这个命令会先同步数据库,然后升级所有已安装的包。注意,如果它提示你需要关闭终端重新打开,那就照做,然后再跑一次pacman -Syu确保全部更新完毕。

国内用户可能会觉得下载速度慢,这时候可以换镜像源。编辑/etc/pacman.d/mirrorlist.mingw64、/etc/pacman.d/mirrorlist.msys等文件,把国内的镜像地址放到前面。具体哪个镜像最快,取决于你所在的网络环境,可以多试几个。换完源之后记得再跑一次pacman -Syu让数据库生效。

安装交叉编译工具链之前,先把基础开发工具装好:

pacman -S base-devel git wget unzip

base-devel是一组基础开发工具的集合,包括 gcc、make、autoconf、automake、libtool 等。这些在后续编译 XLSXIO 时都会用到。git用来拉取源码,wget和unzip用来下载和解压依赖包。

2.3 交叉编译工具链的选型与安装

交叉编译工具链的选择取决于你的目标平台。假设我们的目标是一个常见的嵌入式 Linux 平台,那么需要安装对应的mingw-w64-x86_64-系列包吗?不是的,这里有个容易混淆的地方:MSYS2 的 pacman 仓库里提供的mingw-w64-前缀包,是用来在 Windows 上编译 Windows 程序的,不是用来交叉编译到 Linux 的。

真正的交叉编译工具链,比如arm-linux-gnueabihf-gcc或者aarch64-linux-gnu-gcc,在 MSYS2 的官方仓库里并不直接提供。你需要自己下载预编译的工具链,或者从源码构建。对于大多数场景,下载预编译的工具链是更实际的选择。常见的来源包括 Linaro、Bootlin 等,它们提供了针对不同架构和 libc 的预编译工具链。

下载下来通常是一个压缩包,解压到某个目录,比如/opt/toolchain,然后把bin目录加到PATH里:

export PATH=/opt/toolchain/bin:$PATH

你可以把这行加到~/.bashrc里,这样每次打开 MSYS 终端都会自动生效。验证一下工具链是否可用:

arm-linux-gnueabihf-gcc --version

如果能看到版本信息,说明工具链已经就位。接下来还需要确认目标平台的 sysroot 是否完整,因为交叉编译时头文件和库文件都来自 sysroot,而不是宿主机的/usr/include。工具链压缩包里通常会自带一个 sysroot,如果没有,你需要从目标平台的文件系统里拷贝一份过来。

注意:工具链的版本要和目标平台的 libc 版本匹配。如果目标平台用的是比较老的 glibc,而你用的工具链是基于新 glibc 构建的,编译出来的程序可能在目标平台上跑不起来,报GLIBC_2.xx not found之类的错误。这种情况下要么换工具链,要么在目标平台上升级 libc。

3. XLSXIO 的依赖梳理与交叉编译前的准备

3.1 XLSXIO 到底依赖了什么

XLSXIO 是一个用于读写 Excel XLSX 文件的 C 库,它的核心依赖包括 zlib(用于处理压缩的 XML 内容)和 expat 或 libxml2(用于解析 XML)。具体依赖哪个 XML 解析器,取决于编译时的配置选项。此外,它可能还会用到 minizip 或者自己实现的 zip 处理逻辑。

在开始交叉编译之前,必须先把这些依赖库也交叉编译好。这是一个典型的依赖链问题:XLSXIO 依赖 zlib 和 XML 解析器,而 XML 解析器可能又依赖 libiconv 之类的库。你需要按照依赖关系从底向上依次编译。

我建议在 MSYS2 里建一个专门的工作目录,比如~/cross,然后在里面分别建src、build、install三个子目录。src放源码,build放编译中间文件,install放最终安装的产物。这样做的目的是保持宿主机环境和交叉编译环境隔离,避免头文件和库文件互相污染。

3.2 交叉编译 zlib 的完整过程

zlib 的交叉编译相对简单,因为它不依赖其他库,而且构建系统是手写的 configure 脚本,对交叉编译的支持比较直接。

首先下载 zlib 源码并解压:

cd ~/cross/src wget https://zlib.net/zlib-1.3.1.tar.gz tar xzf zlib-1.3.1.tar.gz cd zlib-1.3.1

然后设置交叉编译所需的环境变量。这里的关键是CHOST和--prefix:

export CROSS_PREFIX=arm-linux-gnueabihf- export INSTALL_DIR=~/cross/install CHOST=${CROSS_PREFIX} ./configure --prefix=$INSTALL_DIR

CHOST告诉 configure 脚本使用哪个交叉编译器。zlib 的 configure 脚本会据此推导出CC、AR、RANLIB等工具的名称。如果工具链的命名规则不是标准的arm-linux-gnueabihf-前缀,你可能需要手动指定CC和AR。

configure 完成后,直接make和make install:

make -j$(nproc) make install

安装完成后,检查$INSTALL_DIR/lib下是否有libz.a或libz.so,以及$INSTALL_DIR/include下是否有zlib.h。如果有,说明 zlib 交叉编译成功。

3.3 XML 解析器的选择与编译要点

XML 解析器我通常选 expat,因为它足够轻量,依赖少,交叉编译起来比 libxml2 省事。libxml2 功能更全,但依赖链更长,可能会牵扯出 libiconv、liblzma 等一堆东西。

expat 的交叉编译流程和 zlib 类似:

cd ~/cross/src wget https://github.com/libexpat/libexpat/releases/download/R_2_6_0/expat-2.6.0.tar.gz tar xzf expat-2.6.0.tar.gz cd expat-2.6.0 ./configure --host=${CROSS_PREFIX%-} --prefix=$INSTALL_DIR make -j$(nproc) make install

注意这里的--host参数,它和 zlib 的CHOST作用类似,都是告诉 configure 脚本目标平台的工具链前缀。${CROSS_PREFIX%-}是一个 bash 参数扩展,用来去掉末尾的连字符,因为--host通常不需要末尾的连字符。

编译完成后,同样检查$INSTALL_DIR下是否有 expat 的头文件和库文件。

提示:如果你在 configure 阶段遇到 “cannot run test program while cross compiling” 之类的错误,说明某个测试程序试图在宿主机上运行目标平台的二进制。这时候需要找到对应的 configure 检查项,手动指定缓存变量来跳过。比如ac_cv_func_malloc_0_nonnull=yes这种。具体要跳过哪些,得看报错信息。

4. 用交叉工具链编译 XLSXIO 的实操步骤

4.1 获取源码与配置编译选项

XLSXIO 的源码可以从其项目仓库获取。假设你已经把源码放到了~/cross/src/xlsxio目录下。

进入源码目录,先看一下它的构建系统。XLSXIO 用的是 Makefile,不是 CMake 或 autotools。这意味着交叉编译的配置方式会有所不同,你需要通过环境变量或者直接修改 Makefile 来指定交叉编译器。

先试着跑一下make看看默认行为:

cd ~/cross/src/xlsxio make

如果它直接开始用宿主机的 gcc 编译,那就需要干预了。XLSXIO 的 Makefile 通常会读取CC、AR、RANLIB等环境变量,所以你可以这样:

make CC=${CROSS_PREFIX}gcc AR=${CROSS_PREFIX}ar RANLIB=${CROSS_PREFIX}ranlib \ CFLAGS="-I$INSTALL_DIR/include" \ LDFLAGS="-L$INSTALL_DIR/lib"

但这样还不够,因为 XLSXIO 可能还需要知道 zlib 和 expat 的位置。有些版本的 Makefile 支持ZLIB_CFLAGS、ZLIB_LIBS、EXPAT_CFLAGS、EXPAT_LIBS这样的变量,你可以显式传入:

make CC=${CROSS_PREFIX}gcc \ ZLIB_CFLAGS="-I$INSTALL_DIR/include" \ ZLIB_LIBS="-L$INSTALL_DIR/lib -lz" \ EXPAT_CFLAGS="-I$INSTALL_DIR/include" \ EXPAT_LIBS="-L$INSTALL_DIR/lib -lexpat"

如果 Makefile 不支持这些变量,那就需要手动编辑 Makefile,把CC、CFLAGS、LDFLAGS等改成对应的值。编辑之前先备份一份原始 Makefile,方便对照。

4.2 处理编译过程中的链接错误

交叉编译时最常见的错误就是链接阶段找不到库。比如:

undefined reference to `inflateInit2_'

这说明链接器没找到 zlib。检查一下LDFLAGS里是否包含了-L$INSTALL_DIR/lib,以及LIBS里是否有-lz。如果路径和库名都没问题,那可能是链接顺序的问题。在链接命令中,库的顺序很重要:依赖别人的库要放在被依赖的库前面。比如 XLSXIO 依赖 zlib,那么-lxlsxio应该放在-lz前面。

另一个常见错误是:

cannot find -lz

这通常是因为-L指定的路径不对,或者该路径下确实没有libz.so或libz.a。用ls $INSTALL_DIR/lib确认一下。如果只有静态库没有动态库,而链接器默认找动态库,那就加上-static或者显式指定-l:libz.a。

还有一种情况是头文件找不到:

fatal error: zlib.h: No such file or directory

这说明CFLAGS里的-I路径不对,或者 zlib 的头文件根本没安装到那个路径下。回到 zlib 的编译目录,确认make install是否成功执行,以及$INSTALL_DIR/include/zlib.h是否存在。

4.3 验证编译产物与目标平台测试

编译成功后,会在源码目录下生成libxlsxio_read和libxlsxio_write等库文件,以及一些示例程序。用file命令检查一下这些文件的架构:

file libxlsxio_read.so

如果输出显示ELF 32-bit LSB shared object, ARM, EABI5之类的信息,说明交叉编译成功。如果显示的是PE32+ executable (DLL) (console) x86-64,那说明用的还是宿主机的编译器,交叉编译没生效。

把生成的库文件和头文件拷贝到目标平台上,写一个简单的测试程序验证一下:

#include <xlsxio_read.h> #include <stdio.h> int main() { xlsxioreader reader = xlsxioread_open("test.xlsx"); if (!reader) { printf("Failed to open file\n"); return 1; } printf("File opened successfully\n"); xlsxioread_close(reader); return 0; }

用交叉编译器编译这个测试程序,然后放到目标平台上运行。如果目标平台没有动态库,记得用-static静态链接,或者把动态库也拷贝过去并设置好LD_LIBRARY_PATH。

注意:目标平台的 libc 版本和工具链的 libc 版本不一致时,即使编译通过,运行时也可能出问题。最稳妥的办法是在目标平台上直接编译,或者用和目标平台完全一致的 sysroot 来交叉编译。如果做不到,至少要在目标平台上做充分的测试。

5. 那些让我多花了两小时才搞定的坑

5.1 路径分隔符与反斜杠转义

MSYS2 虽然提供了 POSIX 兼容层,但它毕竟跑在 Windows 上,路径分隔符的问题时不时就会冒出来。比如你在 Makefile 里写了-I C:\msys64\home\user\cross\install\include,反斜杠会被 Make 解释为转义字符,导致路径变成C:msys64homeusercrossinstallinclude。正确的写法是用正斜杠:-I /home/user/cross/install/include,或者用 MSYS2 的路径转换工具cygpath。

另一个容易踩坑的地方是PATH环境变量。MSYS2 的PATH里既有 Windows 风格的路径,也有 Unix 风格的路径。当你调用交叉编译器时,它可能会去 Windows 的PATH里找一些工具,结果找到的是 Windows 版本而不是 MSYS2 版本。这种情况下,把 MSYS2 的/usr/bin和/mingw64/bin放到PATH的最前面通常能解决问题。

5.2 configure 脚本的缓存变量陷阱

交叉编译时,configure 脚本无法运行目标平台的测试程序,所以很多检查会失败或者给出错误的结果。这时候需要手动指定缓存变量。比如:

./configure --host=arm-linux-gnueabihf \ ac_cv_func_malloc_0_nonnull=yes \ ac_cv_func_realloc_0_nonnull=yes

这两个变量在编译很多 GNU 工具时都会用到,因为它们的 configure 脚本会尝试运行一个测试程序来判断malloc(0)是否返回非空指针。交叉编译时这个测试跑不了,所以得手动告诉它答案。

不同的库需要的缓存变量不一样,遇到报错时,仔细看 configure 的输出,找到那个失败的检查项,然后搜索对应的ac_cv_变量名。有时候需要翻 configure 脚本本身才能找到准确的变量名。

5.3 静态库与动态库的链接顺序

链接顺序这个问题在交叉编译时比本机编译更容易暴露,因为交叉工具链的默认行为可能和宿主机 gcc 不同。GNU ld 的规则是:从左到右扫描库,只保留当前未解析符号所需的库。这意味着如果-lz出现在-lxlsxio前面,而-lxlsxio里的符号引用了 zlib 的函数,链接器在处理-lz时还不知道后面需要这些符号,就会把-lz丢弃,导致最终链接失败。

正确的顺序是:

${CROSS_PREFIX}gcc -o test test.c -L$INSTALL_DIR/lib -lxlsxio_read -lz -lexpat

依赖别人的库放在前面,被依赖的库放在后面。如果依赖关系复杂,可以用-Wl,--start-group和-Wl,--end-group把一组库包起来,让链接器反复扫描直到所有符号都解析完毕。不过这会稍微增加链接时间,库不多的话没必要用。

5.4 头文件版本不匹配导致的诡异编译错误

有一次我编译 XLSXIO 时遇到一个奇怪的错误,说某个结构体成员不存在。查了半天发现是$INSTALL_DIR/include下的 expat 头文件版本和实际链接的 expat 库版本不一致。原因是之前编译 expat 时安装到了这个目录,后来换了 expat 版本重新编译,但旧的头文件没被覆盖,导致头文件和库文件对不上。

解决办法很简单:每次重新编译依赖库之前,先清空$INSTALL_DIR,或者至少把对应的头文件和库文件删掉。我现在的习惯是在$INSTALL_DIR下按库名建子目录,比如$INSTALL_DIR/zlib、$INSTALL_DIR/expat,这样各个库的文件不会混在一起,升级时也容易清理。

6. 交叉编译环境的日常维护与复用

6.1 把配置固化成脚本

每次编译都手动敲一堆环境变量和 configure 参数,不仅累,还容易出错。我建议把交叉编译的配置写成一个 shell 脚本,比如cross-env.sh:

#!/bin/bash export CROSS_PREFIX=arm-linux-gnueabihf- export SYSROOT=/opt/toolchain/arm-linux-gnueabihf/sysroot export INSTALL_DIR=$HOME/cross/install export PATH=/opt/toolchain/bin:$PATH export CC=${CROSS_PREFIX}gcc export CXX=${CROSS_PREFIX}g++ export AR=${CROSS_PREFIX}ar export RANLIB=${CROSS_PREFIX}ranlib export CFLAGS="-I$INSTALL_DIR/include" export CXXFLAGS="-I$INSTALL_DIR/include" export LDFLAGS="-L$INSTALL_DIR/lib"

每次开新终端时source一下这个脚本,所有环境变量就都设好了。这样也方便在脚本里切换不同的目标平台,比如再写一个cross-env-aarch64.sh,用的时候 source 对应的那个就行。

6.2 用 pkg-config 管理依赖路径

当依赖库多起来之后,手动维护CFLAGS和LDFLAGS里的-I和-L路径会变得很繁琐。pkg-config 可以帮你自动生成这些参数,前提是每个库安装时都生成了.pc文件。

交叉编译时,需要设置PKG_CONFIG_PATH指向$INSTALL_DIR/lib/pkgconfig,并且设置PKG_CONFIG_SYSROOT_DIR和PKG_CONFIG_LIBDIR来避免 pkg-config 去宿主机路径里找库:

export PKG_CONFIG_PATH=$INSTALL_DIR/lib/pkgconfig export PKG_CONFIG_SYSROOT_DIR=$SYSROOT export PKG_CONFIG_LIBDIR=$INSTALL_DIR/lib/pkgconfig

然后就可以用pkg-config --cflags --libs zlib来获取编译和链接参数了。不过要注意,不是所有库都会生成.pc文件,zlib 和 expat 的较新版本都支持,但一些老库可能没有。没有的话就只能手动指定。

6.3 版本管理与回滚策略

交叉编译工具链和依赖库的版本管理是个容易被忽视的问题。今天编译通过了,明天换了个工具链版本,可能就编不过了。我的做法是给每个目标平台建一个独立的目录,里面放工具链、sysroot、依赖库和最终的产物,目录名里带上版本号和日期,比如arm-20240115-glibc2.28。这样即使升级了工具链,旧的目录还在,随时可以回滚。

另外,把编译依赖库的脚本也保存下来,最好带上下载链接和校验和。这样换一台机器时,直接跑脚本就能重建整个环境,不用凭记忆去回忆当时用了哪个版本、哪个参数。

提示:如果团队里有多个人需要这套环境,可以考虑把工具链和依赖库打包成一个压缩包,放到内部文件服务器上。新人入职时直接下载解压,设置好环境变量就能用,省去大量重复劳动。

7. 从 XLSXIO 延伸出去的通用交叉编译思路

XLSXIO 的交叉编译过程其实是一个缩影,大多数 C/C++ 库的交叉编译都遵循类似的模式:准备工具链和 sysroot、按依赖顺序编译依赖库、配置目标库的构建系统、处理链接和头文件路径、验证产物。掌握了这个流程之后,换一个库无非就是换一下 configure 参数和依赖列表。

我个人的经验是,交叉编译最难的部分往往不是编译本身,而是环境配置和依赖管理。工具链的版本、sysroot 的完整性、依赖库的编译选项,任何一个环节出问题都会导致最终失败。所以与其急着编译目标库,不如先把基础环境搭稳,把依赖库一个个编译好、验证好,最后再编译目标库。这样即使出了问题,排查范围也小得多。

另外,不要害怕看 configure 脚本和 Makefile 的源码。很多时候报错信息只是表象,真正的原因藏在脚本的某个条件判断里。花十分钟读一下相关代码,比在网上搜半天可能更有效。交叉编译这件事,耐心和细致比技术本身更重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询