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  }