package buildstep import ( "context" "crypto/sha256" "encoding/hex" "fmt" "os" "path" "path/filepath" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/forgekey" ) // ModuleTree is what a merge holds of one module directory: its tree id, the sha256 of its module.json and the sha256 // of each lock file it has (go.sum, package-lock.json), from which the checker and the indexer take the module's inputs // (novox/hq ADR 0484 clause 3). type ModuleTree struct { Tree string `json:"tree"` Manifest string `json:"manifest"` Locks map[string]string `json:"locks,omitempty"` // Paths are, of a module whose every artifact names its paths, each named path's git object id at the commit; Tree // is then their delivery.NamedTree with Manifest (novox/hq ADR 0499 clause 2). Absent otherwise. Paths map[string]string `json:"paths,omitempty"` } 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 "."); for a module whose every artifact names its paths, the named tree of those paths and // their objects (novox/hq ADR 0499 clause 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]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", firstLine(r.Err+" "+r.Stderr)) } trees := map[string]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, firstLine(t.Err+" "+t.Stderr)) } mt := 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 and its named Tree 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 reads the manifest here, as // the indexer does, and no other build step reads paths from one (ADR 0495 clause 1). A manifest that does not parse // 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 *ModuleTree) error { m, err := catalogue.ParseManifest(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, firstLine(r.Err+" "+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 } named, err := delivery.NamedTree(mt.Manifest, mt.Paths) if err != nil { return fmt.Errorf("the module %s: %w", mdir, err) } mt.Tree = named return nil } func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }