cmd/go: stamp tags and flags in build info

Toolchain flags (like -gcflags), build tags (including race and msan),
and cgo variables (including CGO_ENABLED, CGO_CPPFLAGS and others) are
now stamped into binaries.

For #37475

Change-Id: I9023e682c0618f91805434946c6bc937536b69bb
Reviewed-on: https://go-review.googlesource.com/c/go/+/355493
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go
index b6ea5a3..d04ba04 100644
--- a/src/cmd/go/alldocs.go
+++ b/src/cmd/go/alldocs.go
@@ -131,6 +131,11 @@
 //
 // 	-asmflags '[pattern=]arg list'
 // 		arguments to pass on each go tool asm invocation.
+// 	-buildinfo
+// 		Whether to stamp binaries with build flags. By default, the compiler name
+// 		(gc or gccgo), toolchain flags (like -gcflags), and environment variables
+// 		containing flags (like CGO_CFLAGS) are stamped into binaries. Use
+// 		-buildinfo=false to omit build information. See also -buildvcs.
 // 	-buildmode mode
 // 		build mode to use. See 'go help buildmode' for more.
 // 	-buildvcs
@@ -138,7 +143,7 @@
 // 		version control information is stamped into a binary if the main package
 // 		and the main module containing it are in the repository containing the
 // 		current directory (if there is a repository). Use -buildvcs=false to
-// 		omit version control information.
+// 		omit version control information. See also -buildinfo.
 // 	-compiler name
 // 		name of compiler to use, as in runtime.Compiler (gccgo or gc).
 // 	-gccgoflags '[pattern=]arg list'
diff --git a/src/cmd/go/go_test.go b/src/cmd/go/go_test.go
index 07e9962..d67d01a 100644
--- a/src/cmd/go/go_test.go
+++ b/src/cmd/go/go_test.go
@@ -1380,7 +1380,7 @@
 	for buf.Len() < sys.ExecArgLengthLimit+1 {
 		buf.WriteString(testStr)
 	}
-	tg.run("run", "-ldflags", fmt.Sprintf(`-X "main.extern=%s"`, buf.String()), tg.path("main.go"))
+	tg.run("run", "-buildinfo=false", "-ldflags", fmt.Sprintf(`-X "main.extern=%s"`, buf.String()), tg.path("main.go"))
 	if tg.stderr.String() != buf.String() {
 		t.Errorf("strings differ")
 	}
