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 }