github.com/emcfarlane/larking@v0.0.0-20220605172417-1704b45ee6c3/starlib/starlarkrule/build.go (about)

     1  // Copyright 2022 Edward McFarlane. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Laze is a task scheduler inspired by Bazel and the Go build tool.
     6  // https://github.com/golang/go/blob/master/src/cmd/go/internal/work/action.go
     7  package starlarkrule
     8  
     9  import (
    10  	"container/heap"
    11  	"errors"
    12  	"fmt"
    13  	"io/fs"
    14  	"path"
    15  	"strings"
    16  	"time"
    17  
    18  	//"github.com/emcfarlane/larking/starlib"
    19  	"github.com/emcfarlane/larking/starlib/starlarkstruct"
    20  	"github.com/emcfarlane/larking/starlib/starlarkthread"
    21  	"github.com/go-logr/logr"
    22  	"go.starlark.net/starlark"
    23  	"gocloud.dev/blob"
    24  )
    25  
    26  // An Action represents a single action in the action graph.
    27  type Action struct {
    28  	*Label // Label is the unique key for an action.
    29  
    30  	Deps []*Action // Actions this action depends on.
    31  
    32  	Func ImplFunc // Implementation is run when built.
    33  
    34  	triggers []*Action // reverse of deps
    35  	pending  int       // number of actions pending
    36  	priority int       // relative execution priority
    37  
    38  	// Results
    39  	Value     []*AttrArgs //starlark.Value // caller values
    40  	Error     error       // caller error
    41  	Failed    bool        // whether the action failed
    42  	TimeReady time.Time
    43  	TimeDone  time.Time
    44  }
    45  
    46  func (a *Action) String() string { return "action(...)" }
    47  func (a *Action) Type() string   { return "action" }
    48  
    49  // Get provides lookup of a key *Attrs to an *AttrArgs if found.
    50  func (a *Action) Get(k starlark.Value) (v starlark.Value, found bool, err error) {
    51  	key, ok := k.(*Attrs)
    52  	if !ok {
    53  		err = fmt.Errorf("invalid key type: %s", k.Type())
    54  		return
    55  	}
    56  	for _, args := range a.Value {
    57  		v = args
    58  		attrs := args.Attrs()
    59  
    60  		found, err = starlark.Equal(key, attrs)
    61  		if found || err != nil {
    62  			return
    63  		}
    64  	}
    65  	return starlark.None, false, nil
    66  }
    67  
    68  // FailureErr is a DFS on the failed action, returns nil if not failed.
    69  func (a *Action) FailureErr() error {
    70  	if !a.Failed {
    71  		return nil
    72  	}
    73  	if a.Error != nil {
    74  		return a.Error
    75  	}
    76  	for _, a := range a.Deps {
    77  		if err := a.FailureErr(); err != nil {
    78  			return err
    79  		}
    80  	}
    81  	// TODO: panic?
    82  	return fmt.Errorf("unknown failure: %s", a.Key())
    83  }
    84  
    85  // An actionQueue is a priority queue of actions.
    86  type actionQueue []*Action
    87  
    88  // Implement heap.Interface
    89  func (q *actionQueue) Len() int           { return len(*q) }
    90  func (q *actionQueue) Swap(i, j int)      { (*q)[i], (*q)[j] = (*q)[j], (*q)[i] }
    91  func (q *actionQueue) Less(i, j int) bool { return (*q)[i].priority < (*q)[j].priority }
    92  func (q *actionQueue) Push(x interface{}) { *q = append(*q, x.(*Action)) }
    93  func (q *actionQueue) Pop() interface{} {
    94  	n := len(*q) - 1
    95  	x := (*q)[n]
    96  	*q = (*q)[:n]
    97  	return x
    98  }
    99  
   100  func (q *actionQueue) push(a *Action) {
   101  	a.TimeReady = time.Now()
   102  	heap.Push(q, a)
   103  }
   104  
   105  func (q *actionQueue) pop() *Action {
   106  	return heap.Pop(q).(*Action)
   107  }
   108  
   109  // A Builder holds global state about a build.
   110  type Builder struct {
   111  	//opts builderOptions
   112  	//loader    *starlib.Loader
   113  	//resources *starlarkthread.ResourceStore // resources
   114  
   115  	dir *Label // directory
   116  	//tmpDir string // temporary directory TODO: caching tmp dir?
   117  
   118  	actionCache map[string]*Action // a cache of already-constructed actions
   119  	targetCache map[string]*Target // a cache of created targets
   120  	moduleCache map[string]bool    // a cache of modules
   121  
   122  }
   123  
   124  func NewBuilder(l *Label) (*Builder, error) {
   125  	return &Builder{
   126  		dir: l,
   127  	}, nil
   128  }
   129  
   130  func (b *Builder) addAction(action *Action) (*Action, error) {
   131  	labelURL := action.Label.String()
   132  	if _, ok := b.actionCache[labelURL]; ok {
   133  		return nil, fmt.Errorf("duplicate action: %s", labelURL)
   134  	}
   135  	if b.actionCache == nil {
   136  		b.actionCache = make(map[string]*Action)
   137  	}
   138  	b.actionCache[labelURL] = action
   139  	return action, nil
   140  }
   141  
   142  func (b *Builder) RegisterTarget(thread *starlark.Thread, target *Target) error {
   143  	ctx := starlarkthread.GetContext(thread)
   144  	log := logr.FromContextOrDiscard(ctx)
   145  
   146  	// We are in a dir/BUILD.star file
   147  	// Create the target name dir:name
   148  	labelURL := target.label.String()
   149  
   150  	if _, ok := b.targetCache[labelURL]; ok {
   151  		return fmt.Errorf("duplicate target: %s", labelURL)
   152  	}
   153  	if b.targetCache == nil {
   154  		b.targetCache = make(map[string]*Target)
   155  	}
   156  	b.targetCache[labelURL] = target
   157  
   158  	bktURL := target.label.BucketURL()
   159  	key := target.label.Key()
   160  	log.Info("registered target", "bkt", bktURL, "key", key)
   161  	return nil
   162  }
   163  
   164  type ImplFunc func(thread *starlark.Thread) ([]*AttrArgs, error)
   165  
   166  func makeDefaultImpl(label *Label) ImplFunc {
   167  	return func(thread *starlark.Thread) ([]*AttrArgs, error) {
   168  		ctx := starlarkthread.GetContext(thread)
   169  		log := logr.FromContextOrDiscard(ctx)
   170  
   171  		key := label.Key()
   172  		bktURL := label.BucketURL()
   173  		log.Info("running default impl", "bkt", bktURL, "key", key)
   174  		// TODO: pool bkts.
   175  		bkt, err := blob.OpenBucket(ctx, bktURL)
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  		defer bkt.Close()
   180  
   181  		ok, err := bkt.Exists(ctx, key)
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  		if !ok {
   186  			return nil, fmt.Errorf("not exists: %v", label)
   187  		}
   188  
   189  		files := []starlark.Value{label}
   190  
   191  		source, err := ParseLabel(thread.Name)
   192  		if err != nil {
   193  			return nil, err
   194  		}
   195  
   196  		args, err := DefaultInfo.MakeArgs(
   197  			source,
   198  			[]starlark.Tuple{{
   199  				starlark.String("files"),
   200  				starlark.NewList(files),
   201  			}},
   202  		)
   203  		if err != nil {
   204  			return nil, err
   205  		}
   206  
   207  		return []*AttrArgs{args}, nil
   208  	}
   209  }
   210  
   211  func (b *Builder) createAction(thread *starlark.Thread, label *Label) (*Action, error) {
   212  	ctx := starlarkthread.GetContext(thread)
   213  	log := logr.FromContextOrDiscard(ctx)
   214  
   215  	// TODO: validate URL type
   216  	// TODO: label needs to be cleaned...
   217  	u := label.String()
   218  	if action, ok := b.actionCache[u]; ok {
   219  		return action, nil
   220  	}
   221  
   222  	labelKey := label.Key()
   223  	dir := path.Dir(labelKey)
   224  	labelURL := label.String()
   225  	bktURL := label.BucketURL()
   226  
   227  	log.Info("creating action", "bkt", bktURL, "key", labelKey)
   228  
   229  	moduleKey := path.Join(dir, "BUILD.star")
   230  	mod, err := label.Parse(moduleKey)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	moduleURL := mod.String()
   235  
   236  	// Load module.
   237  	if ok := b.moduleCache[moduleURL]; !ok { //&& exists(moduleKey) {
   238  		log.Info("loading module", "bkt", bktURL, "key", moduleKey)
   239  
   240  		_, err := thread.Load(thread, moduleKey)
   241  		if err != nil {
   242  			if !errors.Is(err, fs.ErrNotExist) {
   243  				log.Error(err, "failed to load", "key", moduleKey)
   244  				return nil, err
   245  			}
   246  			// ignore not found
   247  
   248  		} else {
   249  			// rule will inject the value?
   250  			//for key, val := range d {
   251  			//	fmt.Println(" - ", key, val)
   252  			//}
   253  			if b.moduleCache == nil {
   254  				b.moduleCache = make(map[string]bool)
   255  			}
   256  			b.moduleCache[moduleURL] = true
   257  		}
   258  	}
   259  
   260  	// Load rule, or file.
   261  	t, ok := b.targetCache[labelURL]
   262  	if !ok {
   263  		cleanURL := label.CleanURL()
   264  		t, ok = b.targetCache[cleanURL]
   265  		if !ok {
   266  			log.Info("unknown label target", "bkt", bktURL, "key", labelKey)
   267  			return b.addAction(&Action{
   268  				Label: label,
   269  				Deps:  nil,
   270  				Func:  makeDefaultImpl(label),
   271  			})
   272  		}
   273  
   274  		kvs, err := label.KeyArgs()
   275  		if err != nil {
   276  			log.Error(err, "invalid key args")
   277  			return nil, err
   278  		}
   279  
   280  		t = t.Clone()
   281  
   282  		// Parse query params, override args.
   283  		if err := t.SetQuery(kvs); err != nil {
   284  			log.Error(err, "failed to set key args")
   285  			return nil, err
   286  		}
   287  
   288  		log.Info("registered query target", "bkt", bktURL, "key", labelKey)
   289  		b.targetCache[labelURL] = t
   290  
   291  	}
   292  	log.Info("found label target", "bkt", bktURL, "key", labelKey)
   293  
   294  	// TODO: caching the ins & outs?
   295  	// should caching be done on the action execution?
   296  
   297  	// Find arg deps as attributes and resolve args to targets.
   298  	args := t.Args()
   299  	rule := t.Rule()
   300  	attrs := rule.Attrs()
   301  
   302  	n := args.Len()
   303  	deps := make([]*Action, 0, n/2)
   304  	createAction := func(arg starlark.Value) (*Action, error) {
   305  		l, err := AsLabel(arg)
   306  		if err != nil {
   307  			return nil, err
   308  		}
   309  		action, err := b.createAction(thread, l)
   310  		if err != nil {
   311  			return nil, fmt.Errorf("create action: %w", err)
   312  		}
   313  		deps = append(deps, action)
   314  		return action, nil
   315  	}
   316  	for i := 0; i < n; i++ {
   317  		key, arg := args.KeyIndex(i)
   318  		attr, _ := attrs.Get(key)
   319  
   320  		switch attr.Typ {
   321  		case AttrTypeLabel:
   322  			action, err := createAction(arg)
   323  			if err != nil {
   324  				return nil, err
   325  			}
   326  			if err := args.SetField(key, action); err != nil {
   327  				return nil, err
   328  			}
   329  
   330  		case AttrTypeLabelList:
   331  			v := arg.(starlark.Indexable)
   332  			elems := make([]starlark.Value, v.Len())
   333  			for i, n := 0, v.Len(); i < n; i++ {
   334  				x := v.Index(i)
   335  				action, err := createAction(x)
   336  				if err != nil {
   337  					return nil, err
   338  				}
   339  				elems[i] = action
   340  			}
   341  			if err := args.SetField(key, starlark.NewList(elems)); err != nil {
   342  				return nil, err
   343  			}
   344  
   345  		case AttrTypeLabelKeyedStringDict:
   346  			panic("TODO")
   347  
   348  		default:
   349  			continue
   350  		}
   351  	}
   352  
   353  	ruleCtx := starlarkstruct.FromKeyValues(
   354  		starlark.String("ctx"),
   355  		"actions", Actions(),
   356  		"attrs", t.Args(),
   357  		"build_dir", starlark.String(dir),
   358  		"build_file", starlark.String(moduleKey),
   359  		"dir", starlark.String(b.dir.Key()),
   360  		"label", label,
   361  		//"outs", starext.MakeBuiltin("ctx.outs", rule.Outs().MakeAttrs),
   362  	)
   363  	ruleCtx.Freeze()
   364  
   365  	return b.addAction(&Action{
   366  		Label: label,
   367  		Deps:  deps,
   368  		Func: func(thread *starlark.Thread) ([]*AttrArgs, error) {
   369  			args := starlark.Tuple{ruleCtx}
   370  			val, err := starlark.Call(thread, rule.Impl(), args, nil)
   371  			if err != nil {
   372  				return nil, err
   373  			}
   374  
   375  			provides := rule.Provides()
   376  
   377  			var results []*AttrArgs
   378  			switch v := val.(type) {
   379  			case *starlark.List:
   380  
   381  				results = make([]*AttrArgs, v.Len())
   382  				for i, n := 0, v.Len(); i < n; i++ {
   383  					args, ok := v.Index(i).(*AttrArgs)
   384  					if !ok {
   385  						return nil, fmt.Errorf(
   386  							"unexpect value in list [%d] %s",
   387  							i, v.Index(i).Type(),
   388  						)
   389  					}
   390  					attrs := args.Attrs()
   391  					if _, err := provides.Delete(attrs); err != nil {
   392  						panic(err)
   393  					}
   394  					results[i] = args
   395  				}
   396  
   397  			case starlark.NoneType:
   398  				// pass
   399  
   400  			default:
   401  				return nil, fmt.Errorf("unknown return type: %s", val.Type())
   402  			}
   403  
   404  			if provides.Len() > 0 {
   405  				var buf strings.Builder
   406  				iter := provides.Iterate()
   407  				var (
   408  					p     starlark.Value
   409  					first bool
   410  				)
   411  				for iter.Next(&p) {
   412  					if !first {
   413  						buf.WriteString(", ")
   414  					}
   415  					attrs := p.(*Attrs)
   416  					buf.WriteString(attrs.String())
   417  					first = true
   418  				}
   419  				iter.Done()
   420  				return nil, fmt.Errorf("missing: %v", buf.String())
   421  			}
   422  			return results, nil
   423  		},
   424  	})
   425  }
   426  
   427  //// TODO: caching with tmp dir.
   428  //func (b *Builder) init(ctx context.Context) error {
   429  //	//tmpDir, err := ioutil.TempDir("", "laze")
   430  //	//if err != nil {
   431  //	//	return err
   432  //	//}
   433  //	//b.tmpDir = tmpDir
   434  //	return nil
   435  //}
   436  //
   437  //func (b *Builder) cleanup() error {
   438  //	if b.tmpDir != "" {
   439  //		fmt.Println("cleanup", b.tmpDir)
   440  //		// return os.RemoveAll(b.tmpDir)
   441  //	}
   442  //	return nil
   443  //	//if b.WorkDir != "" {
   444  //	//	start := time.Now()
   445  //	//	for {
   446  //	//		err := os.RemoveAll(b.WorkDir)
   447  //	//		if err == nil {
   448  //	//			break
   449  //	//		}
   450  //	//		// On some configurations of Windows, directories containing executable
   451  //	//		// files may be locked for a while after the executable exits (perhaps
   452  //	//		// due to antivirus scans?). It's probably worth a little extra latency
   453  //	//		// on exit to avoid filling up the user's temporary directory with leaked
   454  //	//		// files. (See golang.org/issue/30789.)
   455  //	//		if runtime.GOOS != "windows" || time.Since(start) >= 500*time.Millisecond {
   456  //	//			return fmt.Errorf("failed to remove work dir: %v", err)
   457  //	//		}
   458  //	//		time.Sleep(5 * time.Millisecond)
   459  //	//	}
   460  //	//}
   461  //	//return nil
   462  //}
   463  
   464  func (b *Builder) Build(thread *starlark.Thread, label *Label) (*Action, error) {
   465  	setBuilder(thread, b)
   466  
   467  	// create action
   468  	root, err := b.createAction(thread, label)
   469  	if err != nil {
   470  		return nil, err
   471  	}
   472  	return root, nil
   473  }
   474  
   475  // actionList returns the list of actions in the dag rooted at root
   476  // as visited in a depth-first post-order traversal.
   477  func actionList(root *Action) []*Action {
   478  	seen := map[*Action]bool{}
   479  	all := []*Action{}
   480  	var walk func(*Action)
   481  	walk = func(a *Action) {
   482  		if seen[a] {
   483  			return
   484  		}
   485  		seen[a] = true
   486  		for _, a1 := range a.Deps {
   487  			walk(a1)
   488  		}
   489  		all = append(all, a)
   490  	}
   491  	walk(root)
   492  	return all
   493  }
   494  
   495  func (b *Builder) Run(root *Action, threads ...*starlark.Thread) {
   496  	if len(threads) == 0 {
   497  		panic("missing threads")
   498  	}
   499  
   500  	// Build list of all actions, assigning depth-first post-order priority.
   501  	all := actionList(root)
   502  	for i, a := range all {
   503  		a.priority = i
   504  	}
   505  
   506  	var (
   507  		readyN int
   508  		ready  actionQueue
   509  	)
   510  
   511  	// Initialize per-action execution state.
   512  	for _, a := range all {
   513  		for _, a1 := range a.Deps {
   514  			a1.triggers = append(a1.triggers, a)
   515  		}
   516  		a.pending = len(a.Deps)
   517  		if a.pending == 0 {
   518  			ready.push(a)
   519  			readyN++
   520  		}
   521  	}
   522  
   523  	// Now we have the list of actions lets run them...
   524  	par := len(threads)
   525  	jobs := make(chan *Action, par)
   526  	done := make(chan *Action, par)
   527  	workerN := par
   528  	for i := 0; i < par; i++ {
   529  		thread := threads[i]
   530  		ctx := starlarkthread.GetContext(thread)
   531  		log := logr.FromContextOrDiscard(ctx)
   532  
   533  		go func() {
   534  			for a := range jobs {
   535  
   536  				// Run job.
   537  				var value []*AttrArgs
   538  				var err error
   539  
   540  				if a.Func != nil && !a.Failed {
   541  					log.Info("running action", "key", a.Key())
   542  					thread.Name = a.Label.String()
   543  					value, err = a.Func(thread)
   544  					thread.Name = ""
   545  					log.Info("completed action", "key", a.Key())
   546  				}
   547  				if err != nil {
   548  					log.Error(err, "action failed", "key", a.Key())
   549  					a.Failed = true
   550  					a.Error = err
   551  				}
   552  				a.Value = value
   553  				a.TimeDone = time.Now()
   554  				done <- a
   555  			}
   556  		}()
   557  	}
   558  	defer close(jobs)
   559  
   560  	for i := len(all); i > 0; i-- {
   561  		// Send ready actions to available workers via the jobs queue.
   562  		for readyN > 0 && workerN > 0 {
   563  			jobs <- ready.pop()
   564  			readyN--
   565  			workerN--
   566  		}
   567  
   568  		// Wait for completed actions via the done queue.
   569  		a := <-done
   570  		workerN++
   571  
   572  		for _, a0 := range a.triggers {
   573  			if a.Failed {
   574  				a0.Failed = true
   575  			}
   576  			if a0.pending--; a0.pending == 0 {
   577  				ready.push(a0)
   578  				readyN++
   579  			}
   580  		}
   581  	}
   582  }