了解Go编译处理(二)—— go build

前言

Go是编译型语言,Go程序需要经过编译生成可执行文件才能运行,实现编译的命令就是go build

go build使用

对指定文件或文件夹编译时的命令如下:

go build [-o output] [-i] [build flags] [packages]

Go支持交叉编译,可以跨平台编译,如在mac平台编译在linux平台运行的包。

如需要交叉编译,可以在go build前添加目标平台、平台架构环境变量参数,如下命令:

GOOS=linux GOARCH=amd64 go build [-o output] [-i] [build flags] [packages]

GOOS、GOARCH是通过改变环境变量,实现指定平台相关信息的,这些环境变量会在正式编译期获取。

go build溯源

这里说下查找源码的简单小方法:当无法直接通过调用间的跳转找到源码时,可以直接通过全局搜索的方式来找相关的源码。如:我们要找go命令的源码,我们知道go命令的参数解析都是经过flag实现的。直接运行go命令,可以看到相关的help,搜索任一命令对应的解释,如:build的compile packages and dependencies,经过简单的排查即可找到对应的源码。

go tool

go命令对应的源码入口在/cmd/go/main.go文件中,命令的入口为main func,相关的说明在了解Go编译处理(一)—— go tool一文中已做说明,本文不再赘述。

func init() {
    base.Go.Commands = []*base.Command{
        ...
        work.CmdBuild,
        ...
    }
}

关于build对应的Command为work.CmdBuild。

CmdBuild

work.CmdBuild的具体源码位于/cmd/go/internal/work/build.go中,此文件中也包含了install命令对应的CmdInstall

声明
var CmdBuild = &base.Command{
    UsageLine: "go build [-o output] [-i] [build flags] [packages]",
    Short:     "compile packages and dependencies",
    Long: `Build compiles the packages named by the import paths,
    See also: go install, go get, go clean.
    `,
}
const concurrentGCBackendCompilationEnabledByDefault = true

CmdBuild是base.Command的实例,包含了对usage的说明。

init

build.go中存在两个init func。

第一个init func中指定了CmdBuild的运行fun,添加了对参数-i-o的解析(build.go中同时也包含了install命令的处理)。

func init() {
    // break init cycle
    CmdBuild.Run = runBuild
    CmdInstall.Run = runInstall

    CmdBuild.Flag.BoolVar(&cfg.BuildI, "i", false, "")
    CmdBuild.Flag.StringVar(&cfg.BuildO, "o", "", "output file or directory")

    CmdInstall.Flag.BoolVar(&cfg.BuildI, "i", false, "")

    AddBuildFlags(CmdBuild, DefaultBuildFlags)
    AddBuildFlags(CmdInstall, DefaultBuildFlags)
}

第二个init func需要重点说明下。

const Compiler = "gc"
func init() {
    switch build.Default.Compiler {
    case "gc", "gccgo":
        buildCompiler{}.Set(build.Default.Compiler)
    }
}
var Default Context = defaultContext()
func defaultContext() Context {
    var c Context

    c.GOARCH = envOr("GOARCH", runtime.GOARCH)
    c.GOOS = envOr("GOOS", runtime.GOOS)
    c.GOROOT = pathpkg.Clean(runtime.GOROOT())
    c.GOPATH = envOr("GOPATH", defaultGOPATH())
    c.Compiler = runtime.Compiler

    // Each major Go release in the Go 1.x series adds a new
    // "go1.x" release tag. That is, the go1.x tag is present in
    // all releases >= Go 1.x. Code that requires Go 1.x or later
    // should say "+build go1.x", and code that should only be
    // built before Go 1.x (perhaps it is the stub to use in that
    // case) should say "+build !go1.x".
    // The last element in ReleaseTags is the current release.
    for i := 1; i <= goversion.Version; i++ {
        c.ReleaseTags = append(c.ReleaseTags, "go1."+strconv.Itoa(i))
    }

    defaultReleaseTags = append([]string{}, c.ReleaseTags...) // our own private copy

    env := os.Getenv("CGO_ENABLED")
    if env == "" {
        env = defaultCGO_ENABLED
    }
    switch env {
    case "1":
        c.CgoEnabled = true
    case "0":
        c.CgoEnabled = false
    default:
        // cgo must be explicitly enabled for cross compilation builds
        if runtime.GOARCH == c.GOARCH && runtime.GOOS == c.GOOS {
            c.CgoEnabled = cgoEnabled[c.GOOS+"/"+c.GOARCH]
            break
        }
        c.CgoEnabled = false
    }

    return c
}
type buildCompiler struct{}

func (c buildCompiler) Set(value string) error {
    switch value {
    case "gc":
        BuildToolchain = gcToolchain{}
    case "gccgo":
        BuildToolchain = gccgoToolchain{}
    default:
        return fmt.Errorf("unknown compiler %q", value)
    }
    cfg.BuildToolchainName = value
    cfg.BuildToolchainCompiler = BuildToolchain.compiler
    cfg.BuildToolchainLinker = BuildToolchain.linker
    cfg.BuildContext.Compiler = value
    return nil
}

