cmd/dist是生成Go distribution的引导工具。它负责构建C程序(像Go编译器)和初始化Go工具的拷贝(即go_bootstrap)。
dist本身是用C语言写的。所有和C库,甚至和C标准库的交互,都通过一个系统相关的文件封装起来,以方便移植,如:plan9.c、unix.c和windows.c等。通过这个可移植层提供功能,供其他文件使用。在这个可移植层中,为了避免和现有函数混淆,相同功能的函数带有前缀:x。例如:xprintf是printf的可移植版本。
到目前为止,dist中大部分普通数据类型是字符串或字符串数组。然而,dist中使用两个命名的数据结构:Buf和Vec,保存它们拥有的所有数据,而不是使用char*和char**。Buf相当于Go中的[]byte,Vec相当于Go中的[]string。对Buf的操作函数以b开头;对Vec的操作函数以v开头。
a.h arg.h arm.c buf.c build.c buildgc.c buildruntime.c goc2c.c main.c plan9.c README unix.c windows.c
注:go1.0.3源码中没有plan9.c文件,tips中有该文件。
学习前看README。本文简介部分基本来自该文件。
定义了一些类型,比如简介中提到的Buf和Vec;声明了dist中所有.c文件中的函数
Buf和Vec的结构:
// A Buf is a byte buffer, like Go's []byte.
typedef struct Buf Buf;
struct Buf
{
char *p;
int len;
int cap;
};
// A Vec is a string vector, like Go's []string.
typedef struct Vec Vec;
struct Vec
{
char **p;
int len;
int cap;
};
可以看到,结构很像Go中slice的内部定义
该文件中声明了一个变量和定义了五个宏,它和libc.h中的定义是一样的。源码中大量使用了这些宏,特别是ARGBEGIN和ARGEND。通过源码,可以知道,ARGBEGIN宏其实就是循环遍历命令行参数(不包括dist和其子命令),参数要求以-开头展开如下(以build.c中的cmdclean为例):
for((argv0?0:(argv0=*argv)),argv++,argc--; argv[0] && argv[0][0]=='-' && argv[0][1]; argc--, argv++) {
char *_args, *_argt;
char _argc;
_args = &argv[0][1];
if(_args[0]=='-' && _args[1]==0){
argc--;
argv++;
break;
}
_argc = 0;
while((_argc = *_args++) != 0)
switch(_argc){
case 'v':
vflag++;
break;
default:
usage();
}
_argt=0;
USED(_argt);
USED(_argc);
USED(_args);
}
USED(argv);
USED(argc);
感觉上main.c是dist的入口。实际上特定操作系统的文件才包含了main入口函数。main.c中只是一个“伪入口”函数:xmain
如简介中所说,特定操作系统的文件封装了特定操作系统的一些函数,以方便移植。
提供了对Buf和Vec的操作
初始化对dist的任何调用,即运行dist时需要调用build.c中的函数执行初始化
构建cmd/gc时的辅助文件
构建pkg/runtime时的辅助文件
将.goc文件转为.c文件。一个.goc文件是一个组合体:包含Go代码和C代码。注意:goc文件和cgo是不一样的。
以Linux操作系统为例,Windows版本基本一样,只是系统调用等不一样,区别就是unix.c和windows.c的不同。
1)在unix.c中的main入口函数中,首先设置了gohostos和gohostarch,这是本机的操作系统和系统架构。main中会调用build.c中的init和main.c中的xmain
2)build.c中init函数负责全局状态的初始化,如:goroot、goos、goarch等
3)main.c中xmain函数是main的可移植版本,即特定平台的main将入口转发到xmain中
4)根据传递给dist的具体命令执行相应的函数
在main.c中定义了可用的命令:
// cmdtab records the available commands.
static struct {
char *name;
void (*f)(int, char**);
} cmdtab[] = {
{"banner", cmdbanner},
{"bootstrap", cmdbootstrap},
{"clean", cmdclean},
{"env", cmdenv},
{"install", cmdinstall},
{"version", cmdversion},
};
这些命令对应的f是在build.c中实现的。
输出Go安装完成的提示。
提示语句类似:
---
Installed Go for linux/amd64 in /home/polaris/go
Installed commands in /home/polaris/go/bin
*** You need to add /home/polaris/go/bin to your PATH.
其中,如果go/bin已经在PATH中,则不会提示最后一句。
这是一个重要的命令,通过这个命令生成Go引导工具:go_bootstrap,其实就是go命令,以及其他工具。 该命令首先会执行build.c中的setup函数,初始化Go目录树,为构建Go做准备,初始化工作包括:创建bin目录、pkg目录及其子目录(如obj/tool等),同时做一些必要的清除工作。
接着为本机操作系统和架构构建Go,具体构建顺序在build.c中的buildorder数组中定义了,这个顺序在安装Go的过程中的输出可以看到。实际的构建是通过build.c中的install函数执行的。
install函数比较长,做的事情就是:根据传给它的源码目录(这是按照buildorder中来的),用gcc或其他编译器(如6a/6c/6g)编译源码,生成pkg/obj、pkg/tool和pkg中的C库、命令和Go包。注意,这个时候tool中有一个go_bootstrap,这个其实就是go命令。(可以通过在make.bash中的./cmd/dist/dist bootstrap $buildall -v后面加上exit 0来看到这个中间过程)
总结一下这一步所做的事情: ①没有构建全部的工具(从buildorder中也可以看出),构建了关键的工具go_bootstrap ②构建了C库(在pkg/obj/下) ③构建了编译器等([568]a/c/g/l) ④将runtime包中的goc文件转为c文件 ⑤构建一些Go包(标准库),之所以构建这些包,是因为cmd/go命令是用Go语言写的,它依赖这些包。但实际上接下来会将这些构建好的Go包删除,这里实际上产生一些文件(要产生的文件定义在gentab结构中),产生这些文件的目的是使runtime包能够通过go命令统一编译。 go_bootstrap是通过[568]g构建的
关于产生的文件,这里详细解释一下: 在build.c中的deptab结构数组中定义了库和工具的依赖关系,其中pkg/runtime包依赖一些文件:
{"pkg/runtime", {
"zasm_$GOOS_$GOARCH.h",
"zgoarch_$GOARCH.go",
"zgoos_$GOOS.go",
"zruntime_defs_$GOOS_$GOARCH.go",
"zversion.go",
}}
这些文件是通过该过程动态生成的,具体怎么生成,在gentab结构数组中有定义,
static struct {
char *nameprefix;
void (*gen)(char*, char*);
} gentab[] = {
// 以下两个是编译器依赖,在buildgc.c中实现
{"opnames.h", gcopnames}, // gc编译器依赖,在gcopnames函数中通过cmd/gc/go.h生成
{"enam.c", mkenam}, // [568]c/g/l依赖,在mkenam函数中通过对应的[568].out.h文件生成
// 以下是runtime包依赖的,在buildruntime.c中实现
{"zasm_", mkzasm}, // 汇编文件需要用到
{"zgoarch_", mkzgoarch}, // 当前主机的架构
{"zgoos_", mkzgoos}, // 当前主机的操作系统
{"zruntime_defs_", mkzruntimedefs}, // 数据结构定义(Go版),和runtime.h中一些结构定义类似。
// 从runtimedefs结构数组中的文件提取。
{"zversion.go", mkzversion}, // 定义GOROOT和GoVersion,分别是goroot_final和goversion
};
可见,安装并不需要配置GOOS、GOARCH之类的环境变量,在unix.c或windows.c中会判断出当前环境
对于交叉编译,第一次准备目标环境时,会install(“pkg/runtime”)
另外,该过程会将runtime.h和cgocall.h拷贝到了pkg/$GOOS_$GOARCH目录下,这是给cgo编译的时候使用的。
关于make.bash中接下来通过go_bootstrap安装其他命令和包的过程,在后续文章中分析。
删除临时对象。clean -i会将安装了的对象也删除(即pkg中的.a文件)
输出默认的环境变量。env -p会输出PATH环境变量。
默认的输出格式是:
GOROOT="/home/polaris/go"
GOBIN="/home/polaris/go/bin"
GOARCH="amd64"
GOOS="linux"
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOTOOLDIR="/home/polaris/go/pkg/tool/linux_amd64"
GOCHAR="6" // 5、6、8啥意思大家懂的
而env -w是:
set GOROOT=/home/polaris/go
set GOBIN=/home/polaris/go/bin
set GOARCH=amd64
set GOOS=linux
set GOHOSTARCH=amd64
set GOHOSTOS=linux
set GOTOOLDIR=/home/polaris/go/pkg/tool/linux_amd64
set GOCHAR=6
在make.bash中,执行eval $(./cmd/dist/dist env -p),就相当于在make.bash中定义了上面的变量,包括PATH。
由此可见,源码安装的时候,并不需要设置GOROOT等环境变量。当然,安装完后必须的环境变量还是需要设置的,要不然会提示go命令找不到,因为这里设置的环境变量只是临时用的。
通过build.c中install方法安装某个包,比如:
go tool dist install pkg/runtime
注意这种方式和go install的区别
输出当前Go版本信息。版本信息是通过build.c中的findgoversion函数得到的。
从上面的分析可以看出,在Go中,通过gcc编译出dist后,由dist负责其他编译器等的编译,包括各种环境,这样,Go包中的.s、.c、.g都由自身的编译器[568]s/c/g/、链接器[568]l和打包工具pack 负责编译、链接和打包,完全自给自足。