The part build-step lacks the check job's toolchain and types the check answer's fetch as the fetch arguments; both are kept in askrun_gaps.go until the part has them. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
165 lines
6.1 KiB
Go
165 lines
6.1 KiB
Go
package buildstep
|
|
|
|
// **Regenerate and compare** (CAT-BG5, CAT-BN2; novox/hq ADR 0608 §5, design 26 *Contracts modules*). A module whose
|
|
// package artifact names `generated-from` holds contracts only: every file under its generated roots is written by the
|
|
// seat generator (package seatgen beside this file, command cmd/seatgen) from the definitions in that directory. A build module regenerates them with the generator
|
|
// it was built with (its version is this package's requirement, so a new generator is a new build module and a new
|
|
// fingerprint of what it builds) and names each file out of step: one that differs, one missing, one under a generated
|
|
// root the generator did not write. The test step fails on any; a trunk build's ask answers them in its out-of-step
|
|
// (sdkstep.BuildAnswer.OutOfStep), so the builder's build end raises generated-types-out-of-step even where no test
|
|
// step runs. Each build module compares its own roots: mesh-build-go `go`, mesh-build-node `ts`.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/seatgen"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// GeneratedRootGo and GeneratedRootTS are the generated roots of a contracts module, one per build module.
|
|
const (
|
|
GeneratedRootGo = "go"
|
|
GeneratedRootTS = "ts"
|
|
)
|
|
|
|
var goModuleLine = regexp.MustCompile(`(?m)^module\s+(\S+)`)
|
|
|
|
// OutOfStep is every file under root, a path from the module's directory, that the generator writes otherwise than the
|
|
// module holds it, for each `generated-from` its package artifacts name, sorted and each once; none for a module naming
|
|
// none. The generated trees' names are read from the seats already generated in <from> (generatedNames).
|
|
func OutOfStep(moduleDir, root string) ([]string, error) {
|
|
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, nil // no manifest here: nothing names generated-from
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the module's manifest cannot be read for its generated files: %w", err)
|
|
}
|
|
var m catalogue.Manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return nil, fmt.Errorf("the module's manifest does not parse: %w", err)
|
|
}
|
|
froms := map[string]bool{}
|
|
if m.Build != nil {
|
|
for _, a := range m.Build.Artifacts {
|
|
if a.GeneratedFrom != "" {
|
|
froms[filepath.Clean(a.GeneratedFrom)] = true
|
|
}
|
|
}
|
|
}
|
|
seen := map[string]bool{}
|
|
for from := range froms {
|
|
if filepath.IsAbs(from) || from == ".." || strings.HasPrefix(from, "../") {
|
|
return nil, fmt.Errorf("generated-from %q is not inside the module", from)
|
|
}
|
|
dir := filepath.Join(moduleDir, from)
|
|
goModule, tsPackage, err := generatedNames(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g, err := seatgen.Load(dir, goModule, tsPackage)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the seat definitions in %s do not load: %w", from, err)
|
|
}
|
|
files, err := seatgen.Generate(g)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the seat definitions in %s do not generate: %w", from, err)
|
|
}
|
|
out, err := seatgen.Compare(dir, files)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, line := range out {
|
|
p, _, _ := strings.Cut(line, ": ")
|
|
if p == root || strings.HasPrefix(p, root+"/") {
|
|
seen[filepath.ToSlash(filepath.Join(from, p))] = true
|
|
}
|
|
}
|
|
}
|
|
paths := make([]string, 0, len(seen))
|
|
for p := range seen {
|
|
paths = append(paths, p)
|
|
}
|
|
sort.Strings(paths)
|
|
return paths, nil
|
|
}
|
|
|
|
// generatedNames reads the names the generated trees are rooted at from the seats already generated in dir: a seat's Go
|
|
// module is `<root>/seats/<seat without hyphens>` and its npm package `<root>-<seat>` (a contracts module has no root
|
|
// package of either kind, ADR 0635 §1). A module with no seat generated yet gives none, and the generator says so only
|
|
// if a seat is defined whole.
|
|
func generatedNames(dir string) (goModule, tsPackage string, err error) {
|
|
names, _ := filepath.Glob(filepath.Join(dir, "seats", "*", "seat.json"))
|
|
for _, p := range names {
|
|
seat := filepath.Base(filepath.Dir(p))
|
|
pkg := strings.ReplaceAll(seat, "-", "")
|
|
if goModule == "" {
|
|
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootGo, "seats", pkg, "go.mod")); err == nil {
|
|
if m := goModuleLine.FindSubmatch(raw); m != nil {
|
|
goModule = strings.TrimSuffix(string(m[1]), "/seats/"+pkg)
|
|
}
|
|
}
|
|
}
|
|
if tsPackage == "" {
|
|
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootTS, "seats", pkg, "package.json")); err == nil {
|
|
var p struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if json.Unmarshal(raw, &p) == nil {
|
|
tsPackage = strings.TrimSuffix(p.Name, "-"+seat)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return goModule, tsPackage, nil
|
|
}
|
|
|
|
// ModuleDirs is every directory in tree that holds a module.json, in any folder (ADR 0634 §8: a module is found by its
|
|
// manifest, whether it lies at the root, under modules/ or in a folder of another module), the tree's own first, then
|
|
// sorted. Hidden directories, node_modules and testdata are not entered.
|
|
func ModuleDirs(tree string) ([]string, error) {
|
|
var out []string
|
|
err := filepath.WalkDir(tree, func(p string, e os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if e.IsDir() {
|
|
if n := e.Name(); p != tree && (strings.HasPrefix(n, ".") || n == "node_modules" || n == "testdata") {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if e.Name() == "module.json" {
|
|
out = append(out, filepath.Dir(p))
|
|
}
|
|
return nil
|
|
})
|
|
sort.Strings(out)
|
|
return out, err
|
|
}
|
|
|
|
// CompareStep is AskSteps.Compare for a build module comparing root: it reads {"workspace", "module"}, the workspace's
|
|
// tree where tree says it is (the build module's own layout), and answers {"out-of-step": [...]}.
|
|
func CompareStep(root string, tree func(workspace string) string) StepFunc {
|
|
return func(_ context.Context, args map[string]any) (any, error) {
|
|
ws, _ := args["workspace"].(string)
|
|
mod, _ := args["module"].(string)
|
|
if ws == "" {
|
|
return nil, Refuse("the compare names no workspace")
|
|
}
|
|
paths, err := OutOfStep(filepath.Join(tree(ws), filepath.FromSlash(mod)), root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"out-of-step": paths}, nil
|
|
}
|
|
}
|