diff --git a/cmd/playground/main.go b/cmd/playground/main.go index b5507ba3..a8accd1c 100644 --- a/cmd/playground/main.go +++ b/cmd/playground/main.go @@ -44,15 +44,15 @@ func main() { flag.StringVar(&args.playgroundUrl, "playground-url", goplay.DefaultPlaygroundURL, "Go Playground URL") flag.BoolVar(&args.debug, "debug", false, "Enable debug mode") - goRoot, ok := os.LookupEnv("GOROOT") - if !ok { - fmt.Println("environment variable GOROOT is not defined") - os.Exit(1) - } - - flag.Parse() l := getLogger(args.debug) defer l.Sync() //nolint:errcheck + flag.Parse() + + goRoot, err := compiler.GOROOT() + if err != nil { + l.Fatal("failed to find GOROOT environment variable value", zap.Error(err)) + } + if err := start(goRoot, args); err != nil { l.Sugar().Fatal(err) } diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index f3b6b1f1..e208e98b 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -7,7 +7,6 @@ import ( "go.uber.org/zap" "io" "os" - "os/exec" ) var buildArgs = []string{ @@ -38,13 +37,7 @@ func NewBuildService(log *zap.SugaredLogger, store storage.StoreProvider) BuildS } func (s BuildService) buildSource(ctx context.Context, outputLocation, sourceLocation string) error { - cmd := exec.CommandContext(ctx, "go", - "build", - "-o", - outputLocation, - sourceLocation, - ) - + cmd := newGoToolCommand(ctx, "build", "-o", outputLocation, sourceLocation) cmd.Env = buildArgs buff := &bytes.Buffer{} cmd.Stderr = buff diff --git a/pkg/compiler/env.go b/pkg/compiler/env.go new file mode 100644 index 00000000..a167a4ac --- /dev/null +++ b/pkg/compiler/env.go @@ -0,0 +1,66 @@ +package compiler + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "os/exec" + "strings" + "time" +) + +const goRootEnv = "GOROOT" + +// ErrUndefinedEnvVariable occurs when requested environment variable is undefined or empty +var ErrUndefinedEnvVariable = errors.New("environment variable is undefined or empty") + +// GOROOT returns host GOROOT variable from OS environment vars or from Go tool environment. +func GOROOT() (string, error) { + return LookupEnv(context.Background(), goRootEnv) +} + +func newGoToolCommand(ctx context.Context, args ...string) *exec.Cmd { + cmd := exec.CommandContext(ctx, "go", args...) + cmd.Env = os.Environ() + return cmd +} + +func getEnvFromGo(ctx context.Context, envName string) (string, error) { + cmd := newGoToolCommand(ctx, "env", envName) + buff := new(bytes.Buffer) + cmd.Stdout = buff + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("command %q returned an error: %w", strings.Join(cmd.Args, " "), err) + } + + return buff.String(), nil +} + +// LookupEnv gets variable by name from shell environment or using Go environment using "go env" tool. +func LookupEnv(ctx context.Context, varName string) (string, error) { + envVar, ok := os.LookupEnv(varName) + if ok { + envVar = strings.TrimSpace(envVar) + if envVar != "" { + return envVar, nil + } + } + + cmdCtx, cf := context.WithTimeout(ctx, 5*time.Second) + defer cf() + + // Lookup env variable using "go env" tool if not defined in environment. + val, err := getEnvFromGo(cmdCtx, varName) + if err != nil { + return "", err + } + + val = strings.TrimSpace(val) + if val == "" { + return "", ErrUndefinedEnvVariable + } + + return val, nil +} diff --git a/pkg/compiler/env_test.go b/pkg/compiler/env_test.go new file mode 100644 index 00000000..ff8afefd --- /dev/null +++ b/pkg/compiler/env_test.go @@ -0,0 +1,62 @@ +package compiler + +import ( + "context" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLookupEnv(t *testing.T) { + cases := map[string]struct { + envName string + expectErr string + expectValue string + beforeRun func(t *testing.T) + }{ + "read value from shell": { + envName: "FOO", + expectValue: "FOOBAR", + beforeRun: func(t *testing.T) { + t.Setenv("FOO", "FOOBAR") + }, + }, + "fallback to go env tool": { + envName: "GOHOSTOS", + expectValue: runtime.GOOS, + }, + "failed to call go tool": { + envName: "GOHOSTARCH", + expectErr: `command "go env GOHOSTARCH" returned an error`, + beforeRun: func(t *testing.T) { + // "go test" sets all env vars before run + t.Setenv("GOHOSTARCH", "") + // Break path lookup to simulate error + t.Setenv("PATH", ".") + }, + }, + "undefined variable": { + envName: "THIS_ENV_VAR_SHOULD_NOT_EXIST", + expectErr: ErrUndefinedEnvVariable.Error(), + }, + } + + for k, v := range cases { + t.Run(k, func(t *testing.T) { + if v.beforeRun != nil { + v.beforeRun(t) + } + got, err := LookupEnv(context.Background(), v.envName) + if v.expectErr != "" { + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), v.expectErr), + "error %q should include %q", err.Error(), v.expectErr) + return + } + require.NoError(t, err) + require.Equal(t, v.expectValue, got) + }) + } +}