github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/parse/asp/targets.go (about) 1 package asp 2 3 import ( 4 "os" 5 "path" 6 "strings" 7 "time" 8 9 "cli" 10 "core" 11 "fs" 12 ) 13 14 // filegroupCommand is the command we put on filegroup rules. 15 const filegroupCommand = pyString("filegroup") 16 17 // hashFilegroupCommand is similarly the command for hash_filegroup rules. 18 const hashFilegroupCommand = pyString("hash_filegroup") 19 20 // createTarget creates a new build target as part of build_rule(). 21 func createTarget(s *scope, args []pyObject) *core.BuildTarget { 22 isTruthy := func(i int) bool { 23 return args[i] != nil && args[i] != None && (args[i] == &True || args[i].IsTruthy()) 24 } 25 26 name := string(args[0].(pyString)) 27 testCmd := args[2] 28 container := isTruthy(19) 29 test := isTruthy(14) 30 31 // A bunch of error checking first 32 s.NAssert(name == "all", "'all' is a reserved build target name.") 33 s.NAssert(name == "", "Target name is empty") 34 s.NAssert(strings.ContainsRune(name, '/'), "/ is a reserved character in build target names") 35 s.NAssert(strings.ContainsRune(name, ':'), ": is a reserved character in build target names") 36 s.NAssert(container && !test, "Only tests can have container=True") 37 38 if tag := args[34]; tag != nil { 39 if tagStr := string(tag.(pyString)); tagStr != "" { 40 name = tagName(name, tagStr) 41 } 42 } 43 label, err := core.TryNewBuildLabel(s.pkg.Name, name) 44 s.Assert(err == nil, "Invalid build target name %s", name) 45 46 target := core.NewBuildTarget(label) 47 target.Subrepo = s.pkg.Subrepo 48 target.IsBinary = isTruthy(13) 49 target.IsTest = test 50 target.NeedsTransitiveDependencies = isTruthy(17) 51 target.OutputIsComplete = isTruthy(18) 52 target.Containerise = container 53 target.Sandbox = isTruthy(20) 54 target.TestOnly = test || isTruthy(15) 55 target.ShowProgress = isTruthy(36) 56 target.IsRemoteFile = isTruthy(37) 57 if timeout := args[24]; timeout != nil { 58 target.BuildTimeout = time.Duration(timeout.(pyInt)) * time.Second 59 } 60 target.Stamp = isTruthy(33) 61 target.IsHashFilegroup = args[1] == hashFilegroupCommand 62 target.IsFilegroup = args[1] == filegroupCommand || target.IsHashFilegroup 63 if desc := args[16]; desc != nil && desc != None { 64 target.BuildingDescription = string(desc.(pyString)) 65 } 66 if target.IsBinary { 67 target.AddLabel("bin") 68 } 69 target.Command, target.Commands = decodeCommands(s, args[1]) 70 if test { 71 if flaky := args[23]; flaky != nil { 72 if flaky == True { 73 target.Flakiness = 3 74 target.AddLabel("flaky") // Automatically label flaky tests 75 } else if i, ok := flaky.(pyInt); ok && i > 0 { 76 target.Flakiness = int(i) 77 target.AddLabel("flaky") 78 } 79 } 80 // Automatically label containerised tests. 81 if target.Containerise { 82 target.AddLabel("container") 83 } 84 if testCmd != nil && testCmd != None { 85 target.TestCommand, target.TestCommands = decodeCommands(s, args[2]) 86 } 87 if timeout := args[25]; timeout != nil { 88 target.TestTimeout = time.Duration(timeout.(pyInt)) * time.Second 89 } 90 target.TestSandbox = isTruthy(21) 91 target.NoTestOutput = isTruthy(22) 92 } 93 return target 94 } 95 96 // decodeCommands takes a Python object and returns it as a string and a map; only one will be set. 97 func decodeCommands(s *scope, obj pyObject) (string, map[string]string) { 98 if obj == nil || obj == None { 99 return "", nil 100 } else if cmd, ok := obj.(pyString); ok { 101 return strings.TrimSpace(string(cmd)), nil 102 } 103 cmds, ok := asDict(obj) 104 s.Assert(ok, "Unknown type for command [%s]", obj.Type()) 105 // Have to convert all the keys too 106 m := make(map[string]string, len(cmds)) 107 for k, v := range cmds { 108 if v != None { 109 sv, ok := v.(pyString) 110 s.Assert(ok, "Unknown type for command") 111 m[k] = strings.TrimSpace(string(sv)) 112 } 113 } 114 return "", m 115 } 116 117 // populateTarget sets the assorted attributes on a build target. 118 func populateTarget(s *scope, t *core.BuildTarget, args []pyObject) { 119 if t.IsRemoteFile { 120 for _, url := range args[37].(pyList) { 121 t.AddSource(core.URLLabel(url.(pyString))) 122 } 123 } else { 124 addMaybeNamed(s, "srcs", args[3], t.AddSource, t.AddNamedSource, false, false) 125 } 126 addMaybeNamed(s, "tools", args[9], t.AddTool, t.AddNamedTool, true, true) 127 addMaybeNamed(s, "system_srcs", args[32], t.AddSource, nil, true, false) 128 addMaybeNamed(s, "data", args[4], t.AddDatum, nil, false, false) 129 addMaybeNamedOutput(s, "outs", args[5], t.AddOutput, t.AddNamedOutput, t, false) 130 addMaybeNamedOutput(s, "optional_outs", args[35], t.AddOptionalOutput, nil, t, true) 131 addMaybeNamedOutput(s, "test_outputs", args[31], t.AddTestOutput, nil, t, false) 132 addDependencies(s, "deps", args[6], t, false) 133 addDependencies(s, "exported_deps", args[7], t, true) 134 addStrings(s, "labels", args[10], t.AddLabel) 135 addStrings(s, "hashes", args[12], t.AddHash) 136 addStrings(s, "licences", args[30], t.AddLicence) 137 addStrings(s, "requires", args[28], t.AddRequire) 138 addStrings(s, "visibility", args[11], func(str string) { 139 t.Visibility = append(t.Visibility, parseVisibility(s, str)) 140 }) 141 addStrings(s, "secrets", args[8], func(str string) { 142 s.NAssert(strings.HasPrefix(str, "//"), "Secret %s of %s cannot be a build label", str, t.Label.Name) 143 s.Assert(strings.HasPrefix(str, "/") || strings.HasPrefix(str, "~"), "Secret '%s' of %s is not an absolute path", str, t.Label.Name) 144 t.Secrets = append(t.Secrets, str) 145 }) 146 addProvides(s, "provides", args[29], t) 147 setContainerSettings(s, "container", args[19], t) 148 if f := callbackFunction(s, "pre_build", args[26], 1, "argument"); f != nil { 149 t.PreBuildFunction = &preBuildFunction{f: f, s: s} 150 } 151 if f := callbackFunction(s, "post_build", args[27], 2, "arguments"); f != nil { 152 t.PostBuildFunction = &postBuildFunction{f: f, s: s} 153 } 154 } 155 156 // addMaybeNamed adds inputs to a target, possibly in named groups. 157 func addMaybeNamed(s *scope, name string, obj pyObject, anon func(core.BuildInput), named func(string, core.BuildInput), systemAllowed, tool bool) { 158 if obj == nil { 159 return 160 } 161 if l, ok := asList(obj); ok { 162 for _, li := range l { 163 if bi := parseBuildInput(s, li, name, systemAllowed, tool); bi != nil { 164 anon(bi) 165 } 166 } 167 } else if d, ok := asDict(obj); ok { 168 s.Assert(named != nil, "%s cannot be given as a dict", name) 169 for k, v := range d { 170 if v != None { 171 l, ok := asList(v) 172 s.Assert(ok, "Values of %s must be lists of strings", name) 173 for _, li := range l { 174 if bi := parseBuildInput(s, li, name, systemAllowed, tool); bi != nil { 175 named(k, bi) 176 } 177 } 178 } 179 } 180 } else if obj != None { 181 s.Assert(false, "Argument %s must be a list or dict, not %s", name, obj.Type()) 182 } 183 } 184 185 // addMaybeNamedOutput adds outputs to a target, possibly in a named group 186 func addMaybeNamedOutput(s *scope, name string, obj pyObject, anon func(string), named func(string, string), t *core.BuildTarget, optional bool) { 187 if obj == nil { 188 return 189 } 190 if l, ok := asList(obj); ok { 191 for _, li := range l { 192 if li != None { 193 out, ok := li.(pyString) 194 s.Assert(ok, "outs must be strings") 195 anon(string(out)) 196 if !optional || !strings.HasPrefix(string(out), "*") { 197 s.pkg.MustRegisterOutput(string(out), t) 198 } 199 } 200 } 201 } else if d, ok := asDict(obj); ok { 202 s.Assert(named != nil, "%s cannot be given as a dict", name) 203 for k, v := range d { 204 l, ok := asList(v) 205 s.Assert(ok, "Values must be lists of strings") 206 for _, li := range l { 207 if li != None { 208 out, ok := li.(pyString) 209 s.Assert(ok, "outs must be strings") 210 named(k, string(out)) 211 if !optional || !strings.HasPrefix(string(out), "*") { 212 s.pkg.MustRegisterOutput(string(out), t) 213 } 214 } 215 } 216 } 217 } else if obj != None { 218 s.Assert(false, "Argument %s must be a list or dict, not %s", name, obj.Type()) 219 } 220 } 221 222 // addDependencies adds dependencies to a target, which may or may not be exported. 223 func addDependencies(s *scope, name string, obj pyObject, target *core.BuildTarget, exported bool) { 224 addStrings(s, name, obj, func(str string) { 225 if s.state.Config.Bazel.Compatibility && !core.LooksLikeABuildLabel(str) && !strings.HasPrefix(str, "@") { 226 // *sigh*... Bazel seems to allow an implicit : on the start of dependencies 227 str = ":" + str 228 } 229 target.AddMaybeExportedDependency(parseLabel(s, str), exported, false) 230 }) 231 } 232 233 // addStrings adds an arbitrary set of strings to the target (e.g. labels etc). 234 func addStrings(s *scope, name string, obj pyObject, f func(string)) { 235 if obj != nil && obj != None { 236 l, ok := asList(obj) 237 s.Assert(ok, "Argument %s must be a list, not %s", name, obj.Type()) 238 for _, li := range l { 239 str, ok := li.(pyString) 240 s.Assert(ok || li == None, "%s must be strings", name) 241 if str != "" && li != None { 242 f(string(str)) 243 } 244 } 245 } 246 } 247 248 // addProvides adds a set of provides to the target, which is a dict of string -> label 249 func addProvides(s *scope, name string, obj pyObject, t *core.BuildTarget) { 250 if obj != nil && obj != None { 251 d, ok := asDict(obj) 252 s.Assert(ok, "Argument %s must be a dict, not %s", name, obj.Type()) 253 for k, v := range d { 254 str, ok := v.(pyString) 255 s.Assert(ok, "%s values must be strings", name) 256 t.AddProvide(k, parseLabel(s, string(str))) 257 } 258 } 259 } 260 261 // setContainerSettings sets any custom container settings on the target. 262 func setContainerSettings(s *scope, name string, obj pyObject, t *core.BuildTarget) { 263 if obj != nil && obj != None && obj != True && obj != False { 264 d, ok := asDict(obj) 265 s.Assert(ok, "Argument %s must be a dict, not %s", name, obj.Type()) 266 for k, v := range d { 267 str, ok := v.(pyString) 268 s.Assert(ok, "%s keys must be strings", name) 269 err := t.SetContainerSetting(strings.Replace(k, "_", "", -1), string(str)) 270 s.Assert(err == nil, "%s", err) 271 } 272 } 273 } 274 275 // parseVisibility converts a visibility string to a build label. 276 // Mostly they are just build labels but other things are allowed too (e.g. "PUBLIC"). 277 func parseVisibility(s *scope, vis string) core.BuildLabel { 278 if vis == "PUBLIC" || (s.state.Config.Bazel.Compatibility && vis == "//visibility:public") { 279 return core.WholeGraph[0] 280 } 281 l := parseLabel(s, vis) 282 if s.state.Config.Bazel.Compatibility { 283 // Bazel has a couple of special aliases for this stuff. 284 if l.Name == "__pkg__" { 285 l.Name = "all" 286 } else if l.Name == "__subpackages__" { 287 l.Name = "..." 288 } 289 } 290 return l 291 } 292 293 func parseBuildInput(s *scope, in pyObject, name string, systemAllowed, tool bool) core.BuildInput { 294 src, ok := in.(pyString) 295 if !ok { 296 s.Assert(in == None, "Items in %s must be strings", name) 297 return nil 298 } 299 return parseSource(s, string(src), systemAllowed, tool) 300 } 301 302 // parseSource parses an incoming source label as either a file or a build label. 303 // Identifies if the file is owned by this package and returns an error if not. 304 func parseSource(s *scope, src string, systemAllowed, tool bool) core.BuildInput { 305 if core.LooksLikeABuildLabel(src) { 306 if tool && s.pkg.Subrepo != nil && s.pkg.Subrepo.IsCrossCompile { 307 // Tools always use the host configuration. 308 // TODO(peterebden): this should really use something involving named output labels; 309 // right now we don't have a package handy to call that but we 310 // don't use them for tools anywhere either... 311 return core.ParseBuildLabel(src, s.pkg.Subrepo.MakeRelativeName(s.pkg.Name)) 312 } 313 label, subrepo := core.MustParseNamedOutputLabel(src, s.pkg) 314 verifyArchSubrepo(s, subrepo) 315 return label 316 } 317 s.Assert(src != "", "Empty source path") 318 s.Assert(!strings.Contains(src, "../"), "%s is an invalid path; build target paths can't contain ../", src) 319 if src[0] == '/' || src[0] == '~' { 320 s.Assert(systemAllowed, "%s is an absolute path; that's not allowed", src) 321 return core.SystemFileLabel{Path: src} 322 } else if strings.Contains(src, "/") { 323 // Target is in a subdirectory, check nobody else owns that. 324 for dir := path.Dir(path.Join(s.pkg.Name, src)); dir != s.pkg.Name && dir != "."; dir = path.Dir(dir) { 325 s.Assert(!fs.IsPackage(s.state.Config.Parse.BuildFileName, dir), "Trying to use file %s, but that belongs to another package (%s)", src, dir) 326 } 327 } else if tool { 328 // "go" as a source is interpreted as a file, as a tool it's interpreted as something on the PATH. 329 return core.SystemPathLabel{Name: src, Path: s.state.Config.Build.Path} 330 } 331 // Make sure it's not the actual build file. 332 for _, filename := range s.state.Config.Parse.BuildFileName { 333 s.Assert(filename != src, "You can't specify the BUILD file as an input to a rule") 334 } 335 if s.pkg.Subrepo != nil { 336 return core.SubrepoFileLabel{ 337 File: src, 338 Package: s.pkg.Subrepo.MakeRelativeName(s.pkg.Name), 339 FullPackage: s.pkg.Subrepo.Dir(s.pkg.Name), 340 } 341 } 342 return core.FileLabel{File: src, Package: s.pkg.Name} 343 } 344 345 // parseLabel parses a build label, handling potential cross-architecture labels. 346 func parseLabel(s *scope, label string) core.BuildLabel { 347 l, subrepo := core.ParseBuildLabelSubrepo(label, s.pkg) 348 verifyArchSubrepo(s, subrepo) 349 return l.ForPackage(s.pkg) 350 } 351 352 // verifyArchSubrepo creates a subrepo for the given architecture if one doesn't exist. 353 func verifyArchSubrepo(s *scope, subrepo string) { 354 if subrepo != "" && s.state.Graph.Subrepo(subrepo) == nil { 355 // Subrepo is specified but doesn't exist - might be an arch-specific one. 356 var arch cli.Arch 357 if err := arch.UnmarshalFlag(subrepo); err == nil { 358 s.state.Graph.MaybeAddSubrepo(core.SubrepoForArch(s.state, arch)) 359 } 360 } 361 } 362 363 // callbackFunction extracts a pre- or post-build function for a target. 364 func callbackFunction(s *scope, name string, obj pyObject, requiredArguments int, arguments string) *pyFunc { 365 if obj != nil && obj != None { 366 f := obj.(*pyFunc) 367 s.Assert(len(f.args) == requiredArguments, "%s callbacks must take exactly %d %s (%s takes %d)", name, requiredArguments, arguments, f.name, len(f.args)) 368 return f 369 } 370 return nil 371 } 372 373 // A preBuildFunction implements the core.PreBuildFunction interface 374 type preBuildFunction struct { 375 f *pyFunc 376 s *scope 377 } 378 379 func (f *preBuildFunction) Call(target *core.BuildTarget) error { 380 s := f.f.scope.NewPackagedScope(f.f.scope.state.Graph.PackageOrDie(target.Label.PackageName)) 381 s.Callback = true 382 s.Set(f.f.args[0], pyString(target.Label.Name)) 383 return annotateCallbackError(s, target, s.interpreter.interpretStatements(s, f.f.code)) 384 } 385 386 func (f *preBuildFunction) String() string { 387 return f.f.String() 388 } 389 390 // A postBuildFunction implements the core.PostBuildFunction interface 391 type postBuildFunction struct { 392 f *pyFunc 393 s *scope 394 } 395 396 func (f *postBuildFunction) Call(target *core.BuildTarget, output string) error { 397 log.Debug("Running post-build function for %s. Build output:\n%s", target.Label, output) 398 s := f.f.scope.NewPackagedScope(f.f.scope.state.Graph.PackageOrDie(target.Label.PackageName)) 399 s.Callback = true 400 s.Set(f.f.args[0], pyString(target.Label.Name)) 401 s.Set(f.f.args[1], fromStringList(strings.Split(strings.TrimSpace(output), "\n"))) 402 return annotateCallbackError(s, target, s.interpreter.interpretStatements(s, f.f.code)) 403 } 404 405 func (f *postBuildFunction) String() string { 406 return f.f.String() 407 } 408 409 // annotateCallbackError adds some information to an error on failure about where it was in the file. 410 func annotateCallbackError(s *scope, target *core.BuildTarget, err error) error { 411 if err == nil { 412 return nil 413 } 414 // Something went wrong, find the BUILD file and attach some info. 415 pkg := s.state.Graph.Package(target.Label.PackageName) 416 f, _ := os.Open(pkg.Filename) 417 return s.interpreter.parser.annotate(err, f) 418 } 419 420 // asList converts an object to a pyList, accounting for frozen lists. 421 func asList(obj pyObject) (pyList, bool) { 422 if l, ok := obj.(pyList); ok { 423 return l, true 424 } else if l, ok := obj.(pyFrozenList); ok { 425 return l.pyList, true 426 } 427 return nil, false 428 } 429 430 // asDict converts an object to a pyDict, accounting for frozen dicts. 431 func asDict(obj pyObject) (pyDict, bool) { 432 if d, ok := obj.(pyDict); ok { 433 return d, true 434 } else if d, ok := obj.(pyFrozenDict); ok { 435 return d.pyDict, true 436 } 437 return nil, false 438 }