此init func获取了默认的GOARCH、GOOS、GOROOT、GOPATH、Compiler,默认的Compiler为"gc",所以默认BuildToolchain为gcToolchain{},后续将由gcToolchain{}负责具体的编译调用。

runBuild

具体的执行func。

func runBuild(cmd *base.Command, args []string) {
    BuildInit()//对mod、instrument、buildMode进行初始化及检查,最后对输出编译包的路径进行检查。
    var b Builder
    b.Init()//对于编译缓存参数的初始化,对工作目录、GOOS-ARCH及tag进行检查。

    pkgs := load.PackagesForBuild(args)//检查所有的package,若有错误则不能编译

    explicitO := len(cfg.BuildO) > 0 //指定输出路径

    if len(pkgs) == 1 && pkgs[0].Name == "main" && cfg.BuildO == "" {
        cfg.BuildO = pkgs[0].DefaultExecName()//默认执行文件名
        cfg.BuildO += cfg.ExeSuffix//执行文件后缀
    }

    // sanity check some often mis-used options
    switch cfg.BuildContext.Compiler {//健全检查一些经常误用的选项
    case "gccgo":
        if load.BuildGcflags.Present() {
            fmt.Println("go build: when using gccgo toolchain, please pass compiler flags using -gccgoflags, not -gcflags")
        }
        if load.BuildLdflags.Present() {
            fmt.Println("go build: when using gccgo toolchain, please pass linker flags using -gccgoflags, not -ldflags")
        }
    case "gc":
        if load.BuildGccgoflags.Present() {
            fmt.Println("go build: when using gc toolchain, please pass compile flags using -gcflags, and linker flags using -ldflags")
        }
    }

    depMode := ModeBuild
    if cfg.BuildI {//对install的处理
        depMode = ModeInstall
    }

    pkgs = omitTestOnly(pkgsFilter(load.Packages(args)))//忽略仅测试的package

    // Special case -o /dev/null by not writing at all.
    if cfg.BuildO == os.DevNull {//指针特殊输出路径的处理
        cfg.BuildO = ""
    }

    if cfg.BuildO != "" {//针对输出路径的处理
        // If the -o name exists and is a directory, then
        // write all main packages to that directory.
        // Otherwise require only a single package be built.
        if fi, err := os.Stat(cfg.BuildO); err == nil && fi.IsDir() {//输出路径已存在且是文件夹
            if !explicitO {
                base.Fatalf("go build: build output %q already exists and is a directory", cfg.BuildO)
            }
            a := &Action{Mode: "go build"}//指定Action的Mode为build
            for _, p := range pkgs {
                if p.Name != "main" {//build的package需从main package开始
                    continue
                }
                //目标packag的参数设置
                p.Target = filepath.Join(cfg.BuildO, p.DefaultExecName())
                p.Target += cfg.ExeSuffix
                p.Stale = true
                p.StaleReason = "build -o flag in use"
                a.Deps = append(a.Deps, b.AutoAction(ModeInstall, depMode, p))//记录针对所有package的编译操作
            }
            if len(a.Deps) == 0 {//没有main则报错
                base.Fatalf("go build: no main packages to build")
            }
            //根据action进行操作
            b.Do(a)
            return
        }
        if len(pkgs) > 1 {//输出目录不存在或非文件夹
            base.Fatalf("go build: cannot write multiple packages to non-directory %s", cfg.BuildO)
        } else if len(pkgs) == 0 {//没有package
            base.Fatalf("no packages to build")
        }
        p := pkgs[0]
        p.Target = cfg.BuildO
        p.Stale = true // must build - not up to date
        p.StaleReason = "build -o flag in use"
        a := b.AutoAction(ModeInstall, depMode, p)
        b.Do(a)
        return
    }

    a := &Action{Mode: "go build"}
    for _, p := range pkgs {
        a.Deps = append(a.Deps, b.AutoAction(ModeBuild, depMode, p))
    }
    if cfg.BuildBuildmode == "shared" {
        a = b.buildmodeShared(ModeBuild, depMode, args, pkgs, a)
    }
    b.Do(a)
}

BuildInit主要是对mod、instrument、buildMode进行初始化及检查,最后对输出编译包的路径进行检查。

大致处理步骤:

  1. 进行build前的初始化操作
  2. 检查所有参与build的package(若不指定则以运行命令的当前文件夹为指定package)
  3. 确认输出路径及文件后缀
  4. 健全检查,避免错误使用
  5. 忽略仅测试的package
  6. 判断输出路径
  • 若不为空
    • 若输出路径存在且是文件夹,
      • 指定Action Mode,仅从main package开始Action关联,Action中包含对每个package build的处理
    • 若输出路径不存在或不是文件夹,仅能build一个package,否则报错退出
  • 若为空
    • 对所有package进行Action关联
  1. 执行Action