diff --git a/src/cmd/go/internal/cfg/cfg.go b/src/cmd/go/internal/cfg/cfg.go
index e1bf11f..37e9b26 100644
--- a/src/cmd/go/internal/cfg/cfg.go
+++ b/src/cmd/go/internal/cfg/cfg.go
@@ -25,6 +25,7 @@
 // These are general "build flags" used by build and other commands.
 var (
 	BuildA                 bool   // -a flag
+	BuildBuildinfo         bool   // -buildinfo flag
 	BuildBuildmode         string // -buildmode flag
 	BuildBuildvcs          bool   // -buildvcs flag
 	BuildContext           = defaultContext()
diff --git a/src/cmd/go/internal/load/flag.go b/src/cmd/go/internal/load/flag.go
index 4e0cb5b..d0d5716 100644
--- a/src/cmd/go/internal/load/flag.go
+++ b/src/cmd/go/internal/load/flag.go
@@ -22,6 +22,7 @@
 // that allows specifying different effective flags for different packages.
 // See 'go help build' for more details about per-package flags.
 type PerPackageFlag struct {
+	raw     string
 	present bool
 	values  []ppfValue
 }
@@ -39,6 +40,7 @@
 
 // set is the implementation of Set, taking a cwd (current working directory) for easier testing.
 func (f *PerPackageFlag) set(v, cwd string) error {
+	f.raw = v
 	f.present = true
 	match := func(p *Package) bool { return p.Internal.CmdlinePkg || p.Internal.CmdlineFiles } // default predicate with no pattern
 	// For backwards compatibility with earlier flag splitting, ignore spaces around flags.
@@ -72,9 +74,7 @@
 	return nil
 }
 
-// String is required to implement flag.Value.
-// It is not used, because cmd/go never calls flag.PrintDefaults.
-func (f *PerPackageFlag) String() string { return "<PerPackageFlag>" }
+func (f *PerPackageFlag) String() string { return f.raw }
 
 // Present reports whether the flag appeared on the command line.
 func (f *PerPackageFlag) Present() bool {
diff --git a/src/cmd/go/internal/load/pkg.go b/src/cmd/go/internal/load/pkg.go
index 473fa7a..716994b 100644
--- a/src/cmd/go/internal/load/pkg.go
+++ b/src/cmd/go/internal/load/pkg.go
@@ -2268,6 +2268,35 @@
 		Main: main,
 		Deps: deps,
 	}
+	appendSetting := func(key, value string) {
+		info.Settings = append(info.Settings, debug.BuildSetting{Key: key, Value: value})
+	}
+
+	// Add command-line flags relevant to the build.
+	// This is informational, not an exhaustive list.
+	if cfg.BuildBuildinfo {
+		appendSetting("compiler", cfg.BuildContext.Compiler)
+		if BuildAsmflags.present {
+			appendSetting("asmflags", BuildAsmflags.String())
+		}
+		if BuildGcflags.present && cfg.BuildContext.Compiler == "gc" {
+			appendSetting("gcflags", BuildGcflags.String())
+		}
+		if BuildGccgoflags.present && cfg.BuildContext.Compiler == "gccgo" {
+			appendSetting("gccgoflags", BuildGccgoflags.String())
+		}
+		if BuildLdflags.present {
+			appendSetting("ldflags", BuildLdflags.String())
+		}
+		tags := append(cfg.BuildContext.BuildTags, cfg.BuildContext.ToolTags...)
+		appendSetting("tags", strings.Join(tags, ","))
+		appendSetting("CGO_ENABLED", strconv.FormatBool(cfg.BuildContext.CgoEnabled))
+		if cfg.BuildContext.CgoEnabled {
+			for _, name := range []string{"CGO_CPPFLAGS", "CGO_CFLAGS", "CGO_CXXFLAGS", "CGO_LDFLAGS"} {
+				appendSetting(name, cfg.Getenv(name))
+			}
+		}
+	}
 
 	// Add VCS status if all conditions are true:
 	//
@@ -2328,10 +2357,10 @@
 			setVCSError(err)
 			return
 		}
-		info.Settings = []debug.BuildSetting{
+		info.Settings = append(info.Settings, []debug.BuildSetting{
 			{Key: vcsCmd.Cmd + "revision", Value: st.Revision},
 			{Key: vcsCmd.Cmd + "uncommitted", Value: strconv.FormatBool(st.Uncommitted)},
-		}
+		}...)
 	}
 
 	text, err := info.MarshalText()
diff --git a/src/cmd/go/internal/work/build.go b/src/cmd/go/internal/work/build.go
index 114abab..e10f647 100644
--- a/src/cmd/go/internal/work/build.go
+++ b/src/cmd/go/internal/work/build.go
@@ -85,6 +85,11 @@
 
 	-asmflags '[pattern=]arg list'
 		arguments to pass on each go tool asm invocation.
+	-buildinfo
+		Whether to stamp binaries with build flags. By default, the compiler name
+		(gc or gccgo), toolchain flags (like -gcflags), and environment variables
+		containing flags (like CGO_CFLAGS) are stamped into binaries. Use
+		-buildinfo=false to omit build information. See also -buildvcs.
 	-buildmode mode
 		build mode to use. See 'go help buildmode' for more.
 	-buildvcs
@@ -92,7 +97,7 @@
 		version control information is stamped into a binary if the main package
 		and the main module containing it are in the repository containing the
 		current directory (if there is a repository). Use -buildvcs=false to
-		omit version control information.
+		omit version control information. See also -buildinfo.
 	-compiler name
 		name of compiler to use, as in runtime.Compiler (gccgo or gc).
 	-gccgoflags '[pattern=]arg list'
