cmd/bent: also environment-substitute BuildFlags

do these substitutions late, to allow access to BENT-specific EVs

Change-Id: Ica21c2ee22c1ac926b4cedd6f892b76e13f6752b
Reviewed-on: https://go-review.googlesource.com/c/benchmarks/+/432357
Run-TryBot: David Chase <drchase@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Michael Pratt <mpratt@google.com>
diff --git a/cmd/bent/bent.go b/cmd/bent/bent.go
index ad75d6c..51645d2 100644
--- a/cmd/bent/bent.go
+++ b/cmd/bent/bent.go
@@ -338,18 +338,10 @@
 			// TODO(jfaller): I don't think we need this "/" anymore... investigate.
 			todo.Configurations[i].Root = os.ExpandEnv(root) + "/"
 		}
-		for j, s := range trial.GcEnv {
-			trial.GcEnv[j] = os.ExpandEnv(s)
-		}
-		for j, s := range trial.RunEnv {
-			trial.RunEnv[j] = os.ExpandEnv(s)
-		}
-		todo.Configurations[i].GcFlags = os.ExpandEnv(trial.GcFlags)
-		for j, s := range trial.RunFlags {
-			trial.RunFlags[j] = os.ExpandEnv(s)
-		}
-		for j, s := range trial.RunWrapper {
-			trial.RunWrapper[j] = os.ExpandEnv(s)
+		if len(trial.RunWrapper) > 0 {
+			// Args will be expanded later with BENT_ environment variables injected.
+			// TODO should it use concatenation of os env and configuration env?
+			trial.RunWrapper[0] = os.ExpandEnv(trial.RunWrapper[0])
 		}
 	}
 	for b, v := range configurations {
@@ -378,9 +370,6 @@
 				benchmarks[bench.Name] = false
 			}
 		}
-		for j, s := range bench.GcEnv {
-			bench.GcEnv[j] = os.ExpandEnv(s)
-		}
 		// Trim possible trailing slash, do not want
 		if '/' == bench.Repo[len(bench.Repo)-1] {
 			bench.Repo = bench.Repo[:len(bench.Repo)-1]
@@ -480,6 +469,12 @@
 	defaultEnv = replaceEnv(defaultEnv, "GOARCH", runtime.GOARCH)
 	defaultEnv = ifMissingAddEnv(defaultEnv, "GO111MODULE", "auto")
 
+	envRoot := os.Getenv("ROOT")
+	if envRoot == "" {
+		envRoot = os.Getenv("PWD")
+	}
+	defaultEnv = append(defaultEnv, "ROOT="+envRoot)
+
 	var needSandbox bool    // true if any benchmark needs a sandbox
 	var needNotSandbox bool // true if any benchmark needs to be not sandboxed
 
@@ -976,10 +971,17 @@
 					return x
 				}
 
+				testBinaryName := config.benchName(&b)
+
+				runEnv := []string{}
+				runEnv = append(runEnv, "BENT_CONFIG="+config.Name)
+				runEnv = append(runEnv, "BENT_BENCH="+b.Name)
+				runEnv = append(runEnv, "BENT_I="+strconv.FormatInt(int64(i), 10))
+				runEnv = append(runEnv, "BENT_BINARY="+testBinaryName)
+
 				configWrapper := wrapperFor(config.RunWrapper)
 				benchWrapper := wrapperFor(b.RunWrapper)
 
-				testBinaryName := config.benchName(&b)
 				var s string
 				var rc int
 
@@ -1007,13 +1009,15 @@
 					if root != "" {
 						cmd.Env = replaceEnv(cmd.Env, "GOROOT", root)
 					}
-					cmd.Env = replaceEnvs(cmd.Env, config.RunEnv)
 					cmd.Env = append(cmd.Env, "BENT_DIR="+dirs.wd)
 					cmd.Env = append(cmd.Env, "BENT_PROFILES="+path.Join(dirs.wd, config.thingBenchName("profiles")))
-					cmd.Env = append(cmd.Env, "BENT_BINARY="+testBinaryName)
-					cmd.Env = append(cmd.Env, "BENT_I="+strconv.FormatInt(int64(i), 10))
+
+					cmd.Env = append(cmd.Env, runEnv...)
+					cmd.Env = append(cmd.Env, sliceExpandEnv(config.RunEnv, cmd.Env)...)
+
 					cmd.Args = append(cmd.Args, config.RunFlags...)
 					cmd.Args = append(cmd.Args, moreArgs...)