AutoAction
// AutoAction returns the "right" action for go build or go install of p.
func (b *Builder) AutoAction(mode, depMode BuildMode, p *load.Package) *Action {
    if p.Name == "main" {
        return b.LinkAction(mode, depMode, p)//LinkAction也会调用CompileAction
    }
    return b.CompileAction(mode, depMode, p)
}
func (b *Builder) LinkAction(mode, depMode BuildMode, p *load.Package) *Action {
    // Construct link action.
    a := b.cacheAction("link", p, func() *Action {
        a := &Action{
            Mode:    "link",
            Package: p,
        }

        a1 := b.CompileAction(ModeBuild, depMode, p)//与非
        a.Func = (*Builder).link//绑定action的执行func为link
        a.Deps = []*Action{a1}
        a.Objdir = a1.Objdir

        // An executable file. (This is the name of a temporary file.)
        // Because we run the temporary file in 'go run' and 'go test',
        // the name will show up in ps listings. If the caller has specified
        // a name, use that instead of a.out. The binary is generated
        // in an otherwise empty subdirectory named exe to avoid
        // naming conflicts. The only possible conflict is if we were
        // to create a top-level package named exe.
        name := "a.out"
        if p.Internal.ExeName != "" {
            name = p.Internal.ExeName
        } else if (cfg.Goos == "darwin" || cfg.Goos == "windows") && cfg.BuildBuildmode == "c-shared" && p.Target != "" {
            // On OS X, the linker output name gets recorded in the
            // shared library's LC_ID_DYLIB load command.
            // The code invoking the linker knows to pass only the final
            // path element. Arrange that the path element matches what
            // we'll install it as; otherwise the library is only loadable as "a.out".
            // On Windows, DLL file name is recorded in PE file
            // export section, so do like on OS X.
            _, name = filepath.Split(p.Target)
        }
        a.Target = a.Objdir + filepath.Join("exe", name) + cfg.ExeSuffix
        a.built = a.Target
        b.addTransitiveLinkDeps(a, a1, "")

        // Sequence the build of the main package (a1) strictly after the build
        // of all other dependencies that go into the link. It is likely to be after
        // them anyway, but just make sure. This is required by the build ID-based
        // shortcut in (*Builder).useCache(a1), which will call b.linkActionID(a).
        // In order for that linkActionID call to compute the right action ID, all the
        // dependencies of a (except a1) must have completed building and have
        // recorded their build IDs.
        a1.Deps = append(a1.Deps, &Action{Mode: "nop", Deps: a.Deps[1:]})
        return a
    })

    if mode == ModeInstall || mode == ModeBuggyInstall {
        a = b.installAction(a, mode)
    }

    return a
}

main package负责链接各非main package。

CompileAction
func (b *Builder) CompileAction(mode, depMode BuildMode, p *load.Package) *Action {
    vetOnly := mode&ModeVetOnly != 0
    mode &^= ModeVetOnly

    if mode != ModeBuild && (p.Internal.Local || p.Module != nil) && p.Target == "" {
        // Imported via local path or using modules. No permanent target.
        mode = ModeBuild//导入本地路径或modules的处理
    }
    if mode != ModeBuild && p.Name == "main" {
        // We never install the .a file for a main package.
        mode = ModeBuild//对main package的处理
    }

    // Construct package build action.
    a := b.cacheAction("build", p, func() *Action {
        a := &Action{
            Mode:    "build",
            Package: p,
            Func:    (*Builder).build,//绑定action的执行func为build
            Objdir:  b.NewObjdir(),
        }

        if p.Error == nil || !p.Error.IsImportCycle {
            for _, p1 := range p.Internal.Imports {//依赖包的处理
                a.Deps = append(a.Deps, b.CompileAction(depMode, depMode, p1))
            }
        }

        if p.Standard {//go标准包的处理
            switch p.ImportPath {
            case "builtin", "unsafe"://特殊包的处理,builtin、unsafe不编译
                // Fake packages - nothing to build.
                a.Mode = "built-in package"
                a.Func = nil
                return a
            }

            // gccgo standard library is "fake" too.
            if cfg.BuildToolchainName == "gccgo" {//gccgo模式下的特殊处理
                // the target name is needed for cgo.
                a.Mode = "gccgo stdlib"
                a.Target = p.Target
                a.Func = nil
                return a
            }
        }

        return a
    })

    // Find the build action; the cache entry may have been replaced
    // by the install action during (*Builder).installAction.
    buildAction := a
    switch buildAction.Mode {
    case "build", "built-in package", "gccgo stdlib":
        // ok
    case "build-install":
        buildAction = a.Deps[0]
    default:
        panic("lost build action: " + buildAction.Mode)
    }
    buildAction.needBuild = buildAction.needBuild || !vetOnly

    // Construct install action.
    if mode == ModeInstall || mode == ModeBuggyInstall {//对于install的处理
        a = b.installAction(a, mode)
    }

    return a
}

CompileAction获取当前package p的Action,Action封装了处理的相关信息。若p是标准包,则其处理func为nil,即不处理。若封装后的Action,其处理会在Do中进行。

Do

Action的执行过程,注释已添加至代码中。

func (b *Builder) Do(root *Action) {
    if !b.IsCmdList {
        // If we're doing real work, take time at the end to trim the cache.
        c := cache.Default()
        defer c.Trim()
    }

    all := actionList(root)//将root的deps转化为slice
    for i, a := range all {
        a.priority = i//设置优先级为顺序
    }

    // Write action graph, without timing information, in case we fail and exit early.
    writeActionGraph := func() {
        if file := cfg.DebugActiongraph; file != "" {
            if strings.HasSuffix(file, ".go") {
                // Do not overwrite Go source code in:
                //    go build -debug-actiongraph x.go
                base.Fatalf("go: refusing to write action graph to %v\n", file)
            }
            js := actionGraphJSON(root)//转化为json
            if err := ioutil.WriteFile(file, []byte(js), 0666); err != nil {
                fmt.Fprintf(os.Stderr, "go: writing action graph: %v\n", err)
                base.SetExitStatus(1)
            }
        }
    }
    writeActionGraph()//如果开启了DebugActiongraph,则记录action graph到文件中,此时不带时间信息

    b.readySema = make(chan bool, len(all))

    // Initialize per-action execution state.
    // 初始化每个action的执行状态
    for _, a := range all {
        for _, a1 := range a.Deps {
            a1.triggers = append(a1.triggers, a)
        }
        a.pending = len(a.Deps)
        if a.pending == 0 {
            b.ready.push(a)//以栈的形式存入,形成编译依赖链
            b.readySema <- true
        }
    }

    // Handle runs a single action and takes care of triggering
    // any actions that are runnable as a result.
    handle := func(a *Action) {//针对单个action的处理
        if a.json != nil {//针对DebugActiongraph的处理,添加时间
            a.json.TimeStart = time.Now()
        }
        var err error
        if a.Func != nil && (!a.Failed || a.IgnoreFail) {
            err = a.Func(b, a)//执行action绑定的操作
        }
        if a.json != nil {
            a.json.TimeDone = time.Now()
        }

        // The actions run in parallel but all the updates to the
        // shared work state are serialized through b.exec.
        b.exec.Lock()
        defer b.exec.Unlock()

        if err != nil {//发生错误
            if err == errPrintedOutput {
                base.SetExitStatus(2)
            } else {
                base.Errorf("%s", err)
            }
            a.Failed = true
        }

        for _, a0 := range a.triggers {
            if a.Failed {
                a0.Failed = true
            }
            if a0.pending--; a0.pending == 0 {
                b.ready.push(a0)
                b.readySema <- true//完成
            }
        }

        if a == root {//完全完成
            close(b.readySema)
        }
    }

    var wg sync.WaitGroup

    // Kick off goroutines according to parallelism.
    // If we are using the -n flag (just printing commands)
    // drop the parallelism to 1, both to make the output
    // deterministic and because there is no real work anyway.
    par := cfg.BuildP
    if cfg.BuildN {
        par = 1
    }
    for i := 0; i < par; i++ {
        wg.Add(1)
        go func() {//并发执行
            defer wg.Done()
            for {
                select {
                case _, ok := <-b.readySema:
                    if !ok {//完成结束
                        return
                    }
                    // Receiving a value from b.readySema entitles
                    // us to take from the ready queue.
                    b.exec.Lock()
                    a := b.ready.pop()//移出最后的action,优先级最大
                    b.exec.Unlock()
                    handle(a)//执行对单个action的处理
                case <-base.Interrupted://命令强制结束
                    base.SetExitStatus(1)
                    return
                }
            }
        }()
    }

    wg.Wait()

    // Write action graph again, this time with timing information.
    writeActionGraph()//如果开启了DebugActiongraph,则记录action graph到文件中,此时带时间信息,记录了action的开始及完成时间
}

Do中是以并发执行这样可以提高执行的效率。

build

针对单个package的编译处理,此处仅关注.go文件的编译,其他文件的处理过程忽略。

