美文网首页
如何拿到 go build -x 输出内容

如何拿到 go build -x 输出内容

作者: 追风骚年 | 来源:发表于2021-11-15 14:12 被阅读0次

    我在使用 go build -x -a -w main.go > build.sh时,本来想把 build 的整个过程导出,然后可以分析 build 实际执行的情况,但是发现 build.sh 一直是空文件,这就引起了我的好奇,为什么将日志重定向到文件没有生效。

    我去翻了一下源码,发现
    $GOROOT/src/cmd/go/internal/cfg/cfg.go

    // These are general "build flags" used by build and other commands.
    var (
    ...
        BuildV                 bool // -v flag
        BuildWork              bool // -work flag
        BuildX                 bool // -x flag
    ....
    )
    

    有如下全局变量定义,如果 build 后面添加 flag ,则如数记录在这里。

    $GOROOT/src/cmd/go/internal/work/gc.go:389

        if cfg.BuildN || cfg.BuildX {
            cmdline := str.StringList(base.Tool("pack"), "r", absAfile, absOfiles)
            b.Showcmd(p.Dir, "%s # internal", joinUnambiguously(cmdline))
        }
    

    后面有很多判断,就是根据这个 BuildX 判断是否需要打印执行日志。

    这里继续追一下 Showcmd 函数,就是打印的函数,

    $GOROOT/src/cmd/go/internal/work/exec.go:1784

    func (b *Builder) Showcmd(dir string, format string, args ...interface{}) {
        b.output.Lock()
        defer b.output.Unlock()
        b.Print(b.fmtcmd(dir, format, args...) + "\n")
    }
    

    这里反正就是一些拼接。

    $GOROOT/src/cmd/go/internal/work/action.go:37

    type Builder struct {
    ...
        Print       func(args ...interface{}) (int, error)
    }
    

    b.Print 定义是 func(args ...interface{}) (int, error),再去追一下 fmtcmd 函数。

    $GOROOT/src/cmd/go/internal/work/exec.go:1763

    func (b *Builder) fmtcmd(dir string, format string, args ...interface{}) string {
        cmd := fmt.Sprintf(format, args...)
        if dir != "" && dir != "/" {
            dot := " ."
            if dir[len(dir)-1] == filepath.Separator {
                dot += string(filepath.Separator)
            }
            cmd = strings.ReplaceAll(" "+cmd, " "+dir, dot)[1:]
            if b.scriptDir != dir {
                b.scriptDir = dir
                cmd = "cd " + dir + "\n" + cmd
            }
        }
        if b.WorkDir != "" {
            cmd = strings.ReplaceAll(cmd, b.WorkDir, "$WORK")
        }
        return cmd
    }
    

    做了一些判断,当前路径和执行脚本路径,则 cd 到脚本路径下。
    逻辑就是这个函数返回了一个字符串,然后 b.Print 打印出来了。
    所以要看一下 这个 Print 是如何赋值的。

    所以要看一下入口文件 go build 的入口文件,如何定义的了。
    $GOROOT/src/cmd/go/main.go 为 go 这个 cmd 的入口文件,
    文件的 init 函数当中有一行定义

    
    func init() {
        base.Go.Commands = []*base.Command{
            bug.CmdBug,
            work.CmdBuild, # 这里是重点
            clean.CmdClean,
            doc.CmdDoc,
            envcmd.CmdEnv,
            fix.CmdFix,
            fmtcmd.CmdFmt,
            generate.CmdGenerate,
            get.CmdGet,
            work.CmdInstall,
            list.CmdList,
            modcmd.CmdMod,
            run.CmdRun,
            test.CmdTest,
            tool.CmdTool,
            version.CmdVersion,
            vet.CmdVet,
    
            help.HelpBuildmode,
            help.HelpC,
            help.HelpCache,
            help.HelpEnvironment,
            help.HelpFileType,
            modload.HelpGoMod,
            help.HelpGopath,
            get.HelpGopathGet,
            modfetch.HelpGoproxy,
            help.HelpImportPath,
            modload.HelpModules,
            modget.HelpModuleGet,
            help.HelpPackages,
            test.HelpTestflag,
            test.HelpTestfunc,
        }
    

    work.CmdBuild 是整个 go build 子命令的所有定义模块

    $GOROOT/src/cmd/go/internal/work/build.go:150

    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")
    
        CmdInstall.Flag.BoolVar(&cfg.BuildI, "i", false, "")
    
        AddBuildFlags(CmdBuild)
        AddBuildFlags(CmdInstall)
    }
    

    init 为当前 package 的初始化方法。
    $GOROOT/src/cmd/go/internal/work/build.go:279

    func runBuild(cmd *base.Command, args []string) {
        BuildInit()
        var b Builder
        b.Init() # 重点
           ...
    }
    

    runBuild 里面发现 Print 的定义

    
    func (b *Builder) Init() {
    # 就是现在
        b.Print = func(a ...interface{}) (int, error) {
            return fmt.Fprint(os.Stderr, a...)
        }
        b.actionCache = make(map[cacheKey]*Action)
        b.mkdirCache = make(map[string]bool)
        b.toolIDCache = make(map[string]string)
        b.buildIDCache = make(map[string]string)
    
        if cfg.BuildN {
            b.WorkDir = "$WORK"
        } else {
            tmp, err := ioutil.TempDir(os.Getenv("GOTMPDIR"), "go-build")
            if err != nil {
                base.Fatalf("go: creating work dir: %v", err)
            }
            if !filepath.IsAbs(tmp) {
                abs, err := filepath.Abs(tmp)
                if err != nil {
                    os.RemoveAll(tmp)
                    base.Fatalf("go: creating work dir: %v", err)
                }
                tmp = abs
            }
            b.WorkDir = tmp
            if cfg.BuildX || cfg.BuildWork {
                fmt.Fprintf(os.Stderr, "WORK=%s\n", b.WorkDir)
            }
            if !cfg.BuildWork {
                workdir := b.WorkDir
                base.AtExit(func() { os.RemoveAll(workdir) })
            }
        }
    
        if _, ok := cfg.OSArchSupportsCgo[cfg.Goos+"/"+cfg.Goarch]; !ok && cfg.BuildContext.Compiler == "gc" {
            fmt.Fprintf(os.Stderr, "cmd/go: unsupported GOOS/GOARCH pair %s/%s\n", cfg.Goos, cfg.Goarch)
            os.Exit(2)
        }
        for _, tag := range cfg.BuildContext.BuildTags {
            if strings.Contains(tag, ",") {
                fmt.Fprintf(os.Stderr, "cmd/go: -tags space-separated list contains comma\n")
                os.Exit(2)
            }
        }
    }
    

    经过层层封装,这里的第一个赋值就是 b.Print ,清楚写着最终是调用的系统的打印方法,将输出打印到标准输出 Stderr。


    经过上面源码的探索,发现 go build -x 最终是将日志写到标准错误流当中,由于 Linux 系统定义的三个标准流的如下,0 表示输入流,1表示标准输出流,2表示标准错误流。
    又因为默认重定向的符号 >,是省略了一个 1,实际是 1>
    所以将以上的命令修改如下 go build -x -a -w main.go 2> build.sh 就可以将日志输出到文件中。

    顺便说一句,如果是将正确日志,错误日志都输出到日志文件则可以如下:
    go build -x -a -w main.go > build.sh &2>1
    &2 表示错误流重定向到 1 标准输出流。

    相关文章

      网友评论

          本文标题:如何拿到 go build -x 输出内容

          本文链接:https://www.haomeiwen.com/subject/thqozltx.html