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