package build import ( "context" "encoding/json" "errors" "fmt" "maps" "sort" "strings" "sync" "time" "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" ) // ToolchainEntries are the nodes' toolchain entries of a language (buildrun.ToolchainSet). type ToolchainEntries interface { Entries(language string) []delivery.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 // 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 delivery.StepAsk) error Wait(ctx context.Context, a delivery.StepAsk) (delivery.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 delivery.StepAsk) (bool, error) } Toolchains ToolchainEntries Manifests Manifests // 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 controller'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) once sync.Once sem chan struct{} wg sync.WaitGroup mu sync.Mutex known map[string]delivery.Module // by modules key inflight map[string]bool // by build key again map[string]delivery.Module // by build key: the newest value that arrived while the key was being built plans map[string]planned // by manifest digest cands map[string]delivery.Candidate // by candidates key (candidates.go) via map[string]via // by build key: a candidate's fetch (candidates.go) 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 } type planned struct { plan Plan err error } func (b *Builder) init() { b.once.Do(func() { limit := b.Limit if limit <= 0 { limit = 256 } b.sem = make(chan struct{}, limit) b.cands, b.via, b.trunkFP = map[string]delivery.Candidate{}, map[string]via{}, map[string]bool{} b.known, b.inflight, b.again, b.plans, b.tried = map[string]delivery.Module{}, map[string]bool{}, map[string]delivery.Module{}, map[string]planned{}, map[string]bool{} b.published, b.held = map[string]bool{}, map[string]bool{} }) } 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 the indexer's 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. func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.init() m, err := delivery.DecodeModule(value) if err != nil { b.logf("modules.%s holds what is not a module, left: %v", key, err) return } b.mu.Lock() b.known[key] = m if bk, err := delivery.BuilderBuildKey(key, m.FP2); err == nil && m.FP2 != "" { // A fingerprint the trunk carries is built from the trunk's commit, whoever asked for it first. b.trunkFP[bk] = true delete(b.via, bk) } b.mu.Unlock() b.consider(ctx, key, m) } // 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. func (b *Builder) Sweep(ctx context.Context) error { b.init() keys, err := b.Claimer.Store.Keys(ctx) if err != nil { return err } b.mu.Lock() current := map[string]bool{} for name, m := range b.known { if m.FP2 != "" { if k, err := delivery.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 := delivery.DecodeBuilderBuild(value) if err != nil { continue } newly = b.notePublished(cur) || newly if current[key] || cur.State != delivery.BuildBuilding { continue } closed := delivery.BuilderBuild{State: delivery.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 := delivery.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 delivery.BuilderBuild) bool { if v.State != delivery.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 controller's event artifacts-gone in (issue 1937): a take-in 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 delivery.ArtifactsGone) { b.init() b.mu.Lock() m, ok := b.known[e.Module] if key, err := delivery.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, delivery.ArtifactsGoneEvent, e.Fingerprint) return } b.logf("%s: heard %s (%s): asking the artifact store and building the key again", e.Module, delivery.ArtifactsGoneEvent, 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]delivery.Module, 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]) } b.mu.Lock() cands := make([]delivery.Candidate, 0, len(b.cands)) for _, c := range b.cands { cands = append(cands, c) } b.mu.Unlock() sort.Slice(cands, func(i, j int) bool { return cands[i].Group < cands[j].Group }) for _, c := range cands { b.considerCandidate(ctx, c) } } func sortedKeys(m map[string]delivery.Module) []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.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 delivery.Module) { if m.FP2 == "" || m.Manifest == "" { return } key, err := delivery.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 } }() } // 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 delivery.Module) { plan, planErr, read := b.planOf(ctx, m.Manifest) nothing := read && planErr == nil && len(plan.Tasks) == 0 if planErr == nil && !nothing { if why := b.stale(m, plan); why != "" { // 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 0477 clause 2). Nothing is written. b.logf("%s at %s is not built: %s", module, m.FP2, why) return } } key, _ := delivery.BuilderBuildKey(module, m.FP2) b.mu.Lock() trunk := b.trunkFP[key] first := !b.tried[key] b.tried[key] = true b.mu.Unlock() cl, err := b.Claimer.Claim(ctx, module, m.FP2, m.Manifest, ClaimOptions{TakeOver: first, ImageBuilder: imageSeat, Trunk: trunk, Published: b.isPublished, Stored: b.storedOnce(key)}) if err != nil { b.logf("%s: %v", module, err) return } if cl == nil { return } 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 or the package built unpublished as a candidate's; the // other artifacts stay as the done key has them (ADR 0487 clauses 5 and 8, ADR 0498 clause 6). var prior *delivery.BuilderBuild var only map[string]bool publishing := false if cl.Build.State == delivery.BuildDone { prior, only = &cl.Build, map[string]bool{} if trunk { for _, bm := range publishable(cl.Build, imageSeat) { only[bm] = true publishing = true } } } var result delivery.BuilderBuild 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 = delivery.BuilderBuild{State: delivery.BuildDone, Step: "nothing to build: the manifest names no artifact"} } else { result = b.build(ctx, module, m, plan, planErr, prior, only, b.notes(cl)) } 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 = b.Claimer.By, m.Manifest if len(cl.Gone) > 0 && result.State == delivery.BuildDone { result.Step += " (built again: the artifact store no longer held " + strings.Join(cl.Gone, ", ") + ")" } if publishing && result.State == delivery.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) return } if result.State == delivery.BuildFailed { b.logf("%s: failed: %s", cl.Key, result.Error) } if result.State == delivery.BuildQueued { b.logf("%s: waits for the package %s to be published", cl.Key, result.Waits) } 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 delivery.Module, plan Plan) string { for _, bm := range plan.Modules() { if !plan.NeedsToolchain(bm) { continue // an archive is packed with no toolchain } language := buildrun.LanguageOf[bm] tc, none := delivery.AgreedToolchain(b.Toolchains.Entries(language)) if none != "" { return fmt.Sprintf("the %s toolchain is not agreed: %s", language, none) } 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) delivery.BuilderBuild { return delivery.BuilderBuild{State: delivery.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 tasks []Task toolchain *delivery.ToolchainUsed digests map[string]string // artifact -> digest references map[string]string // artifact -> the reference an image was pushed to (a trunk build only) err error } func (b *Builder) build(ctx context.Context, module string, m delivery.Module, plan Plan, planErr error, prior *delivery.BuilderBuild, only map[string]bool, asked *askNotes) delivery.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 := delivery.RepositoryName(m.Repository) var lanes []*lane used := map[string]delivery.ToolchainUsed{} 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 *delivery.ToolchainUsed if bm != ImageBuildModule && plan.NeedsToolchain(bm) { // The ask names the toolchain the fingerprint was computed with; a node building with another refuses it. language := buildrun.LanguageOf[bm] tc, none := delivery.AgreedToolchain(b.Toolchains.Entries(language)) if none != "" { return failed("the %s toolchain is not agreed: %s", language, none) } if want := m.Toolchains[language]; want.Version != tc.Version || want.Recipe != tc.Recipe { return delivery.BuilderBuild{} // the agreed toolchain moved since try looked: nothing is written } u := delivery.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, tasks: tasks, toolchain: toolchain, digests: map[string]string{}, references: map[string]string{}}) } 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() 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 delivery.BuilderBuild{State: delivery.BuildQueued, Waits: ref, Step: "waits for the package " + ref + " to be published"} } return failed("%s", keyError(errs)) } var artifacts []delivery.BuiltArtifact 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 := delivery.BuiltArtifact{Name: t.Artifact, Kind: t.Kind, Digest: d} for _, l := range lanes { if l.module != t.Module { continue } r := l.references[t.Artifact] if r == "" && stored(t.Kind) { // 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", delivery.NotStored, t.Artifact, l.node) } if r != "" { built.Reference = r } } artifacts = append(artifacts, built) } 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") for lang, u := range prior.Toolchains { if _, ok := used[lang]; !ok && buildrun.LanguageOf[bm] == lang { used[lang] = u } } } step := "built " + strings.Join(steps, "; ") if len(used) == 0 { used = nil } done := delivery.BuilderBuild{State: delivery.BuildDone, By: b.Claimer.By, Manifest: m.Manifest, Step: step, Artifacts: artifacts, Toolchains: used} if _, err := delivery.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", delivery.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 = delivery.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 := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.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 (delivery.FailedUntilChanged), first the ones not built again. var keyPrefixes = []string{delivery.NotPinned, delivery.VersionTaken, delivery.RefusedKind, delivery.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 { 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 } // askLanguage is the build seat language of each build module (delivery.AskSubject). var askLanguage = map[string]string{buildrun.GoBuildModule: delivery.AskGo, buildrun.NodeBuildModule: delivery.AskNode, ImageBuildModule: delivery.AskImage} // 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 } func (b *Builder) notes(cl *Claim) *askNotes { n := &askNotes{b: b, cl: cl} if cl.Build.State == delivery.BuildBuilding && len(cl.Build.Asks) > 0 { n.resumed = maps.Clone(cl.Build.Asks) } 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 = delivery.BuildBuilding, n.b.Claimer.By, step, "", 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 delivery.Module, module string, asked *askNotes) { req := buildstep.FetchRequest{Repository: repository, Commit: m.Commit} if key, err := delivery.BuilderBuildKey(module, m.FP2); err == nil { b.mu.Lock() v, candidate := b.via[key] b.mu.Unlock() if candidate { req.Ref, req.Base = v.Ref, v.Base } } 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) } candidate := req.Base != "" language := askLanguage[l.module] kind := delivery.AskDelivery if candidate { kind = delivery.AskCheck } job := &delivery.BuildJob{Module: module, Path: m.Path, FP: m.FP2, Toolchain: l.toolchain, // Only a trunk key's build pushes an image or publishes a package; a candidate's never does (ADR 0487 clause 12, // ADR 0498 clauses 5 and 6). A bundle or an archive is pushed either way (clause 3). Publish: !candidate} for _, t := range l.tasks { a := delivery.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 } job.Artifacts = append(job.Artifacts, a) } if needsFetch(l.tasks) { job.Fetch = req.Args() } ask := delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: language, Asker: delivery.BuilderModule, At: time.Now(), Build: job} 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 { l.err = fmt.Errorf("the ask of %s could not be written to the key: %w", l.module, err) return } if err := b.Asks.Put(ctx, ask); err != nil { l.err = err return } } step, err := b.Asks.Wait(ctx, ask) if err != nil { l.err = fmt.Errorf("%s: %w", l.module, err) return } l.node = step.Node if step.State != delivery.StepPassed { l.err = fmt.Errorf("%s on %s: the ask %s %s: %s", l.module, step.Node, ask.ID, step.State, step.Reason) return } var answer delivery.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 } built := map[string]delivery.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 if a.Reference != "" && (stored(t.Kind) || !candidate) { 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 }