Builder.Candidate, ForgetCandidate, considerCandidate, trunkHead, TrunkMoved, the cands and via maps, the package-member wait that read via.Members (memberBuilds, memberEnded, MemberBoundEnded, the member bound key) and the committed build output modules/mesh-delivery-builder/mesh-delivery-builder are removed. Nothing fed Candidate in the process: the builder follows no candidate and learns a run's builds from the build step ask. Their tests go with them. Builds: design 47 G A-84 (the builder follows no candidate), §194, §348, §473 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
152 lines
5.6 KiB
Go
152 lines
5.6 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
)
|
|
|
|
// **A candidate's build is the runner's step ask** (design 47 G A-97, A-84, A-99): the runner's asking half asks the seat
|
|
// mesh-builds a `build` step naming each entry by fingerprint, and the builder, its one entry point for a candidate, reads each
|
|
// entry through the seat mesh-catalog's `entry` (the repository and the commit are the member's head), builds it as it builds any
|
|
// fingerprint (the existing claim, plan and lanes) and answers the step's end. A build is per fingerprint, so no pull request
|
|
// number is needed, and the builder follows no candidate.
|
|
|
|
// BuildStep builds the entries of one step ask and answers its end: passed when every entry's build is done, failed when one
|
|
// failed (said), could not run when an entry could not be read, nothing to run for an ask naming none. The ask's id and the node
|
|
// are the caller's to set.
|
|
func (b *Builder) BuildStep(ctx context.Context, run meshbuilds.StepRun) meshbuilds.StepEnded {
|
|
b.init()
|
|
started := b.now()
|
|
end := meshbuilds.StepEnded{V: 1, Pipeline: run.Pipeline, Step: meshbuilds.StepEndedStepBuild, Rerun: run.Rerun, By: run.By, Entries: run.Entries, Node: b.Node, Started: &started}
|
|
finish := func(outcome, said string) meshbuilds.StepEnded {
|
|
at := b.now()
|
|
end.Ended, end.Outcome, end.Said = &at, outcome, said
|
|
return end
|
|
}
|
|
if len(run.Entries) == 0 {
|
|
return finish(meshbuilds.StepEndedOutcomeNothingToRun, "the ask names no entry")
|
|
}
|
|
if b.StepEntry == nil {
|
|
return finish(meshbuilds.StepEndedOutcomeCouldNotRun, "the builder reads no entry of the seat mesh-catalog")
|
|
}
|
|
var failed []string
|
|
for _, e := range run.Entries {
|
|
outcome, said := b.buildEntry(ctx, e)
|
|
switch outcome {
|
|
case meshbuilds.StepEndedOutcomeCouldNotRun:
|
|
return finish(outcome, said)
|
|
case meshbuilds.StepEndedOutcomeFailed:
|
|
failed = append(failed, said)
|
|
}
|
|
}
|
|
if len(failed) > 0 {
|
|
return finish(meshbuilds.StepEndedOutcomeFailed, strings.Join(failed, "; "))
|
|
}
|
|
return finish(meshbuilds.StepEndedOutcomePassed, "")
|
|
}
|
|
|
|
// buildEntry builds one entry at its fingerprint and waits for its end.
|
|
func (b *Builder) buildEntry(ctx context.Context, e meshbuilds.BuildEntry) (outcome, said string) {
|
|
entry, err := b.StepEntry(ctx, e.Fingerprint)
|
|
if err != nil {
|
|
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("the entry %s at %s cannot be read: %v", e.Module, e.Fingerprint, err)
|
|
}
|
|
if entry.Module != e.Module || (e.Commit != "" && entry.Commit != e.Commit) {
|
|
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("the entry of %s at %s is of %s at %s", e.Module, e.Fingerprint, entry.Module, entry.Commit)
|
|
}
|
|
key, err := builds.BuilderBuildKey(entry.Module, entry.Fp)
|
|
if err != nil {
|
|
return meshbuilds.StepEndedOutcomeCouldNotRun, err.Error()
|
|
}
|
|
ended := b.awaitEnd(key)
|
|
defer b.dropEnd(key, ended)
|
|
if done, failedWords, found := b.keyEnd(ctx, key); found {
|
|
if done {
|
|
return meshbuilds.StepEndedOutcomePassed, ""
|
|
}
|
|
return meshbuilds.StepEndedOutcomeFailed, failedWords
|
|
}
|
|
m := meshcatalog.ModulesByModuleValue{Repository: entry.Repository, Commit: entry.Commit, Fp2: entry.Fp, Manifest: entry.ManifestDigest, Path: entry.Path,
|
|
Toolchains: CatalogToolchains(b.agreedFor(ctx, entry.ManifestDigest)), Packages: entry.Packages}
|
|
b.consider(ctx, entry.Module, m)
|
|
select {
|
|
case log := <-ended:
|
|
if log.Outcome == builds.BuildDone {
|
|
return meshbuilds.StepEndedOutcomePassed, ""
|
|
}
|
|
return meshbuilds.StepEndedOutcomeFailed, fmt.Sprintf("%s at %s: %s %s", log.Module, log.FP, log.Outcome, log.Error)
|
|
case <-ctx.Done():
|
|
return meshbuilds.StepEndedOutcomeCouldNotRun, fmt.Sprintf("%s at %s: the builder stopped before its build ended", entry.Module, entry.Fp)
|
|
}
|
|
}
|
|
|
|
// keyEnd reads the build key: whether it is done, and its error when it failed for good; found only for a key that has ended.
|
|
func (b *Builder) keyEnd(ctx context.Context, key string) (done bool, failed string, found bool) {
|
|
raw, _, ok, err := b.Claimer.Store.Get(ctx, key)
|
|
if err != nil || !ok {
|
|
return false, "", false
|
|
}
|
|
v, err := builds.DecodeBuilderBuild(raw)
|
|
if err != nil {
|
|
return false, "", false
|
|
}
|
|
switch {
|
|
case v.State == builds.BuildDone && !Unpublished(v, b.Seats):
|
|
return true, "", true
|
|
case v.State == builds.BuildFailed && builds.FailedUntilChanged(v.Error):
|
|
return false, key + ": " + v.Error, true
|
|
}
|
|
return false, "", false
|
|
}
|
|
|
|
// awaitEnd registers a wait for the end of the build of key, heard at its log entry (logBuild).
|
|
func (b *Builder) awaitEnd(key string) chan builds.BuildLogEntry {
|
|
ch := make(chan builds.BuildLogEntry, 1)
|
|
b.mu.Lock()
|
|
if b.stepWaits == nil {
|
|
b.stepWaits = map[string][]chan builds.BuildLogEntry{}
|
|
}
|
|
b.stepWaits[key] = append(b.stepWaits[key], ch)
|
|
b.mu.Unlock()
|
|
return ch
|
|
}
|
|
|
|
func (b *Builder) dropEnd(key string, ch chan builds.BuildLogEntry) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
for i, c := range b.stepWaits[key] {
|
|
if c == ch {
|
|
b.stepWaits[key] = append(b.stepWaits[key][:i], b.stepWaits[key][i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
if len(b.stepWaits[key]) == 0 {
|
|
delete(b.stepWaits, key)
|
|
}
|
|
}
|
|
|
|
// stepEnded tells the step asks waiting for a build that its end was logged.
|
|
func (b *Builder) stepEnded(e builds.BuildLogEntry) {
|
|
if e.Event != builds.BuildEndedEvent {
|
|
return
|
|
}
|
|
key, err := builds.BuilderBuildKey(e.Module, e.FP)
|
|
if err != nil {
|
|
return
|
|
}
|
|
b.mu.Lock()
|
|
chans := append([]chan builds.BuildLogEntry(nil), b.stepWaits[key]...)
|
|
b.mu.Unlock()
|
|
for _, ch := range chans {
|
|
select {
|
|
case ch <- e:
|
|
default:
|
|
}
|
|
}
|
|
}
|