+					cmd.Args = sliceExpandEnv(cmd.Args, cmd.Env)
 
 					config.say("shortname: " + b.Name + "\n")
 					config.say("toolchain: " + config.Name + "\n")
@@ -1021,23 +1025,29 @@
 				} else {
 					// docker run --net=none -e GOROOT=... -w /src/github.com/minio/minio/cmd $D /testbin/cmd_Config.test -test.short -test.run=Nope -test.v -test.bench=Benchmark'(Get|Put|List)'
 					// TODO(jfaller): I don't think we need either of these "/" below, investigate...
+
+					// TODO this all very undertested right now.
+
 					bin := "/" + path.Join(dirs.testBinDir, testBinaryName)
 					wrappersAndBin = append(wrappersAndBin, bin)
 
 					cmd := exec.Command("docker", "run", "--net=none", "-w", b.RunDir)
-					for _, e := range config.RunEnv {
+
+					for _, e := range runEnv {
 						cmd.Args = append(cmd.Args, "-e", e)
 					}
+
 					cmd.Args = append(cmd.Args, "-e", "BENT_DIR=/") // TODO this is not going to work well
 					cmd.Args = append(cmd.Args, "-e", "BENT_PROFILES="+path.Join(dirs.wd, config.thingBenchName("profiles")))
-					cmd.Args = append(cmd.Args, "-e", "BENT_BINARY="+testBinaryName)
-					cmd.Args = append(cmd.Args, "-e", "BENT_I="+strconv.FormatInt(int64(i), 10))
 					cmd.Args = append(cmd.Args, container)
 					cmd.Args = append(cmd.Args, wrappersAndBin...)
 					cmd.Args = append(cmd.Args, "-test.run="+b.Tests)
 					cmd.Args = append(cmd.Args, "-test.bench="+b.Benchmarks)
+
 					cmd.Args = append(cmd.Args, config.RunFlags...)
 					cmd.Args = append(cmd.Args, moreArgs...)
+					cmd.Args = sliceExpandEnv(cmd.Args, runEnv)
+
 					config.say("shortname: " + b.Name + "\n")
 					config.say("toolchain: " + config.Name + "\n")
 					s, rc = todo.Configurations[j].runBinary(dirs.wd, cmd, false)
@@ -1240,6 +1250,33 @@
 	return ""
 }
 
+func expandEnv(s string, env []string) string {
+	expand := func(s string) string {
+		return getenv(env, s)
+	}
+	return os.Expand(s, expand)
+}
+
+func sliceExpandEnv(slice, env []string) []string {
+	result := slice
+	changed := false
+	expand := func(s string) string {
+		return getenv(env, s)
+	}
+	for j, s := range slice {
+		v := os.Expand(s, expand)
+		if !changed {
+			if v == s {
+				continue
+			}
+			changed = true
+			result = append([]string{}, slice[0:j]...)
+		}
+		result = append(result, v)
+	}
+	return result
+}
+
 // replaceEnv returns a new environment derived from env
 // by removing any existing definition of ev and adding ev=evv.
 func replaceEnv(env []string, ev string, evv string) []string {
diff --git a/cmd/bent/configuration.go b/cmd/bent/configuration.go
index 9bfd68d..00b745d 100644
--- a/cmd/bent/configuration.go
+++ b/cmd/bent/configuration.go
@@ -99,7 +99,7 @@
 	}
 }
 
