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 }