lib: Address https://v.io/i/1220

- reverts https://v.io/c/20483
- changes v23test to not share a bin dir across test main's
(bin dir is still shared across tests within a given test
main, but that's safe since "go test" will not run such
tests concurrently by default)
- changes v23test.StartRootMountTable and
v23test.StartSyncbase to use gosh.Shell.FuncCmd
(refactoring syncbased and mounttabled as needed to make
this possible)

A side effect of this change is that tests should run much
faster, because for the common case of tests that previously
used StartRootMountTable and/or StartSyncbase, but didn't
directly call BuildGoPkg, we'll no longer build Go packages
during test execution.

MultiPart: 2/5

Change-Id: Iba81f1db56ca21f2d887e26496d7a87ad288b076
diff --git a/gosh/shell.go b/gosh/shell.go
index 3c9df0b..3bab48d 100644
--- a/gosh/shell.go
+++ b/gosh/shell.go
@@ -150,7 +150,11 @@
 	sh.handleError(sh.wait())
 }
 
-// Move moves a file.
+// Move moves a file from 'oldpath' to 'newpath'. It first attempts os.Rename;
+// if that fails, it copies 'oldpath' to 'newpath', then deletes 'oldpath'.
+// Requires that 'newpath' does not exist, and that the parent directory of
+// 'newpath' does exist. Currently only supports moving an individual file;
+// moving a directory is not yet supported.
 func (sh *Shell) Move(oldpath, newpath string) {
 	sh.Ok()
 	sh.handleError(sh.move(oldpath, newpath))
@@ -340,7 +344,7 @@
 	return res
 }
 
