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

     1  package core
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path"
     7  	"strings"
     8  
     9  	"github.com/jessevdk/go-flags"
    10  	"gopkg.in/op/go-logging.v1"
    11  )
    12  
    13  var log = logging.MustGetLogger("core")
    14  
    15  // A BuildLabel is a representation of an identifier of a build target, e.g. //spam/eggs:ham
    16  // corresponds to BuildLabel{PackageName: spam/eggs name: ham}
    17  // BuildLabels are always absolute, so relative identifiers
    18  // like :ham are always parsed into an absolute form.
    19  // There is also implicit expansion of the final element of a target (ala Blaze)
    20  // so //spam/eggs is equivalent to //spam/eggs:eggs
    21  type BuildLabel struct {
    22  	PackageName string
    23  	Name        string
    24  }
    25  
    26  // WholeGraph represents parsing the entire graph (i.e. //...).
    27  // We use this specially in one or two places.
    28  var WholeGraph = []BuildLabel{{PackageName: "", Name: "..."}}
    29  
    30  // BuildLabelStdin is used to indicate that we're going to consume build labels from stdin.
    31  var BuildLabelStdin = BuildLabel{PackageName: "", Name: "_STDIN"}
    32  
    33  // OriginalTarget is used to indicate one of the originally requested targets on the command line.
    34  var OriginalTarget = BuildLabel{PackageName: "", Name: "_ORIGINAL"}
    35  
    36  // String returns a string representation of this build label.
    37  func (label BuildLabel) String() string {
    38  	if label.Name != "" {
    39  		return "//" + label.PackageName + ":" + label.Name
    40  	}
    41  	return "//" + label.PackageName
    42  }
    43  
    44  // NewBuildLabel constructs a new build label from the given components. Panics on failure.
    45  func NewBuildLabel(pkgName, name string) BuildLabel {
    46  	label, err := TryNewBuildLabel(pkgName, name)
    47  	if err != nil {
    48  		panic(err)
    49  	}
    50  	return label
    51  }
    52  
    53  // TryNewBuildLabel constructs a new build label from the given components.
    54  func TryNewBuildLabel(pkgName, name string) (BuildLabel, error) {
    55  	if err := validateNames(pkgName, name); err != nil {
    56  		return BuildLabel{}, err
    57  	}
    58  	return BuildLabel{PackageName: pkgName, Name: name}, nil
    59  }
    60  
    61  // validateNames returns an error if the package name of target name isn't accepted.
    62  func validateNames(pkgName, name string) error {
    63  	if !validatePackageName(pkgName) {
    64  		return fmt.Errorf("Invalid package name: %s", pkgName)
    65  	} else if !validateTargetName(name) {
    66  		return fmt.Errorf("Invalid target name: %s", name)
    67  	} else if err := validateSuffixes(pkgName, name); err != nil {
    68  		return err
    69  	}
    70  	return nil
    71  }
    72  
    73  // validateSuffixes checks that there are no invalid suffixes on the target name.
    74  func validateSuffixes(pkgName, name string) error {
    75  	if strings.HasSuffix(name, buildDirSuffix) ||
    76  		strings.HasSuffix(name, testDirSuffix) ||
    77  		strings.HasSuffix(pkgName, buildDirSuffix) ||
    78  		strings.HasSuffix(pkgName, testDirSuffix) {
    79  
    80  		return fmt.Errorf("._build and ._test are reserved suffixes")
    81  	}
    82  	return nil
    83  }
    84  
    85  // validatePackageName checks whether this string is a valid package name and returns true if so.
    86  func validatePackageName(name string) bool {
    87  	return name == "" || (name[0] != '/' && name[len(name)-1] != '/' && !strings.ContainsAny(name, `|$*?[]{}:()&\`) && !strings.Contains(name, "//"))
    88  }
    89  
    90  // validateTargetName checks whether this string is a valid target name and returns true if so.
    91  func validateTargetName(name string) bool {
    92  	return name != "" && !strings.ContainsAny(name, `|$*?[]{}:()&/\`) && (name[0] != '.' || name == "...") &&
    93  		!strings.HasSuffix(name, buildDirSuffix) && !strings.HasSuffix(name, testDirSuffix)
    94  }
    95  
    96  // ParseBuildLabel parses a single build label from a string. Panics on failure.
    97  func ParseBuildLabel(target, currentPath string) BuildLabel {
    98  	label, err := TryParseBuildLabel(target, currentPath)
    99  	if err != nil {
   100  		panic(err)
   101  	}
   102  	return label
   103  }
   104  
   105  // TryParseBuildLabel attempts to parse a single build label from a string. Returns an error if unsuccessful.
   106  func TryParseBuildLabel(target, currentPath string) (BuildLabel, error) {
   107  	if pkg, name, _ := parseBuildLabelParts(target, currentPath, nil); name != "" {
   108  		return BuildLabel{PackageName: pkg, Name: name}, nil
   109  	}
   110  	return BuildLabel{}, fmt.Errorf("Invalid build label: %s", target)
   111  }
   112  
   113  // ParseBuildLabelSubrepo parses a build label, and returns a separate indicator of the subrepo it was in.
   114  // It panics on error.
   115  func ParseBuildLabelSubrepo(target string, pkg *Package) (BuildLabel, string) {
   116  	if p, name, subrepo := parseBuildLabelParts(target, pkg.Name, pkg.Subrepo); name != "" {
   117  		return BuildLabel{PackageName: p, Name: name}, subrepo
   118  	}
   119  	// It's gonna fail, let this guy panic for us.
   120  	return ParseBuildLabel(target, pkg.Name), ""
   121  }
   122  
   123  // parseBuildLabelParts parses a build label into the package & name parts.
   124  // If valid, the name string will always be populated; the package string might not be if it's a local form.
   125  func parseBuildLabelParts(target, currentPath string, subrepo *Subrepo) (string, string, string) {
   126  	if len(target) < 2 { // Always must start with // or : and must have at least one char following.
   127  		return "", "", ""
   128  	} else if target[0] == ':' {
   129  		if !validateTargetName(target[1:]) {
   130  			return "", "", ""
   131  		}
   132  		return currentPath, target[1:], ""
   133  	} else if target[0] == '@' {
   134  		// @subrepo//pkg:target or @subrepo:target syntax
   135  		idx := strings.Index(target, "//")
   136  		if idx == -1 {
   137  			if idx = strings.IndexRune(target, ':'); idx == -1 {
   138  				return "", "", ""
   139  			}
   140  		}
   141  		pkg, name, _ := parseBuildLabelParts(target[idx:], currentPath, subrepo)
   142  		if pkg == "" && name == "" {
   143  			return "", "", ""
   144  		}
   145  		s := target[1:idx]
   146  		if subrepo == nil || !strings.HasPrefix(pkg, s) {
   147  			pkg = path.Join(s, pkg) // Combine it to //subrepo/pkg:target
   148  		}
   149  		return pkg, name, s
   150  	} else if target[0] != '/' || target[1] != '/' {
   151  		return "", "", ""
   152  	} else if idx := strings.IndexRune(target, ':'); idx != -1 {
   153  		pkg := target[2:idx]
   154  		name := target[idx+1:]
   155  		// Check ... explicitly to prevent :... which isn't allowed.
   156  		if !validatePackageName(pkg) || !validateTargetName(name) || name == "..." {
   157  			return "", "", ""
   158  		}
   159  		return pkg, name, ""
   160  	} else if !validatePackageName(target[2:]) {
   161  		return "", "", ""
   162  	}
   163  	// Must be the abbreviated form (//pkg) or subtargets (//pkg/...), there's no : in it.
   164  	if strings.HasSuffix(target, "/...") {
   165  		return strings.TrimRight(target[2:len(target)-3], "/"), "...", ""
   166  	} else if idx := strings.LastIndexByte(target, '/'); idx != -1 {
   167  		return target[2:], target[idx+1:], ""
   168  	}
   169  	return target[2:], target[2:], ""
   170  }
   171  
   172  // As above, but allows parsing of relative labels (eg. src/parse/rules:python_rules)
   173  // which is convenient at the shell prompt
   174  func parseMaybeRelativeBuildLabel(target, subdir string) (BuildLabel, error) {
   175  	// Try the ones that don't need locating the repo root first.
   176  	startsWithColon := strings.HasPrefix(target, ":")
   177  	if !startsWithColon {
   178  		if label, err := TryParseBuildLabel(target, ""); err == nil || strings.HasPrefix(target, "//") {
   179  			return label, err
   180  		}
   181  	}
   182  	// Now we need to locate the repo root and initial package.
   183  	// Deliberately leave this till after the above to facilitate the --repo_root flag.
   184  	if subdir == "" {
   185  		MustFindRepoRoot()
   186  		subdir = initialPackage
   187  	}
   188  	if startsWithColon {
   189  		return TryParseBuildLabel(target, subdir)
   190  	}
   191  	// Presumably it's just underneath this directory (note that if it was absolute we returned above)
   192  	return TryParseBuildLabel("//"+path.Join(subdir, target), "")
   193  }
   194  
   195  // ParseBuildLabels parses a bunch of build labels from strings. It dies on failure.
   196  // Relative labels are allowed since this is generally used at initialisation.
   197  func ParseBuildLabels(targets []string) []BuildLabel {
   198  	ret := make([]BuildLabel, len(targets))
   199  	for i, target := range targets {
   200  		if label, err := parseMaybeRelativeBuildLabel(target, ""); err != nil {
   201  			log.Fatalf("%s", err)
   202  		} else {
   203  			ret[i] = label
   204  		}
   205  	}
   206  	return ret
   207  }
   208  
   209  // IsAllSubpackages returns true if the label ends in ..., ie. it includes all subpackages.
   210  func (label BuildLabel) IsAllSubpackages() bool {
   211  	return label.Name == "..."
   212  }
   213  
   214  // IsAllTargets returns true if the label is the pseudo-label referring to all targets in this package.
   215  func (label BuildLabel) IsAllTargets() bool {
   216  	return label.Name == "all"
   217  }
   218  
   219  // Includes returns true if label includes the other label (//pkg:target1 is covered by //pkg:all etc).
   220  func (label BuildLabel) Includes(that BuildLabel) bool {
   221  	if (label.PackageName == "" && label.IsAllSubpackages()) ||
   222  		that.PackageName == label.PackageName ||
   223  		strings.HasPrefix(that.PackageName, label.PackageName+"/") {
   224  		// We're in the same package or a subpackage of this visibility spec
   225  		if label.IsAllSubpackages() {
   226  			return true
   227  		} else if label.PackageName == that.PackageName {
   228  			if label.Name == that.Name || label.IsAllTargets() {
   229  				return true
   230  			}
   231  		}
   232  	}
   233  	return false
   234  }
   235  
   236  // Less returns true if this build label would sort less than another one.
   237  func (label BuildLabel) Less(other BuildLabel) bool {
   238  	if label.PackageName == other.PackageName {
   239  		return label.Name < other.Name
   240  	}
   241  	return label.PackageName < other.PackageName
   242  }
   243  
   244  // Paths is an implementation of BuildInput interface; we use build labels directly as inputs.
   245  func (label BuildLabel) Paths(graph *BuildGraph) []string {
   246  	return addPathPrefix(graph.TargetOrDie(label).Outputs(), label.PackageName)
   247  }
   248  
   249  // FullPaths is an implementation of BuildInput interface.
   250  func (label BuildLabel) FullPaths(graph *BuildGraph) []string {
   251  	target := graph.TargetOrDie(label)
   252  	return addPathPrefix(target.Outputs(), target.OutDir())
   253  }
   254  
   255  // addPathPrefix adds a prefix to all the entries in a slice.
   256  func addPathPrefix(paths []string, prefix string) []string {
   257  	ret := make([]string, len(paths))
   258  	for i, output := range paths {
   259  		ret[i] = path.Join(prefix, output)
   260  	}
   261  	return ret
   262  }
   263  
   264  // LocalPaths is an implementation of BuildInput interface.
   265  func (label BuildLabel) LocalPaths(graph *BuildGraph) []string {
   266  	return graph.TargetOrDie(label).Outputs()
   267  }
   268  
   269  // Label is an implementation of BuildInput interface. It always returns this label.
   270  func (label BuildLabel) Label() *BuildLabel {
   271  	return &label
   272  }
   273  
   274  func (label BuildLabel) nonOutputLabel() *BuildLabel {
   275  	return &label
   276  }
   277  
   278  // ForPackage converts this build label to one that's relative for the given package.
   279  func (label BuildLabel) ForPackage(pkg *Package) BuildLabel {
   280  	// TODO(peterebden): HasPrefix here is not super elegant. We should probably be able to avoid it
   281  	//                   if we were more selective about calling this.
   282  	if pkg.Subrepo != nil && !strings.HasPrefix(label.PackageName, pkg.Subrepo.Name) {
   283  		return BuildLabel{PackageName: path.Join(pkg.Subrepo.Name, label.PackageName), Name: label.Name}
   284  	}
   285  	return label
   286  }
   287  
   288  // UnmarshalFlag unmarshals a build label from a command line flag. Implementation of flags.Unmarshaler interface.
   289  func (label *BuildLabel) UnmarshalFlag(value string) error {
   290  	// This is only allowable here, not in any other usage of build labels.
   291  	if value == "-" {
   292  		*label = BuildLabelStdin
   293  		return nil
   294  	} else if l, err := parseMaybeRelativeBuildLabel(value, ""); err != nil {
   295  		// This has to be fatal because of the way we're using the flags package;
   296  		// we lose incoming flags if we return errors.
   297  		// But don't die in completion mode.
   298  		if os.Getenv("PLZ_COMPLETE") == "" {
   299  			log.Fatalf("%s", err)
   300  		}
   301  	} else {
   302  		*label = l
   303  	}
   304  	return nil
   305  }
   306  
   307  // UnmarshalText implements the encoding.TextUnmarshaler interface.
   308  // This is used by gcfg to unmarshal the config files.
   309  func (label *BuildLabel) UnmarshalText(text []byte) error {
   310  	l, err := TryParseBuildLabel(string(text), "")
   311  	*label = l
   312  	return err
   313  }
   314  
   315  // Parent returns what would be the parent of a build label, or the label itself if it's parentless.
   316  // Note that there is not a concrete guarantee that the returned label exists in the build graph,
   317  // and that the label returned is the ultimate ancestor (ie. not necessarily immediate parent).
   318  func (label BuildLabel) Parent() BuildLabel {
   319  	index := strings.IndexRune(label.Name, '#')
   320  	if index == -1 || !strings.HasPrefix(label.Name, "_") {
   321  		return label
   322  	}
   323  	label.Name = strings.TrimLeft(label.Name[:index], "_")
   324  	return label
   325  }
   326  
   327  // HasParent returns true if the build label has a parent that's not itself.
   328  func (label BuildLabel) HasParent() bool {
   329  	return label.Parent() != label
   330  }
   331  
   332  // IsEmpty returns true if this is an empty build label, i.e. nothing's populated it yet.
   333  func (label BuildLabel) IsEmpty() bool {
   334  	return label.PackageName == "" && label.Name == ""
   335  }
   336  
   337  // PackageDir returns a path to the directory this target is in.
   338  // This is equivalent to PackageName in all cases except when at the repo root, when this
   339  // will return . instead. This is often easier to use in build rules.
   340  func (label BuildLabel) PackageDir() string {
   341  	if label.PackageName == "" {
   342  		return "."
   343  	}
   344  	return label.PackageName
   345  }
   346  
   347  // Complete implements the flags.Completer interface, which is used for shell completion.
   348  // Unfortunately it's rather awkward to handle here; we need to do a proper parse in order
   349  // to find out what the possible build labels are, and we're not ready for that yet.
   350  // Returning to main is also awkward since the flags haven't parsed properly; all in all
   351  // it seems an easier (albeit inelegant) solution to start things over by re-execing ourselves.
   352  func (label BuildLabel) Complete(match string) []flags.Completion {
   353  	if match == "" {
   354  		os.Exit(0)
   355  	}
   356  	os.Setenv("PLZ_COMPLETE", match)
   357  	os.Unsetenv("GO_FLAGS_COMPLETION")
   358  	exec, _ := os.Executable()
   359  	out, _, err := ExecWithTimeout(nil, "", os.Environ(), 0, 0, false, false, append([]string{exec}, os.Args[1:]...))
   360  	if err != nil {
   361  		return nil
   362  	}
   363  	ret := []flags.Completion{}
   364  	for _, line := range strings.Split(string(out), "\n") {
   365  		if line != "" {
   366  			ret = append(ret, flags.Completion{Item: line})
   367  		}
   368  	}
   369  	return ret
   370  }
   371  
   372  // LooksLikeABuildLabel returns true if the string appears to be a build label, false if not.
   373  // Useful for cases like rule sources where sources can be a filename or a label.
   374  func LooksLikeABuildLabel(str string) bool {
   375  	return strings.HasPrefix(str, "//") || strings.HasPrefix(str, ":") || (strings.HasPrefix(str, "@") && (strings.ContainsRune(str, ':') || strings.Contains(str, "//")))
   376  }
   377  
   378  // BuildLabels makes slices of build labels sortable.
   379  type BuildLabels []BuildLabel
   380  
   381  func (slice BuildLabels) Len() int {
   382  	return len(slice)
   383  }
   384  func (slice BuildLabels) Less(i, j int) bool {
   385  	return slice[i].Less(slice[j])
   386  }
   387  func (slice BuildLabels) Swap(i, j int) {
   388  	slice[i], slice[j] = slice[j], slice[i]
   389  }