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 }