package build import ( "cmp" "context" "encoding/json" "errors" "fmt" "maps" "slices" "sort" "strconv" "strings" "sync" "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient" sweep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshartifactsweep" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" sdkstep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep" repotrunk "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/trunk" "git.novox.be/novox/mesh-sdk/go/busretry" "git.novox.be/novox/mesh-sdk/go/catalogue" "mesh-delivery-builder/internal/versions" ) // ToolchainEntries are the nodes' toolchain entries of a language (buildrun.ToolchainSet). type ToolchainEntries interface { Entries(language string) []sdkstep.NodeToolchain } // Builder builds, dark, every new-form fingerprint the indexer writes (novox/hq ADR 0453 clause 4, ADR 0478): it is // handed each modules value as the watch of the indexer's bucket sees it, and for each module with a fingerprint and an // artifact to build claims the fingerprint's build key, builds it by asking the build seats (one ask per build module, // pulled by whichever build node has a free slot: ADR 0525), and writes the key done or failed. It holds no timer; a build starts from a modules value or a // change of the toolchains. type Builder struct { Claimer Claimer // StepEntry reads the entry of a fingerprint through the seat mesh-catalog's entry (design 47 G A-84, A-97). StepEntry func(ctx context.Context, fingerprint string) (meshcatalog.CatalogEntry, error) // Seats finds the build modules: the holders of the build seats on the seat list and the catalog half (design 47 G A-82). Seats *buildrun.BuildSeats // Asks puts each build module's part of a build as one ask on its build seat's stream and waits for its step (novox/hq // ADR 0525 §1, §3, §6; issue 1887 A4): the builder chooses no node, whichever build node pulls the ask builds it. Asks interface { Put(ctx context.Context, a sdkstep.StepAsk) error Wait(ctx context.Context, a sdkstep.StepAsk) (sdkstep.Step, error) } // Held says whether an ask a key names is on its stream or a build node wrote a step for it (issue 1910): a key taken // over at start whose ask is neither was left by a builder that stopped between naming the ask and putting it, and // the ask is put then, under the same id. Nil takes every named ask as held. Held interface { Held(ctx context.Context, a sdkstep.StepAsk) (bool, error) } Toolchains ToolchainEntries // Waits keeps the toolchain waits of design 47 G §672. Nil holds none: an entry whose fingerprint names another toolchain is // left unbuilt, as before. Waits Waits Manifests Manifests // Left, when set, is told a key a look claimed and left building because its end could not be written (issue 1966, // novox/hq ADR 0668 §3; 1748 note 27): no look holds it then, so nothing ends it but builder_build_again or the // builder's next start. Ended is told each key whose end was written. Left func(ctx context.Context, key, why string) Ended func(ctx context.Context, key string) // Limit is how many builds the builder has asked at once; their asks wait in the build seats' streams, which are the // backlog (ADR 0525 §9), not here. 256 when 0. Limit int Logf func(format string, args ...any) // Answered is told the build module whose step answered a reference (issue 1886): what the builder says it writes // rests on it. Nil tells nobody. Answered func(buildModule string) // Stored answers which references the artifact store no longer holds (the seat mesh-artifact-sweep's verb held; issue 1937): a done // key of the trunk's fingerprint is asked before it is left done (ClaimOptions.Stored). Nil asks nothing. Stored func(ctx context.Context, references []string) (gone []string, err error) // Members answers the module and artifact that build a package, from the catalog half's view of the packages (design 47 G // §373, §465): the part a resolved package names in a build's dependencies. Nil names none. Members func(ctx context.Context, pkg string) (module, artifact string, found bool, err error) // Gone says why an ask with no ended step can no longer be answered, or "" when it can (issue 1945): asked once the // ask has waited CheckAfter, and again each CheckAfter it goes on waiting. An ask that cannot be answered is asked // again under a new id, at most Askings times. Nil, or CheckAfter 0, never asks it. Gone interface { Gone(ctx context.Context, a sdkstep.StepAsk) (string, error) } CheckAfter time.Duration // Askings is how many times one build module's part of a build is asked, the first included, before its key is failed // with why; DefaultAskings when 0. Askings int // DeclineBound and DeclinePause bound and pace the asks build nodes off the agreed toolchain declined (ADR 0584 clause 2): // they are no askings. DefaultDeclineBound and DefaultDeclinePause when 0; a negative DeclinePause puts it again at once. DeclineBound, DeclinePause time.Duration // Said is told each ask asked again (the builder's event build.asked-again). Nil tells nobody. Said func(AskedAgain) // Log is told each build's start and end, the entries of the builder's log (novox/hq ADR 0571 §1). Nil tells nobody. Log func(builds.BuildLogEntry) // Now is the builder's clock: of the log, and of the decline bound (DeclineBound runs from the first decline); nil is // time.Now. Node is the node the builder runs on, which asked the build. Now func() time.Time Node string // Numbers numbers each trunk build at its claim and records what it was built with (design 51 *The versions*); nil // numbers nothing. Numbers Numbers // Reserved reserves the version of a trunk build at its start (design 47 G §369); nil reserves nothing. Reserved Reservations // Forms answers what each node's engine states it reads (ADR 0618 §2, ADR 0624 §1): present, the nodes whose engine // answered when last asked, and counted, every node the assignments names, present or away, a node with no statement // stating nothing (the baseline's first forms). A done key names each artifact in the forms these keep, the oldest in // its single reference, and its needs. Nil: no node states anything, and the first forms are kept. Forms func() (present, counted map[string][]string) once sync.Once sem chan struct{} wg sync.WaitGroup mu sync.Mutex // fillWaits are the builds that said wait-held for the fill (module.fingerprint), so it is said once. fillWaits sync.Map known map[string]meshcatalog.ModulesByModuleValue // by modules key: the first watched branch's heard map[string]meshcatalog.ModulesByModuleValue // by branch and module (take): what supersedes what origin map[string]Origin // by build key: the catalog revision its entry came from (take) edges map[string][]meshcatalog.Edge // by build key: the edges of its entry (take), whose parts a package-published names deps map[string]map[string][]meshbuilds.Dependency // by build key: what each artifact resolved and was built with (keepDependencies) inflight map[string]bool // by build key again map[string]meshcatalog.ModulesByModuleValue // by build key: the newest value that arrived while the key was being built plans map[string]planned // by manifest digest stepWaits map[string][]chan builds.BuildLogEntry // by build key: the step asks waiting for its end (step.go) // outOfStep are, by build key, the out-of-step files a build's lanes answered, until its end entry takes them. outOfStep map[string][]string trunkFP map[string]bool // by build key: a trunk module value carries this fingerprint tried map[string]bool // by build key: looked at since this process started // published are the package versions `@` a done key of this bucket names as a package's reference: a key // waiting for one is built once it is here (ADR 0500 clause 6). published map[string]bool // held are the done keys' references the store answered held, by ` `, asked once per process: a // blob collected later is found by the take-in, whose artifacts-gone forgets the key here (issue 1937). held map[string]bool // waiting are the lanes that wait for an ask's step, by build key and language: AskAgain asks through them. waiting map[string]map[string]chan string // hands are the requests to build a key again by hand that wait for the claim and its asks (BuildAgain), by build key. hands map[string]*handWatch // events are the keys an event asks the builder to build again (Retake), until their look claims them. events map[string]bool // released are the keys builder_release is building now, whatever toolchain their fingerprint names (§276, §672). released map[string]bool // builds are the ends that withdraw the asks of a key's build in flight, by build key (ADR 0664 §4): a newer fingerprint of // the module takes its older one's build out, and the asker drops that build's asks. builds map[string]context.CancelCauseFunc } type planned struct { plan Plan err error } // Looking says a look of this builder holds key now, by itself or asked by hand. func (b *Builder) Looking(key string) bool { b.init() b.mu.Lock() defer b.mu.Unlock() return b.inflight[key] || b.hands[key] != nil } func (b *Builder) init() { b.once.Do(func() { limit := b.Limit if limit <= 0 { limit = 256 } b.sem = make(chan struct{}, limit) b.trunkFP = map[string]bool{} b.heard, b.known = map[string]meshcatalog.ModulesByModuleValue{}, map[string]meshcatalog.ModulesByModuleValue{} b.origin, b.edges = map[string]Origin{}, map[string][]meshcatalog.Edge{} b.inflight, b.again, b.plans, b.tried = map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{} b.published, b.held, b.waiting = map[string]bool{}, map[string]bool{}, map[string]map[string]chan string{} b.hands, b.events = map[string]*handWatch{}, map[string]bool{} b.builds = map[string]context.CancelCauseFunc{} }) } func (b *Builder) logf(format string, args ...any) { if b.Logf != nil { b.Logf(format, args...) } } // Wait returns when every build started so far has ended. func (b *Builder) Wait() { b.wg.Wait() } // Module takes one value of a modules bucket in (the key is the module's name) and builds it if it is to be built. A value that // is not a module is said and left. The builder takes its modules from the catalog half (Entry); this is the same take of one // module's value, with no branch, the first one's. func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.init() m, err := meshcatalogclient.DecodeModule(value) if err != nil { b.logf("modules.%s holds what is not a module, left: %v", key, err) return } b.take(ctx, Origin{First: true}, key, m, nil) } // Origin is the catalog revision an entry came from (design 47 G §371, A-57): the watched branch it is of, as named, the source // revision the revision was taken in from, and whether the branch is the first of the module's registration, the only one whose // builds reserve a version and whose entry is the module's value for what the builder reads by name (a version, a build again). type Origin struct { Branch string Source int64 First bool } // Entry takes the catalog graph entry of one module on one watched branch in (design 47 G §348, §614) and builds its // fingerprint if it is to be built. An entry of a branch other than the first builds its fingerprint, reserves none, and // supersedes only that branch's older one. An entry naming no module is said and left. func (b *Builder) Entry(ctx context.Context, o Origin, e meshcatalog.CatalogEntry) { b.init() if e.Module == "" { b.logf("the catalog half of the branch %q holds an entry that names no module, left", o.Branch) return } b.take(ctx, o, e.Module, ModuleOf(e), e.Edges) } // EntryGone takes a module's entry out of a branch's half (the module was dropped from it, or the branch is followed no more). func (b *Builder) EntryGone(branch string, first bool, module string) { b.init() b.mu.Lock() delete(b.heard, branch+"\x00"+module) b.mu.Unlock() if first { b.Forget(module) } } func (b *Builder) take(ctx context.Context, o Origin, key string, m meshcatalog.ModulesByModuleValue, e []meshcatalog.Edge) { b.mu.Lock() hk := o.Branch + "\x00" + key old, had := b.heard[hk] b.heard[hk] = m if o.First { b.known[key] = m } var superseded context.CancelCauseFunc if had && old.Fp2 != "" && old.Fp2 != m.Fp2 { if ok, err := builds.BuilderBuildKey(key, old.Fp2); err == nil { superseded = b.builds[ok] } } if bk, err := builds.BuilderBuildKey(key, m.Fp2); err == nil && m.Fp2 != "" { // A fingerprint a watched branch carries is built from that branch's commit, whoever asked for it first. b.trunkFP[bk] = true b.origin[bk] = o b.edges[bk] = e } b.mu.Unlock() if superseded != nil { // A newer fingerprint of the module: the build of the older one in flight is taken out, and its asks are withdrawn // from the build seats (ADR 0664 §4). superseded(buildrun.Withdrawn{Why: "the build of " + key + " at " + old.Fp2 + " was superseded by the fingerprint " + m.Fp2}) } b.consider(ctx, key, m) } // ModuleOf is a catalog entry in the form the builder builds by: its fingerprint, manifest digest, source (repository, // directory, commit, the source revision it was taken in at), the toolchains its fingerprint was computed with, the bases it is // built against, its packages and needs, and the named paths of its artifacts. func ModuleOf(e meshcatalog.CatalogEntry) meshcatalog.ModulesByModuleValue { m := meshcatalog.ModulesByModuleValue{Repository: e.Repository, Path: e.Path, Commit: e.Commit, Fp2: e.Fp, None2: e.None, Manifest: e.ManifestDigest, Packages: e.Packages, Needs: e.Needs} if len(e.Bases) > 0 { m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{Bases: e.Bases} } if in := e.Inputs; in != nil { m.Src, m.Toolchains, m.Problems = in.Src, in.Toolchains, in.Problems if len(in.Paths) > 0 { if m.Inputs2 == nil { m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{} } m.Inputs2.Paths = map[string]string{} for artifact, paths := range in.Paths { for _, p := range paths { m.Inputs2.Paths[p] = artifact } } } } return m } // originOf is the catalog revision the entry of a build key came from; a key no entry named is of the first branch, as a build // of the trunk's value was before branches were watched. func (b *Builder) originOf(key string) Origin { b.mu.Lock() defer b.mu.Unlock() if o, ok := b.origin[key]; ok { return o } return Origin{First: true} } // NodesChanged is Reconsider after a toolchain moved, so a build that waited for the nodes to agree on one may go on. func (b *Builder) NodesChanged(ctx context.Context) { b.Reconsider(ctx) } // Sweep reads, at start, every key of the bucket: it keeps the package versions done keys name as published (a key // waiting for one is then looked at again, ADR 0500 clause 6), and closes every key left building for a fingerprint its // module no longer has (issue 1660, ADR 0478 clause 6): the key can never be built, since nothing asks for it, and would // stay building for ever. It fails the key naming who left it. A key of the fingerprint the module has now is not // touched: the build takes it over. Call it once the modules bucket has been read, so every module is known. // // pictured are the build keys the followed halves' pictures name as current: a follower is ready before it has told its // entries (views.Follower closes Ready, then calls Changed), so at ready the entries may not be taken in yet, and a key of // a fingerprint a picture holds is never closed for want of the entry's arrival (the restart that put its asks a second time). func (b *Builder) Sweep(ctx context.Context, pictured ...string) error { b.init() keys, err := b.Claimer.Store.Keys(ctx) if err != nil { return err } b.mu.Lock() current := map[string]bool{} for _, k := range pictured { current[k] = true } // What every followed branch carries now is current, not only the first branch's (design 47 G §348). for _, held := range []map[string]meshcatalog.ModulesByModuleValue{b.known, b.heard} { for hk, m := range held { name := hk[strings.LastIndex(hk, "\x00")+1:] if m.Fp2 != "" { if k, err := builds.BuilderBuildKey(name, m.Fp2); err == nil { current[k] = true } } } } b.mu.Unlock() newly := false for _, key := range keys { value, rev, found, err := b.Claimer.Store.Get(ctx, key) if err != nil || !found { continue } cur, err := builds.DecodeBuilderBuild(value) if err != nil { continue } newly = b.notePublished(cur) || newly if current[key] || cur.State != builds.BuildBuilding { continue } closed := builds.BuilderBuild{State: builds.BuildFailed, By: b.Claimer.By, Manifest: cur.Manifest, Step: cur.Step, Error: fmt.Sprintf("closed at start: left building by %s, and the module no longer has this fingerprint, so nothing will build it", cur.By)} data, err := builds.EncodeBuilderBuild(closed) if err != nil { return err } if _, err := b.Claimer.Store.Update(ctx, key, data, rev); err != nil && !errors.Is(err, ErrConflict) { return fmt.Errorf("%s: %w", key, err) } b.logf("%s: closed, left building by %s for a fingerprint the module no longer has", key, cur.By) } if newly { // A key read before the scan may wait for a package the scan found published (ADR 0500 clause 6). b.Reconsider(ctx) } return nil } // notePublished keeps the package versions a done build names as references, and says whether one was new. func (b *Builder) notePublished(v builds.BuilderBuild) bool { if v.State != builds.BuildDone { return false } b.mu.Lock() defer b.mu.Unlock() newly := false for _, a := range v.Artifacts { if a.Kind == catalogue.ArtifactPackage && a.Reference != "" && !b.published[a.Reference] { b.published[a.Reference] = true newly = true } } return newly } func (b *Builder) isPublished(ref string) bool { b.mu.Lock() defer b.mu.Unlock() return b.published[ref] } // Forget takes a module out (its key was deleted). func (b *Builder) Forget(key string) { b.init() b.mu.Lock() delete(b.known, key) b.mu.Unlock() } // storedOnce is Stored for one key, asked once per process while the store answers every reference held. func (b *Builder) storedOnce(key string) func(ctx context.Context, references []string) ([]string, error) { if b.Stored == nil { return nil } return func(ctx context.Context, references []string) ([]string, error) { id := key + " " + strings.Join(references, " ") b.mu.Lock() known := b.held[id] b.mu.Unlock() if known { return nil, nil } gone, err := b.Stored(ctx, references) if err == nil && len(gone) == 0 { b.mu.Lock() b.held[id] = true b.mu.Unlock() } return gone, err } } // ArtifactsGone takes the seat mesh-artifact-sweep's event artifacts-gone in (issue 1937, ADR 0571 §2): the sweep found // that the store does not hold what the module's key names. The module is looked at again, which asks the store (ClaimOptions.Stored) and // builds the key again; an event for a fingerprint the module no longer has is said and left. The event only wakes the // builder: what is built rests on the store's answer, never on the event alone. func (b *Builder) ArtifactsGone(ctx context.Context, e sweep.ArtifactsGone) { b.init() b.mu.Lock() m, ok := b.known[e.Module] if key, err := builds.BuilderBuildKey(e.Module, e.Fingerprint); err == nil { for id := range b.held { if strings.HasPrefix(id, key+" ") { delete(b.held, id) } } } b.mu.Unlock() if !ok || m.Fp2 != e.Fingerprint { b.logf("%s: heard %s for %s, a fingerprint the module does not have now: nothing to build", e.Module, sweep.EventArtifactsGone, e.Fingerprint) return } b.logf("%s: heard %s (%s): asking the artifact store and building the key again", e.Module, sweep.EventArtifactsGone, strings.Join(e.Gone, ", ")) b.consider(ctx, e.Module, m) } // Reconsider looks at every module again: the toolchains changed, so a build that failed for them may build now. func (b *Builder) Reconsider(ctx context.Context) { b.init() b.mu.Lock() known := make(map[string]meshcatalog.ModulesByModuleValue, len(b.known)) for k, m := range b.known { known[k] = m } b.mu.Unlock() for _, k := range sortedKeys(known) { b.consider(ctx, k, known[k]) } } func sortedKeys(m map[string]meshcatalog.ModulesByModuleValue) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) return keys } // planOf is the plan of the manifest with this digest, read once: a manifest by digest never changes. A manifest that // cannot be read is not kept, so it is read again when the module is. func (b *Builder) planOf(ctx context.Context, digest string) (Plan, error, bool) { b.mu.Lock() p, ok := b.plans[digest] b.mu.Unlock() if ok { return p.plan, p.err, true } m, err := b.Manifests.Manifest(ctx, digest) if err != nil { return Plan{}, fmt.Errorf("its manifest %s cannot be read: %w", digest, err), false } plan, err := PlanOf(m, b.Seats) var none *NoBuildModule if errors.As(err, &none) { return plan, err, false // the seat list or the catalog half names no build module yet: made again when they do } b.mu.Lock() b.plans[digest] = planned{plan, err} b.mu.Unlock() return plan, err, true } func (b *Builder) consider(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue) { if m.Fp2 == "" || m.Manifest == "" { return } key, err := builds.BuilderBuildKey(module, m.Fp2) if err != nil { b.logf("modules.%s: %v", module, err) return } b.mu.Lock() if b.inflight[key] { b.again[key] = m // looked at once more when the build ends, so a change is never lost b.mu.Unlock() return } b.inflight[key] = true b.mu.Unlock() b.wg.Add(1) go func() { defer b.wg.Done() b.sem <- struct{}{} defer func() { <-b.sem }() for { b.try(ctx, module, m) b.mu.Lock() next, more := b.again[key] delete(b.again, key) if !more || ctx.Err() != nil { delete(b.inflight, key) b.mu.Unlock() return } b.mu.Unlock() m = next } }() } // startOnce looks at the module for a key the caller has set inflight already (BuildAgain): the by-hand look runs once, and // a look the builder queued for the key meanwhile (again) still runs after it, as consider's loop does. func (b *Builder) startOnce(ctx context.Context, key, module string, m meshcatalog.ModulesByModuleValue) { b.wg.Add(1) go func() { defer b.wg.Done() b.sem <- struct{}{} defer func() { <-b.sem }() for { b.try(ctx, module, m) b.mu.Lock() next, more := b.again[key] delete(b.again, key) if !more || ctx.Err() != nil { delete(b.inflight, key) b.mu.Unlock() return } b.mu.Unlock() m = next // a look the builder queued meanwhile is not lost (consider's rule); it is not a second by-hand build } }() } // try claims the fingerprint and builds it, writing the key done or failed. A build that ends because ctx did is left // building by this builder: it takes it again when it starts (ADR 0478 clause 5). func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue) { key, _ := builds.BuilderBuildKey(module, m.Fp2) defer b.handEnded(key) defer b.unrelease(key) plan, planErr, read := b.planOf(ctx, m.Manifest) var noModule *NoBuildModule if errors.As(planErr, &noModule) { // The seat list or the catalog half does not name the build module yet: nothing is claimed, and the look is made again // when they do (Reconsider, at each change of either; design 47 G A-82). b.logf("%s at %s is not built yet: %v", module, m.Fp2, planErr) return } nothing := read && planErr == nil && len(plan.Tasks) == 0 if planErr == nil && !nothing && !b.isReleased(key) { if why := b.stale(m, plan); why != "" { if w, ok := b.toolchainWait(module, m, plan); ok && b.Waits != nil { b.Waits.Hold(ctx, w) } // The fingerprint names a toolchain the build nodes no longer agree on: a new fingerprint is coming, and a build // now would be of this source with another toolchain (ADR 0584 clause 1, which changes ADR 0477 clause 2). Nothing is written. b.logf("%s at %s is not built: %s", module, m.Fp2, why) return } if b.Waits != nil { b.Waits.End(ctx, module, m.Fp2, WaitRan) // a toolchain-agreed names the version the fingerprint names } } b.mu.Lock() trunk := b.trunkFP[key] first := !b.tried[key] b.tried[key] = true byHand := b.hands[key] != nil byEvent := b.events[key] delete(b.events, key) b.mu.Unlock() var numbering versions.Numbering if trunk && planErr == nil && !nothing { var goOn bool if numbering, goOn = b.number(ctx, module, m, plan); !goOn { return } } // Only a build of the registration's first watched branch reserves and releases a version (design 47 G §371). reserving := trunk && b.originOf(key).First && planErr == nil && !nothing && b.reserves(ctx, module, m, plan) if reserving && b.waitFill(module, m) { return } // A key built again by hand is taken over as it stands too: one left building by a look that ended without its write // (issue 1966) is the hand tool's to finish. cl, err := b.Claimer.Claim(ctx, module, m.Fp2, m.Manifest, ClaimOptions{TakeOver: first || byHand, ByHand: byHand, ByEvent: byEvent, ImageBuilder: imageSeat, Seats: b.Seats, Trunk: trunk, Published: b.isPublished, Stored: b.storedOnce(key)}) if err != nil { b.logf("%s: %v", module, err) return } if cl == nil { return } b.handClaimed(key, cl.Rev) if b.Waits != nil { b.Waits.End(ctx, module, m.Fp2, WaitPublished) // a key queued behind a package is claimed once it is published } b.logBuild(LogStarted(module, m, "", b.Node, b.now())) if cl.Unasked != nil { b.logf("%v", cl.Unasked) } if len(cl.Gone) > 0 { // Said aloud (issue 1937): the key was done, and the store's collector deleted what it names. b.logf("%s: built again: the artifact store no longer holds %s", cl.Key, strings.Join(cl.Gone, ", ")) } // A done key taken again builds only what is missing: the image built unpublished as a candidate's (A-289); the // other artifacts stay as the done key has them (ADR 0487 clauses 5 and 8, ADR 0498 clause 6). var prior *builds.BuilderBuild var only map[string]bool publishing := false if cl.Build.State == builds.BuildDone { prior, only = &cl.Build, map[string]bool{} if trunk { for _, bm := range publishable(cl.Build, b.Seats, imageSeat) { only[bm] = true publishing = true } } } var reserved versions.ReservedVersion hasReserved := false if reserving && cl.Build.State != builds.BuildDone { reserved, hasReserved = b.reserve(ctx, module, m, plan, b.originOf(key)) } var result builds.BuilderBuild if numbering.Refused != "" && cl.Build.State != builds.BuildDone { // Refused, naming the state it could not read, and built against no guess (ADR 0666 §15; design 47, test 70): the key // fails with why, and is taken again at the next look. result = failed("%s", numbering.Refused) } else if nothing { // A manifest with no artifact is built by building nothing: its key is done with no artifact, claimed and written as // any other, so the controller has a done key to register it from (issue 1613, ADR 0478 clause 1, ADR 0501 clause 1). result = builds.BuilderBuild{State: builds.BuildDone, Step: "nothing to build: the manifest names no artifact"} } else { run, withdraw := context.WithCancelCause(ctx) b.mu.Lock() b.builds[key] = withdraw b.mu.Unlock() result = b.build(run, module, m, plan, planErr, prior, only, b.notes(cl), reserved.Version) b.mu.Lock() delete(b.builds, key) b.mu.Unlock() withdraw(nil) var w buildrun.Withdrawn if ctx.Err() == nil && errors.As(context.Cause(run), &w) { // Taken out, not stopped: the key says so, and the asks are withdrawn (the asker dropped them). result = builds.BuilderBuild{State: builds.BuildFailed, Error: keyError([]string{"withdrawn: " + w.Why})} } } if ctx.Err() != nil { return } if result.State == "" { b.logf("%s: left building, the toolchains moved under it", cl.Key) return } result.By, result.Manifest, result.EventBuilds = b.Claimer.By, m.Manifest, cl.Build.EventBuilds if len(cl.Gone) > 0 && result.State == builds.BuildDone { result.Step += " (built again: the artifact store no longer held " + strings.Join(cl.Gone, ", ") + ")" } if publishing && result.State == builds.BuildFailed { // The trunk's image or package was built only as a candidate's and its push or publish failed: the key says so, so the builds view, the // checker and the failed-key retake see it, and a look does not build the image again for nothing. result.Error = keyError([]string{"publishing what the merged commit built failed: " + result.Error}) } if err := b.Claimer.Finish(ctx, cl, result); err != nil { b.logf("%s: the build could not be written: %v", cl.Key, err) if b.Left != nil && !errors.Is(err, ErrConflict) && ctx.Err() == nil { b.Left(ctx, cl.Key, err.Error()) // left building with no look holding it (issue 1966) } return } if b.Ended != nil { b.Ended(ctx, cl.Key) } ended := LogEnded(module, m, "", b.Node, b.now(), result) ended.OutOfStep = b.takeOutOfStep(key) // the generated files the build module's compare found out of step (design 26) b.logBuild(ended) if result.State == builds.BuildDone && b.Numbers != nil && len(numbering.Versions) > 0 { if err := b.Numbers.Built(ctx, module, m, plan, numbering, result.Artifacts); err != nil { b.logf("%s: %v", cl.Key, err) } } if result.State == builds.BuildDone && hasReserved { digests := map[string]string{} for _, a := range result.Artifacts { digests[module+":"+a.Name] = a.Digest } if err := b.Reserved.Built(ctx, module, reserved.Version, digests, b.takeDependencies(key)); err != nil { b.logf("%s: the digests of %s are not recorded: %v", cl.Key, reserved.Version, err) } } if result.State == builds.BuildFailed { b.logf("%s: failed: %s", cl.Key, result.Error) } if result.State == builds.BuildQueued { b.logf("%s: waits for the package %s to be published", cl.Key, result.Waits) if b.Waits != nil { // Said as a wait in the one wait form, with its part, for the watchman to time (design 47 G A-192; ADR 0603 §8). b.Waits.Hold(ctx, ToolchainWait{Module: module, Fingerprint: m.Fp2, Package: result.Waits, Since: b.now()}) } } if b.notePublished(result) { // A key may wait for the package this build published (ADR 0500 clause 6). b.Reconsider(ctx) } } // stale is why the module's fingerprint is not built now, or empty when it is: the toolchain each language of the plan // has agreed (novox/hq ADR 0465) must be the one the fingerprint was computed with (Module.Toolchains). func (b *Builder) stale(m meshcatalog.ModulesByModuleValue, plan Plan) string { for _, bm := range plan.Modules() { if !plan.NeedsToolchain(bm) { continue // an archive is packed with no toolchain } language, _ := b.Seats.LanguageOf(bm) ag := sdkstep.ToolchainAgreement(b.Toolchains.Entries(language)) if ag.Wait != "" { return fmt.Sprintf("the %s toolchain is not agreed: %s", language, ag.Wait) } tc := ag.Toolchain want, ok := m.Toolchains[language] if !ok || want.Version != tc.Version || want.Recipe != tc.Recipe { return fmt.Sprintf("the fingerprint names %s %s (recipe %s) and the nodes agree on %s (recipe %s)", language, want.Version, want.Recipe, tc.Version, tc.Recipe) } } return "" } func failed(format string, args ...any) builds.BuilderBuild { return builds.BuilderBuild{State: builds.BuildFailed, Error: fmt.Sprintf(format, args...)} } // lane is the build of a plan's tasks of one build module on one node. type lane struct { module, node string // node is the one that pulled the lane's ask version string // the version the build reserved, MAJOR.MINOR.PATCH, empty for a build that reserves none (design 47 G §573) tasks []Task toolchain *builds.ToolchainUsed digests map[string]string // artifact -> digest references map[string]string // artifact -> the reference an image was pushed to (a trunk build only) imports []string // the mesh packages the lane's programs import, as its build module answered them resolved map[string][]sdkstep.AskResolved // artifact -> the mesh packages its build resolved (design 47 G §466) tcDigest string // the digest of the toolchain program the building node ran, kind 3's (design 47 G §466, §472) tcVersion string // and the version it ran kept string // the digest of the artifact the build node kept instead of storing it (BuildAnswer.Kept), else empty outOfStep []string // the generated files its build module's compare found out of step, passed or failed err error } func (b *Builder) build(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, planErr error, prior *builds.BuilderBuild, only map[string]bool, asked *askNotes, version string) builds.BuilderBuild { switch { case planErr != nil: return failed("%s", planErr) case m.Path == "": // Empty is not known (a root module's path is "."): the indexer writes it again (issue 1615). return failed("the module's key names no directory (path), so the build tools cannot be told where %s lives", module) } repository := repotrunk.RepositoryName(m.Repository) var lanes []*lane used := map[string]builds.ToolchainUsed{} var sitting []string // the nodes that sit out of a language's build, said in the key's step (ADR 0584 clause 2) var kept []string // build modules whose artifacts stay as the prior done key has them for _, bm := range plan.Modules() { if prior != nil && len(only) > 0 && !only[bm] { kept = append(kept, bm) continue } var toolchain *builds.ToolchainUsed if plan.NeedsToolchain(bm) { // The ask names the toolchain the fingerprint was computed with; a node building with another refuses it. language, _ := b.Seats.LanguageOf(bm) ag := sdkstep.ToolchainAgreement(b.Toolchains.Entries(language)) if ag.Wait != "" { return failed("the %s toolchain is not agreed: %s", language, ag.Wait) } tc := ag.Toolchain sitting = append(sitting, SittingOutWords(language, ag)...) released := false if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil { released = b.isReleased(key) // builder_release builds it with the running build module (§276) } if want := m.Toolchains[language]; !released && (want.Version != tc.Version || want.Recipe != tc.Recipe) { return builds.BuilderBuild{} // the agreed toolchain moved since try looked: nothing is written } u := builds.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe} used[language], toolchain = u, &u } var tasks []Task for _, t := range plan.Tasks { if t.Module == bm { tasks = append(tasks, t) } } // One ask builds each fingerprint's part of a build module, on whichever node pulls it (ADR 0525 §3). lanes = append(lanes, &lane{module: bm, version: version, tasks: tasks, toolchain: toolchain, digests: map[string]string{}, references: map[string]string{}, resolved: map[string][]sdkstep.AskResolved{}}) } if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil { b.handLanes(key, len(lanes)) } var wg sync.WaitGroup for _, l := range lanes { wg.Add(1) go func() { defer wg.Done(); b.runLane(ctx, l, repository, m, module, asked) }() } wg.Wait() if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil { b.keepOutOfStep(key, lanes) b.keepDependencies(key, module, lanes) } var errs []string for _, l := range lanes { if l.err != nil { errs = append(errs, l.err.Error()) } } if len(errs) > 0 { if ref := waitsOf(errs); ref != "" { // The module's lock names a version of a package the registry does not hold yet: the key waits for it, queued, and // is not failed (ADR 0500 clause 6). return builds.BuilderBuild{State: builds.BuildQueued, Waits: ref, Step: "waits for the package " + ref + " to be published"} } return failed("%s", keyError(errs)) } var artifacts []builds.BuiltArtifact var present, counted map[string][]string if b.Forms != nil { present, counted = b.Forms() } keptModule := map[string]bool{} for _, bm := range kept { keptModule[bm] = true } for _, t := range plan.Tasks { if keptModule[t.Module] { found := false for _, a := range prior.Artifacts { if a.Name == t.Artifact { artifacts, found = append(artifacts, a), true } } if !found { return failed("the done build holds no artifact %s to keep", t.Artifact) } continue } byNode := map[string]string{} for _, l := range lanes { if d, ok := l.digests[t.Artifact]; ok && l.module == t.Module { byNode[l.node] = d } } d, err := buildrun.Agree(t.Artifact, byNode) if err != nil { return failed("%s", err) } built := builds.BuiltArtifact{Name: t.Artifact, Kind: t.Kind, Digest: d} var answered []string for _, l := range lanes { if l.module != t.Module { continue } r := l.references[t.Artifact] if r == "" && stored(t.Kind) && (l.kept == "" || strings.TrimPrefix(d, "sha256:") != l.kept) { // A bundle, an archive or an upstream copy is done only when its push was answered with where it is (ADR 0498 // clause 1); a candidate's included (clause 3). return failed("%s%s on %s: the build answered no reference, so its push to the artifact store was not answered", builds.NotStored, t.Artifact, l.node) } if r != "" { answered = append(answered, r) } } // The forms a counted node's engine reads are kept, the oldest in the single reference (ADR 0624 §1, §3). artifacts = append(artifacts, forms.Current.Choose(built, answered, present, counted)) } var steps []string for _, l := range lanes { steps = append(steps, fmt.Sprintf("%s on %s", l.module, l.node)) } for _, bm := range kept { steps = append(steps, bm+" kept") kept, _ := b.Seats.LanguageOf(bm) for lang, u := range prior.Toolchains { if _, ok := used[lang]; !ok && kept == lang { used[lang] = u } } } step := "built " + strings.Join(steps, "; ") if len(sitting) > 0 { step += " (" + strings.Join(sitting, "; ") + ")" } if len(used) == 0 { used = nil } done := builds.BuilderBuild{State: builds.BuildDone, By: b.Claimer.By, Manifest: m.Manifest, Step: step, Artifacts: artifacts, Toolchains: used, Imports: importsOf(lanes), Needs: forms.Current.Needs(artifacts)} // the forms it is written in (ADR 0618 §1) for _, l := range lanes { if l.kept != "" { done.KeptOn = l.node // design 47 G §671: the build node that keeps the build } } if _, err := builds.EncodeBuilderBuild(done); err != nil { // A reference the build module answered that does not name the artifact and its digest is no reference. return failed("%s%v", builds.NotStored, err) } return done } // stored is whether an artifact of this kind is in the artifact store once built, a candidate's too, so its done key // names its reference (ADR 0498 clauses 1, 3 and 4); an image or a package names one only when built for the trunk. func stored(kind string) bool { return kind == catalogue.ArtifactBundle || kind == catalogue.ArtifactArchive || kind == catalogue.ArtifactUpstream } // waitsFor opens the reason of a fetch step whose module's lock names a package version the registry does not hold, then // `@` (the Node build module's, ADR 0500 clause 6). const waitsFor = builds.WaitsFor // waitsOf is the package version a lane's error says the module waits for, or "" when none does or what it names is not // `@`. func waitsOf(errs []string) string { for _, e := range errs { at := strings.Index(e, waitsFor) if at < 0 { continue } ref, _, _ := strings.Cut(e[at+len(waitsFor):], " ") ref = strings.TrimRight(ref, ":;,") if _, err := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildQueued, Waits: ref}); err == nil { return ref } } return "" } // keyPrefixes are the errors a build module's step may open its reason with that the key's error must open with too, so // the claim reads them (builds.FailedUntilChanged), first the ones not built again. var keyPrefixes = []string{builds.NotPinned, builds.VersionTaken, builds.RefusedKind, builds.NotStored} // keyError is the error of a key from its lanes' errors: when one names a prefix of keyPrefixes inside the step's words // (` on : the step failed: not stored: ...`), the key's error opens with that prefix, so a reader of the // key need not search for it. func keyError(errs []string) string { // A transport fault is the key's own words only when every lane failed on one: beside a real failure it is said after it, // so the key is not built again on an event for a failure the source made (design 47 G §568). errs = slices.Clone(errs) slices.SortStableFunc(errs, func(a, b string) int { return boolInt(strings.HasPrefix(a, builds.TransportFaultPrefix)) - boolInt(strings.HasPrefix(b, builds.TransportFaultPrefix)) }) for _, prefix := range keyPrefixes { for i, e := range errs { if strings.HasPrefix(e, prefix) { return strings.Join(errs, "; ") } if at := strings.Index(e, prefix); at >= 0 { rest := append([]string{prefix + e[:at] + e[at+len(prefix):]}, append(append([]string(nil), errs[:i]...), errs[i+1:]...)...) return strings.Join(rest, "; ") } } } return strings.Join(errs, "; ") } // imageSeat says an image build is asked of the image build seat, which takes it when a node pulls it: a key failed for // want of an image builder before R4g is taken again (ADR 0525 §1). func imageSeat() bool { return true } // askNotes writes a claimed key's asks into it, building, before each is put (ADR 0525 §1; issue 1887 A4b), and holds // the asks a key taken over at start already names, so they are waited for and never put again. type askNotes struct { mu sync.Mutex b *Builder cl *Claim resumed map[string]string // step is the step words of a key taken over at start: an ask asked again says its asking there (askingOf). step string } func (b *Builder) notes(cl *Claim) *askNotes { n := &askNotes{b: b, cl: cl} if cl.Build.State == builds.BuildBuilding && len(cl.Build.Asks) > 0 { n.resumed, n.step = maps.Clone(cl.Build.Asks), cl.Build.Step } return n } // resume is the id of the ask a taken-over key names for language, or "". func (n *askNotes) resume(language string) string { n.mu.Lock() defer n.mu.Unlock() return n.resumed[language] } // note writes the ask's id into the key, building, by compare-and-set at the claim's revision. func (n *askNotes) note(ctx context.Context, language, id, step string) error { n.mu.Lock() defer n.mu.Unlock() v := n.cl.Build v.State, v.By, v.Step, v.Error, v.Artifacts, v.Needs = builds.BuildBuilding, n.b.Claimer.By, step, "", nil, nil v.Asks = maps.Clone(v.Asks) if v.Asks == nil { v.Asks = map[string]string{} } v.Asks[language] = id return n.b.Claimer.Finish(ctx, n.cl, v) } // runLane puts the lane's part of the build as one ask on its build seat's stream (the delivery stream for a trunk key, the // check stream for a candidate's) and reads what the node that pulled it built (ADR 0525 §1, §3, §6). An upstream artifact // is copied by its pinned reference with no fetch (ADR 0498 clause 4); a lane of copies alone fetches nothing. A lane of a // key taken over at start waits for the ask the key names and puts none. func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m meshcatalog.ModulesByModuleValue, module string, asked *askNotes) { req := sdkstep.Fetch{Repository: repository, Commit: m.Commit} if m.Inputs2 != nil && len(m.Inputs2.Paths) > 0 { // A module whose every artifact names its paths is built from those alone (ADR 0499 clause 4): the paths come from // the indexer's kept inputs, never from a manifest (the lead's decision L8 of R4e). req.Dir = m.Path for p := range m.Inputs2.Paths { req.Paths = append(req.Paths, p) } sort.Strings(req.Paths) } seat, _ := b.Seats.OfModule(l.module) language := seat.AskLanguage() kind := sdkstep.AskDelivery job := &sdkstep.BuildJob{Module: module, Path: m.Path, Fp: m.Fp2, Toolchain: buildToolchain(language, l.toolchain), Publish: true} for _, t := range l.tasks { a := sdkstep.AskArtifact{Artifact: t.Artifact} if t.Kind == catalogue.ArtifactUpstream { a.From = t.From // the copy runs no code of a change: the module's name, the artifact and its pin } else if seat, _, ok := versions.SeatOf(t.Kind, t.Language); ok && l.version != "" { // A build under a reserved version names the registry seat each artifact is published through, so a Go package is // published and tagged under it (design 47 G §370; design 51 *Publishing*). a.Seat = seat } job.Artifacts = append(job.Artifacts, a) } if needsFetch(l.tasks) { job.Fetch = &req } ask := sdkstep.StepAsk{ID: sdkstep.NewID(), Kind: kind, Language: language, Asker: builds.BuilderModule, At: time.Now(), Version: l.version, Build: job} if language == sdkstep.AskGo && job.Fetch != nil && len(m.Packages) > 0 { // The versions of the mesh packages the fingerprint was computed with (novox/hq ADR 0573 §2): the build module writes // them into the workspace, whatever the repository's go.mod names. ask.Packages = maps.Clone(m.Packages) for _, fb := range req.Builds { delete(ask.Packages, fb.Package) // a package `builds` names is named in neither packages nor package (§465) } } if id := asked.resume(language); id != "" { ask.ID = id // put before this builder started again: the stream holds it, or a node already ran it if b.Held != nil { held, err := b.Held.Held(ctx, ask) if err != nil { // Said, and the key is not failed: the ask may well be on its stream and its build finish (issue 1910). b.logf("whether the ask %s of %s the key names is on its stream cannot be read, so it is waited for as put: %v", ask.ID, l.module, err) held = true } if !held { // The builder that named it stopped before it put it (issue 1910): put it now, under the same id. b.logf("the ask %s of %s that the key names was never put: it is put now", ask.ID, l.module) if err := b.Asks.Put(ctx, ask); err != nil { l.err = err return } } } } else { if err := asked.note(ctx, language, ask.ID, "asked "+l.module); err != nil { // The note and the put are one act (novox/hq ADR 0668 §1; issue 1748 note 27): a note a transport fault kept from // the key fails the key with that fault's words, which builder_build_again takes (ADR 0664 §3). l.err = fmt.Errorf("the ask of %s could not be written to the key: %w", l.module, err) if busretry.TimedOut(err) || busretry.Undelivered(err) { seat, _ := sdkstep.BuildSeat(language) l.err = errors.New(buildrun.TransportFault(buildrun.FaultClass(err.Error()), seat, "no node", l.err.Error())) } return } if err := b.Asks.Put(ctx, ask); err != nil { l.err = err return } // A request to build the key again by hand is answered the ask as asked once it is put, not once it is noted: a // put the stream refuses after the note answers the key's failure (BuildAgainResult.Asks). b.handAsk(asked.cl.Key, language, ask.ID) } step, ask, err := b.answer(ctx, l, ask, asked, module, m.Fp2) if err != nil { l.err = fmt.Errorf("%s: %w", l.module, err) return } l.node = step.Node // The build module's compare of a contracts module's generated files (design 26, CAT-BG5/BN2) answers the files out // of step as sdkstep.BuildAnswer.OutOfStep, in a failed step's result too: read before the step's state is. var compared sdkstep.BuildAnswer if len(step.Result) > 0 && json.Unmarshal(step.Result, &compared) == nil { l.outOfStep = compared.OutOfStep } if step.State != sdkstep.StepPassed { l.err = fmt.Errorf("%s on %s: the ask %s %s: %s", l.module, step.Node, ask.ID, step.State, step.Reason) if strings.HasPrefix(step.Reason, builds.TransportFaultPrefix) { l.err = errors.New(step.Reason) // a key's error opens with it (design 47 G §568) } return } var answer sdkstep.BuildAnswer if err := json.Unmarshal(step.Result, &answer); err != nil { l.err = fmt.Errorf("the ask of %s on %s answered what is not a build: %s", l.module, step.Node, step.Result) return } l.imports = answer.Imports l.kept = answer.Kept if answer.Toolchain != nil { l.tcDigest, l.tcVersion = answer.Toolchain.Digest, answer.Toolchain.Version } built := map[string]sdkstep.AskBuilt{} for _, a := range answer.Artifacts { built[a.Artifact] = a } for _, t := range l.tasks { a, ok := built[t.Artifact] if !ok || a.Digest == "" { l.err = fmt.Errorf("the build of %s of %s on %s answered no digest: %s", t.Artifact, module, step.Node, step.Result) return } l.digests[t.Artifact] = a.Digest l.resolved[t.Artifact] = a.Resolved if a.Reference != "" { l.references[t.Artifact] = a.Reference } if a.Reference != "" && b.Answered != nil { b.Answered(l.module) } } } // needsFetch is whether any of the tasks builds from the module's source: every one but an upstream copy. func needsFetch(tasks []Task) bool { for _, t := range tasks { if t.Kind != catalogue.ArtifactUpstream { return true } } return false } func (b *Builder) now() time.Time { if b.Now != nil { return b.Now() } return time.Now() } // logBuild tells the log an entry, said in the journal when it cannot be one. func (b *Builder) logBuild(e builds.BuildLogEntry) { b.stepEnded(e) if b.Log == nil { return } if _, err := builds.EncodeBuildLogEntry(e); err != nil { b.logf("%s at %s: no log entry: %v", e.Module, e.FP, err) return } b.Log(e) } // LogStarted is the log's entry of a build of module's fingerprint starting (ADR 0571 §1): the module, the fingerprint, the // source revision, where it is built from (repository, directory, commit, the candidate's ref), the node of the builder // that asked it and the time. func LogStarted(module string, m meshcatalog.ModulesByModuleValue, ref, by string, at time.Time) builds.BuildLogEntry { dir := m.Path if dir == "" { dir = meshcatalogclient.RootPath } return builds.BuildLogEntry{Event: builds.BuildStartedEvent, Module: module, FP: m.Fp2, Source: strconv.FormatUint(uint64(m.Src), 10), Repository: m.Repository, Directory: dir, Commit: m.Commit, Ref: ref, Node: by, At: at.UTC()} } // LogEnded is the log's entry of that build ending: the outcome (done, failed or queued), the manifest built, the bases it // was built against and each artifact made, as the done key has them: what the operator's command nox builds shows of a build. func LogEnded(module string, m meshcatalog.ModulesByModuleValue, ref, by string, at time.Time, result builds.BuilderBuild) builds.BuildLogEntry { e := LogStarted(module, m, ref, by, at) e.Event, e.Outcome, e.Manifest = builds.BuildEndedEvent, result.State, m.Manifest if result.Manifest != "" { e.Manifest = result.Manifest } if m.Inputs2 != nil { e.Against = append([]string(nil), m.Inputs2.Bases...) } e.Made = append([]builds.BuiltArtifact(nil), result.Artifacts...) e.KeptOn = result.KeptOn e.Imports = append([]string(nil), result.Imports...) // the indexer keeps them on the module (novox/hq ADR 0593 §2) e.Error, e.Step = result.Error, result.Step // a failed build's reason, not only its outcome e.Needs = append([]string(nil), result.Needs...) return e } // keepOutOfStep holds, by build key, the out-of-step files the lanes' build modules answered, sorted and each once, until // the build's end entry takes them (takeOutOfStep); a build with none holds nothing. func (b *Builder) keepOutOfStep(key string, lanes []*lane) { seen := map[string]bool{} for _, l := range lanes { for _, f := range l.outOfStep { seen[f] = true } } b.mu.Lock() defer b.mu.Unlock() if len(seen) == 0 { delete(b.outOfStep, key) return } if b.outOfStep == nil { b.outOfStep = map[string][]string{} } b.outOfStep[key] = slices.Sorted(maps.Keys(seen)) } // takeOutOfStep answers the out-of-step files held for a build key and forgets them. func (b *Builder) takeOutOfStep(key string) []string { b.mu.Lock() defer b.mu.Unlock() files := b.outOfStep[key] delete(b.outOfStep, key) return files } // importsOf are the mesh packages the lanes' programs import, as their build modules answered them (novox/hq ADR 0593 // §2): sorted and each once; nil when none answered any. A lane kept from a prior build answered none, so a build that // kept one reports only what it built. func importsOf(lanes []*lane) []string { seen := map[string]bool{} for _, l := range lanes { for _, p := range l.imports { seen[p] = true } } if len(seen) == 0 { return nil } return slices.Sorted(maps.Keys(seen)) } // SittingOutWords says, per node that is not on the agreed toolchain of a language, that it sits out and which version // it has against the agreed one: "ace sits out: typescript 26.12.1, agreed 26.13.0" (novox/hq ADR 0584 clause 2). func SittingOutWords(language string, a sdkstep.Agreement) []string { var out []string for _, s := range a.SittingOut { out = append(out, fmt.Sprintf("%s sits out: %s %s, agreed %s", s.Node, language, s.Version, a.Toolchain.Version)) } return out } // keepDependencies keeps, for a build key, what its step results say each artifact resolved (kind 1) and was built with (kind 3) as // the dependencies `package-published` carries (design 47 G §466, §673, A-57): the part of a kind-1 item is the artifact reference of // the package, and of the kind-3 item the toolchain's part, each as the edges of the entry name it. A part no edge names is said and // its item left out. func (b *Builder) keepDependencies(key, module string, lanes []*lane) { b.mu.Lock() edges := b.edges[key] b.mu.Unlock() out := map[string][]meshbuilds.Dependency{} for _, l := range lanes { for _, t := range l.tasks { ref := module + ":" + t.Artifact var deps []meshbuilds.Dependency for _, r := range l.resolved[t.Artifact] { part := "" for _, e := range edges { if e.Kind == 1 && e.To == ref && e.Why == r.Package { part = cmp.Or(e.Part, e.From) } } if part == "" && b.Members != nil { if mod, art, ok, err := b.Members(context.Background(), r.Package); ok && err == nil { part = mod + ":" + art } } if part == "" { b.logf("%s: the package %s the build of %s resolved names no artifact: its dependency is not said", key, r.Package, ref) continue } deps = append(deps, meshbuilds.Dependency{Kind: 1, Part: part, Version: r.Version, Digest: r.Digest}) } if l.tcDigest != "" { part := "" for _, e := range edges { if e.Kind == 3 && e.To == ref { part = cmp.Or(e.Part, e.From) } } if part == "" { b.logf("%s: no edge of kind 3 names the toolchain part the build of %s used: its dependency is not said", key, ref) } else { deps = append(deps, meshbuilds.Dependency{Kind: 3, Part: part, Version: l.tcVersion, Digest: l.tcDigest}) } } if len(deps) > 0 { out[ref] = deps } } } b.mu.Lock() defer b.mu.Unlock() if b.deps == nil { b.deps = map[string]map[string][]meshbuilds.Dependency{} } if len(out) == 0 { delete(b.deps, key) return } b.deps[key] = out } // takeDependencies answers the dependencies held for a build key and forgets them. func (b *Builder) takeDependencies(key string) map[string][]meshbuilds.Dependency { b.mu.Lock() defer b.mu.Unlock() d := b.deps[key] delete(b.deps, key) return d }