-func (config *Configuration) runOtherBenchmarks(b *Benchmark, cwd string) {
+func (config *Configuration) runOtherBenchmarks(b *Benchmark, cwd string, cmdEnv []string) {
 	// Run various other "benchmark" commands on the built binaries, e.g., size, quality of debugging information.
 	if config.Disabled {
 		return
@@ -122,13 +122,10 @@
 		testBinaryName := config.benchName(b)
 		c := exec.Command(cmd, path.Join(cwd, dirs.testBinDir, testBinaryName), b.Name)
 
-		c.Env = defaultEnv
+		c.Env = cmdEnv
 		if !b.NotSandboxed {
 			c.Env = replaceEnv(c.Env, "GOOS", "linux")
 		}
-		// Match the build environment here.
-		c.Env = replaceEnvs(c.Env, b.GcEnv)
-		c.Env = replaceEnvs(c.Env, config.GcEnv)
 
 		if verbose > 0 {
 			fmt.Println(asCommandLine(cwd, c))
@@ -174,19 +171,7 @@
 
 	cmd := exec.Command(gocmd, "test", "-vet=off", "-c")
 	compileTo := path.Join(dirs.wd, dirs.testBinDir, config.benchName(bench))
-	cmd.Args = append(cmd.Args, "-o", compileTo)
-	cmd.Args = append(cmd.Args, bench.BuildFlags...)
-	// Do not normally need -a because cache was emptied first and std was -a installed with these flags.
-	// But for -a=1, do it anyway
-	if explicitAll == 1 {
-		cmd.Args = append(cmd.Args, "-a")
-	}
-	cmd.Args = append(cmd.Args, config.BuildFlags...)
-	if config.GcFlags != "" {
-		cmd.Args = append(cmd.Args, "-gcflags="+config.GcFlags)
-	}
-	cmd.Args = append(cmd.Args, bench.Repo)
-	cmd.Dir = bench.BuildDir // use module-mode
+
 	cmd.Env = defaultEnv
 	if !bench.NotSandboxed {
 		cmd.Env = replaceEnv(cmd.Env, "GOOS", "linux")
@@ -194,8 +179,32 @@
 	if root != "" {
 		cmd.Env = replaceEnv(cmd.Env, "GOROOT", root)
 	}
-	cmd.Env = replaceEnvs(cmd.Env, bench.GcEnv)
-	cmd.Env = replaceEnvs(cmd.Env, config.GcEnv)
+	cmd.Env = append(cmd.Env, "BENT_BENCH="+bench.Name)
+	cmd.Env = append(cmd.Env, "BENT_CONFIG="+config.Name)
+	cmd.Env = append(cmd.Env, "BENT_I="+fmt.Sprintf("%d", count))
+	cmd.Env = replaceEnvs(cmd.Env, sliceExpandEnv(bench.GcEnv, cmd.Env))
+	cmd.Env = replaceEnvs(cmd.Env, sliceExpandEnv(config.GcEnv, cmd.Env))
+	configGoArch := getenv(cmd.Env, "GOARCH")
+
+	cmdEnv := append([]string{}, cmd.Env...) // for after-build
+	if configGoArch == "" {
+		// inject a default, since the after-build may not be a go program
+		cmdEnv = append(cmdEnv, "GOARCH="+runtime.GOARCH)
+	}
+
+	cmd.Args = append(cmd.Args, "-o", compileTo)
+	cmd.Args = append(cmd.Args, sliceExpandEnv(bench.BuildFlags, cmd.Env)...)
+	// Do not normally need -a because cache was emptied first and std was -a installed with these flags.
+	// But for -a=1, do it anyway
+	if explicitAll == 1 {
+		cmd.Args = append(cmd.Args, "-a")
+	}
+	cmd.Args = append(cmd.Args, sliceExpandEnv(config.BuildFlags, cmd.Env)...)
+	if config.GcFlags != "" {
+		cmd.Args = append(cmd.Args, "-gcflags="+expandEnv(config.GcFlags, cmd.Env))
+	}
+	cmd.Args = append(cmd.Args, bench.Repo)
+	cmd.Dir = bench.BuildDir // use module-mode
 
 	if verbose > 0 {
 		fmt.Println(asCommandLine(cwd, cmd))
@@ -232,7 +241,8 @@
 	// Report and record build stats to testbin
 
 	buf := new(bytes.Buffer)
-	configGoArch := getenv(config.GcEnv, "GOARCH")
+
+	var s string
 	if configGoArch != runtime.GOARCH && configGoArch != "" {
 		s := fmt.Sprintf("goarch: %s-%s\n", runtime.GOARCH, configGoArch)
 		if verbose > 0 {
@@ -240,7 +250,7 @@
 		}
 		buf.WriteString(s)
 	}
-	s := fmt.Sprintf("Benchmark%s 1 %d build-real-ns/op %d build-user-ns/op %d build-sys-ns/op\n",
+	s = fmt.Sprintf("Benchmark%s 1 %d build-real-ns/op %d build-user-ns/op %d build-sys-ns/op\n",
 		strings.Title(bench.Name), bs.RealTime.Nanoseconds(), bs.UserTime.Nanoseconds(), bs.SysTime.Nanoseconds())
 	if verbose > 0 {
 		fmt.Print(s)
@@ -267,7 +277,7 @@
 
 	// Do this here before any cleanup.
 	if count == 0 {
-		config.runOtherBenchmarks(bench, cwd)
+		config.runOtherBenchmarks(bench, cwd, cmdEnv)
 	}
 
 	return ""