// build is the action for building a single package.
// Note that any new influence on this logic must be reported in b.buildActionID above as well.
func (b *Builder) build(a *Action) (err error) {
    p := a.Package

    bit := func(x uint32, b bool) uint32 {
        if b {
            return x
        }
        return 0
    }

    cachedBuild := false
    //正常编译时need为needBuild,即1
    need := bit(needBuild, !b.IsCmdList && a.needBuild || b.NeedExport) |
        bit(needCgoHdr, b.needCgoHdr(a)) |
        bit(needVet, a.needVet) |
        bit(needCompiledGoFiles, b.NeedCompiledGoFiles)

    if !p.BinaryOnly {//可以重编译的package
        if b.useCache(a, b.buildActionID(a), p.Target) {//如果存在缓存可用
            // We found the main output in the cache.
            // If we don't need any other outputs, we can stop.
            // Otherwise, we need to write files to a.Objdir (needVet, needCgoHdr).
            // Remember that we might have them in cache
            // and check again after we create a.Objdir.
            cachedBuild = true
            a.output = []byte{} // start saving output in case we miss any cache results
            need &^= needBuild
            if b.NeedExport {
                p.Export = a.built
            }
            if need&needCompiledGoFiles != 0 {
                if err := b.loadCachedSrcFiles(a); err == nil {//加载缓存
                    need &^= needCompiledGoFiles
                }
            }
        }

        // Source files might be cached, even if the full action is not
        // (e.g., go list -compiled -find).
        // 源文件缓存的处理
        if !cachedBuild && need&needCompiledGoFiles != 0 {
            if err := b.loadCachedSrcFiles(a); err == nil {
                need &^= needCompiledGoFiles
            }
        }

        if need == 0 {
            return nil
        }
        defer b.flushOutput(a)//将缓存的结果写入b中
    }

    defer func() {//针对错误的处理
        if err != nil && err != errPrintedOutput {
            err = fmt.Errorf("go build %s: %v", a.Package.ImportPath, err)
        }
        if err != nil && b.IsCmdList && b.NeedError && p.Error == nil {
            p.Error = &load.PackageError{Err: err}
        }
    }()
    if cfg.BuildN {
        // In -n mode, print a banner between packages.
        // The banner is five lines so that when changes to
        // different sections of the bootstrap script have to
        // be merged, the banners give patch something
        // to use to find its context.
        b.Print("\n#\n# " + a.Package.ImportPath + "\n#\n\n")
    }

    if cfg.BuildV {
        b.Print(a.Package.ImportPath + "\n")
    }
    //不能重编译的package报错
    if a.Package.BinaryOnly {
        p.Stale = true
        p.StaleReason = "binary-only packages are no longer supported"
        if b.IsCmdList {
            return nil
        }
        return errors.New("binary-only packages are no longer supported")
    }
    //创建中间编译文件的目录
    if err := b.Mkdir(a.Objdir); err != nil {
        return err
    }
    objdir := a.Objdir

    // Load cached cgo header, but only if we're skipping the main build (cachedBuild==true).
    if cachedBuild && need&needCgoHdr != 0 {
        if err := b.loadCachedCgoHdr(a); err == nil {
            need &^= needCgoHdr
        }
    }

    // Load cached vet config, but only if that's all we have left
    // (need == needVet, not testing just the one bit).
    // If we are going to do a full build anyway,
    // we're going to regenerate the files below anyway.
    if need == needVet {//针对vet的处理
        if err := b.loadCachedVet(a); err == nil {
            need &^= needVet
        }
    }
    if need == 0 {
        return nil
    }

    // make target directory
    // 创建package编译的目标文件的目录
    dir, _ := filepath.Split(a.Target)
    if dir != "" {
        if err := b.Mkdir(dir); err != nil {
            return err
        }
    }

    gofiles := str.StringList(a.Package.GoFiles)//.go文件
    cgofiles := str.StringList(a.Package.CgoFiles)//导入C的.go文件
    cfiles := str.StringList(a.Package.CFiles)//.c文件
    sfiles := str.StringList(a.Package.SFiles)//.s文件
    cxxfiles := str.StringList(a.Package.CXXFiles)//.cc, .cpp and .cxx文件
    var objects, cgoObjects, pcCFLAGS, pcLDFLAGS []string
    //对包含c或swig的处理
    if a.Package.UsesCgo() || a.Package.UsesSwig() {
        if pcCFLAGS, pcLDFLAGS, err = b.getPkgConfigFlags(a.Package); err != nil {
            return
        }
    }

    // Run SWIG on each .swig and .swigcxx file.
    // Each run will generate two files, a .go file and a .c or .cxx file.
    // The .go file will use import "C" and is to be processed by cgo.
    if a.Package.UsesSwig() {
        outGo, outC, outCXX, err := b.swig(a, a.Package, objdir, pcCFLAGS)
        if err != nil {
            return err
        }
        cgofiles = append(cgofiles, outGo...)
        cfiles = append(cfiles, outC...)
        cxxfiles = append(cxxfiles, outCXX...)
    }

    // If we're doing coverage, preprocess the .go files and put them in the work directory
    // cover模式
    if a.Package.Internal.CoverMode != "" {
        for i, file := range str.StringList(gofiles, cgofiles) {
            var sourceFile string
            var coverFile string
            var key string
            if strings.HasSuffix(file, ".cgo1.go") {
                // cgo files have absolute paths
                base := filepath.Base(file)
                sourceFile = file
                coverFile = objdir + base
                key = strings.TrimSuffix(base, ".cgo1.go") + ".go"
            } else {
                sourceFile = filepath.Join(a.Package.Dir, file)
                coverFile = objdir + file
                key = file
            }
            coverFile = strings.TrimSuffix(coverFile, ".go") + ".cover.go"
            cover := a.Package.Internal.CoverVars[key]
            if cover == nil || base.IsTestFile(file) {
                // Not covering this file.
                continue
            }
            if err := b.cover(a, coverFile, sourceFile, cover.Var); err != nil {
                return err
            }
            if i < len(gofiles) {
                gofiles[i] = coverFile
            } else {
                cgofiles[i-len(gofiles)] = coverFile
            }
        }
    }

    // Run cgo.
    if a.Package.UsesCgo() || a.Package.UsesSwig() {
        // In a package using cgo, cgo compiles the C, C++ and assembly files with gcc.
        // There is one exception: runtime/cgo's job is to bridge the
        // cgo and non-cgo worlds, so it necessarily has files in both.
        // In that case gcc only gets the gcc_* files.
        var gccfiles []string
        gccfiles = append(gccfiles, cfiles...)
        cfiles = nil
        if a.Package.Standard && a.Package.ImportPath == "runtime/cgo" {
            filter := func(files, nongcc, gcc []string) ([]string, []string) {
                for _, f := range files {
                    if strings.HasPrefix(f, "gcc_") {
                        gcc = append(gcc, f)
                    } else {
                        nongcc = append(nongcc, f)
                    }
                }
                return nongcc, gcc
            }
            sfiles, gccfiles = filter(sfiles, sfiles[:0], gccfiles)
        } else {
            for _, sfile := range sfiles {
                data, err := ioutil.ReadFile(filepath.Join(a.Package.Dir, sfile))
                if err == nil {
                    if bytes.HasPrefix(data, []byte("TEXT")) || bytes.Contains(data, []byte("\nTEXT")) ||
                        bytes.HasPrefix(data, []byte("DATA")) || bytes.Contains(data, []byte("\nDATA")) ||
                        bytes.HasPrefix(data, []byte("GLOBL")) || bytes.Contains(data, []byte("\nGLOBL")) {
                        return fmt.Errorf("package using cgo has Go assembly file %s", sfile)
                    }
                }
            }
            gccfiles = append(gccfiles, sfiles...)
            sfiles = nil
        }

        outGo, outObj, err := b.cgo(a, base.Tool("cgo"), objdir, pcCFLAGS, pcLDFLAGS, mkAbsFiles(a.Package.Dir, cgofiles), gccfiles, cxxfiles, a.Package.MFiles, a.Package.FFiles)
        if err != nil {
            return err
        }
        if cfg.BuildToolchainName == "gccgo" {
            cgoObjects = append(cgoObjects, a.Objdir+"_cgo_flags")
        }
        cgoObjects = append(cgoObjects, outObj...)
        gofiles = append(gofiles, outGo...)

        switch cfg.BuildBuildmode {
        case "c-archive", "c-shared":
            b.cacheCgoHdr(a)
        }
    }

    var srcfiles []string // .go and non-.go
    srcfiles = append(srcfiles, gofiles...)
    srcfiles = append(srcfiles, sfiles...)
    srcfiles = append(srcfiles, cfiles...)
    srcfiles = append(srcfiles, cxxfiles...)
    b.cacheSrcFiles(a, srcfiles)

    // Running cgo generated the cgo header.
    need &^= needCgoHdr

    // Sanity check only, since Package.load already checked as well.
    if len(gofiles) == 0 {
        return &load.NoGoError{Package: a.Package}
    }

    // Prepare Go vet config if needed.
    if need&needVet != 0 {
        buildVetConfig(a, srcfiles)
        need &^= needVet
    }
    if need&needCompiledGoFiles != 0 {
        if err := b.loadCachedSrcFiles(a); err != nil {
            return fmt.Errorf("loading compiled Go files from cache: %w", err)
        }
        need &^= needCompiledGoFiles
    }
    if need == 0 {
        // Nothing left to do.
        return nil
    }

    // Collect symbol ABI requirements from assembly.
    // 从.s文件中获取ABI
    symabis, err := BuildToolchain.symabis(b, a, sfiles)
    if err != nil {
        return err
    }

    // Prepare Go import config.
    // We start it off with a comment so it can't be empty, so icfg.Bytes() below is never nil.
    // It should never be empty anyway, but there have been bugs in the past that resulted
    // in empty configs, which then unfortunately turn into "no config passed to compiler",
    // and the compiler falls back to looking in pkg itself, which mostly works,
    // except when it doesn't.
    var icfg bytes.Buffer
    fmt.Fprintf(&icfg, "# import config\n")
    for i, raw := range a.Package.Internal.RawImports {
        final := a.Package.Imports[i]
        if final != raw {
            fmt.Fprintf(&icfg, "importmap %s=%s\n", raw, final)
        }
    }
    for _, a1 := range a.Deps {
        p1 := a1.Package
        if p1 == nil || p1.ImportPath == "" || a1.built == "" {
            continue
        }
        fmt.Fprintf(&icfg, "packagefile %s=%s\n", p1.ImportPath, a1.built)
    }

    if p.Internal.BuildInfo != "" && cfg.ModulesEnabled {
        if err := b.writeFile(objdir+"_gomod_.go", load.ModInfoProg(p.Internal.BuildInfo, cfg.BuildToolchainName == "gccgo")); err != nil {
            return err
        }
        gofiles = append(gofiles, objdir+"_gomod_.go")
    }

    // Compile Go.
    // 编译go文件,
    objpkg := objdir + "_pkg_.a"
    // 具体go的编译
    ofile, out, err := BuildToolchain.gc(b, a, objpkg, icfg.Bytes(), symabis, len(sfiles) > 0, gofiles)
    if len(out) > 0 {
        output := b.processOutput(out)
        if p.Module != nil && !allowedVersion(p.Module.GoVersion) {
            output += "note: module requires Go " + p.Module.GoVersion + "\n"
        }
        b.showOutput(a, a.Package.Dir, a.Package.Desc(), output)
        if err != nil {
            return errPrintedOutput
        }
    }
    if err != nil {
        if p.Module != nil && !allowedVersion(p.Module.GoVersion) {
            b.showOutput(a, a.Package.Dir, a.Package.Desc(), "note: module requires Go "+p.Module.GoVersion)
        }
        return err
    }
    if ofile != objpkg {
        objects = append(objects, ofile)
    }

    // Copy .h files named for goos or goarch or goos_goarch
    // to names using GOOS and GOARCH.
    // For example, defs_linux_amd64.h becomes defs_GOOS_GOARCH.h.
    //拷贝.h文件
    _goos_goarch := "_" + cfg.Goos + "_" + cfg.Goarch
    _goos := "_" + cfg.Goos
    _goarch := "_" + cfg.Goarch
    for _, file := range a.Package.HFiles {
        name, ext := fileExtSplit(file)
        switch {
        case strings.HasSuffix(name, _goos_goarch):
            targ := file[:len(name)-len(_goos_goarch)] + "_GOOS_GOARCH." + ext
            if err := b.copyFile(objdir+targ, filepath.Join(a.Package.Dir, file), 0666, true); err != nil {
                return err
            }
        case strings.HasSuffix(name, _goarch):
            targ := file[:len(name)-len(_goarch)] + "_GOARCH." + ext
            if err := b.copyFile(objdir+targ, filepath.Join(a.Package.Dir, file), 0666, true); err != nil {
                return err
            }
        case strings.HasSuffix(name, _goos):
            targ := file[:len(name)-len(_goos)] + "_GOOS." + ext
            if err := b.copyFile(objdir+targ, filepath.Join(a.Package.Dir, file), 0666, true); err != nil {
                return err
            }
        }
    }
    //编译C文件,必须使用CGO模式
    for _, file := range cfiles {
        out := file[:len(file)-len(".c")] + ".o"
        if err := BuildToolchain.cc(b, a, objdir+out, file); err != nil {
            return err
        }
        objects = append(objects, out)
    }

    // Assemble .s files.
    if len(sfiles) > 0 {
        ofiles, err := BuildToolchain.asm(b, a, sfiles)
        if err != nil {
            return err
        }
        objects = append(objects, ofiles...)
    }

    // For gccgo on ELF systems, we write the build ID as an assembler file.
    // This lets us set the SHF_EXCLUDE flag.
    // This is read by readGccgoArchive in cmd/internal/buildid/buildid.go.
    if a.buildID != "" && cfg.BuildToolchainName == "gccgo" {
        switch cfg.Goos {
        case "aix", "android", "dragonfly", "freebsd", "illumos", "linux", "netbsd", "openbsd", "solaris":
            asmfile, err := b.gccgoBuildIDFile(a)
            if err != nil {
                return err
            }
            ofiles, err := BuildToolchain.asm(b, a, []string{asmfile})
            if err != nil {
                return err
            }
            objects = append(objects, ofiles...)
        }
    }

    // NOTE(rsc): On Windows, it is critically important that the
    // gcc-compiled objects (cgoObjects) be listed after the ordinary
    // objects in the archive. I do not know why this is.
    // https://golang.org/issue/2601
    objects = append(objects, cgoObjects...)

    // Add system object files.
    for _, syso := range a.Package.SysoFiles {
        objects = append(objects, filepath.Join(a.Package.Dir, syso))
    }

    // Pack into archive in objdir directory.
    // If the Go compiler wrote an archive, we only need to add the
    // object files for non-Go sources to the archive.
    // If the Go compiler wrote an archive and the package is entirely
    // Go sources, there is no pack to execute at all.
    if len(objects) > 0 {//打包编译的文件至objpkg中
        if err := BuildToolchain.pack(b, a, objpkg, objects); err != nil {
            return err
        }
    }
    //更新objpkg的buildID
    if err := b.updateBuildID(a, objpkg, true); err != nil {
        return err
    }
    //保存编译的路径
    a.built = objpkg
    return nil
}

