Files
mesh-catalog/modules/mesh-build-go/buildstep/generated.go
T

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
// (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-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
}
}