lib: make gosh.BuildGoPkg a package-level helper

Also updates v23test's BuildGoPkg to be package-level.

MultiPart: 3/6

Change-Id: I53f94d47b08a84591c6223eea05bf2dbab035fda
diff --git a/gosh/.api b/gosh/.api
index fc7b9c5..10ee7c1 100644
--- a/gosh/.api
+++ b/gosh/.api
@@ -1,3 +1,4 @@
+pkg gosh, func BuildGoPkg(*Shell, string, string, ...string) string
 pkg gosh, func InitChildMain()
 pkg gosh, func InitMain()
 pkg gosh, func NewPipeline(*Cmd, ...*Cmd) *Pipeline
@@ -36,7 +37,6 @@
 pkg gosh, method (*Pipeline) Terminate(os.Signal)
 pkg gosh, method (*Pipeline) Wait()
 pkg gosh, method (*Shell) AddCleanupHandler(func())
-pkg gosh, method (*Shell) BuildGoPkg(string, ...string) string
 pkg gosh, method (*Shell) Cleanup()
 pkg gosh, method (*Shell) Cmd(string, ...string) *Cmd
 pkg gosh, method (*Shell) FuncCmd(*Func, ...interface{}) *Cmd
@@ -62,7 +62,6 @@
 pkg gosh, type Cmd struct, Vars map[string]string
 pkg gosh, type Func struct
 pkg gosh, type Opts struct
-pkg gosh, type Opts struct, BinDir string
 pkg gosh, type Opts struct, ChildOutputDir string
 pkg gosh, type Opts struct, Fatalf func(string, ...interface{})
 pkg gosh, type Opts struct, Logf func(string, ...interface{})
diff --git a/gosh/internal/gosh_example/main.go b/gosh/internal/gosh_example/main.go
index bbe0eee..a3072ad 100644
--- a/gosh/internal/gosh_example/main.go
+++ b/gosh/internal/gosh_example/main.go
@@ -17,14 +17,15 @@
 	defer sh.Cleanup()
 
 	// Start server.
-	binPath := sh.BuildGoPkg("v.io/x/lib/gosh/internal/gosh_example_server")
+	binDir := sh.MakeTempDir()
+	binPath := gosh.BuildGoPkg(sh, binDir, "v.io/x/lib/gosh/internal/gosh_example_server")
 	c := sh.Cmd(binPath)
 	c.Start()
 	addr := c.AwaitVars("addr")["addr"]
 	fmt.Println(addr)
 
 	// Run client.
-	binPath = sh.BuildGoPkg("v.io/x/lib/gosh/internal/gosh_example_client")
+	binPath = gosh.BuildGoPkg(sh, binDir, "v.io/x/lib/gosh/internal/gosh_example_client")
 	c = sh.Cmd(binPath, "-addr="+addr)
 	fmt.Print(c.Stdout())
 }
diff --git a/gosh/shell.go b/gosh/shell.go
index 5340b17..c318b57 100644
--- a/gosh/shell.go
+++ b/gosh/shell.go
@@ -30,7 +30,6 @@
 )
 
 const (
-	envBinDir         = "GOSH_BIN_DIR"
 	envChildOutputDir = "GOSH_CHILD_OUTPUT_DIR"
 	envExitAfter      = "GOSH_EXIT_AFTER"
 	envInvocation     = "GOSH_INVOCATION"
@@ -81,9 +80,6 @@
 	// files in this directory.
 	// If not specified, defaults to GOSH_CHILD_OUTPUT_DIR.
 	ChildOutputDir string
-	// Directory where BuildGoPkg() writes compiled binaries.
-	// If not specified, defaults to GOSH_BIN_DIR.
-	BinDir string
 }
 
 // NewShell returns a new Shell.
@@ -134,17 +130,6 @@
 	sh.HandleError(sh.move(oldpath, newpath))
 }
 
