Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/plan.go
T
jschoubben 4332246f79 U2a: the builder plans a Go package on the Go build seat instead of refusing it
A module whose only artifact is a Go package (mesh-wireguard's go-overlay) never had a
done build: PlanOf refused every package of language go. The Go build module already packs
and publishes a Go package; the builder now asks it.

Builds: design 47 G §370; design 51 *Publishing*

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 16:13:13 +01:00

144 lines
6.3 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 and packages, archives, images and upstream copies", 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, a Go package by the Go one (design 47 G §370) 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 == "go" || a.Language == "typescript"):
// A Go package is packed by the Go build module, tagged with the version its build reserved (design 47 G §370).
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
}