package buildstep import ( "context" "crypto/sha256" "encoding/hex" "fmt" "os" "path" "path/filepath" "strings" sdkstep "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/forgekey" ) var lockFiles = []string{"go.sum", "package-lock.json"} // ModuleTrees are the trees, in commit, of every directory of the workspace dir that holds a module.json, by directory // (the repository's root is "."): the directory's git tree, the digests of its manifest and lock files, and, for a module // whose every artifact names its paths, those paths' git objects (novox/hq ADR 0499 clause 2). It is raw: the indexer alone // computes a module's named tree and reads its mesh packages from it (novox/hq ADR 0593 §2). The workspace is at commit, so // the manifests and the lock files are read from it. env is the environment git runs with (nil for none). Both build // modules' fetch steps answer it for a merge. func ModuleTrees(ctx context.Context, run forgekey.Runner, env []string, dir, commit string) (map[string]sdkstep.ModuleTree, error) { r := run(ctx, env, "git", "-C", dir, "ls-files", "--", "module.json", "*/module.json") if r.Status != 0 || r.Err != "" { return nil, fmt.Errorf("git ls-files: %s", forgekey.Said(r.Err+"\n"+r.Stderr)) } trees := map[string]sdkstep.ModuleTree{} for _, l := range strings.Split(r.Stdout, "\n") { if l = strings.TrimSpace(l); l == "" { continue } mdir := filepath.ToSlash(filepath.Dir(l)) spec := commit + ":" + mdir if mdir == "." { spec = commit + "^{tree}" } t := run(ctx, env, "git", "-C", dir, "rev-parse", spec) if t.Status != 0 || t.Err != "" { return nil, fmt.Errorf("git rev-parse %s: %s", spec, forgekey.Said(t.Err+"\n"+t.Stderr)) } mt := sdkstep.ModuleTree{Tree: strings.TrimSpace(t.Stdout)} raw, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(l))) if err != nil { return nil, err } mt.Manifest = sha256Hex(raw) if err := narrow(ctx, run, env, dir, commit, mdir, raw, &mt); err != nil { return nil, err } for _, lock := range lockFiles { if raw, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(mdir), lock)); err == nil { if mt.Locks == nil { mt.Locks = map[string]string{} } mt.Locks[lock] = sha256Hex(raw) } } trees[mdir] = mt } return trees, nil } // narrow sets mt's Paths when the module's manifest names paths on every artifact (novox/hq ADR 0499 // clause 2): one `git ls-tree -z` gives each named path's object at commit. The merge preview decodes the manifest here // (catalogue.Decode: a build step judges no manifest, novox/hq ADR 0578 §3), and no other build step reads paths from one // (ADR 0495 clause 1). A manifest that does not decode is not narrowed: the indexer gives it none. A named path absent at // commit fails, naming the module and the path. func narrow(ctx context.Context, run forgekey.Runner, env []string, dir, commit, mdir string, raw []byte, mt *sdkstep.ModuleTree) error { m, err := catalogue.Decode(raw) if err != nil { return nil } paths, every := m.NamedPaths() if !every { return nil } full := make([]string, len(paths)) for i, p := range paths { full[i] = path.Join(mdir, p) } r := run(ctx, env, "git", append([]string{"-C", dir, "ls-tree", "-z", commit, "--"}, full...)...) if r.Status != 0 || r.Err != "" { return fmt.Errorf("git ls-tree %s of the module %s: %s", commit, mdir, forgekey.Said(r.Err+"\n"+r.Stderr)) } objects := map[string]string{} for _, l := range strings.Split(r.Stdout, "\x00") { meta, name, ok := strings.Cut(l, "\t") fields := strings.Fields(meta) if !ok || len(fields) != 3 { continue } objects[name] = fields[2] } mt.Paths = map[string]string{} for i, p := range paths { object := objects[full[i]] if object == "" { return fmt.Errorf("the module %s names the path %q, which is not in %s", mdir, p, commit) } mt.Paths[p] = object } return nil } func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }