The builder finds a build module as the indexer's seed does: the build seat whose acting rule `build` has an `on` matching the artifact's language or kind:image, and that seat's holder, read from the seat list's definitions and the claims of the catalog half's manifests. Task.Module, every lane key, the unpublished modules, the toolchain sources, the ends watched, the sayer and builder_stuck (through `<seat>.steps`) derive from it; the floor's sources are the holders of the seats git and mesh-catalog. buildrun.GoBuildModule, NodeBuildModule, LanguageOf, buildstep.BuildModuleOf use, build.ImageBuildModule and StuckBuildModules are gone from the builder (the checker keeps its own names in two small files until its part reads them from the list). Builds: design 47 G §7, §428, §704, §204, A-82 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
143 lines
6.1 KiB
Go
143 lines
6.1 KiB
Go
// Package build is the builder's logic (novox/hq ADR 0453 clause 4, ADR 0478), apart from the bus: what a manifest asks
|
|
// to be built and where, the claim of a build key, the choice of build nodes, the build modules' long steps and the run of
|
|
// one build. The bus is reached through the small interfaces here, so each part is tested with fakes.
|
|
package build
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"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-sdk/go/catalogue"
|
|
)
|
|
|
|
// NoImageBuilderPrefix opens the error of a build key failed because no node runs the image build module (ADR 0487 clause 5): such a
|
|
// key is not built again as a new revision, only when a node reports the module (ClaimOptions.ImageBuilder) or the fingerprint
|
|
// changes.
|
|
const NoImageBuilderPrefix = "no image builder:"
|
|
|
|
// Task is one artifact to build: its name and kind in the manifest, the build module that builds it and the language
|
|
// of the toolchain it is built with.
|
|
type Task struct {
|
|
Artifact string
|
|
Kind string
|
|
Module string
|
|
Language string
|
|
// From is, for an upstream artifact, the reference its manifest pins, which the image build module copies (ADR 0498
|
|
// clause 4).
|
|
From string
|
|
}
|
|
|
|
// Plan is what a manifest asks the builder to build, by artifact name.
|
|
type Plan struct {
|
|
Tasks []Task
|
|
// Series is the manifest's MAJOR.MINOR (design 47 G §368): the series its build reserves a version in (§369).
|
|
Series string
|
|
}
|
|
|
|
// Modules are the build modules the plan calls, sorted.
|
|
func (p Plan) Modules() []string {
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
for _, t := range p.Tasks {
|
|
if !seen[t.Module] {
|
|
seen[t.Module] = true
|
|
out = append(out, t.Module)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// NeedsToolchain is whether any task the build module builds is built with a toolchain: a bundle is, an archive is not
|
|
// (the indexer gives an archive no language, so a fingerprint names no toolchain for it; ADR 0478 clause 1).
|
|
func (p Plan) NeedsToolchain(module string) bool {
|
|
for _, t := range p.Tasks {
|
|
if t.Module == module && t.Kind != catalogue.ArtifactArchive && t.Kind != catalogue.ArtifactImage && t.Kind != catalogue.ArtifactUpstream {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Refused is the error of a plan the builder does not build: an artifact of a kind (or of a language) no build module
|
|
// builds. It opens builds.RefusedKind, so its key is not built again until the fingerprint changes.
|
|
type Refused struct{ Artifact, What string }
|
|
|
|
func (r *Refused) Error() string {
|
|
return fmt.Sprintf("%s%s (artifact %s): no build module builds it; the builder builds Go and TypeScript bundles, archives, images, upstream copies and TypeScript packages", builds.RefusedKind, r.What, r.Artifact)
|
|
}
|
|
|
|
// NotPinned is the error of a plan with an upstream artifact whose reference names no digest (ADR 0498 clause 4): it opens
|
|
// builds.NotPinned, so its key is not built again until the fingerprint changes.
|
|
type NotPinned struct{ Artifact, From string }
|
|
|
|
func (n *NotPinned) Error() string {
|
|
return fmt.Sprintf("%s%s: %q names no digest (@sha256:), so what would be copied is whatever its tag means today", builds.NotPinned, n.Artifact, n.From)
|
|
}
|
|
|
|
// NoBuildModule is the error of an artifact whose build seat or its holder the seat list and the catalog half do not name (yet):
|
|
// the plan is not kept, so it is made again when they are read (design 47 G A-82).
|
|
type NoBuildModule struct {
|
|
Artifact string
|
|
Err error
|
|
}
|
|
|
|
func (n *NoBuildModule) Error() string { return fmt.Sprintf("artifact %s: %v", n.Artifact, n.Err) }
|
|
func (n *NoBuildModule) Unwrap() error { return n.Err }
|
|
|
|
// PlanOf is the plan of a manifest (ADR 0478 clauses 1 and 2, ADR 0498 clauses 4 and 5): a Go bundle and an archive are
|
|
// built by the Go build module, an image by the image build module (ADR 0487), which also copies an upstream image pinned
|
|
// by its digest, and a TypeScript bundle or package by the Node one, each in the order of the artifacts' names. A manifest
|
|
// with an upstream artifact pinned by no digest, or a bundle or a package of another language, or a kind no build module
|
|
// builds, is refused as a whole, naming the first such artifact by name. A manifest with no artifact has an empty plan.
|
|
func PlanOf(m catalogue.Manifest, seats *buildrun.BuildSeats) (Plan, error) {
|
|
if m.Build == nil {
|
|
return Plan{}, nil
|
|
}
|
|
artifacts := append([]catalogue.Artifact(nil), m.Build.Artifacts...)
|
|
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
|
|
p := Plan{Series: m.Version}
|
|
task := func(a catalogue.Artifact, language, kind string) error {
|
|
bm, err := seats.For(language, kind)
|
|
if err != nil {
|
|
return &NoBuildModule{Artifact: a.Name, Err: err}
|
|
}
|
|
t := Task{Artifact: a.Name, Kind: a.Kind, Module: bm.Module, Language: bm.Toolchain, From: a.From}
|
|
if a.Kind != catalogue.ArtifactUpstream {
|
|
t.From = ""
|
|
}
|
|
p.Tasks = append(p.Tasks, t)
|
|
return nil
|
|
}
|
|
for _, a := range artifacts {
|
|
var err error
|
|
switch {
|
|
case a.Kind == catalogue.ArtifactArchive:
|
|
err = task(a, "go", a.Kind) // an archive is packed by the Go build seat's holder, which the indexer gives no language
|
|
case a.Kind == catalogue.ArtifactBundle && (a.Language == "go" || a.Language == "typescript"):
|
|
err = task(a, a.Language, a.Kind)
|
|
case a.Kind == catalogue.ArtifactImage:
|
|
err = task(a, "", "image")
|
|
case a.Kind == catalogue.ArtifactUpstream && !strings.Contains(a.From, "@sha256:"):
|
|
return Plan{}, &NotPinned{Artifact: a.Name, From: a.From}
|
|
case a.Kind == catalogue.ArtifactUpstream:
|
|
err = task(a, "", "image")
|
|
case a.Kind == catalogue.ArtifactPackage && a.Language == "typescript":
|
|
err = task(a, a.Language, a.Kind)
|
|
case a.Kind == catalogue.ArtifactBundle:
|
|
return Plan{}, &Refused{Artifact: a.Name, What: fmt.Sprintf("bundle of language %q", a.Language)}
|
|
case a.Kind == catalogue.ArtifactPackage:
|
|
return Plan{}, &Refused{Artifact: a.Name, What: fmt.Sprintf("package of language %q", a.Language)}
|
|
default:
|
|
return Plan{}, &Refused{Artifact: a.Name, What: a.Kind}
|
|
}
|
|
if err != nil {
|
|
return Plan{}, err
|
|
}
|
|
}
|
|
return p, nil
|
|
}
|