Its version is the build module's own, so a new generator is a new build of mesh-build-go and mesh-build-node and a new fingerprint of what they compare; no repository of its own. Moved from seatgen c1fdd7c unchanged but for the import path. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
133 lines
5.0 KiB
Go
133 lines
5.0 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
|
|
// (delivery.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-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep/seatgen"
|
|
)
|
|
|
|
// 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 Go tree's module path is read from <from>/go/go.mod and the TypeScript package's name from
|
|
// <from>/ts/package.json, as the contracts module states them.
|
|
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
|
|
}
|
|
|
|
func generatedNames(dir string) (goModule, tsPackage string, err error) {
|
|
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootGo, "go.mod")); err == nil {
|
|
if m := goModuleLine.FindSubmatch(raw); m != nil {
|
|
goModule = string(m[1])
|
|
}
|
|
}
|
|
if raw, err := os.ReadFile(filepath.Join(dir, GeneratedRootTS, "package.json")); err == nil {
|
|
var p struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if json.Unmarshal(raw, &p) == nil {
|
|
tsPackage = p.Name
|
|
}
|
|
}
|
|
if goModule == "" || tsPackage == "" {
|
|
return "", "", errors.New("a contracts module names its generated Go module in go/go.mod and its TypeScript package in ts/package.json; " +
|
|
filepath.Base(dir) + " names " + map[bool]string{true: "not the TypeScript package", false: "not the Go module"}[goModule != ""])
|
|
}
|
|
return goModule, tsPackage, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|