buildlet: add Path option to ExecOpts
Change-Id: I1353ecc14dfe232886dcfadc47547bbcf91f0b6d
Reviewed-on: https://go-review.googlesource.com/10303
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/buildlet/buildletclient.go b/buildlet/buildletclient.go
index 65d6b21..f555045 100644
--- a/buildlet/buildletclient.go
+++ b/buildlet/buildletclient.go
@@ -206,6 +206,16 @@
// process's environment.
ExtraEnv []string
+ // Path, if non-nil, specifies the PATH variable of the executed
+ // process's environment. A non-nil empty list clears the path.
+ // The following expansions apply:
+ // - the string "$PATH" expands to any existing PATH element(s)
+ // - the substring "$WORKDIR" expands to buildlet's temp workdir
+ // After expansions, the list is joined with an OS-specific list
+ // separator and supplied to the executed process as its PATH
+ // environment variable.
+ Path []string
+
// SystemLevel controls whether the command is run outside of
// the buildlet's environment.
SystemLevel bool
@@ -232,12 +242,19 @@
if opts.SystemLevel {
mode = "sys"
}
+ path := opts.Path
+ if len(path) == 0 && path != nil {
+ // url.Values doesn't distinguish between a nil slice and
+ // a non-nil zero-length slice, so use this sentinel value.
+ path = []string{"$EMPTY"}
+ }
form := url.Values{
"cmd": {cmd},
"mode": {mode},
"dir": {opts.Dir},
"cmdArg": opts.Args,
"env": opts.ExtraEnv,
+ "path": path,
"debug": {fmt.Sprint(opts.Debug)},
}
req, err := http.NewRequest("POST", c.URL()+"/exec", strings.NewReader(form.Encode()))
diff --git a/cmd/buildlet/buildlet.go b/cmd/buildlet/buildlet.go
index 2c512f0..3e2f191 100644
--- a/cmd/buildlet/buildlet.go
+++ b/cmd/buildlet/buildlet.go
@@ -38,6 +38,7 @@
"time"
"golang.org/x/build/buildlet"
+ "golang.org/x/build/envutil"
"google.golang.org/cloud/compute/metadata"
)
@@ -620,12 +621,16 @@
f.Flush()
}
+ env := append(baseEnv(), r.PostForm["env"]...)
+ env = envutil.Dedup(runtime.GOOS == "windows", env)
+ env = setPathEnv(env, r.PostForm["path"], *workDir)
+
cmd := exec.Command(absCmd, r.PostForm["cmdArg"]...)
cmd.Dir = dir
cmdOutput := flushWriter{w}
cmd.Stdout = cmdOutput
cmd.Stderr = cmdOutput
- cmd.Env = append(baseEnv(), r.PostForm["env"]...)
+ cmd.Env = env
if debug {
fmt.Fprintf(cmdOutput, ":: Running %s with args %q and env %q in dir %s\n\n",
@@ -659,6 +664,59 @@
log.Printf("Run = %s", state)
}
+// setPathEnv returns a copy of the provided environment with any existing
+// PATH variables replaced by the user-provided path.
+// These substitutions are applied to user-supplied path elements:
+// - the string "$PATH" expands to the original PATH elements
+// - the substring "$WORKDIR" expands to the provided workDir
+// A path of just ["$EMPTY"] removes the PATH variable from the environment.
+func setPathEnv(env, path []string, workDir string) []string {
+ if len(path) == 0 {
+ return env
+ }
+
+ var (
+ pathIdx = -1
+ pathOrig = ""
+ )
+ for i, s := range env {
+ if strings.HasPrefix(s, "PATH=") {
+ pathIdx = i
+ pathOrig = strings.TrimPrefix(s, "PATH=")
+ break
+ }
+ }
+ if len(path) == 1 && path[0] == "$EMPTY" {
+ // Remove existing path variable if it exists.
+ if pathIdx >= 0 {
+ env = append(env[:pathIdx], env[pathIdx+1:]...)
+ }
+ return env
+ }
+
+ // Apply substitions to a copy of the path argument.
+ path = append([]string{}, path...)
+ for i, s := range path {
+ if s == "$PATH" {
+ path[i] = pathOrig // ok if empty
+ } else {
+ path[i] = strings.Replace(s, "$WORKDIR", workDir, -1)
+ }
+ }
+
+ // Put the new PATH in env.
+ env = append([]string{}, env...)
+ // TODO(adg): check that this works on plan 9
+ pathEnv := "PATH=" + strings.Join(path, string(filepath.ListSeparator))
+ if pathIdx >= 0 {
+ env[pathIdx] = pathEnv
+ } else {
+ env = append(env, pathEnv)
+ }
+
+ return env
+}
+
func baseEnv() []string {
if runtime.GOOS == "windows" {
return windowsBaseEnv()
diff --git a/cmd/buildlet/buildlet_test.go b/cmd/buildlet/buildlet_test.go
new file mode 100644
index 0000000..5b9f243
--- /dev/null
+++ b/cmd/buildlet/buildlet_test.go
@@ -0,0 +1,61 @@
+// Copyright 2015 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 main
+
+import (
+ "fmt"
+ "runtime"
+ "testing"
+)
+
+func TestSetPathEnv(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("TODO(adg): make this test work on windows")
+ }
+
+ const workDir = "/workdir"
+
+ for _, c := range []struct {
+ env []string
+ path []string
+ want []string
+ }{
+ { // No change to PATH
+ []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
+ []string{},
+ []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
+ },
+ { // Test sentinel $EMPTY value to clear PATH
+ []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
+ []string{"$EMPTY"},
+ []string{"A=1", "B=2"},
+ },
+ { // Test $WORKDIR expansion
+ []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
+ []string{"/go/bin", "$WORKDIR/foo"},
+ []string{"A=1", "PATH=/go/bin:/workdir/foo", "B=2"},
+ },
+ { // Test $PATH expansion
+ []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
+ []string{"/go/bin", "$PATH", "$WORKDIR/foo"},
+ []string{"A=1", "PATH=/go/bin:/bin:/usr/bin:/workdir/foo", "B=2"},
+ },
+ { // Test $PATH expansion (prepend only)
+ []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
+ []string{"/go/bin", "/a/b", "$PATH"},
+ []string{"A=1", "PATH=/go/bin:/a/b:/bin:/usr/bin", "B=2"},
+ },
+ { // Test $PATH expansion (append only)
+ []string{"A=1", "PATH=/bin:/usr/bin", "B=2"},
+ []string{"$PATH", "/go/bin", "/a/b"},
+ []string{"A=1", "PATH=/bin:/usr/bin:/go/bin:/a/b", "B=2"},
+ },
+ } {
+ got := setPathEnv(c.env, c.path, workDir)
+ if g, w := fmt.Sprint(got), fmt.Sprint(c.want); g != w {
+ t.Errorf("setPathEnv(%q, %q) = %q, want %q", c.env, c.path, g, w)
+ }
+ }
+}