The indexer needs a module's manifest, lock hashes and toolchain to compute its new-form fingerprint, and has none of them (ADR 0465 clauses 1, 2, 5): the forge messages now carry the first two, a module keeps its inputs so it can be computed again when a toolchain is reported, and the build modules' toolchain keys have a layout and an agreement rule.
128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ToolchainsState is the name of the state a build module keeps its toolchains in; its bucket is
|
|
// `<module>_toolchains`, built as the SDK names a module's state, by ToolchainsBucket (novox/hq ADR 0465 clause 2).
|
|
const ToolchainsState = "toolchains"
|
|
|
|
// ToolchainsBucket is the bucket of a build module's toolchains.
|
|
func ToolchainsBucket(module string) string { return module + "_" + ToolchainsState }
|
|
|
|
// ToolchainNoReport is the reason AgreedToolchain gives when no node has reported a language's toolchain.
|
|
const ToolchainNoReport = "toolchain not yet reported"
|
|
|
|
func checkToolchainLanguage(language string) error {
|
|
switch language {
|
|
case "go", "typescript":
|
|
return nil
|
|
}
|
|
return fmt.Errorf("delivery: the toolchain language %q is not go or typescript", language)
|
|
}
|
|
|
|
// ToolchainKey is the key of a node's toolchain of one language: `<node>.<language>`. It refuses a node that is not a
|
|
// name and a language other than go or typescript.
|
|
func ToolchainKey(node, language string) (string, error) {
|
|
if err := checkName("node", node); err != nil {
|
|
return "", err
|
|
}
|
|
if err := checkToolchainLanguage(language); err != nil {
|
|
return "", err
|
|
}
|
|
return node + "." + language, nil
|
|
}
|
|
|
|
// ParseToolchainKey is the inverse of ToolchainKey.
|
|
func ParseToolchainKey(key string) (node, language string, err error) {
|
|
node, language, ok := strings.Cut(key, ".")
|
|
if !ok {
|
|
return "", "", fmt.Errorf("delivery: %q is not a toolchain key, <node>.<language>", key)
|
|
}
|
|
if _, err := ToolchainKey(node, language); err != nil {
|
|
return "", "", fmt.Errorf("delivery: %q is not a toolchain key: %w", key, err)
|
|
}
|
|
return node, language, nil
|
|
}
|
|
|
|
// Toolchain is the value of a toolchains key: the version of the language's toolchain installed on the node and the
|
|
// build module's recipe version, and when the module saw them.
|
|
type Toolchain struct {
|
|
V int `json:"v"`
|
|
Language string `json:"language"`
|
|
Version string `json:"version"`
|
|
Recipe string `json:"recipe"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
func (t Toolchain) check() error {
|
|
if err := checkToolchainLanguage(t.Language); err != nil {
|
|
return err
|
|
}
|
|
if t.Version == "" || t.Recipe == "" {
|
|
return fmt.Errorf("delivery: a %s toolchain needs its version and its recipe", t.Language)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeToolchain is t as JSON at Version; it refuses a language other than go or typescript and an empty version or
|
|
// recipe.
|
|
func EncodeToolchain(t Toolchain) ([]byte, error) {
|
|
if err := t.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
t.V = Version
|
|
return json.Marshal(t)
|
|
}
|
|
|
|
// DecodeToolchain reads a toolchains value; it refuses an unknown version (ErrUnknownVersion) and what EncodeToolchain
|
|
// refuses.
|
|
func DecodeToolchain(data []byte) (Toolchain, error) {
|
|
var t Toolchain
|
|
if err := decode(data, &t); err != nil {
|
|
return Toolchain{}, err
|
|
}
|
|
if err := t.check(); err != nil {
|
|
return Toolchain{}, err
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// NodeToolchain is one node's entry of a language, as read from the toolchains bucket.
|
|
type NodeToolchain struct {
|
|
Node string
|
|
Toolchain
|
|
}
|
|
|
|
// AgreedToolchain is the one Toolchain2 every node's entry of a language agrees on, or none and why: "toolchain not
|
|
// yet reported" with no entries, and "builders disagree" naming each differing version and recipe with the nodes
|
|
// reporting it when two differ (novox/hq ADR 0405 clause 2). The answer does not depend on the entries' order. The
|
|
// entries must all be of one language.
|
|
func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) {
|
|
if len(entries) == 0 {
|
|
return Toolchain2{}, ToolchainNoReport
|
|
}
|
|
groups := map[Toolchain2][]string{}
|
|
for _, e := range entries {
|
|
k := Toolchain2{Language: e.Language, Version: e.Version, Recipe: e.Recipe}
|
|
groups[k] = append(groups[k], e.Node)
|
|
}
|
|
if len(groups) == 1 {
|
|
for k := range groups {
|
|
return k, ""
|
|
}
|
|
}
|
|
parts := make([]string, 0, len(groups))
|
|
for k, nodes := range groups {
|
|
sort.Strings(nodes)
|
|
parts = append(parts, fmt.Sprintf("%s recipe %s on %s", k.Version, k.Recipe, strings.Join(nodes, ", ")))
|
|
}
|
|
sort.Strings(parts)
|
|
return Toolchain2{}, "builders disagree: " + strings.Join(parts, "; ")
|
|
}
|