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 (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 `/seats/` and its npm package `-` (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 } }