-func copyFile(from, to string) error {
+func copyFile(to, from string) error {
 	fi, err := os.Stat(from)
 	if err != nil {
 		return err
@@ -363,18 +367,33 @@
 }
 
 func (sh *Shell) move(oldpath, newpath string) error {
-	var err error
-	if err = os.Rename(oldpath, newpath); err != nil {
-		// Concurrent, same-directory rename operations sometimes fail on certain
-		// filesystems, so we retry once after a random backoff.
-		time.Sleep(time.Duration(rand.Int63n(1000)) * time.Millisecond)
-		err = os.Rename(oldpath, newpath)
-	}
-	// If the error was a LinkError, try copying the file over.
-	if _, ok := err.(*os.LinkError); !ok {
+	fi, err := os.Stat(oldpath)
+	if err != nil {
 		return err
 	}
-	if err := copyFile(oldpath, newpath); err != nil {
+	if fi.Mode().IsDir() {
+		return errors.New("gosh: moving a directory is not yet supported")
+	}
+	if _, err := os.Stat(newpath); !os.IsNotExist(err) {
+		return errors.New("gosh: destination file must not exist")
+	}
+	if _, err := os.Stat(filepath.Dir(newpath)); err != nil {
+		if os.IsNotExist(err) {
+			return errors.New("gosh: destination file's parent directory must exist")
+		}
+		return err
+	}
+	if err := os.Rename(oldpath, newpath); err == nil {
+		return nil
+	}
+	// Concurrent, same-directory rename operations sometimes fail on certain
+	// systems, so we retry once after a random backoff.
+	time.Sleep(time.Duration(rand.Int63n(1000)) * time.Millisecond)
+	if err := os.Rename(oldpath, newpath); err == nil {
+		return nil
+	}
+	// Try copying the file over.
+	if err := copyFile(newpath, oldpath); err != nil {
 		return err
 	}
 	return os.Remove(oldpath)
@@ -540,6 +559,9 @@
 // parent process, it returns immediately with no effect. In a child process for
 // a Shell.FuncCmd command, it runs the specified function, then exits.
 func InitMain() {
+	if calledInitMain {
+		panic("gosh: already called gosh.InitMain")
+	}
 	calledInitMain = true
 	s := os.Getenv(envInvocation)
 	if s == "" {
@@ -629,5 +651,6 @@
 	if err := sh.move(tempBinPath, binPath); err != nil {
 		return "", err
 	}
+	sh.tb.Logf("Built executable: %s\n", binPath)
 	return binPath, nil
 }
diff --git a/gosh/shell_test.go b/gosh/shell_test.go
index 6362f77..74fea0b 100644
--- a/gosh/shell_test.go
+++ b/gosh/shell_test.go
@@ -7,7 +7,6 @@
 // TODO(sadovsky): Add more tests:
 // - effects of Shell.Cleanup
 // - Shell.{PropagateChildOutput,ChildOutputDir,Vars,Args}
-// - Shell.{Move,MakeTempFile}
 // - Cmd.{IgnoreParentExit,ExitAfter,PropagateOutput}
 // - Cmd.Clone
 
@@ -83,8 +82,8 @@
 	sh.ContinueOnError = continueOnError
 }
 
-////////////////////////////////////////
-// Simple functions
+////////////////////////////////////////////////////////////////////////////////
+// Simple registered functions
 
 // Simplified versions of various Unix commands.
 var (
@@ -128,8 +127,8 @@
 	})
 )
 
-////////////////////////////////////////
-// Tests
+////////////////////////////////////////////////////////////////////////////////
+// Shell tests
 
 type customTB struct {
 	t             *testing.T
@@ -221,6 +220,229 @@
 	eq(t, getwdEvalSymlinks(t), startDir)
 }
 
+func TestMakeTempDir(t *testing.T) {
+	sh := gosh.NewShell(t)
+	defer sh.Cleanup()
+
+	name := sh.MakeTempDir()
+	fi, err := os.Stat(name)
+	ok(t, err)
+	eq(t, fi.Mode().IsDir(), true)
+}
+
+func TestMakeTempFile(t *testing.T) {
+	sh := gosh.NewShell(t)
+	defer sh.Cleanup()
+
+	file := sh.MakeTempFile()
+	fi, err := file.Stat()
+	ok(t, err)
+	eq(t, fi.Mode().IsRegular(), true)
+}
+
+func TestMove(t *testing.T) {
+	sh := gosh.NewShell(t)
+	defer sh.Cleanup()
+
+	// TODO(sadovsky): Run all tests twice: once with oldpath and newpath on the
+	// same volume, and once with oldpath and newpath on different volumes.
+	src, dst := sh.MakeTempDir(), sh.MakeTempDir()
+	// Foo files exist, bar files do not.
+	srcFoo, dstFoo := filepath.Join(src, "srcFoo"), filepath.Join(dst, "dstFoo")
+	srcBar, dstBar := filepath.Join(src, "srcBar"), filepath.Join(dst, "dstBar")
+	ioutil.WriteFile(srcFoo, []byte("srcFoo"), 0600)
+	ioutil.WriteFile(dstFoo, []byte("dstFoo"), 0600)
+
+	// Move should fail if source does not exist.
+	setsErr(t, sh, func() { sh.Move(srcBar, dstBar) })
+
+	// Move should fail if source is a directory.
+	setsErr(t, sh, func() { sh.Move(src, dstBar) })
+
+	// Move should fail if destination exists, regardless of whether it is a
+	// regular file or a directory.
+	setsErr(t, sh, func() { sh.Move(srcFoo, dstFoo) })
+	setsErr(t, sh, func() { sh.Move(srcFoo, dst) })
+
+	// Move should fail if destination's parent does not exist.
+	setsErr(t, sh, func() { sh.Move(srcFoo, filepath.Join(dst, "subdir", "a")) })
+
+	// Move should succeed if source exists and is a file, destination does not
+	// exist, and destination's parent exists.
+	sh.Move(srcFoo, dstBar)
+	if _, err := os.Stat(srcFoo); !os.IsNotExist(err) {
+		t.Fatalf("got %v, expected IsNotExist", err)
+	}
+	buf, err := ioutil.ReadFile(dstBar)
+	ok(t, err)
+	eq(t, string(buf), "srcFoo")
+}
+
+func TestShellWait(t *testing.T) {
+	sh := gosh.NewShell(t)
+	defer sh.Cleanup()
+
+	d0 := time.Duration(0)
+	d200 := 200 * time.Millisecond
+
+	c0 := sh.FuncCmd(sleepFunc, d0, 0)   // not started
+	c1 := sh.Cmd("/#invalid#/!binary!")  // failed to start
+	c2 := sh.FuncCmd(sleepFunc, d200, 0) // running and will succeed
+	c3 := sh.FuncCmd(sleepFunc, d200, 1) // running and will fail
+	c4 := sh.FuncCmd(sleepFunc, d0, 0)   // succeeded
+	c5 := sh.FuncCmd(sleepFunc, d0, 0)   // succeeded, called wait
+	c6 := sh.FuncCmd(sleepFunc, d0, 1)   // failed
+	c7 := sh.FuncCmd(sleepFunc, d0, 1)   // failed, called wait
+
+	c3.ExitErrorIsOk = true
+	c6.ExitErrorIsOk = true
+	c7.ExitErrorIsOk = true
+
+	// Make sure c1 fails to start. This indirectly tests that Shell.Cleanup works
+	// even if Cmd.Start failed.
+	setsErr(t, sh, c1.Start)
+
+	// Start commands, then wait for them to exit.
+	for _, c := range []*gosh.Cmd{c2, c3, c4, c5, c6, c7} {
+		c.Start()
+	}
+	// Wait for a bit to allow the zero-sleep commands to exit.
+	time.Sleep(100 * time.Millisecond)
+	c5.Wait()
+	c7.Wait()
+	sh.Wait()
+
+	// It should be possible to run existing unstarted commands, and to create and
+	// run new commands, after calling Shell.Wait.
+	c0.Run()
+	sh.FuncCmd(sleepFunc, d0, 0).Run()
+	sh.FuncCmd(sleepFunc, d0, 0).Start()
+
+	// Call Shell.Wait again.
+	sh.Wait()
+}
+
+// Tests that Shell.Ok panics under various conditions.
+func TestOkPanics(t *testing.T) {
+	func() { // errDidNotCallNewShell
+		sh := gosh.Shell{}
+		defer func() { neq(t, recover(), nil) }()
+		sh.Ok()
+	}()
+	func() { // errShellErrIsNotNil
+		sh := gosh.NewShell(t)
+		sh.ContinueOnError = true
+		defer sh.Cleanup()
+		sh.Err = fakeError
+		defer func() { neq(t, recover(), nil) }()
+		sh.Ok()
+	}()
+	func() { // errAlreadyCalledCleanup
+		sh := gosh.NewShell(t)
+		sh.ContinueOnError = true
+		sh.Cleanup()
+		defer func() { neq(t, recover(), nil) }()
+		sh.Ok()
+	}()
+}
+
+// Tests that Shell.HandleError panics under various conditions.
+func TestHandleErrorPanics(t *testing.T) {
+	func() { // errDidNotCallNewShell
+		sh := gosh.Shell{}
+		defer func() { neq(t, recover(), nil) }()
+		sh.HandleError(fakeError)
+	}()
+	func() { // errShellErrIsNotNil
+		sh := gosh.NewShell(t)
+		sh.ContinueOnError = true
+		defer sh.Cleanup()
+		sh.Err = fakeError
+		defer func() { neq(t, recover(), nil) }()
+		sh.HandleError(fakeError)
+	}()
+	func() { // errAlreadyCalledCleanup
+		sh := gosh.NewShell(t)
+		sh.ContinueOnError = true
+		sh.Cleanup()
+		defer func() { neq(t, recover(), nil) }()
+		sh.HandleError(fakeError)
+	}()
+}
+
+// Tests that Shell.Cleanup panics under various conditions.
+func TestCleanupPanics(t *testing.T) {
+	func() { // errDidNotCallNewShell
+		sh := gosh.Shell{}
+		defer func() { neq(t, recover(), nil) }()
+		sh.Cleanup()
+	}()
+}
+
+// Tests that Shell.Cleanup succeeds even if sh.Err is not nil.
+func TestCleanupAfterError(t *testing.T) {
+	sh := gosh.NewShell(t)
+	sh.Err = fakeError
+	sh.Cleanup()
+}
+
+// Tests that Shell.Cleanup can be called multiple times.
+func TestMultipleCleanup(t *testing.T) {
+	sh := gosh.NewShell(t)
+	sh.Cleanup()
+	sh.Cleanup()
+}
+
+// Tests that Shell.HandleError logs errors using an appropriate runtime.Caller
+// skip value.
+func TestHandleErrorLogging(t *testing.T) {
+	tb := &customTB{t: t, buf: &bytes.Buffer{}}
+	sh := gosh.NewShell(tb)
+	defer sh.Cleanup()
+
+	// Call HandleError, then check that the stack trace and error got logged.
+	tb.Reset()
+	sh.HandleError(fakeError)
+	_, file, line, _ := runtime.Caller(0)
+	got, wantSuffix := tb.buf.String(), fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line-1, fakeError)
+	if !strings.HasSuffix(got, wantSuffix) {
+		t.Fatalf("got %v, want suffix %v", got, wantSuffix)
+	}
+	if got == wantSuffix {
+		t.Fatalf("missing stack trace: %v", got)
+	}
+	sh.Err = nil
+
+	// Same as above, but with ContinueOnError set to true. Only the error should
+	// get logged.
+	sh.ContinueOnError = true
+	tb.Reset()
+	sh.HandleError(fakeError)
+	_, file, line, _ = runtime.Caller(0)
+	got, want := tb.buf.String(), fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line-1, fakeError)
+	eq(t, got, want)
+	sh.Err = nil
+
+	// Same as above, but calling HandleErrorWithSkip, with skip set to 1.
+	tb.Reset()
+	sh.HandleErrorWithSkip(fakeError, 1)
+	_, file, line, _ = runtime.Caller(0)
+	got, want = tb.buf.String(), fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line-1, fakeError)
+	eq(t, got, want)
+	sh.Err = nil
+
+	// Same as above, but with skip set to 2.
+	tb.Reset()
+	sh.HandleErrorWithSkip(fakeError, 2)
+	_, file, line, _ = runtime.Caller(1)
+	got, want = tb.buf.String(), fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line, fakeError)
+	eq(t, got, want)
+	sh.Err = nil
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Cmd tests
+
 const (
 	helloWorldPkg = "v.io/x/lib/gosh/internal/hello_world"
 	helloWorldStr = "Hello, world!\n"
@@ -266,51 +488,6 @@
 	eq(t, c.Stdout(), helloWorldStr)
 }
 
