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  }