build的步骤如下:

  1. 不能从源码重新编译package报错处理。
  2. 根据buidlID确认没有变更的(意味着package源文件没有发生变化),可以直接使用原编译缓存则直接使用;需要重新编译,则使用缓存源文件进行编译。
  3. 若无编译缓存,有缓存源文件,如需要,使用缓存源文件进行编译。
  4. 确认中间生成文件路径
  5. needCgoHdr、needVet
  6. 确认最终生成文件的路径
  7. 根据文件类型分类
  8. swig文件处理
  9. cover mode的处理,需要使用cover工具
  10. cgo处理,需要使用cgo工具
  11. 缓存所有源文件
  12. needVet、needCompiledGoFiles的处理
  13. 获取ABI,需要用到asm工具
  14. import处理
  15. 编译go文件,需要使用compile工具
  16. 复制.h文件
  17. 编译c文件
  18. 组装.s文件,需要使用asm工具
  19. 添加系统组件
  20. 打包至最终生成文件路径,需要使用pack工具
  21. 更新最终生成文件的buildID,需要使用buildid工具
  22. 保存最终生成文件路径
gc

此处的gc的意思是go compile。

func (gcToolchain) gc(b *Builder, a *Action, archive string, importcfg []byte, symabis string, asmhdr bool, gofiles []string) (ofile string, output []byte, err error) {
    p := a.Package
    objdir := a.Objdir
    // 目标路径不存在则使用默认地址
    if archive != "" {
        ofile = archive
    } else {
        out := "_go_.o"
        ofile = objdir + out
    }

    ...
    // 拼接调用compile工具的参数,compile位于$GOROOT/pkg/tool/$GOOS_$GOARCH下
    args := []interface{}{cfg.BuildToolexec, base.Tool("compile"), "-o", ofile, "-trimpath", a.trimpath(), gcflags, gcargs, "-D", p.Internal.LocalPrefix}
    ...

    output, err = b.runOut(a, p.Dir, nil, args...)
    return ofile, output, err
}
//base.Tool("compile")是获取compile的地址

