// 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" "git.novox.be/novox/mesh-sdk/go/catalogue" ) // The build modules a task is given to, and the language whose toolchain each reports (the indexer's ToolchainModules). const ( GoBuildModule = "mesh-build-go" NodeBuildModule = "mesh-build-node" ) // RefusedPrefix opens the error of a build key that is failed for a kind no builder builds yet (ADR 0478 clause 2): such // a key is not built again as a new revision. const RefusedPrefix = "refused kind:" // 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 } // Plan is what a manifest asks the builder to build, by artifact name. type Plan struct{ Tasks []Task } // 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 { 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) it has no build // module for. type Refused struct{ Artifact, What string } func (r *Refused) Error() string { return fmt.Sprintf("%s %s (artifact %s): the builder builds Go and TypeScript bundles and archives; build-agent builds it", RefusedPrefix, r.What, r.Artifact) } // PlanOf is the plan of a manifest (ADR 0478 clauses 1 and 2): a Go bundle and an archive are built by the Go build // module, a TypeScript bundle by the Node one, each in the order of the artifacts' names. A manifest with an image, // upstream or package artifact, or a bundle of another language, 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) (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 }) var p Plan for _, a := range artifacts { switch { case a.Kind == catalogue.ArtifactArchive: p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: GoBuildModule, Language: "go"}) case a.Kind == catalogue.ArtifactBundle && a.Language == "go": p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: GoBuildModule, Language: "go"}) case a.Kind == catalogue.ArtifactBundle && a.Language == "typescript": p.Tasks = append(p.Tasks, Task{Artifact: a.Name, Kind: a.Kind, Module: NodeBuildModule, Language: "typescript"}) case a.Kind == catalogue.ArtifactBundle: return Plan{}, &Refused{Artifact: a.Name, What: fmt.Sprintf("bundle of language %q", a.Language)} default: return Plan{}, &Refused{Artifact: a.Name, What: a.Kind} } } return p, nil }