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

2021-11-15  本文已影响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 标准输出流。

上一篇 下一篇

猜你喜欢

热点阅读