blob: 14b99f573549913ca145ffc93bcdcac292a2a242 [file] [log] [blame]
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package debug
import (
"bytes"
"fmt"
"runtime"
"strings"
)
// exported from runtime
func modinfo() string
// ReadBuildInfo returns the build information embedded
// in the running binary. The information is available only
// in binaries built with module support.
func ReadBuildInfo() (info *BuildInfo, ok bool) {
data := modinfo()
if len(data) < 32 {
return nil, false
}
data = data[16 : len(data)-16]
bi := &BuildInfo{}
if err := bi.UnmarshalText([]byte(data)); err != nil {
return nil, false
}
// The go version is stored separately from other build info, mostly for
// historical reasons. It is not part of the modinfo() string, and
// ParseBuildInfo does not recognize it. We inject it here to hide this
// awkwardness from the user.
bi.GoVersion = runtime.Version()
return bi, true
}
// BuildInfo represents the build information read from a Go binary.
type BuildInfo struct {
GoVersion string // Version of Go that produced this binary.
Path string // The main package path
Main Module // The module containing the main package
Deps []*Module // Module dependencies
Settings []BuildSetting // Other information about the build.
}
// Module represents a module.
type Module struct {
Path string // module path
Version string // module version
Sum string // checksum
Replace *Module // replaced by this module
}
// BuildSetting describes a setting that may be used to understand how the
// binary was built. For example, VCS commit and dirty status is stored here.
type BuildSetting struct {
// Key and Value describe the build setting. They must not contain tabs
// or newlines.
Key, Value string
}
func (bi *BuildInfo) MarshalText() ([]byte, error) {
buf := &bytes.Buffer{}
if bi.GoVersion != "" {
fmt.Fprintf(buf, "go\t%s\n", bi.GoVersion)
}
if bi.Path != "" {
fmt.Fprintf(buf, "path\t%s\n", bi.Path)
}
var formatMod func(string, Module)
formatMod = func(word string, m Module) {
buf.WriteString(word)
buf.WriteByte('\t')
buf.WriteString(m.Path)
mv := m.Version
if mv == "" {
mv = "(devel)"
}
buf.WriteByte('\t')
buf.WriteString(mv)
if m.Replace == nil {
buf.WriteByte('\t')
buf.WriteString(m.Sum)
} else {
buf.WriteByte('\n')
formatMod("=>", *m.Replace)
}
buf.WriteByte('\n')
}
if bi.Main.Path != "" {
formatMod("mod", bi.Main)
}
for _, dep := range bi.Deps {
formatMod("dep", *dep)
}
for _, s := range bi.Settings {
if strings.ContainsAny(s.Key, "\n\t") || strings.ContainsAny(s.Value, "\n\t") {
return nil, fmt.Errorf("build setting %q contains tab or newline", s.Key)
}
fmt.Fprintf(buf, "build\t%s\t%s\n", s.Key, s.Value)
}
return buf.Bytes(), nil
}
func (bi *BuildInfo) UnmarshalText(data []byte) (err error) {
*bi = BuildInfo{}
lineNum := 1
defer func() {
if err != nil {
err = fmt.Errorf("could not parse Go build info: line %d: %w", lineNum, err)
}
}()
var (
pathLine = []byte("path\t")
modLine = []byte("mod\t")
depLine = []byte("dep\t")
repLine = []byte("=>\t")
buildLine = []byte("build\t")
newline = []byte("\n")
tab = []byte("\t")
)
readModuleLine := func(elem [][]byte) (Module, error) {
if len(elem) != 2 && len(elem) != 3 {
return Module{}, fmt.Errorf("expected 2 or 3 columns; got %d", len(elem))
}
sum := ""
if len(elem) == 3 {
sum = string(elem[2])
}
return Module{
Path: string(elem[0]),
Version: string(elem[1]),
Sum: sum,
}, nil
}
var (
last *Module
line []byte
ok bool
)
// Reverse of BuildInfo.String(), except for go version.
for len(data) > 0 {
line, data, ok = bytes.Cut(data, newline)
if !ok {
break
}
switch {
case bytes.HasPrefix(line, pathLine):
elem := line[len(pathLine):]
bi.Path = string(elem)
case bytes.HasPrefix(line, modLine):
elem := bytes.Split(line[len(modLine):], tab)
last = &bi.Main
*last, err = readModuleLine(elem)
if err != nil {
return err
}
case bytes.HasPrefix(line, depLine):
elem := bytes.Split(line[len(depLine):], tab)
last = new(Module)
bi.Deps = append(bi.Deps, last)
*last, err = readModuleLine(elem)
if err != nil {
return err
}
case bytes.HasPrefix(line, repLine):
elem := bytes.Split(line[len(repLine):], tab)
if len(elem) != 3 {
return fmt.Errorf("expected 3 columns for replacement; got %d", len(elem))
}
if last == nil {
return fmt.Errorf("replacement with no module on previous line")
}
last.Replace = &Module{
Path: string(elem[0]),
Version: string(elem[1]),
Sum: string(elem[2]),
}
last = nil
case bytes.HasPrefix(line, buildLine):
elem := bytes.Split(line[len(buildLine):], tab)
if len(elem) != 2 {
return fmt.Errorf("expected 2 columns for build setting; got %d", len(elem))
}
if len(elem[0]) == 0 {
return fmt.Errorf("empty key")
}
bi.Settings = append(bi.Settings, BuildSetting{Key: string(elem[0]), Value: string(elem[1])})
}
lineNum++
}
return nil
}