-// BuildGoPkg compiles a Go package using the "go build" command and writes the
-// resulting binary to sh.Opts.BinDir, or to the -o flag location if specified.
-// Returns the absolute path to the binary.
-func (sh *Shell) BuildGoPkg(pkg string, flags ...string) string {
-	// TODO(sadovsky): Convert BuildGoPkg into a utility function.
-	sh.Ok()
-	res, err := sh.buildGoPkg(pkg, flags...)
-	sh.HandleError(err)
-	return res
-}
-
 // MakeTempFile creates a new temporary file in os.TempDir, opens the file for
 // reading and writing, and returns the resulting *os.File.
 func (sh *Shell) MakeTempFile() *os.File {
@@ -238,7 +223,7 @@
 	}
 	// Filter out any gosh env vars coming from outside.
 	shVars := copyMap(osVars)
-	for _, key := range []string{envBinDir, envChildOutputDir, envExitAfter, envInvocation, envWatchParent} {
+	for _, key := range []string{envChildOutputDir, envExitAfter, envInvocation, envWatchParent} {
 		delete(shVars, key)
 	}
 	sh := &Shell{
@@ -247,16 +232,6 @@
 		calledNewShell: true,
 		cleanupDone:    make(chan struct{}),
 	}
-	if sh.Opts.BinDir == "" {
-		sh.Opts.BinDir = osVars[envBinDir]
-		if sh.Opts.BinDir == "" {
-			var err error
-			if sh.Opts.BinDir, err = sh.makeTempDir(); err != nil {
-				sh.cleanup()
-				return sh, err
-			}
-		}
-	}
 	sh.cleanupOnSignal()
 	return sh, nil
 }
@@ -385,55 +360,6 @@
 	return os.Remove(oldpath)
 }
 
-func extractOutputFlag(flags ...string) (string, []string) {
-	for i, f := range flags {
-		if f == "-o" && len(flags) > i {
-			return flags[i+1], append(flags[:i], flags[i+2:]...)
-		}
-	}
-	return "", flags
-}
-
-func (sh *Shell) buildGoPkg(pkg string, flags ...string) (string, error) {
-	outputFlag, flags := extractOutputFlag(flags...)
-	binPath := filepath.Join(sh.Opts.BinDir, path.Base(pkg))
-	if outputFlag != "" {
-		if filepath.IsAbs(outputFlag) {
-			binPath = outputFlag
-		} else {
-			binPath = filepath.Join(sh.Opts.BinDir, outputFlag)
-		}
-	}
-	// If this binary has already been built, don't rebuild it.
-	if _, err := os.Stat(binPath); err == nil {
-		return binPath, nil
-	} else if !os.IsNotExist(err) {
-		return "", err
-	}
-	// Build binary to tempBinPath (in a fresh temporary directory), then move it
-	// to binPath.
-	tempDir, err := ioutil.TempDir(sh.Opts.BinDir, "")
-	if err != nil {
-		return "", err
-	}
-	defer os.RemoveAll(tempDir)
-	tempBinPath := filepath.Join(tempDir, path.Base(pkg))
-	args := []string{"build", "-o", tempBinPath}
-	args = append(args, flags...)
-	args = append(args, pkg)
-	c, err := sh.cmd(nil, "go", args...)
-	if err != nil {
-		return "", err
-	}
-	if err := c.run(); err != nil {
-		return "", err
-	}
-	if err := sh.move(tempBinPath, binPath); err != nil {
-		return "", err
-	}
-	return binPath, nil
-}
-
 func (sh *Shell) makeTempFile() (*os.File, error) {
 	sh.cleanupMu.Lock()
 	defer sh.cleanupMu.Unlock()
@@ -610,3 +536,78 @@
 	}
 	os.Exit(0)
 }