-// Tests BuildGoPkg's handling of the -o flag.
-func TestBuildGoPkg(t *testing.T) {
-	if testing.Short() {
-		t.Skip()
-	}
-	sh := gosh.NewShell(t)
-	defer sh.Cleanup()
-
-	// 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(), helloWorldStr)
-
-	// 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(), 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
 // relative names.
 func TestLookPath(t *testing.T) {
@@ -387,7 +564,7 @@
 }
 
 // Tests that AwaitVars returns immediately when the process exits.
-func TestAwaitProcessExit(t *testing.T) {
+func TestAwaitVarsProcessExit(t *testing.T) {
 	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
 
@@ -717,50 +894,6 @@
 	setsErr(t, sh, func() { c.Terminate(os.Interrupt) })
 }
 
-func TestShellWait(t *testing.T) {
-	sh := gosh.NewShell(t)
-	defer sh.Cleanup()
-
-	d0 := time.Duration(0)
-	d200 := 200 * time.Millisecond
-
-	c0 := sh.FuncCmd(sleepFunc, d0, 0)   // not started
-	c1 := sh.Cmd("/#invalid#/!binary!")  // failed to start
-	c2 := sh.FuncCmd(sleepFunc, d200, 0) // running and will succeed
-	c3 := sh.FuncCmd(sleepFunc, d200, 1) // running and will fail
-	c4 := sh.FuncCmd(sleepFunc, d0, 0)   // succeeded
-	c5 := sh.FuncCmd(sleepFunc, d0, 0)   // succeeded, called wait
-	c6 := sh.FuncCmd(sleepFunc, d0, 1)   // failed
-	c7 := sh.FuncCmd(sleepFunc, d0, 1)   // failed, called wait
-
-	c3.ExitErrorIsOk = true
-	c6.ExitErrorIsOk = true
-	c7.ExitErrorIsOk = true
-
-	// Make sure c1 fails to start. This indirectly tests that Shell.Cleanup works
-	// even if Cmd.Start failed.
-	setsErr(t, sh, c1.Start)
-
-	// Start commands, then wait for them to exit.
-	for _, c := range []*gosh.Cmd{c2, c3, c4, c5, c6, c7} {
-		c.Start()
-	}
-	// Wait for a bit to allow the zero-sleep commands to exit.
-	time.Sleep(100 * time.Millisecond)
-	c5.Wait()
-	c7.Wait()
-	sh.Wait()
-
-	// It should be possible to run existing unstarted commands, and to create and
-	// run new commands, after calling Shell.Wait.
-	c0.Run()
-	sh.FuncCmd(sleepFunc, d0, 0).Run()
-	sh.FuncCmd(sleepFunc, d0, 0).Start()
-
-	// Call Shell.Wait again.
-	sh.Wait()
-}
-
 func TestExitErrorIsOk(t *testing.T) {
 	sh := gosh.NewShell(t)
 	defer sh.Cleanup()
@@ -823,124 +956,6 @@
 	return 0, w.error
 }
 
-// Tests that Shell.Ok panics under various conditions.
-func TestOkPanics(t *testing.T) {
-	func() { // errDidNotCallNewShell
-		sh := gosh.Shell{}
-		defer func() { neq(t, recover(), nil) }()
-		sh.Ok()
-	}()
-	func() { // errShellErrIsNotNil
-		sh := gosh.NewShell(t)
-		sh.ContinueOnError = true
-		defer sh.Cleanup()
-		sh.Err = fakeError
-		defer func() { neq(t, recover(), nil) }()
-		sh.Ok()
-	}()
-	func() { // errAlreadyCalledCleanup
-		sh := gosh.NewShell(t)
-		sh.ContinueOnError = true
-		sh.Cleanup()
-		defer func() { neq(t, recover(), nil) }()
-		sh.Ok()
-	}()
-}
-
-// Tests that Shell.HandleError panics under various conditions.
-func TestHandleErrorPanics(t *testing.T) {
-	func() { // errDidNotCallNewShell
-		sh := gosh.Shell{}
-		defer func() { neq(t, recover(), nil) }()
-		sh.HandleError(fakeError)
-	}()
-	func() { // errShellErrIsNotNil
-		sh := gosh.NewShell(t)
-		sh.ContinueOnError = true
-		defer sh.Cleanup()
-		sh.Err = fakeError
-		defer func() { neq(t, recover(), nil) }()
-		sh.HandleError(fakeError)
-	}()
-	func() { // errAlreadyCalledCleanup
-		sh := gosh.NewShell(t)
-		sh.ContinueOnError = true
-		sh.Cleanup()
-		defer func() { neq(t, recover(), nil) }()
-		sh.HandleError(fakeError)
-	}()
-}
-
-// Tests that Shell.Cleanup panics under various conditions.
-func TestCleanupPanics(t *testing.T) {
-	func() { // errDidNotCallNewShell
-		sh := gosh.Shell{}
-		defer func() { neq(t, recover(), nil) }()
-		sh.Cleanup()
-	}()
-}
-
-// Tests that Shell.Cleanup succeeds even if sh.Err is not nil.
-func TestCleanupAfterError(t *testing.T) {
-	sh := gosh.NewShell(t)
-	sh.Err = fakeError
-	sh.Cleanup()
-}
-
-// Tests that Shell.Cleanup can be called multiple times.
-func TestMultipleCleanup(t *testing.T) {
-	sh := gosh.NewShell(t)
-	sh.Cleanup()
-	sh.Cleanup()
-}
-
-// Tests that Shell.HandleError logs errors using an appropriate runtime.Caller
-// skip value.
-func TestHandleErrorLogging(t *testing.T) {
-	tb := &customTB{t: t, buf: &bytes.Buffer{}}
-	sh := gosh.NewShell(tb)
-	defer sh.Cleanup()
-
-	// Call HandleError, then check that the stack trace and error got logged.
-	tb.Reset()
-	sh.HandleError(fakeError)
-	_, file, line, _ := runtime.Caller(0)
-	got, wantSuffix := tb.buf.String(), fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line-1, fakeError)
-	if !strings.HasSuffix(got, wantSuffix) {
-		t.Fatalf("got %v, want suffix %v", got, wantSuffix)
-	}
-	if got == wantSuffix {
-		t.Fatalf("missing stack trace: %v", got)
-	}
-	sh.Err = nil
-
-	// Same as above, but with ContinueOnError set to true. Only the error should
-	// get logged.
-	sh.ContinueOnError = true
-	tb.Reset()
-	sh.HandleError(fakeError)
-	_, file, line, _ = runtime.Caller(0)
-	got, want := tb.buf.String(), fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line-1, fakeError)
-	eq(t, got, want)
-	sh.Err = nil
-
-	// Same as above, but calling HandleErrorWithSkip, with skip set to 1.
-	tb.Reset()
-	sh.HandleErrorWithSkip(fakeError, 1)
-	_, file, line, _ = runtime.Caller(0)
-	got, want = tb.buf.String(), fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line-1, fakeError)
-	eq(t, got, want)
-	sh.Err = nil
-
-	// Same as above, but with skip set to 2.
-	tb.Reset()
-	sh.HandleErrorWithSkip(fakeError, 2)
-	_, file, line, _ = runtime.Caller(1)
-	got, want = tb.buf.String(), fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line, fakeError)
-	eq(t, got, want)
-	sh.Err = nil
-}
-
 var cmdFailureFunc = gosh.RegisterFunc("cmdFailureFunc", func(nStdout, nStderr int) error {
 	if _, err := os.Stdout.Write([]byte(strings.Repeat("A", nStdout))); err != nil {
 		return err
@@ -1033,3 +1048,51 @@
 	gosh.InitMain()
 	os.Exit(m.Run())
 }
+
+////////////////////////////////////////////////////////////////////////////////
+// Other tests
+
+// Tests BuildGoPkg's handling of the -o flag.
+func TestBuildGoPkg(t *testing.T) {
+	if testing.Short() {
+		t.Skip()
+	}
+	sh := gosh.NewShell(t)
+	defer sh.Cleanup()
+
+	// 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(), helloWorldStr)
+
+	// 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(), 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)
+}
diff --git a/lookpath/lookpath.go b/lookpath/lookpath.go
index 7ee21a6..0a448a5 100644
--- a/lookpath/lookpath.go
+++ b/lookpath/lookpath.go
@@ -60,7 +60,7 @@
 		}
 		return file, nil
 	}
-	return "", &exec.Error{name, exec.ErrNotFound}
+	return "", &exec.Error{Name: name, Err: exec.ErrNotFound}
 }
 
 // LookPrefix returns the absolute paths of all executables with the given name
@@ -114,7 +114,7 @@
 		sort.Sort(byBase(all))
 		return all, nil
 	}
-	return nil, &exec.Error{prefix + "*", exec.ErrNotFound}
+	return nil, &exec.Error{Name: prefix + "*", Err: exec.ErrNotFound}
 }
 
 type byBase []string