@@ -308,6 +313,7 @@
 	cmd.Flag.Var((*base.StringsFlag)(&cfg.BuildToolexec), "toolexec", "")
 	cmd.Flag.BoolVar(&cfg.BuildTrimpath, "trimpath", false, "")
 	cmd.Flag.BoolVar(&cfg.BuildWork, "work", false, "")
+	cmd.Flag.BoolVar(&cfg.BuildBuildinfo, "buildinfo", true, "")
 	cmd.Flag.BoolVar(&cfg.BuildBuildvcs, "buildvcs", true, "")
 
 	// Undocumented, unstable debugging flags.
diff --git a/src/cmd/go/testdata/script/version_build_settings.txt b/src/cmd/go/testdata/script/version_build_settings.txt
new file mode 100644
index 0000000..7e93643
--- /dev/null
+++ b/src/cmd/go/testdata/script/version_build_settings.txt
@@ -0,0 +1,66 @@
+[short] skip
+
+# Compiler name is always added.
+go build
+go version -m m$GOEXE
+stdout '^\tbuild\tcompiler\tgc$'
+! stdout asmflags|gcflags|ldflags|gccgoflags
+
+# Toolchain flags are added if present.
+# The raw flags are included, with package patterns if specified.
+go build -asmflags=all=-spectre=all
+go version -m m$GOEXE
+stdout '^\tbuild\tasmflags\tall=-spectre=all$'
+
+go build -gcflags=all=-spectre=all
+go version -m m$GOEXE
+stdout '^\tbuild\tgcflags\tall=-spectre=all$'
+
+go build -ldflags=-w
+go version -m m$GOEXE
+stdout '^\tbuild\tldflags\t-w$'
+
+# gccgoflags are not added when gc is used, and vice versa.
+# TODO: test gccgo.
+go build -gccgoflags=all=UNUSED
+go version -m m$GOEXE
+! stdout gccgoflags
+
+# Build and tool tags are added but not release tags.
+# "race" is included with build tags but not "cgo".
+go build -tags=a,b
+go version -m m$GOEXE
+stdout '^\tbuild\ttags\ta,b(,goexperiment\.[a-z0-9]+)*$'
+[race] go build -race
+[race] go version -m m$GOEXE
+[race] stdout '^\tbuild\ttags\t.*race.*$'
+
+# CGO flags are separate settings.
+# CGO_ENABLED is always present.
+# Other flags are added if CGO_ENABLED is true.
+env CGO_ENABLED=0
+go build
+go version -m m$GOEXE
+stdout '^\tbuild\tCGO_ENABLED\tfalse$'
+! stdout CGO_CPPFLAGS|CGO_CFLAGS|CGO_CXXFLAGS|CGO_LDFLAGS
+[cgo] env CGO_ENABLED=1
+[cgo] env CGO_CPPFLAGS=-DFROM_CPPFLAGS=1
+[cgo] env CGO_CFLAGS=-DFROM_CFLAGS=1
+[cgo] env CGO_CXXFLAGS=-DFROM_CXXFLAGS=1
+[cgo] env CGO_LDFLAGS=-L/extra/dir/does/not/exist
+[cgo] go build
+[cgo] go version -m m$GOEXE
+[cgo] stdout '^\tbuild\tCGO_ENABLED\ttrue$'
+[cgo] stdout '^\tbuild\tCGO_CPPFLAGS\t-DFROM_CPPFLAGS=1$'
+[cgo] stdout '^\tbuild\tCGO_CFLAGS\t-DFROM_CFLAGS=1$'
+[cgo] stdout '^\tbuild\tCGO_CXXFLAGS\t-DFROM_CXXFLAGS=1$'
+[cgo] stdout '^\tbuild\tCGO_LDFLAGS\t-L/extra/dir/does/not/exist$'
+
+-- go.mod --
+module example.com/m
+
+go 1.18
+-- m.go --
+package main
+
+func main() {}