|
|
|
@@ -0,0 +1,176 @@
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// zeroVersion is the version a mesh package is required at in every go.mod of the mesh (ADR 0573 §2, §4).
|
|
|
|
|
const zeroVersion = "v0.0.0-00010101000000-000000000000"
|
|
|
|
|
|
|
|
|
|
// workspaceOf is the go.work of the module in first and of every module it requires at the zero version, transitively,
|
|
|
|
|
// each found among the modules in candidates by its module path: each used, and each also replaced at the zero version by
|
|
|
|
|
// its own directory. Go (from 1.27) looks the zero version of a required module up even where the module is a member of
|
|
|
|
|
// the workspace, once a build loads the module graph (any import outside the workspace); a versioned replace is the one
|
|
|
|
|
// form it takes without a lookup, and an unversioned one is refused for a member of the workspace (issue 1921: the
|
|
|
|
|
// composer's build fetched mesh-contracts from the network). Only what first needs is laid (issue 1921, the group check
|
|
|
|
|
// of 8a80e8e86858): a module laid beside that requires one no tree holds (mesh-grants requiring a catalogue module, the
|
|
|
|
|
// catalogue being the repository under check) made Go fetch that module's zero version and fail, though the composer
|
|
|
|
|
// needs neither. A requirement at the zero version no candidate holds is an error naming it and who requires it.
|
|
|
|
|
func workspaceOf(first string, candidates []string) (string, error) {
|
|
|
|
|
paths := map[string]string{}
|
|
|
|
|
for _, d := range candidates {
|
|
|
|
|
path, _, _, err := goModOf(d)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if _, seen := paths[path]; !seen { // the same module laid twice: the first is kept
|
|
|
|
|
paths[path] = d
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var use, replace strings.Builder
|
|
|
|
|
laid := map[string]bool{}
|
|
|
|
|
highest := "1.26.0" // the go directive is the highest among the laid modules, never lower than this
|
|
|
|
|
queue := []string{first}
|
|
|
|
|
for len(queue) > 0 {
|
|
|
|
|
d := queue[0]
|
|
|
|
|
queue = queue[1:]
|
|
|
|
|
path, needs, goV, err := goModOf(d)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if laid[path] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
laid[path] = true
|
|
|
|
|
if goNewer(goV, highest) {
|
|
|
|
|
highest = goV
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprintf(&use, "\t%s\n", d)
|
|
|
|
|
fmt.Fprintf(&replace, "replace %s %s => %s\n", path, zeroVersion, d)
|
|
|
|
|
for _, need := range needs {
|
|
|
|
|
if laid[need] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
dir, ok := paths[need]
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", fmt.Errorf("%s (%s) requires %s at the zero version, and no tree laid beside holds it", path, d, need)
|
|
|
|
|
}
|
|
|
|
|
queue = append(queue, dir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "go " + highest + "\n\nuse (\n" + use.String() + ")\n\n" + replace.String(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// goNewer reports whether the go directive a is higher than b (dotted numbers; a missing part is 0).
|
|
|
|
|
func goNewer(a, b string) bool {
|
|
|
|
|
x, y := strings.Split(a, "."), strings.Split(b, ".")
|
|
|
|
|
for i := 0; i < len(x) || i < len(y); i++ {
|
|
|
|
|
var p, q int
|
|
|
|
|
if i < len(x) {
|
|
|
|
|
p, _ = strconv.Atoi(x[i])
|
|
|
|
|
}
|
|
|
|
|
if i < len(y) {
|
|
|
|
|
q, _ = strconv.Atoi(y[i])
|
|
|
|
|
}
|
|
|
|
|
if p != q {
|
|
|
|
|
return p > q
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// goModOf is the module path of the go.mod in d, the modules it requires at the zero version and its go directive.
|
|
|
|
|
func goModOf(d string) (path string, needs []string, goV string, err error) {
|
|
|
|
|
raw, err := os.ReadFile(filepath.Join(d, "go.mod"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", nil, "", err
|
|
|
|
|
}
|
|
|
|
|
for _, line := range strings.Split(string(raw), "\n") {
|
|
|
|
|
f := strings.Fields(strings.TrimPrefix(strings.TrimSpace(line), "require "))
|
|
|
|
|
switch {
|
|
|
|
|
case len(f) == 2 && f[0] == "module" && path == "":
|
|
|
|
|
path = strings.Trim(f[1], `"`)
|
|
|
|
|
case len(f) == 2 && f[0] == "go" && goV == "":
|
|
|
|
|
goV = f[1]
|
|
|
|
|
case len(f) >= 2 && f[1] == zeroVersion:
|
|
|
|
|
needs = append(needs, strings.Trim(f[0], `"`))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if path == "" {
|
|
|
|
|
return "", nil, "", fmt.Errorf("%s/go.mod names no module", d)
|
|
|
|
|
}
|
|
|
|
|
return path, needs, goV, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// downloadBeside downloads, where the network is, the Go modules of the graph that the tests of a workspace's siblings
|
|
|
|
|
// build: a sibling that is a Go module at its root (the node-engine's repository, whose composer a catalogue test builds)
|
|
|
|
|
// is laid with what it requires at the zero version, found in the fetched tree and the other siblings (workspaceOf), as
|
|
|
|
|
// a go.work, and its graph is downloaded into the module cache. The integration level runs shut off from the network and
|
|
|
|
|
// builds with GOPROXY=off, so a module this fetch did not download is one it cannot get. A sibling whose requirement no
|
|
|
|
|
// tree holds is said and left out: the test that builds it fails naming that module. A download that fails fails the fetch.
|
|
|
|
|
func (m *Module) downloadBeside(ctx context.Context, root, tree string, dirs []string) error {
|
|
|
|
|
beside := filepath.Join(root, "beside")
|
|
|
|
|
entries, err := os.ReadDir(beside)
|
|
|
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
|
|
|
return nil // no siblings placed
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
var candidates, roots, all []string
|
|
|
|
|
for _, e := range entries {
|
|
|
|
|
dir := filepath.Join(beside, e.Name())
|
|
|
|
|
all = append(all, dir)
|
|
|
|
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
|
|
|
|
roots = append(roots, dir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(roots) == 0 {
|
|
|
|
|
return nil // no sibling is a Go module at its root: nothing is walked, nothing downloaded
|
|
|
|
|
}
|
|
|
|
|
for _, d := range dirs {
|
|
|
|
|
candidates = append(candidates, filepath.Join(tree, filepath.FromSlash(d)))
|
|
|
|
|
}
|
|
|
|
|
for _, dir := range all {
|
|
|
|
|
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if d.IsDir() && (d.Name() == ".git" || d.Name() == "testdata" || d.Name() == "node_modules") {
|
|
|
|
|
return filepath.SkipDir
|
|
|
|
|
}
|
|
|
|
|
if !d.IsDir() && d.Name() == "go.mod" {
|
|
|
|
|
candidates = append(candidates, filepath.Dir(p))
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for i, r := range roots {
|
|
|
|
|
work, err := workspaceOf(r, candidates)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if m.Say != nil {
|
|
|
|
|
m.Say("beside %s: its modules are not downloaded: %v", filepath.Base(r), err)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
file := filepath.Join(root, fmt.Sprintf("beside-%d.work", i))
|
|
|
|
|
if err := os.WriteFile(file, []byte(work), 0o600); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
env := append(m.goEnv(), "GOWORK="+file, "GOFLAGS=-mod=readonly") // a workspace takes no -mod=mod
|
|
|
|
|
if res := m.Run(ctx, env, "go", "-C", r, "mod", "download"); res.Status != 0 || res.Err != "" {
|
|
|
|
|
return fmt.Errorf("go mod download of the graph of %s beside: %s", filepath.Base(r), causeLine(res.Err+"\n"+res.Stdout+"\n"+res.Stderr))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|