blob: 6937187522bd237fbefac0c13727754af40fcb86 [file] [log] [blame]
// Copyright 2012 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 envcmd implements the ``go env'' command.
package envcmd
import (
"context"
"encoding/json"
"fmt"
"go/build"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"unicode/utf8"
"cmd/go/internal/base"
"cmd/go/internal/cache"
"cmd/go/internal/cfg"
"cmd/go/internal/fsys"
"cmd/go/internal/load"
"cmd/go/internal/modload"
"cmd/go/internal/work"
)
var CmdEnv = &base.Command{
UsageLine: "go env [-json] [-u] [-w] [var ...]",
Short: "print Go environment information",
Long: `
Env prints Go environment information.
By default env prints information as a shell script
(on Windows, a batch file). If one or more variable
names is given as arguments, env prints the value of
each named variable on its own line.
The -json flag prints the environment in JSON format
instead of as a shell script.
The -u flag requires one or more arguments and unsets
the default setting for the named environment variables,
if one has been set with 'go env -w'.
The -w flag requires one or more arguments of the
form NAME=VALUE and changes the default settings
of the named environment variables to the given values.
For more about environment variables, see 'go help environment'.
`,
}
func init() {
CmdEnv.Run = runEnv // break init cycle
}
var (
envJson = CmdEnv.Flag.Bool("json", false, "")
envU = CmdEnv.Flag.Bool("u", false, "")
envW = CmdEnv.Flag.Bool("w", false, "")
)
func MkEnv() []cfg.EnvVar {
envFile, _ := cfg.EnvFile()
env := []cfg.EnvVar{
{Name: "GO111MODULE", Value: cfg.Getenv("GO111MODULE")},
{Name: "GOARCH", Value: cfg.Goarch},
{Name: "GOBIN", Value: cfg.GOBIN},
{Name: "GOCACHE", Value: cache.DefaultDir()},
{Name: "GOENV", Value: envFile},
{Name: "GOEXE", Value: cfg.ExeSuffix},
{Name: "GOFLAGS", Value: cfg.Getenv("GOFLAGS")},
{Name: "GOHOSTARCH", Value: runtime.GOARCH},
{Name: "GOHOSTOS", Value: runtime.GOOS},
{Name: "GOINSECURE", Value: cfg.GOINSECURE},
{Name: "GOMODCACHE", Value: cfg.GOMODCACHE},
{Name: "GONOPROXY", Value: cfg.GONOPROXY},
{Name: "GONOSUMDB", Value: cfg.GONOSUMDB},
{Name: "GOOS", Value: cfg.Goos},
{Name: "GOPATH", Value: cfg.BuildContext.GOPATH},
{Name: "GOPRIVATE", Value: cfg.GOPRIVATE},
{Name: "GOPROXY", Value: cfg.GOPROXY},
{Name: "GOROOT", Value: cfg.GOROOT},
{Name: "GOSUMDB", Value: cfg.GOSUMDB},
{Name: "GOTMPDIR", Value: cfg.Getenv("GOTMPDIR")},
{Name: "GOTOOLDIR", Value: base.ToolDir},
{Name: "GOVCS", Value: cfg.GOVCS},
{Name: "GOVERSION", Value: runtime.Version()},
}
if work.GccgoBin != "" {
env = append(env, cfg.EnvVar{Name: "GCCGO", Value: work.GccgoBin})
} else {
env = append(env, cfg.EnvVar{Name: "GCCGO", Value: work.GccgoName})
}
key, val := cfg.GetArchEnv()
if key != "" {
env = append(env, cfg.EnvVar{Name: key, Value: val})
}
cc := cfg.DefaultCC(cfg.Goos, cfg.Goarch)
if env := strings.Fields(cfg.Getenv("CC")); len(env) > 0 {
cc = env[0]
}
cxx := cfg.DefaultCXX(cfg.Goos, cfg.Goarch)
if env := strings.Fields(cfg.Getenv("CXX")); len(env) > 0 {
cxx = env[0]
}
env = append(env, cfg.EnvVar{Name: "AR", Value: envOr("AR", "ar")})
env = append(env, cfg.EnvVar{Name: "CC", Value: cc})
env = append(env, cfg.EnvVar{Name: "CXX", Value: cxx})
if cfg.BuildContext.CgoEnabled {
env = append(env, cfg.EnvVar{Name: "CGO_ENABLED", Value: "1"})
} else {
env = append(env, cfg.EnvVar{Name: "CGO_ENABLED", Value: "0"})
}
return env
}
func envOr(name, def string) string {
val := cfg.Getenv(name)
if val != "" {
return val
}
return def
}
func findEnv(env []cfg.EnvVar, name string) string {
for _, e := range env {
if e.Name == name {
return e.Value
}
}
return ""
}
// ExtraEnvVars returns environment variables that should not leak into child processes.
func ExtraEnvVars() []cfg.EnvVar {
gomod := ""
if modload.HasModRoot() {
gomod = filepath.Join(modload.ModRoot(), "go.mod")
} else if modload.Enabled() {
gomod = os.DevNull
}
return []cfg.EnvVar{
{Name: "GOMOD", Value: gomod},
}
}
// ExtraEnvVarsCostly returns environment variables that should not leak into child processes
// but are costly to evaluate.
func ExtraEnvVarsCostly() []cfg.EnvVar {
var b work.Builder
b.Init()
cppflags, cflags, cxxflags, fflags, ldflags, err := b.CFlags(&load.Package{})
if err != nil {
// Should not happen - b.CFlags was given an empty package.
fmt.Fprintf(os.Stderr, "go: invalid cflags: %v\n", err)
return nil
}
cmd := b.GccCmd(".", "")
return []cfg.EnvVar{
// Note: Update the switch in runEnv below when adding to this list.
{Name: "CGO_CFLAGS", Value: strings.Join(cflags, " ")},
{Name: "CGO_CPPFLAGS", Value: strings.Join(cppflags, " ")},
{Name: "CGO_CXXFLAGS", Value: strings.Join(cxxflags, " ")},
{Name: "CGO_FFLAGS", Value: strings.Join(fflags, " ")},
{Name: "CGO_LDFLAGS", Value: strings.Join(ldflags, " ")},
{Name: "PKG_CONFIG", Value: b.PkgconfigCmd()},
{Name: "GOGCCFLAGS", Value: strings.Join(cmd[3:], " ")},
}
}
// argKey returns the KEY part of the arg KEY=VAL, or else arg itself.
func argKey(arg string) string {
i := strings.Index(arg, "=")
if i < 0 {
return arg
}
return arg[:i]
}
func runEnv(ctx context.Context, cmd *base.Command, args []string) {
if *envJson && *envU {
base.Fatalf("go env: cannot use -json with -u")
}
if *envJson && *envW {
base.Fatalf("go env: cannot use -json with -w")
}
if *envU && *envW {
base.Fatalf("go env: cannot use -u with -w")
}
env := cfg.CmdEnv
env = append(env, ExtraEnvVars()...)
if err := fsys.Init(base.Cwd); err != nil {
base.Fatalf("go: %v", err)
}
// Do we need to call ExtraEnvVarsCostly, which is a bit expensive?
needCostly := false
if *envU || *envW {
// We're overwriting or removing default settings,
// so it doesn't really matter what the existing settings are.
//
// Moreover, we haven't validated the new settings yet, so it is
// important that we NOT perform any actions based on them,
// such as initializing the builder to compute other variables.
} else if len(args) == 0 {
// We're listing all environment variables ("go env"),
// including the expensive ones.
needCostly = true
} else {
needCostly = false
checkCostly:
for _, arg := range args {
switch argKey(arg) {
case "CGO_CFLAGS",
"CGO_CPPFLAGS",
"CGO_CXXFLAGS",
"CGO_FFLAGS",
"CGO_LDFLAGS",
"PKG_CONFIG",
"GOGCCFLAGS":
needCostly = true
break checkCostly
}
}
}
if needCostly {
env = append(env, ExtraEnvVarsCostly()...)
}
if *envW {
// Process and sanity-check command line.
if len(args) == 0 {
base.Fatalf("go env -w: no KEY=VALUE arguments given")
}
osEnv := make(map[string]string)
for _, e := range cfg.OrigEnv {
if i := strings.Index(e, "="); i >= 0 {
osEnv[e[:i]] = e[i+1:]
}
}
add := make(map[string]string)
for _, arg := range args {
i := strings.Index(arg, "=")
if i < 0 {
base.Fatalf("go env -w: arguments must be KEY=VALUE: invalid argument: %s", arg)
}
key, val := arg[:i], arg[i+1:]
if err := checkEnvWrite(key, val); err != nil {
base.Fatalf("go env -w: %v", err)
}
if _, ok := add[key]; ok {
base.Fatalf("go env -w: multiple values for key: %s", key)
}
add[key] = val
if osVal := osEnv[key]; osVal != "" && osVal != val {
fmt.Fprintf(os.Stderr, "warning: go env -w %s=... does not override conflicting OS environment variable\n", key)
}
}
goos, okGOOS := add["GOOS"]
goarch, okGOARCH := add["GOARCH"]
if okGOOS || okGOARCH {
if !okGOOS {
goos = cfg.Goos
}
if !okGOARCH {
goarch = cfg.Goarch
}
if err := work.CheckGOOSARCHPair(goos, goarch); err != nil {
base.Fatalf("go env -w: %v", err)
}
}
gotmp, okGOTMP := add["GOTMPDIR"]
if okGOTMP {
if !filepath.IsAbs(gotmp) && gotmp != "" {
base.Fatalf("go env -w: GOTMPDIR must be an absolute path")
}
}
updateEnvFile(add, nil)
return
}
if *envU {
// Process and sanity-check command line.
if len(args) == 0 {
base.Fatalf("go env -u: no arguments given")
}
del := make(map[string]bool)
for _, arg := range args {
if err := checkEnvWrite(arg, ""); err != nil {
base.Fatalf("go env -u: %v", err)
}
del[arg] = true
}
if del["GOOS"] || del["GOARCH"] {
goos, goarch := cfg.Goos, cfg.Goarch
if del["GOOS"] {
goos = getOrigEnv("GOOS")
if goos == "" {
goos = build.Default.GOOS
}
}
if del["GOARCH"] {
goarch = getOrigEnv("GOARCH")
if goarch == "" {
goarch = build.Default.GOARCH
}
}
if err := work.CheckGOOSARCHPair(goos, goarch); err != nil {
base.Fatalf("go env -u: %v", err)
}
}
updateEnvFile(nil, del)
return
}
if len(args) > 0 {
if *envJson {
var es []cfg.EnvVar
for _, name := range args {
e := cfg.EnvVar{Name: name, Value: findEnv(env, name)}
es = append(es, e)
}
printEnvAsJSON(es)
} else {
for _, name := range args {
fmt.Printf("%s\n", findEnv(env, name))
}
}
return
}
if *envJson {
printEnvAsJSON(env)
return
}
for _, e := range env {
if e.Name != "TERM" {
switch runtime.GOOS {
default:
fmt.Printf("%s=\"%s\"\n", e.Name, e.Value)
case "plan9":
if strings.IndexByte(e.Value, '\x00') < 0 {
fmt.Printf("%s='%s'\n", e.Name, strings.ReplaceAll(e.Value, "'", "''"))
} else {
v := strings.Split(e.Value, "\x00")
fmt.Printf("%s=(", e.Name)
for x, s := range v {
if x > 0 {
fmt.Printf(" ")
}
fmt.Printf("%s", s)
}
fmt.Printf(")\n")
}
case "windows":
fmt.Printf("set %s=%s\n", e.Name, e.Value)
}
}
}
}
func printEnvAsJSON(env []cfg.EnvVar) {
m := make(map[string]string)
for _, e := range env {
if e.Name == "TERM" {
continue
}
m[e.Name] = e.Value
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "\t")
if err := enc.Encode(m); err != nil {
base.Fatalf("go env -json: %s", err)
}
}
func getOrigEnv(key string) string {
for _, v := range cfg.OrigEnv {
if strings.HasPrefix(v, key+"=") {
return strings.TrimPrefix(v, key+"=")
}
}
return ""
}
func checkEnvWrite(key, val string) error {
switch key {
case "GOEXE", "GOGCCFLAGS", "GOHOSTARCH", "GOHOSTOS", "GOMOD", "GOTOOLDIR", "GOVERSION":
return fmt.Errorf("%s cannot be modified", key)
case "GOENV":
return fmt.Errorf("%s can only be set using the OS environment", key)
}
// To catch typos and the like, check that we know the variable.
if !cfg.CanGetenv(key) {
return fmt.Errorf("unknown go command variable %s", key)
}
// Some variables can only have one of a few valid values. If set to an
// invalid value, the next cmd/go invocation might fail immediately,
// even 'go env -w' itself.
switch key {
case "GO111MODULE":
switch val {
case "", "auto", "on", "off":
default:
return fmt.Errorf("invalid %s value %q", key, val)
}
case "GOPATH":
if strings.HasPrefix(val, "~") {
return fmt.Errorf("GOPATH entry cannot start with shell metacharacter '~': %q", val)
}
if !filepath.IsAbs(val) && val != "" {
return fmt.Errorf("GOPATH entry is relative; must be absolute path: %q", val)
}
// Make sure CC and CXX are absolute paths
case "CC", "CXX":
if !filepath.IsAbs(val) && val != "" && val != filepath.Base(val) {
return fmt.Errorf("%s entry is relative; must be absolute path: %q", key, val)
}
}
if !utf8.ValidString(val) {
return fmt.Errorf("invalid UTF-8 in %s=... value", key)
}
if strings.Contains(val, "\x00") {
return fmt.Errorf("invalid NUL in %s=... value", key)
}
if strings.ContainsAny(val, "\v\r\n") {
return fmt.Errorf("invalid newline in %s=... value", key)
}
return nil
}
func updateEnvFile(add map[string]string, del map[string]bool) {
file, err := cfg.EnvFile()
if file == "" {
base.Fatalf("go env: cannot find go env config: %v", err)
}
data, err := os.ReadFile(file)
if err != nil && (!os.IsNotExist(err) || len(add) == 0) {
base.Fatalf("go env: reading go env config: %v", err)
}
lines := strings.SplitAfter(string(data), "\n")
if lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
} else {
lines[len(lines)-1] += "\n"
}
// Delete all but last copy of any duplicated variables,
// since the last copy is the one that takes effect.
prev := make(map[string]int)
for l, line := range lines {
if key := lineToKey(line); key != "" {
if p, ok := prev[key]; ok {
lines[p] = ""
}
prev[key] = l
}
}
// Add variables (go env -w). Update existing lines in file if present, add to end otherwise.
for key, val := range add {
if p, ok := prev[key]; ok {
lines[p] = key + "=" + val + "\n"
delete(add, key)
}
}
for key, val := range add {
lines = append(lines, key+"="+val+"\n")
}
// Delete requested variables (go env -u).
for key := range del {
if p, ok := prev[key]; ok {
lines[p] = ""
}
}
// Sort runs of KEY=VALUE lines
// (that is, blocks of lines where blocks are separated
// by comments, blank lines, or invalid lines).
start := 0
for i := 0; i <= len(lines); i++ {
if i == len(lines) || lineToKey(lines[i]) == "" {
sortKeyValues(lines[start:i])
start = i + 1
}
}
data = []byte(strings.Join(lines, ""))
err = os.WriteFile(file, data, 0666)
if err != nil {
// Try creating directory.
os.MkdirAll(filepath.Dir(file), 0777)
err = os.WriteFile(file, data, 0666)
if err != nil {
base.Fatalf("go env: writing go env config: %v", err)
}
}
}
// lineToKey returns the KEY part of the line KEY=VALUE or else an empty string.
func lineToKey(line string) string {
i := strings.Index(line, "=")
if i < 0 || strings.Contains(line[:i], "#") {
return ""
}
return line[:i]
}
// sortKeyValues sorts a sequence of lines by key.
// It differs from sort.Strings in that keys which are GOx where x is an ASCII
// character smaller than = sort after GO=.
// (There are no such keys currently. It used to matter for GO386 which was
// removed in Go 1.16.)
func sortKeyValues(lines []string) {
sort.Slice(lines, func(i, j int) bool {
return lineToKey(lines[i]) < lineToKey(lines[j])
})
}