[翻译]Go 是如何用 go 编译自己的

原文在此《How Go uses Go to build itself》,作者为 Dave Cheney。

————翻译分隔线————

Go 是如何用 go 编译自己的

这篇文章基于 2013 年四月中旬我为悉尼 Go 用户组做的一次关于 Go 构建过程的演讲而成。

在邮件列表或 IRC 的频道里经常有人寻求关于 Go 编译器、运行时和内部原理的细节文档。当前,关于 Go 的内部原理的权威文档的来源就是我鼓励每个人都去阅读的源代码。虽然这样说,但是从 Go 1.0 发布以来 Go 的构建过程就已经稳定了,因此将其记录在这里或许也会有其相应的价值。

这篇文章大致介绍了 Go 编译过程的九个步骤,从源代码开始,结束于经过充分测试的安装好的 Go 。为了简明扼要,所有路径都相对与源代码检出的根路径,$GOROOT/src。

你应当通过阅读在 golang.org 网站上的从源代码安装 Go 来了解更多背景知识。

第一步. all.bash

% cd $GOROOT/src
% ./all.bash

第一步有一点点虎头蛇尾,all.bash 只是调用了另外两个 shell 脚本:make.bashrun.bash。若使用 Windows 或 Plan 9,其过程也基本类似,只是脚本分别以 .bat 或 .rc 结尾。在文章的其他部分,请用适当的操作系统对应的扩展来补全命令。

第二步. make.bash

. ./make.bash --no-banner

make.bash 作为 all.bash 内容的一部分,如果它退出也会中断构建过程。make.bash 有三个主要的任务,第一个任务是验证将编译 Go 的环境是健全的。健全检查已经开发了几年了,是为了能通用的识别出会导致问题的工具,或指出环境中什么地方导致构建失败。

第三步. cmd/dist

gcc -O2 -Wall -Werror -ggdb -o cmd/dist/dist -Icmd/dist cmd/dist/*.c

当健全检查完成后,make.bash 开始编译 cmd/dist。cmd/dist 替换了在 Go 1 之前的基于 Makefile 的系统,并且管理了在 pkg/runtime 中的小部分代码的生成。cmd/dist 是一个 C 程序,为了处理大多数平台的已知问题,它会对系统的 C 编译器和头文件施加影响。cmd/dist 会检测主机的操作系统和架构,$GOHOSTOS 和 $GOHOSTARCH。这可能与为了交叉编译设置的 $GOOS 和 $GOARCH 的值不同。事实上,Go 的构建过程就是在构建一个交叉编译器,不过在大多数情况下主机和目标平台是一样的。接下来,make.bash 使用初始参数调用 cmd/dist 来编译用于支撑编译器套件的库:lib9、libbio 和 libmach,然后是编译器本身。这些工具也是用 C 编写的,并且由系统的 C 编译器编译。

echo "# Building compilers and Go bootstrap tool for host, $GOHOSTOS/$GOHOSTARCH."
buildall="-a"
if [ "$1" = "--no-clean" ]; then
 buildall=""
fi
./cmd/dist/dist bootstrap $buildall -v # builds go_bootstrap

使用编译器套件,cmd/dist 会编译 go 工具:go_bootstrap。go_bootstrap 不是完整的 go 工具,例如为了避免依赖 cgo 因此 pkg/net 被废除了。包含有包和库的目录列表被编译,而其依赖关系是在 cmd/dist 工具里编码的,因此避免在编译 cmd/go 引入新的依赖就极为重要。

第四步. go_bootstrap

现在 go_bootstrap 已经构建完成,make.bash 的最后一步是使用 go_bootstrap 编译完整的 Go 标准库,包括一个完整的 go 工具用以替换。

echo "# Building packages and commands for $GOOS/$GOARCH."
"$GOTOOLDIR"/go_bootstrap install -gcflags "$GO_GCFLAGS" \
    -ldflags "$GO_LDFLAGS" -v std

第五步. run.bash

现在 make.bash 已经完成,回到 all.bash 的执行,这会调用 run.bash。run.bash 的任务是编译和测试标准库、运行时以及语言测试集。

bash run.bash --no-rebuild

由于 make.bash 和 run.bash 都会调用 go install -a std,因此需要使用 –no-rebuild 标志来避免重复前面的步骤,–no-rebuild 跳过了第二个 go install。

# allow all.bash to avoid double-build of everything
rebuild=true
if [ "$1" = "--no-rebuild" ]; then
 shift
else
 echo '# Building packages and commands.'
 time go install -a -v std
 echo
fi

第六步. go test -a std

echo '# Testing packages.'
time go test std -short -timeout=$(expr 120 \* $timeout_scale)s
echo

接下来 run.bash 会在标准库里所有的包上来运行用 testing 包编写的单元测试。由于 $GOPATH 和 $GOROOT 中有着相同的命名空间,所以不能直接使用 go test … 否则 $GOPATH 中的每个包也会被逐一测试,因此创建了一个用于标准库中的包的别名:std。由于一些测试需要比较长的时间,且会消耗大量内存,因此用 -short 标志对一些测试进行了过滤。

第七步. runtime 和 cgo 测试

run.bash 接下来的部分会运行平台对 cgo 支持的测试,执行一些性能测试,并且编译一些伴随 Go 发行版一起的杂项程序。随着时间的流逝,这些杂项程序的清单会越来越长,那么它们也就会不可避免的被从编译过程中悄悄剥离出去。

第八步. go run test

(xcd ../test
unset GOMAXPROCS
time go run run.go
) || exit $?

run.bash 的倒数第二步会调用在 $GOROOT 下的 test 目录里的编译器和运行时的测试。他们是对于编译器和运行时自身的,较为低级细节的测试。会执行语言规格测试,test/bugs 和 test/fixedbugs 子目录保存有那些已经被发现并被修复的问题的独立的测试。驱动测试的是一个小 Go 程序 $GOROOT/test/run.go,会执行 test 目录里的每个 .go 文件。一些 .go 文件的首行包含了指导 run.go 对结果作出判断的指令,例如,程序将会失败,或提供一个确定的输出队列。

第九步. go tool api

echo '# Checking API compatibility.'
go tool api -c $GOROOT/api/go1.txt,$GOROOT/api/go1.1.txt \
    -next $GOROOT/api/next.txt -except $GOROOT/api/except.txt

run.bash 的最后一步调用了 api 工具。api 工具的任务是确保 Go 1 的约定;导出符号、常量、函数、变量、类型和方法都符合 2012 年发布的 Go 1 API。对于 Go 1 在 api/go1.txt 中有描述,而 Go 1.1 在 api/go1.1.txt。一个附加的文件,api/next.txt 定义了 Go 1.1 开始向标准库和运行时补充的符号表。一旦 Go 1.2 发布,这个文件会成为 Go 1.2 的约定,然后会有一个新的 next.txt。还有一个小文件:except.txt,包含有已经被批准的 Go 1 约定的特例。不应该轻易的向这个文件添加内容。

附加的技巧和诀窍

你可能已经发现如果对于无需执行测试的构建 Go,make.bash 会很有用;同样的,构建和测试 Go 运行时,run.bash 也十分有用。这个差异对于以前那样交叉编译 Go 或当前工作于标准库上都十分有用。

更新:感谢 Russ Cox 和 Andrew Gerrand 的反馈和建议。

Join the Conversation

1 Comment

Leave a comment

Your email address will not be published. Required fields are marked *