github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/core/build_env.go (about)

     1  package core
     2  
     3  import (
     4  	"encoding/base64"
     5  	"os"
     6  	"path"
     7  	"regexp"
     8  	"strings"
     9  )
    10  
    11  var home = os.Getenv("HOME")
    12  var homeRex = regexp.MustCompile("(?:^|:)(~(?:[/:]|$))")
    13  
    14  // ExpandHomePath expands all prefixes of ~ without a user specifier TO $HOME.
    15  func ExpandHomePath(path string) string {
    16  	return homeRex.ReplaceAllStringFunc(path, func(subpath string) string {
    17  		return strings.Replace(subpath, "~", home, -1)
    18  	})
    19  }
    20  
    21  // A BuildEnv is a representation of the build environment that also knows how to log itself.
    22  type BuildEnv []string
    23  
    24  // GeneralBuildEnvironment creates the shell env vars used for a command, not based
    25  // on any specific target etc.
    26  func GeneralBuildEnvironment(config *Configuration) BuildEnv {
    27  	env := BuildEnv{
    28  		// Need this for certain tools, for example sass
    29  		"LANG=" + config.Build.Lang,
    30  		// Use a restricted PATH; it'd be easier for the user if we pass it through
    31  		// but really external environment variables shouldn't affect this.
    32  		// The only concession is that ~ is expanded as the user's home directory
    33  		// in PATH entries.
    34  		"PATH=" + ExpandHomePath(strings.Join(config.Build.Path, ":")),
    35  		// Expose the requested build config. We might also want to expose
    36  		// the command that's actually running (although typically this is more useful,
    37  		// because targets using this want to avoid defining different commands).
    38  		"BUILD_CONFIG=" + config.Build.Config,
    39  	}
    40  	if config.Go.GoRoot != "" {
    41  		env = append(env, "GOROOT="+config.Go.GoRoot)
    42  	}
    43  	if config.Cpp.PkgConfigPath != "" {
    44  		env = append(env, "PKG_CONFIG_PATH="+config.Cpp.PkgConfigPath)
    45  	}
    46  	return append(env, config.GetBuildEnv()...)
    47  }
    48  
    49  // BuildEnvironment creates the shell env vars to be passed
    50  // into the exec.Command calls made by plz. Use test=true for plz test targets.
    51  // Note that we lie about the location of HOME in order to keep some tools happy.
    52  // We read this as being slightly more POSIX-compliant than not having it set at all...
    53  func BuildEnvironment(state *BuildState, target *BuildTarget, test bool) BuildEnv {
    54  	sources := target.AllSourcePaths(state.Graph)
    55  	env := GeneralBuildEnvironment(state.Config)
    56  	env = append(
    57  		env,
    58  		"PKG="+target.Label.PackageName,
    59  		"PKG_DIR="+target.Subrepo.MakeRelativeName(target.Label.PackageDir()),
    60  		"NAME="+target.Label.Name,
    61  		"CONFIG="+state.Config.Build.Config,
    62  	)
    63  	if !test {
    64  		tmpDir := path.Join(RepoRoot, target.TmpDir())
    65  		env = append(env,
    66  			"TMP_DIR="+tmpDir,
    67  			"TMPDIR="+tmpDir,
    68  			"SRCS="+strings.Join(sources, " "),
    69  			"OUTS="+strings.Join(target.Outputs(), " "),
    70  			"HOME="+tmpDir,
    71  			"TOOLS="+strings.Join(toolPaths(state, target.Tools), " "),
    72  			// Set a consistent hash seed for Python. Important for build determinism.
    73  			"PYTHONHASHSEED=42",
    74  		)
    75  		// The OUT variable is only available on rules that have a single output.
    76  		if len(target.Outputs()) == 1 {
    77  			env = append(env, "OUT="+path.Join(RepoRoot, target.TmpDir(), target.Outputs()[0]))
    78  		}
    79  		// The SRC variable is only available on rules that have a single source file.
    80  		if len(sources) == 1 {
    81  			env = append(env, "SRC="+sources[0])
    82  		}
    83  		// Similarly, TOOL is only available on rules with a single tool.
    84  		if len(target.Tools) == 1 {
    85  			env = append(env, "TOOL="+toolPath(state, target.Tools[0]))
    86  		}
    87  		// Named source groups if the target declared any.
    88  		for name, srcs := range target.NamedSources {
    89  			paths := target.SourcePaths(state.Graph, srcs)
    90  			env = append(env, "SRCS_"+strings.ToUpper(name)+"="+strings.Join(paths, " "))
    91  		}
    92  		// Named output groups similarly.
    93  		for name, outs := range target.DeclaredNamedOutputs() {
    94  			env = append(env, "OUTS_"+strings.ToUpper(name)+"="+strings.Join(outs, " "))
    95  		}
    96  		// Named tools as well.
    97  		for name, tools := range target.namedTools {
    98  			env = append(env, "TOOLS_"+strings.ToUpper(name)+"="+strings.Join(toolPaths(state, tools), " "))
    99  		}
   100  		// Secrets, again only if they declared any.
   101  		if len(target.Secrets) > 0 {
   102  			env = append(env, "SECRETS="+ExpandHomePath(strings.Join(target.Secrets, " ")))
   103  		}
   104  		if state.Config.Bazel.Compatibility {
   105  			// Obviously this is only a subset of the variables Bazel would expose, but there's
   106  			// no point populating ones that we literally have no clue what they should be.
   107  			// To be honest I don't terribly like these, I'm pretty sure that using $GENDIR in
   108  			// your genrule is not a good sign.
   109  			env = append(env, "GENDIR="+path.Join(RepoRoot, GenDir))
   110  			env = append(env, "BINDIR="+path.Join(RepoRoot, BinDir))
   111  		}
   112  	} else {
   113  		testDir := path.Join(RepoRoot, target.TestDir())
   114  		resultsFile := path.Join(testDir, "test.results")
   115  		env = append(env,
   116  			"TEST_DIR="+testDir,
   117  			"TMP_DIR="+testDir,
   118  			"TMPDIR="+testDir,
   119  			"TEST_ARGS="+strings.Join(state.TestArgs, ","),
   120  			"RESULTS_FILE="+resultsFile,
   121  			// We shouldn't really have specific things like this here, but it really is just easier to set it.
   122  			"GTEST_OUTPUT=xml:"+resultsFile,
   123  		)
   124  		// Ideally we would set this to something useful even within a container, but it ends
   125  		// up being /tmp/test or something which just confuses matters.
   126  		if !target.Containerise {
   127  			env = append(env, "HOME="+testDir)
   128  		}
   129  		if state.NeedCoverage {
   130  			env = append(env, "COVERAGE=true", "COVERAGE_FILE="+path.Join(RepoRoot, target.TestDir(), "test.coverage"))
   131  		}
   132  		if len(target.Outputs()) > 0 {
   133  			env = append(env, "TEST="+path.Join(RepoRoot, target.TestDir(), target.Outputs()[0]))
   134  		}
   135  		if len(target.Data) > 0 {
   136  			env = append(env, "DATA="+strings.Join(target.AllData(state.Graph), " "))
   137  		}
   138  		// Bit of a hack for gcov which needs access to its .gcno files.
   139  		if target.HasLabel("cc") {
   140  			env = append(env, "GCNO_DIR="+path.Join(RepoRoot, GenDir, target.Label.PackageName))
   141  		}
   142  		if state.DebugTests {
   143  			env = append(env, "DEBUG=true")
   144  		}
   145  	}
   146  	return env
   147  }
   148  
   149  // StampedBuildEnvironment returns the shell env vars to be passed into exec.Command.
   150  // Optionally includes a stamp if the target is marked as such.
   151  func StampedBuildEnvironment(state *BuildState, target *BuildTarget, test bool, stamp []byte) BuildEnv {
   152  	env := BuildEnvironment(state, target, test)
   153  	if target.Stamp {
   154  		return append(env, "STAMP="+base64.RawURLEncoding.EncodeToString(stamp))
   155  	}
   156  	return env
   157  }
   158  
   159  func toolPath(state *BuildState, tool BuildInput) string {
   160  	label := tool.Label()
   161  	if label != nil {
   162  		return state.Graph.TargetOrDie(*label).toolPath()
   163  	}
   164  	return tool.Paths(state.Graph)[0]
   165  }
   166  
   167  func toolPaths(state *BuildState, tools []BuildInput) []string {
   168  	ret := make([]string, len(tools))
   169  	for i, tool := range tools {
   170  		ret[i] = toolPath(state, tool)
   171  	}
   172  	return ret
   173  }
   174  
   175  // ReplaceEnvironment is a function suitable for passing to os.Expand to replace environment
   176  // variables from this BuildEnv.
   177  func (env BuildEnv) ReplaceEnvironment(s string) string {
   178  	for _, e := range env {
   179  		if strings.HasPrefix(e, s+"=") {
   180  			return e[len(s)+1:]
   181  		}
   182  	}
   183  	return ""
   184  }
   185  
   186  // Redacted implements the interface for our logging implementation.
   187  func (env BuildEnv) Redacted() interface{} {
   188  	r := make(BuildEnv, len(env))
   189  	for i, e := range env {
   190  		r[i] = e
   191  		split := strings.SplitN(e, "=", 2)
   192  		if len(split) == 2 && (strings.Contains(split[0], "SECRET") || strings.Contains(split[0], "PASSWORD")) {
   193  			r[i] = split[0] + "=" + "************"
   194  		}
   195  	}
   196  	return r
   197  }
   198  
   199  // String implements the fmt.Stringer interface
   200  func (env BuildEnv) String() string {
   201  	return strings.Join(env, "\n")
   202  }