macos跨平台编译问题

musl 跨平台编译器在 Go 中的使用

Posted by John Mactavish on September 3, 2022

背景

我平时编写 Go 代码直接使用的是一个 M1 芯片的 MacBook Pro, 而用于调试程序的环境(即程序直接运行的环境)则是一台远程 Linux 主机。

我一般使用 Goland 的远程运行/调试功能, 通过设置“运行配置”(Run Configuration)的“运行目标”(Run Target)为我的 Linux 主机,我实现了在 Linux 环境调试程序,同时完全享受 Goland 的全部功能(断点、查看调试变量等),就像在本地调试一样。

原理是 Goland 会通过 ssh 连接上远程主机,把源代码或执行文件传输过去,根据配置的远程 Go 环境(Go 命令位置与 GOPATH)运行程序,Goland 然后托管调试流程,把丰富的调试信息展示到本机的界面上。这里推荐在传输时启用 rsync, 它使用的增量传输算法通过仅发送源文件和目标中现有文件之间的差异来减少网络发送的数据量,传输效率更高。另外建议手动配置传输到远程目标中的文件位置,有时目标中缺乏某些 Go Library,我们就可以直接进入远程目标源码位置 go mod tidy,为其下载好依赖。

详情参考 Goland 文档中关于 Run Targets 的一节

问题

这样的体验已经十分好了。但还有一个小问题,现在不仅是运行,即使是编译我也放在了远程 Linux 主机上(通过在“运行配置”中勾选“在远程目标上构建”)。但是这个 Linux 主机的各方面硬件配置都不如我的 MacBook Pro,编译一般都要花个好几秒。我希望通过在本机跨平台编译(crosscompile)来提高编译速度。这样一来,只需将执行文件传输给远程 Linux 即可,还同时免去了在目标 Linux 上从源码编译需要下载依赖的麻烦。

过程

首先,Go 原生支持跨平台编译,只需改变两个环境变量即可。

go env -w GOOS=linux GOARCH=amd64

这里,我们不是用 shell 语法改的环境变量,而是用的 go env -w,它的一大优势就是可以在所有环境中立刻生效。例如,我们新开一个单独的 shell,用 go env 查看一下,可以发现这个变更已经生效了。

这两个变量分别改变了编译产物的目标操作系统与目标 CPU 架构,改为我的 Linux 环境的设置。 我本机原来的设置为 GOOS=darwin GOARCH=arm64

大多数情况下这样就可以了。

但是当我点击运行 Go 单元测试时,Goland 却会抱怨:

go test: -race requires cgo

原来,我在运行单测时启用了 -race flag,这指示 go test 开启 Race Detector————一个可以检查 race condition 的工具。 而这个 Race Detector 却依赖 cgo,详情参考这个 issue

cgo 是什么呢,是一个用于与 C 语言库交互的技术,能力强大。 但参考 Go 语言的作者之一 Dave Cheney 的观点————cgo is not Go,其缺点也很多:

  • 编译慢且复杂,不支持交叉编译————因为实际会使用 C 语言编译工具,也要处理 C 语言的跨平台问题
  • 其他很多 Go 语言的工具不能使用
  • C 与 Go 语言之间的的互相调用繁琐,且有性能开销的
  • C 语言是主导,这时候 go 变得不重要,其实不如你用 Python 或 Lua 调用 C
  • 部署复杂,不再只是一个简单的二进制

这里我们就是被其不能原生跨平台的特点给拦住了。 解决方法是找一个 C 语言的跨平台(我们只需要 macos 到 linux) 编译器来代替本机原生的 clang(和 clang++)。

一个可选项是 FiloSottile/homebrew-musl-cross。按照 README 指引,我们直接下载即可:

brew install filosottile/musl-cross/musl-cross

值得一说的是,即使挂着 VPN,下载时间也是十分的长。更长的则是 brew 卡在

step ==> /usr/local/opt/make/bin/gmake install TARGET=x86_64-linux-musl

这一步骤的时间。按照这个 issue 的反馈,这竟然是符合预期的编译时间。最终这个编译在我的 Macbook Pro 上耗时约两个小时……

注意 musl 是一个 os 级别的系统库,与 glibc 是同一种概念。使用上面这个编译器编译出的是使用的 musl 接口的二进制文件,因此一般需要额外安装支持。但这很简单,例如,在我的 debian 上:

sudo apt install musl

否则,直接运行二进制文件会出现一些奇怪的 No such file or directory 问题。

现在让我们再修改两个 Go 环境变量,以让 Go 使用这个下载下来的跨平台编译器。

go env -w CC=x86_64-linux-musl-gcc CXX=x86_64-linux-musl-g++

同时,让我们确保 CGO_ENABLED 环境变量也是 1(即启用 cgo)。 现在,开始本机跨平台编译!

但还是失败了,报了一些奇怪的类似于

runtime.RaceEnable: relocation target __tsan_go_ignore_sync_end not defined

的符号找不到的错误。显然,这不是工具(指编译器)本身的问题了,而是工具找不到一些必要的链接库。通过查阅这个 issue, 我们才知道 macos 的默认安装的 GOROOT 中有意省去了一些 syso 链接库文件, 而本机跨平台编译 Race Detector 到 amd64 平台的 Linux 额外需要其中的 $GOROOT\src\runtime\race\race_linux_amd64.syso 才能进行。 解决方法也是简单粗暴:直接去 Linux 的 Go 安装包中复制必要的 syso 文件,到我 macos 的 GOROOT 中的对应位置。

需要注意,最后这步只是启用 Race Detector 才需要做的额外工作。参考网上的其他文章,多数的“带 cgo 的 macos 跨平台编译 linux 程序”问题只需要配好编译器即可。

至此,问题完美解决!

总结

仅供参考的一键解决问题的 shell 脚本:

set -ex
# 安装、配置
brew install filosottile/musl-cross/musl-cross # 下载 macos->linux musl 跨平台编译器
go env -w CC=x86_64-linux-musl-gcc CXX=x86_64-linux-musl-g++ # 改为刚下载的编译器
go env -w GOOS=linux GOARCH=amd64 CGO_ENABLED=1 # 根据实际目标平台配置
wget "https://dl.google.com/go/go1.19.linux-amd64.tar.gz" -O go.tar.gz # 或其他 Go 版本
tar -xzf go.tar.gz
mv go/src/runtime/race/*.syso $GOROOT/src/runtime/race # 增加 syso 文件
rm -r go go.tar.gz
ssh devbox # 在远程 Linux 上
sudo apt install musl # 增加 musl 支持

现在,试一试:

cd path_to_your_src
go test -race -c # 启用 race,编译但不执行

参考资料:

记一次在macos交叉编译cgo的坑

使用go语言进行交叉编译的时候遇到的一些问题

【译】MacOS下的交叉编译

EASY WINDOWS AND LINUX CROSS-COMPILERS FOR MACOS