// Command deps makes a module's dependencies available to the catalogue's check, or says why it cannot // (novox/hq issue 385). // // A module's tests run only when its dependencies can be fetched. The build seat clones what a check // needs with the forge credential and forgets that credential before the check runs, so a module pinning // an SDK commit the seat's module cache does not hold cannot fetch it from the forge. The SDK is cloned // beside the check all the same (MESH_CHECK_BESIDE, mesh-controller's besideRefs): every requirement of the // mesh's own forge is taken from the clone of its repository there, at the commit or the tag its version // names, through a go.mod of its own (-modfile) — the module's own files are not changed, and no forge // credential is added. // // Usage: deps -beside -work . It prints the go.mod to build the module with, or // nothing when the module fetches what it needs; when it cannot, it prints why and exits 1, and the check // fails: a module whose tests could not run is a failure, never a note. package main import ( "archive/tar" "bytes" "encoding/json" "errors" "flag" "fmt" "io" "os" "os/exec" "path/filepath" "regexp" "strings" ) // forge is the prefix of the Go modules of the mesh's own forge: [/]. const forge = "git.novox.be/novox/" func main() { beside := flag.String("beside", "", "where the repositories beside the check are cloned (MESH_CHECK_BESIDE)") work := flag.String("work", os.TempDir(), "where the go.mod and the sources taken from beside are written") flag.Parse() if flag.NArg() != 1 { fmt.Println("usage: deps -beside -work ") os.Exit(2) } modfile, err := resolve(flag.Arg(0), *beside, *work) if err != nil { fmt.Println(err) os.Exit(1) } if modfile != "" { fmt.Println(modfile) } } // resolve is the go.mod the module at dir builds with: "" when its own fetches everything, else one // beside it in work that takes every requirement of the mesh's forge from its clone beside the check. func resolve(dir, beside, work string) (string, error) { out, err := goIn(dir, "mod", "download") if err == nil { return "", nil } fetching := lastLine(out) required, err := forgeRequirements(dir) if err != nil { return "", err } if len(required) == 0 { return "", fmt.Errorf("its dependencies cannot be fetched: %s", fetching) } if beside == "" { return "", fmt.Errorf("its dependencies cannot be fetched (%s), and nothing was cloned beside the check to "+ "take %s from", fetching, repositories(required)) } into, err := os.MkdirTemp(work, filepath.Base(dir)+"-") if err != nil { return "", err } modfile := filepath.Join(into, "go.mod") if err := copyFile(filepath.Join(dir, "go.mod"), modfile); err != nil { return "", err } if err := copyFile(filepath.Join(dir, "go.sum"), filepath.Join(into, "go.sum")); err != nil && !errors.Is(err, os.ErrNotExist) { return "", err } for _, r := range required { source, err := export(beside, r, into) if err != nil { return "", fmt.Errorf("its dependencies cannot be fetched (%s), and %s@%s cannot be taken from the "+ "clone beside the check: %w", fetching, r.Path, r.Version, err) } if out, err := goIn(dir, "mod", "edit", "-replace", r.Path+"@"+r.Version+"="+source, modfile); err != nil { return "", fmt.Errorf("%s@%s cannot be taken from the clone beside the check: %s", r.Path, r.Version, lastLine(out)) } } if out, err := goIn(dir, "mod", "download", "-modfile="+modfile); err != nil { return "", fmt.Errorf("its dependencies cannot be fetched even with %s taken from the clone beside the "+ "check: %s", repositories(required), lastLine(out)) } return modfile, nil } // requirement is one module the module requires. type requirement struct { Path string Version string } // forgeRequirements is every requirement of the module at dir that comes from the mesh's own forge. func forgeRequirements(dir string) ([]requirement, error) { out, err := goIn(dir, "mod", "edit", "-json") if err != nil { return nil, fmt.Errorf("its go.mod cannot be read: %s", lastLine(out)) } var mod struct{ Require []requirement } if err := json.Unmarshal(out, &mod); err != nil { return nil, fmt.Errorf("its go.mod cannot be read: %w", err) } var on []requirement for _, r := range mod.Require { if strings.HasPrefix(r.Path, forge) { on = append(on, r) } } return on, nil } // repositoryOf splits a module path of the forge into its repository and the directory in it. func repositoryOf(path string) (string, string) { repo, sub, _ := strings.Cut(strings.TrimPrefix(path, forge), "/") return repo, sub } func repositories(rs []requirement) string { var names []string for _, r := range rs { repo, _ := repositoryOf(r.Path) names = append(names, repo) } return strings.Join(names, ", ") } // pseudo is a pseudo-version's commit: v0.1.14-0.20261012082113-9747f3e1b1ac → 9747f3e1b1ac. var pseudo = regexp.MustCompile(`[-.]\d{14}-([0-9a-f]{12})$`) // refOf is the ref a version names in its repository: a pseudo-version's commit, else the tag a module in // a directory is released under (/). func refOf(sub, version string) string { version = strings.TrimSuffix(version, "+incompatible") if m := pseudo.FindStringSubmatch(version); m != nil { return m[1] } if sub != "" { return sub + "/" + version } return version } // export writes the module r names, at its version, from its repository's clone beside the check into // into, and answers its directory. func export(beside string, r requirement, into string) (string, error) { repo, sub := repositoryOf(r.Path) clone := filepath.Join(beside, repo) if _, err := os.Stat(filepath.Join(clone, ".git")); err != nil { return "", fmt.Errorf("no clone of %s is beside the check", repo) } ref := refOf(sub, r.Version) commit, err := exec.Command("git", "-C", clone, "rev-parse", "--verify", "--quiet", ref+"^{commit}").Output() if err != nil { return "", fmt.Errorf("the clone of %s holds no %s (a commit or tag not pushed to the forge?)", repo, ref) } at := strings.TrimSpace(string(commit)) args := []string{"-C", clone, "archive", "--format=tar", at} if sub != "" { args = append(args, "--", sub) } var stderr bytes.Buffer archive := exec.Command("git", args...) archive.Stderr = &stderr raw, err := archive.Output() if err != nil { return "", fmt.Errorf("git archive of %s at %s: %v %s", repo, ref, err, strings.TrimSpace(stderr.String())) } root := filepath.Join(into, repo+"@"+at[:12]) if err := untar(raw, root); err != nil { return "", err } source := filepath.Join(root, filepath.FromSlash(sub)) if _, err := os.Stat(filepath.Join(source, "go.mod")); err != nil { return "", fmt.Errorf("%s at %s has no go.mod in %q", repo, ref, sub) } return source, nil } // untar writes a tar's directories and regular files under root. func untar(raw []byte, root string) error { r := tar.NewReader(bytes.NewReader(raw)) for { h, err := r.Next() if err == io.EOF { return nil } if err != nil { return err } name := filepath.Clean(filepath.FromSlash(h.Name)) if name == "." || strings.HasPrefix(name, "..") || filepath.IsAbs(name) { continue } path := filepath.Join(root, name) switch h.Typeflag { case tar.TypeDir: if err := os.MkdirAll(path, 0o755); err != nil { return err } case tar.TypeReg: if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } body, err := io.ReadAll(r) if err != nil { return err } if err := os.WriteFile(path, body, 0o644); err != nil { return err } } } } func copyFile(from, to string) error { raw, err := os.ReadFile(from) if err != nil { return err } return os.WriteFile(to, raw, 0o644) } // goIn runs the go command in dir. func goIn(dir string, args ...string) ([]byte, error) { cmd := exec.Command("go", args...) cmd.Dir = dir var stderr bytes.Buffer cmd.Stderr = &stderr out, err := cmd.Output() if err != nil { return append(out, stderr.Bytes()...), err } return out, nil } // lastLine is the last line a command said, which is where go says what failed. func lastLine(out []byte) string { lines := strings.Split(strings.TrimSpace(string(out)), "\n") return strings.TrimSpace(lines[len(lines)-1]) }