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  }