package build import ( "context" "fmt" "sort" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-sdk/go/delivery" ) // How a candidate's fingerprint is built (novox/hq ADR 0484 clause 3, issue 1684): the indexer writes, for each module a // pull request touches, the fingerprint of the new form it has at the head merged onto the trunk (candidates.), and // the builder builds each, once, as a key of its own (builds..) that the checker's line reads. The // build is the trunk module's: the same build modules, the same nodes and capacity; what differs is where the source comes // from: the fetch step is given the pull request's head, the ref that carries it and the trunk commit to merge it onto, as // the checker's fetch is, and builds the merge's module directory. // via is how a build's fetch step reaches its source when it is a candidate's: the ref that carries the head and the trunk // commit the head is merged onto. type via struct{ Ref, Base string } // Candidate takes one value of the indexer's candidates bucket in (the key is the group) and builds the fingerprints it // carries. A value that is not a candidate is said and left. func (b *Builder) Candidate(ctx context.Context, key string, value []byte) { b.init() c, err := delivery.DecodeCandidate(value) if err != nil { b.logf("candidates.%s holds what is not a candidate, left: %v", key, err) return } b.mu.Lock() b.cands[key] = c b.mu.Unlock() b.considerCandidate(ctx, c) } // ForgetCandidate takes a candidate out (its key was deleted). func (b *Builder) ForgetCandidate(key string) { b.init() b.mu.Lock() delete(b.cands, key) b.mu.Unlock() } func (b *Builder) considerCandidate(ctx context.Context, c delivery.Candidate) { for _, p := range c.Pulls { names := make([]string, 0, len(p.Fps)) for name := range p.Fps { names = append(names, name) } sort.Strings(names) for _, name := range names { cm := p.Fps[name] if cm.FP2 == "" || cm.Manifest == "" { continue // no fingerprint of the new form for it (a reason in none2) or no manifest to build by } b.mu.Lock() trunk, known := b.known[name] b.mu.Unlock() if !known || trunk.Path == "" { b.logf("candidates.%s: the module %s has no record of the trunk yet, so its fingerprint %s is not built", c.Group, name, cm.FP2) continue } key, err := delivery.BuilderBuildKey(name, cm.FP2) if err != nil { b.logf("candidates.%s: %v", c.Group, err) continue } m := delivery.Module{Repository: p.Repository, Commit: p.Head.Head, FP2: cm.FP2, Manifest: cm.Manifest, Path: trunk.Path, Toolchains: b.agreedFor(ctx, cm.Manifest), Packages: cm.Packages} // the group's package heads (ADR 0573 §4) if len(cm.Paths) > 0 { // The named paths' objects the preview gave it: the fetch names the paths (runLane, ADR 0499 clause 4). m.Inputs2 = &delivery.KeptInputs2{Paths: cm.Paths} } // The preview is merged onto the trunk head the candidate was previewed onto: the head of the pull request's // repository in the gate's source revision c.Base (ADR 0484 §3). Never the module's value's commit: that is the // commit its fingerprint first appeared at, and may be many merges behind the trunk. base, err := b.trunkHead(ctx, c.Base, p.Repository) if err != nil { b.logf("candidates.%s: the module %s's fingerprint %s is not built: %v", c.Group, name, cm.FP2, err) continue } b.mu.Lock() if _, onTrunk := b.trunkFP[key]; !onTrunk { b.via[key] = via{Ref: fmt.Sprintf("refs/pull/%d/head", p.Number), Base: base} } b.mu.Unlock() b.consider(ctx, name, m) } } } // agreedFor are the toolchains the build nodes agree on now, for the languages of the manifest's plan: the toolchains the // candidate's fingerprint was computed with, as the indexer recomputes it whenever they change. A language with none agreed // is left out, and the build then waits for it as the trunk's does. func (b *Builder) agreedFor(ctx context.Context, manifest string) map[string]delivery.ToolchainUsed { plan, err, _ := b.planOf(ctx, manifest) if err != nil { return nil } out := map[string]delivery.ToolchainUsed{} for _, bm := range plan.Modules() { if !plan.NeedsToolchain(bm) { continue } language := buildrun.LanguageOf[bm] if tc, none := delivery.AgreedToolchain(b.Toolchains.Entries(language)); none == "" { out[language] = delivery.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe} } } if len(out) == 0 { return nil } return out } // trunkHead is the head of repository in the gate's source revision rev (Builder.Trunk), or why it cannot be said. func (b *Builder) trunkHead(ctx context.Context, rev uint64, repository string) (string, error) { if b.Trunk == nil { return "", fmt.Errorf("the gate's source cannot be read here, so the trunk head %s was previewed onto is not known", repository) } head, err := b.Trunk(ctx, rev, repository) if err != nil { return "", fmt.Errorf("the trunk head of %s at source revision %d cannot be read: %w", repository, rev, err) } if head == "" { return "", fmt.Errorf("source revision %d names no trunk head of %s", rev, repository) } return head, nil }