
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 unzipbase-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和--prefixexport CROSS_PREFIXarm-linux-gnueabihf- export INSTALL_DIR~/cross/install CHOST${CROSS_PREFIX} ./configure --prefix$INSTALL_DIRCHOST告诉 configure 脚本使用哪个交叉编译器。zlib 的 configure 脚本会据此推导出CC、AR、RANLIB等工具的名称。如果工具链的命名规则不是标准的arm-linux-gnueabihf-前缀你可能需要手动指定CC和AR。configure 完成后直接make和make installmake -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_nonnullyes这种。具体要跳过哪些得看报错信息。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 --hostarm-linux-gnueabihf \ ac_cv_func_malloc_0_nonnullyes \ ac_cv_func_realloc_0_nonnullyes这两个变量在编译很多 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_PREFIXarm-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 的源码。很多时候报错信息只是表象真正的原因藏在脚本的某个条件判断里。花十分钟读一下相关代码比在网上搜半天可能更有效。交叉编译这件事耐心和细致比技术本身更重要。