func (b *Builder) runOut(a *Action, dir string, env []string, cmdargs ...interface{}) ([]byte, error) {
    cmdline := str.StringList(cmdargs...)

    ...
    var buf bytes.Buffer
    cmd := exec.Command(cmdline[0], cmdline[1:]...)
    cmd.Stdout = &buf
    cmd.Stderr = &buf
    cleanup := passLongArgsInResponseFiles(cmd)
    defer cleanup()
    cmd.Dir = dir
    cmd.Env = base.EnvForDir(cmd.Dir, os.Environ())
    cmd.Env = append(cmd.Env, env...)
    start := time.Now()
    err := cmd.Run()
    ...
    return buf.Bytes(), err
}

gc中是关于具体参数的拼接,并不直接编译,而是通过compile工具,传入拼接后的参数,调用compile的命令执行完成编译。

总结

go命令源码位于/cmd/go/ package中,这里负责着get、build、install等相关命令的处理。

go build的源码位于/cmd/go/internal/work/build.go中,从代码中可以看出,build的过程需要经过以下阶段:

  1. 根据路径获取直接参与build的package(不会循环查找子目录的package),忽略仅测试的package
  2. 对于单个main package的未设置输出路径的设置默认路径
  3. 若有输出路径,则仅从main package开始进行Action的关联,Action中包含对每个package build的处理
  4. 若无输出路径,仅支持对单个package的处理,此此package开始关联
  5. 执行Action,获取action list,添加优先级,按照优先级存入栈中
  6. 从栈中取出action,并发执行每一个action的Func,对应Builder的build func,等待所有action的操作执行结束
  • 如果缓存可用则使用缓存(直接使用缓存的编译文件或根据缓存的源文件重新编译)
  • 否则,根据文件类型分类,依次对swig、c、c++进行相关的build处理,缓存所有源文件
  • 处理.s、import config,编译go代码,拷贝.h,组装.s文件,添加系统组件,
  • 打包更新BuildID
  1. 所有action执行完毕后,即可获取对应的执行文件

build的源码中并不负责代码的编译,而是交给专门的编译工具尽心编译,完整的build过程使用到以下工具:

cover-cgo-compile-asm-pack-buildid-link

部分工具因文件类型的不一或编译参数的设置,可能不会调用。其中关于go代码的编译是在compile中完成的,后续的文章中会对compile进行研究。

公众号

鄙人刚刚开通了公众号,专注于分享Go开发相关内容,望大家感兴趣的支持一下,在此特别感谢。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值