+
+// BuildGoPkg compiles a Go package using the "go build" command and writes the
+// resulting binary to the given binDir, or to the -o flag location if
+// specified. If -o is relative, it is interpreted as relative to binDir. If the
+// binary already exists at the target location, it is not rebuilt. Returns the
+// absolute path to the binary.
+func BuildGoPkg(sh *Shell, binDir, pkg string, flags ...string) string {
+	sh.Ok()
+	res, err := buildGoPkg(sh, binDir, pkg, flags...)
+	sh.HandleError(err)
+	return res
+}
+
+func extractOutputFlag(flags ...string) (outputFlag string, otherFlags []string, err error) {
+	for i := 0; i < len(flags); i++ {
+		v := flags[i]
+		if v == "-o" || v == "--o" {
+			i++
+			if i == len(flags) {
+				return "", nil, errors.New("gosh: passed -o without location")
+			}
+			outputFlag = flags[i]
+		} else {
+			otherFlags = append(otherFlags, v)
+		}
+	}
+	return
+}
+
+func buildGoPkg(sh *Shell, binDir, pkg string, flags ...string) (string, error) {
+	outputFlag, flags, err := extractOutputFlag(flags...)
+	if err != nil {
+		return "", err
+	}
+	var binPath string
+	if outputFlag == "" {
+		binPath = filepath.Join(binDir, path.Base(pkg))
+	} else if filepath.IsAbs(outputFlag) {
+		binPath = outputFlag
+	} else {
+		binPath = filepath.Join(binDir, outputFlag)
+	}
+	// If the binary already exists at the target location, don't rebuild it.
+	if _, err := os.Stat(binPath); err == nil {
+		return binPath, nil
+	} else if !os.IsNotExist(err) {
+		return "", err
+	}
+	// Build binary to tempBinPath (in a fresh temporary directory), then move it
+	// to binPath.
+	tempDir, err := ioutil.TempDir(binDir, "")
+	if err != nil {
+		return "", err
+	}
+	defer os.RemoveAll(tempDir)
+	tempBinPath := filepath.Join(tempDir, path.Base(pkg))
+	args := []string{"build", "-o", tempBinPath}
+	args = append(args, flags...)
+	args = append(args, pkg)
+	c, err := sh.cmd(nil, "go", args...)
+	if err != nil {
+		return "", err
+	}
+	if err := c.run(); err != nil {
+		return "", err
+	}
+	// Create target directory, if needed.
+	if err := os.MkdirAll(filepath.Dir(binPath), 0700); err != nil {
+		return "", err
+	}
+	if err := sh.move(tempBinPath, binPath); err != nil {
+		return "", err
+	}
+	return binPath, nil
+}
diff --git a/gosh/shell_test.go b/gosh/shell_test.go
index 9078c0e..1cb124f 100644
--- a/gosh/shell_test.go
+++ b/gosh/shell_test.go
@@ -7,7 +7,7 @@
 // TODO(sadovsky): Add more tests:
 // - effects of Shell.Cleanup
 // - Shell.{Vars,Args,Rename,MakeTempFile,MakeTempDir}
-// - Shell.Opts.{PropagateChildOutput,ChildOutputDir,BinDir}
+// - Shell.Opts.{PropagateChildOutput,ChildOutputDir}
 // - Cmd.Clone
 // - Cmd.Opts.{IgnoreParentExit,ExitAfter,PropagateOutput}
 
@@ -204,22 +204,28 @@
 	eq(t, getwdEvalSymlinks(t), startDir)
 }
 
+const (
+	helloWorldPkg = "v.io/x/lib/gosh/internal/hello_world"
+	helloWorldStr = "Hello, world!\n"
+)
+
 // Mirrors ExampleCmd in internal/gosh_example/main.go.
 func TestCmd(t *testing.T) {
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
 	// Start server.
-	binPath := sh.BuildGoPkg("v.io/x/lib/gosh/internal/gosh_example_server")
+	binDir := sh.MakeTempDir()
+	binPath := gosh.BuildGoPkg(sh, binDir, "v.io/x/lib/gosh/internal/gosh_example_server")
 	c := sh.Cmd(binPath)
 	c.Start()
 	addr := c.AwaitVars("addr")["addr"]
 	neq(t, addr, "")
 
 	// Run client.
-	binPath = sh.BuildGoPkg("v.io/x/lib/gosh/internal/gosh_example_client")
+	binPath = gosh.BuildGoPkg(sh, binDir, "v.io/x/lib/gosh/internal/gosh_example_client")
 	c = sh.Cmd(binPath, "-addr="+addr)
-	eq(t, c.Stdout(), "Hello, world!\n")
+	eq(t, c.Stdout(), helloWorldStr)
 }
 
 var (
@@ -240,27 +246,52 @@
 
 	// Run client.
 	c = sh.FuncCmd(getFunc, addr)
-	eq(t, c.Stdout(), "Hello, world!\n")
+	eq(t, c.Stdout(), helloWorldStr)
 }
 
