github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/build/command_replacements.go (about) 1 // Replacement of sequences in genrule commands. 2 // 3 // Genrules can contain certain replacement variables which Please substitutes 4 // with locations of the actual thing before running. 5 // The following replacements are currently made: 6 // 7 // $(location //path/to:target) 8 // Expands to the output of the given build rule. The rule can only have one 9 // output (use $locations if there are multiple). 10 // 11 // $(locations //path/to:target) 12 // Expands to all the outputs (space separated) of the given build rule. 13 // Equivalent to $(location ...) for rules with a single output. 14 // 15 // $(exe //path/to:target) 16 // Expands to a command to run the output of the given target from within a 17 // genrule or test directory. For example, 18 // java -jar path/to/target.jar. 19 // The rule must be tagged as 'binary'. 20 // 21 // $(out_exe //path/to:target) 22 // Expands to a command to run the output of the given target. For example, 23 // java -jar plz-out/bin/path/to/target.jar. 24 // The rule must be tagged as 'binary'. 25 // 26 // $(dir //path/to:target) 27 // Expands to the package directory containing the outputs of the given target. 28 // Useful for rules that have multiple outputs where you only need to know 29 // what directory they're in. 30 // 31 // $(out_location //path/to:target) 32 // Expands to a path to the output of the given target, with the preceding plz-out/gen 33 // or plz-out/bin etc. Useful when these things will be run by a user. 34 // 35 // $(worker //path/to:target) 36 // Indicates that this target will be run by a remote worker process. The following 37 // arguments are sent to the remote worker. 38 // This is subject to some additional rules: it must appear initially in the command, 39 // and if "&&" appears subsequently in the command, that part is run locally after 40 // the worker has completed. All workers must be listed as tools of the rule. 41 // 42 // In general it's a good idea to use these where possible in genrules rather than 43 // hardcoding specific paths. 44 45 package build 46 47 import ( 48 "encoding/base64" 49 "fmt" 50 "path" 51 "path/filepath" 52 "regexp" 53 "strings" 54 55 "core" 56 ) 57 58 var locationReplacement = regexp.MustCompile(`\$\(location ([^\)]+)\)`) 59 var locationsReplacement = regexp.MustCompile(`\$\(locations ([^\)]+)\)`) 60 var exeReplacement = regexp.MustCompile(`\$\(exe ([^\)]+)\)`) 61 var outExeReplacement = regexp.MustCompile(`\$\(out_exe ([^\)]+)\)`) 62 var outReplacement = regexp.MustCompile(`\$\(out_location ([^\)]+)\)`) 63 var dirReplacement = regexp.MustCompile(`\$\(dir ([^\)]+)\)`) 64 var hashReplacement = regexp.MustCompile(`\$\(hash ([^\)]+)\)`) 65 var workerReplacement = regexp.MustCompile(`^(.*)\$\(worker ([^\)]+)\) *([^&]*)(?: *&& *(.*))?$`) 66 67 // ReplaceSequences replaces escape sequences in the given string. 68 func ReplaceSequences(state *core.BuildState, target *core.BuildTarget, command string) string { 69 return replaceSequencesInternal(state, target, command, false) 70 } 71 72 // ReplaceTestSequences replaces escape sequences in the given string when running a test. 73 func ReplaceTestSequences(state *core.BuildState, target *core.BuildTarget, command string) string { 74 if command == "" { 75 // An empty test command implies running the test binary. 76 return replaceSequencesInternal(state, target, fmt.Sprintf("$(exe :%s)", target.Label.Name), true) 77 } 78 return replaceSequencesInternal(state, target, command, true) 79 } 80 81 // workerCommandAndArgs returns the worker & its command (if any) and subsequent local command for the rule. 82 func workerCommandAndArgs(state *core.BuildState, target *core.BuildTarget) (string, string, string) { 83 match := workerReplacement.FindStringSubmatch(target.GetCommand(state)) 84 if match == nil { 85 return "", "", ReplaceSequences(state, target, target.GetCommand(state)) 86 } else if match[1] != "" { 87 panic("$(worker) replacements cannot have any commands preceding them.") 88 } 89 return replaceSequence(target, core.ExpandHomePath(match[2]), true, false, false, false, false, false), 90 replaceSequencesInternal(state, target, strings.TrimSpace(match[3]), false), 91 replaceSequencesInternal(state, target, match[4], false) 92 } 93 94 func replaceSequencesInternal(state *core.BuildState, target *core.BuildTarget, command string, test bool) string { 95 cmd := locationReplacement.ReplaceAllStringFunc(command, func(in string) string { 96 return replaceSequence(target, in[11:len(in)-1], false, false, false, false, false, test) 97 }) 98 cmd = locationsReplacement.ReplaceAllStringFunc(cmd, func(in string) string { 99 return replaceSequence(target, in[12:len(in)-1], false, true, false, false, false, test) 100 }) 101 cmd = exeReplacement.ReplaceAllStringFunc(cmd, func(in string) string { 102 return replaceSequence(target, in[6:len(in)-1], true, false, false, false, false, test) 103 }) 104 cmd = outReplacement.ReplaceAllStringFunc(cmd, func(in string) string { 105 return replaceSequence(target, in[15:len(in)-1], false, false, false, true, false, test) 106 }) 107 cmd = outExeReplacement.ReplaceAllStringFunc(cmd, func(in string) string { 108 return replaceSequence(target, in[10:len(in)-1], true, false, false, true, false, test) 109 }) 110 cmd = dirReplacement.ReplaceAllStringFunc(cmd, func(in string) string { 111 return replaceSequence(target, in[6:len(in)-1], false, true, true, false, false, test) 112 }) 113 cmd = hashReplacement.ReplaceAllStringFunc(cmd, func(in string) string { 114 if !target.Stamp { 115 panic(fmt.Sprintf("Target %s can't use $(hash ) replacements without stamp=True", target.Label)) 116 } 117 return replaceSequence(target, in[7:len(in)-1], false, true, true, false, true, test) 118 }) 119 if state.Config.Bazel.Compatibility { 120 // Bazel allows several obscure Make-style variable expansions. 121 // Our replacement here is not very principled but should work better than not doing it at all. 122 cmd = strings.Replace(cmd, "$<", "$SRCS", -1) 123 cmd = strings.Replace(cmd, "$(<)", "$SRCS", -1) 124 cmd = strings.Replace(cmd, "$@D", "$TMP_DIR", -1) 125 cmd = strings.Replace(cmd, "$(@D)", "$TMP_DIR", -1) 126 cmd = strings.Replace(cmd, "$@", "$OUTS", -1) 127 cmd = strings.Replace(cmd, "$(@)", "$OUTS", -1) 128 // It also seemingly allows you to get away with this syntax, which means something 129 // fairly different in Bash, but never mind. 130 cmd = strings.Replace(cmd, "$(SRCS)", "$SRCS", -1) 131 cmd = strings.Replace(cmd, "$(OUTS)", "$OUTS", -1) 132 } 133 // We would ideally check for this when doing matches above, but not easy in 134 // Go since its regular expressions are actually regular and principled. 135 return strings.Replace(cmd, "\\$", "$", -1) 136 } 137 138 // replaceSequence replaces a single escape sequence in a command. 139 func replaceSequence(target *core.BuildTarget, in string, runnable, multiple, dir, outPrefix, hash, test bool) string { 140 if core.LooksLikeABuildLabel(in) { 141 label := core.ParseBuildLabel(in, target.Label.PackageName) 142 return replaceSequenceLabel(target, label, in, runnable, multiple, dir, outPrefix, hash, test, true) 143 } 144 for _, src := range sourcesOrTools(target, runnable) { 145 if label := src.Label(); label != nil && src.String() == in { 146 return replaceSequenceLabel(target, *label, in, runnable, multiple, dir, outPrefix, hash, test, false) 147 } else if runnable && src.String() == in { 148 return src.String() 149 } 150 } 151 if hash { 152 return base64.RawURLEncoding.EncodeToString(mustPathHash(path.Join(target.Label.PackageName, in))) 153 } 154 if strings.HasPrefix(in, "/") { 155 return in // Absolute path, probably on a tool or system src. 156 } 157 return quote(path.Join(target.Label.PackageName, in)) 158 } 159 160 // sourcesOrTools returns either the tools of a target if runnable is true, otherwise its sources. 161 func sourcesOrTools(target *core.BuildTarget, runnable bool) []core.BuildInput { 162 if runnable { 163 return target.Tools 164 } 165 return target.AllSources() 166 } 167 168 func replaceSequenceLabel(target *core.BuildTarget, label core.BuildLabel, in string, runnable, multiple, dir, outPrefix, hash, test, allOutputs bool) string { 169 // Check this label is a dependency of the target, otherwise it's not allowed. 170 if label == target.Label { // targets can always use themselves. 171 return checkAndReplaceSequence(target, target, in, runnable, multiple, dir, outPrefix, hash, test, allOutputs, false) 172 } 173 deps := target.DependenciesFor(label) 174 if len(deps) == 0 { 175 panic(fmt.Sprintf("Rule %s can't use %s; doesn't depend on target %s", target.Label, in, label)) 176 } 177 // TODO(pebers): this does not correctly handle the case where there are multiple deps here 178 // (but is better than the previous case where it never worked at all) 179 return checkAndReplaceSequence(target, deps[0], in, runnable, multiple, dir, outPrefix, hash, test, allOutputs, target.IsTool(label)) 180 } 181 182 func checkAndReplaceSequence(target, dep *core.BuildTarget, in string, runnable, multiple, dir, outPrefix, hash, test, allOutputs, tool bool) string { 183 if allOutputs && !multiple && len(dep.Outputs()) != 1 { 184 // Label must have only one output. 185 panic(fmt.Sprintf("Rule %s can't use %s; %s has multiple outputs.", target.Label, in, dep.Label)) 186 } else if runnable && !dep.IsBinary { 187 panic(fmt.Sprintf("Rule %s can't $(exe %s), it's not executable", target.Label, dep.Label)) 188 } else if runnable && len(dep.Outputs()) == 0 { 189 panic(fmt.Sprintf("Rule %s is tagged as binary but produces no output.", dep.Label)) 190 } 191 if hash { 192 return base64.RawURLEncoding.EncodeToString(mustOutputHash(dep)) 193 } 194 output := "" 195 for _, out := range dep.Outputs() { 196 if allOutputs || out == in { 197 if tool { 198 abs, err := filepath.Abs(handleDir(dep.OutDir(), out, dir)) 199 if err != nil { 200 log.Fatalf("Couldn't calculate relative path: %s", err) 201 } 202 output += quote(abs) + " " 203 } else { 204 output += quote(fileDestination(target, dep, out, dir, outPrefix, test)) + " " 205 } 206 if dir { 207 break 208 } 209 } 210 } 211 if runnable && dep.HasLabel("java_non_exe") { 212 // The target is a Java target that isn't self-executable, hence it needs something to run it. 213 output = "java -jar " + output 214 } 215 return strings.TrimRight(output, " ") 216 } 217 218 func fileDestination(target, dep *core.BuildTarget, out string, dir, outPrefix, test bool) string { 219 if outPrefix { 220 return handleDir(dep.OutDir(), out, dir) 221 } 222 if test && target == dep { 223 // Slightly fiddly case because tests put binaries in a possibly slightly unusual place. 224 return "./" + out 225 } 226 return handleDir(dep.Label.PackageName, out, dir) 227 } 228 229 // Encloses the given string in quotes if needed. 230 func quote(s string) string { 231 if strings.ContainsAny(s, "|&;()<>") { 232 return "\"" + s + "\"" 233 } 234 return s 235 } 236 237 // handleDir chooses either the out dir or the actual output location depending on the 'dir' flag. 238 func handleDir(outDir, output string, dir bool) string { 239 if dir { 240 return outDir 241 } 242 return path.Join(outDir, output) 243 }