-// Tests that BuildGoPkg works even when the -o flag is passed.
-// TODO(sadovsky): Add tests for (1) missing name after -o, (2) multiple
-// instances of -o, (3) using --o instead of -o, and perhaps other cases as
-// well.
+// Tests BuildGoPkg's handling of the -o flag.
 func TestBuildGoPkg(t *testing.T) {
+	if testing.Short() {
+		t.Skip()
+	}
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	tempDir := sh.MakeTempDir()
-	absName := filepath.Join(tempDir, "hw")
-	sh.BuildGoPkg("v.io/x/lib/gosh/internal/hello_world", "-o", absName)
+	// Set -o to an absolute name.
+	relName := "hw"
+	absName := filepath.Join(sh.MakeTempDir(), relName)
+	eq(t, gosh.BuildGoPkg(sh, "", helloWorldPkg, "-o", absName), absName)
 	c := sh.Cmd(absName)
-	eq(t, c.Stdout(), "Hello, world!\n")
+	eq(t, c.Stdout(), helloWorldStr)
 
-	absName = filepath.Join(sh.Opts.BinDir, "hw")
-	sh.BuildGoPkg("v.io/x/lib/gosh/internal/hello_world", "-o", "hw")
+	// Set -o to a relative name with no path separators.
+	binDir := sh.MakeTempDir()
+	absName = filepath.Join(binDir, relName)
+	eq(t, gosh.BuildGoPkg(sh, binDir, helloWorldPkg, "-o", relName), absName)
 	c = sh.Cmd(absName)
-	eq(t, c.Stdout(), "Hello, world!\n")
+	eq(t, c.Stdout(), helloWorldStr)
+
+	// Set -o to a relative name that contains a path separator.
+	relNameWithSlash := filepath.Join("subdir", relName)
+	absName = filepath.Join(binDir, relNameWithSlash)
+	eq(t, gosh.BuildGoPkg(sh, binDir, helloWorldPkg, "-o", relNameWithSlash), absName)
+	c = sh.Cmd(absName)
+	eq(t, c.Stdout(), helloWorldStr)
+
+	// Missing location after -o.
+	setsErr(t, sh, func() { gosh.BuildGoPkg(sh, "", helloWorldPkg, "-o") })
+
+	// Multiple -o.
+	absName = filepath.Join(sh.MakeTempDir(), relName)
+	gosh.BuildGoPkg(sh, "", helloWorldPkg, "-o", relName, "-o", absName)
+	c = sh.Cmd(absName)
+	eq(t, c.Stdout(), helloWorldStr)
+
+	// Use --o instead of -o.
+	absName = filepath.Join(sh.MakeTempDir(), relName)
+	gosh.BuildGoPkg(sh, "", helloWorldPkg, "--o", absName)
+	c = sh.Cmd(absName)
+	eq(t, c.Stdout(), helloWorldStr)
 }
 
 // Tests that Shell.Cmd uses Shell.Vars["PATH"] to locate executables with
@@ -269,12 +300,13 @@
 	sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
 	defer sh.Cleanup()
 
-	tempDir := sh.MakeTempDir()
-	sh.Vars["PATH"] += ":" + tempDir
-	absName, relName := filepath.Join(tempDir, "hw"), "hw"
-	sh.BuildGoPkg("v.io/x/lib/gosh/internal/hello_world", "-o", absName)
+	binDir := sh.MakeTempDir()
+	sh.Vars["PATH"] = binDir + ":" + sh.Vars["PATH"]
+	relName := "hw"
+	absName := filepath.Join(binDir, relName)
+	gosh.BuildGoPkg(sh, "", helloWorldPkg, "-o", absName)
 	c := sh.Cmd(relName)
-	eq(t, c.Stdout(), "Hello, world!\n")
+	eq(t, c.Stdout(), helloWorldStr)
 
 	// Test the case where we cannot find the executable.
 	sh.Vars["PATH"] = ""