diff --git a/gnovm/cmd/gno/doc.go b/gnovm/cmd/gno/doc.go index c54e289cd67..c959b872ed7 100644 --- a/gnovm/cmd/gno/doc.go +++ b/gnovm/cmd/gno/doc.go @@ -2,10 +2,14 @@ package main import ( "context" + "errors" "flag" + "fmt" + "os" "path/filepath" "github.com/gnolang/gno/gnovm/pkg/doc" + "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -74,8 +78,21 @@ func execDoc(cfg *docCfg, args []string, io *commands.IO) error { if cfg.rootDir == "" { cfg.rootDir = guessRootDir() } + + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("could not determine working directory: %w", err) + } + + rd, err := gnomod.FindRootDir(wd) + if err != nil && !errors.Is(err, gnomod.ErrGnoModNotFound) { + return fmt.Errorf("error determining root gno.mod file: %w", err) + } + modDirs := []string{rd} + + // select dirs from which to gather directories dirs := []string{filepath.Join(cfg.rootDir, "gnovm/stdlibs"), filepath.Join(cfg.rootDir, "examples")} - res, err := doc.ResolveDocumentable(dirs, args, cfg.unexported) + res, err := doc.ResolveDocumentable(dirs, modDirs, args, cfg.unexported) if res == nil { return err } diff --git a/gnovm/docs/go-gno-compatibility.md b/gnovm/docs/go-gno-compatibility.md index 3aea27784ec..890186edebf 100644 --- a/gnovm/docs/go-gno-compatibility.md +++ b/gnovm/docs/go-gno-compatibility.md @@ -345,27 +345,27 @@ Additional native types: ## Tooling (`gno` binary) -| go command | gno command | comment | -|-------------------|------------------|-----------------------------------------------| -| go bug | | see https://github.com/gnolang/gno/issues/733 | -| go build | gno build | same intention, limited compatibility | -| go clean | gno clean | same intention, limited compatibility | -| go doc | | see https://github.com/gnolang/gno/pull/610 | -| go env | | | -| go fix | | | -| go fmt | | | -| go generate | | | -| go get | | | -| go help | | | -| go install | | | -| go list | | | -| go mod | | | -| + go mod download | gno mod download | same behavior | -| | gno precompile | | -| go work | | | -| | gno repl | | -| go run | gno run | | -| go test | gno test | limited compatibility | -| go tool | | | -| go version | | | -| go vet | | | +| go command | gno command | comment | +|-------------------|------------------|-----------------------------------------------------------------------| +| go bug | | see https://github.com/gnolang/gno/issues/733 | +| go build | gno build | same intention, limited compatibility | +| go clean | gno clean | same intention, limited compatibility | +| go doc | gno doc | limited compatibility; see https://github.com/gnolang/gno/issues/522 | +| go env | | | +| go fix | | | +| go fmt | | | +| go generate | | | +| go get | | | +| go help | | | +| go install | | | +| go list | | | +| go mod | | | +| + go mod download | gno mod download | same behavior | +| | gno precompile | | +| go work | | | +| | gno repl | | +| go run | gno run | | +| go test | gno test | limited compatibility | +| go tool | | | +| go version | | | +| go vet | | | diff --git a/gnovm/pkg/doc/dirs.go b/gnovm/pkg/doc/dirs.go index c6f90d167e4..c5a82214ce5 100644 --- a/gnovm/pkg/doc/dirs.go +++ b/gnovm/pkg/doc/dirs.go @@ -5,11 +5,15 @@ package doc import ( + "fmt" "log" "os" + "path" "path/filepath" "sort" "strings" + + "github.com/gnolang/gno/gnovm/pkg/gnomod" ) // A bfsDir describes a directory holding code by specifying @@ -30,15 +34,80 @@ type bfsDirs struct { } // newDirs begins scanning the given stdlibs directory. -func newDirs(dirs ...string) *bfsDirs { +// dirs are "gopath-like" directories, such as @/gnovm/stdlibs and @/examples. +// modDirs are user directories, expected to have gno.mod files +func newDirs(dirs []string, modDirs []string) *bfsDirs { d := &bfsDirs{ hist: make([]bfsDir, 0, 256), scan: make(chan bfsDir), } - go d.walk(dirs) + + roots := make([]bfsDir, 0, len(dirs)+len(modDirs)) + for _, dir := range dirs { + roots = append(roots, bfsDir{ + dir: dir, + importPath: "", + }) + } + + for _, mdir := range modDirs { + gm, err := parseGnoMod(filepath.Join(mdir, "gno.mod")) + if err != nil { + log.Printf("%v", err) + continue + } + roots = append(roots, bfsDir{ + dir: mdir, + importPath: gm.Module.Mod.Path, + }) + roots = append(roots, getGnoModDirs(gm)...) + } + + go d.walk(roots) return d } +// tries to parse gno mod file given the filename, using Parse and Validate from +// the gnomod package +func parseGnoMod(fname string) (*gnomod.File, error) { + file, err := os.Stat(fname) + if err != nil { + return nil, fmt.Errorf("could not read gno.mod file: %w", err) + } + if file.IsDir() { + return nil, fmt.Errorf("invalid gno.mod at %q: is a directory", fname) + } + + b, err := os.ReadFile(fname) + if err != nil { + return nil, fmt.Errorf("could not read gno.mod file: %w", err) + } + gm, err := gnomod.Parse(fname, b) + if err != nil { + return nil, fmt.Errorf("error parsing gno.mod file at %q: %w", fname, err) + } + if err := gm.Validate(); err != nil { + return nil, fmt.Errorf("error validating gno.mod file at %q: %w", fname, err) + } + return gm, nil +} + +func getGnoModDirs(gm *gnomod.File) []bfsDir { + // cmd/go makes use of the go list command, we don't have that here. + + dirs := make([]bfsDir, 0, len(gm.Require)) + for _, r := range gm.Require { + mv := gm.Resolve(r) + path := gnomod.PackageDir("", mv) + dirs = append(dirs, bfsDir{ + importPath: mv.Path, + dir: path, + }) + } + + return dirs +} + // Reset puts the scan back at the beginning. func (d *bfsDirs) Reset() { d.offset = 0 @@ -62,7 +131,7 @@ func (d *bfsDirs) Next() (bfsDir, bool) { } // walk walks the trees in the given roots. -func (d *bfsDirs) walk(roots []string) { +func (d *bfsDirs) walk(roots []bfsDir) { for _, root := range roots { d.bfsWalkRoot(root) } @@ -71,28 +140,36 @@ func (d *bfsDirs) walk(roots []string) { // bfsWalkRoot walks a single directory hierarchy in breadth-first lexical order. // Each Go source directory it finds is delivered on d.scan. -func (d *bfsDirs) bfsWalkRoot(root string) { - root = filepath.Clean(root) +func (d *bfsDirs) bfsWalkRoot(root bfsDir) { + root.dir = filepath.Clean(root.dir) // this is the queue of directories to examine in this pass. - this := []string{} + this := []bfsDir{} // next is the queue of directories to examine in the next pass. - next := []string{root} + next := []bfsDir{root} for len(next) > 0 { this, next = next, this[:0] for _, dir := range this { - fd, err := os.Open(dir) + fd, err := os.Open(dir.dir) if err != nil { log.Print(err) continue } - entries, err := fd.Readdir(0) + + // read dir entries. + entries, err := fd.ReadDir(0) fd.Close() if err != nil { log.Print(err) continue } + + // stop at module boundaries + if dir.dir != root.dir && containsGnoMod(entries) { + continue + } + hasGnoFiles := false for _, entry := range entries { name := entry.Name() @@ -111,20 +188,28 @@ func (d *bfsDirs) bfsWalkRoot(root string) { continue } // Remember this (fully qualified) directory for the next pass. - next = append(next, filepath.Join(dir, name)) + next = append(next, bfsDir{ + dir: filepath.Join(dir.dir, name), + importPath: path.Join(dir.importPath, name), + }) } if hasGnoFiles { // It's a candidate. - var importPath string - if len(dir) > len(root) { - importPath = filepath.ToSlash(dir[len(root)+1:]) - } - d.scan <- bfsDir{importPath, dir} + d.scan <- dir } } } } +func containsGnoMod(entries []os.DirEntry) bool { + for _, entry := range entries { + if entry.Name() == "gno.mod" && !entry.IsDir() { + return true + } + } + return false +} + // findPackage finds a package iterating over d where the import path has // name as a suffix (which may be a package name or a fully-qualified path). // returns a list of possible directories. If a directory's import path matched @@ -139,12 +224,14 @@ func (d *bfsDirs) findPackage(name string) []bfsDir { } } sort.Slice(candidates, func(i, j int) bool { - // prefer exact matches with name - if candidates[i].importPath == name { - return true - } else if candidates[j].importPath == name { - return false + // prefer shorter paths -- if we have an exact match it will be of the + // shortest possible pkg path. + ci := strings.Count(candidates[i].importPath, "/") + cj := strings.Count(candidates[j].importPath, "/") + if ci != cj { + return ci < cj } + // use alphabetical ordering otherwise. return candidates[i].importPath < candidates[j].importPath }) return candidates diff --git a/gnovm/pkg/doc/dirs_test.go b/gnovm/pkg/doc/dirs_test.go index a7c4926a8c8..433b707c3a1 100644 --- a/gnovm/pkg/doc/dirs_test.go +++ b/gnovm/pkg/doc/dirs_test.go @@ -1,6 +1,9 @@ package doc import ( + "bytes" + "log" + "os" "path/filepath" "strings" "testing" @@ -9,29 +12,97 @@ import ( "github.com/stretchr/testify/require" ) -func tNewDirs(t *testing.T) (string, *bfsDirs) { +func getwd(t *testing.T) string { t.Helper() - p, err := filepath.Abs("./testdata/dirs") + wd, err := os.Getwd() require.NoError(t, err) - return p, newDirs(p) + return wd +} + +func wdJoin(t *testing.T, arg string) string { + t.Helper() + return filepath.Join(getwd(t), arg) +} + +func TestNewDirs_nonExisting(t *testing.T) { + old := log.Default().Writer() + var buf bytes.Buffer + log.Default().SetOutput(&buf) + defer func() { log.Default().SetOutput(old) }() // in case of panic + + // git doesn't track empty directories; so need to create this one on our own. + de := wdJoin(t, "testdata/dirsempty") + require.NoError(t, os.MkdirAll(de, 0o755)) + + d := newDirs([]string{wdJoin(t, "non/existing/dir"), de}, []string{wdJoin(t, "and/this/one/neither")}) + for _, ok := d.Next(); ok; _, ok = d.Next() { //nolint:revive + } + log.Default().SetOutput(old) + assert.Empty(t, d.hist, "hist should be empty") + assert.Equal(t, strings.Count(buf.String(), "\n"), 2, "output should contain 2 lines") + assert.Contains(t, buf.String(), "non/existing/dir: no such file or directory") + assert.Contains(t, buf.String(), "this/one/neither/gno.mod: no such file or directory") + assert.NotContains(t, buf.String(), "dirsempty: no such file or directory") +} + +func TestNewDirs_invalidModDir(t *testing.T) { + old := log.Default().Writer() + var buf bytes.Buffer + log.Default().SetOutput(&buf) + defer func() { log.Default().SetOutput(old) }() // in case of panic + + d := newDirs(nil, []string{wdJoin(t, "testdata/dirs")}) + for _, ok := d.Next(); ok; _, ok = d.Next() { //nolint:revive + } + log.Default().SetOutput(old) + assert.Empty(t, d.hist, "hist should be len 0 (testdata/dirs is not a valid mod dir)") + assert.Equal(t, strings.Count(buf.String(), "\n"), 1, "output should contain 1 line") + assert.Contains(t, buf.String(), "gno.mod: no such file or directory") +} + +func tNewDirs(t *testing.T) (string, *bfsDirs) { + t.Helper() + + // modify GNO_HOME to testdata/dirsdep -- this allows us to test + // dependency lookup by dirs. + old, ex := os.LookupEnv("GNO_HOME") + os.Setenv("GNO_HOME", wdJoin(t, "testdata/dirsdep")) + t.Cleanup(func() { + if ex { + os.Setenv("GNO_HOME", old) + } else { + os.Unsetenv("GNO_HOME") + } + }) + + return wdJoin(t, "testdata"), + newDirs([]string{wdJoin(t, "testdata/dirs")}, []string{wdJoin(t, "testdata/dirsmod")}) } func TestDirs_findPackage(t *testing.T) { - abs, d := tNewDirs(t) + td, d := tNewDirs(t) tt := []struct { name string res []bfsDir }{ {"rand", []bfsDir{ - {importPath: "rand", dir: filepath.Join(abs, "rand")}, - {importPath: "crypto/rand", dir: filepath.Join(abs, "crypto/rand")}, - {importPath: "math/rand", dir: filepath.Join(abs, "math/rand")}, + {importPath: "rand", dir: filepath.Join(td, "dirs/rand")}, + {importPath: "crypto/rand", dir: filepath.Join(td, "dirs/crypto/rand")}, + {importPath: "math/rand", dir: filepath.Join(td, "dirs/math/rand")}, + {importPath: "dirs.mod/prefix/math/rand", dir: filepath.Join(td, "dirsmod/math/rand")}, }}, {"crypto/rand", []bfsDir{ - {importPath: "crypto/rand", dir: filepath.Join(abs, "crypto/rand")}, + {importPath: "crypto/rand", dir: filepath.Join(td, "dirs/crypto/rand")}, + }}, + {"dep", []bfsDir{ + {importPath: "dirs.mod/dep", dir: filepath.Join(td, "dirsdep/pkg/mod/dirs.mod/dep")}, + }}, + {"alpha", []bfsDir{ + {importPath: "dirs.mod/dep/alpha", dir: filepath.Join(td, "dirsdep/pkg/mod/dirs.mod/dep/alpha")}, + // no testdir/module/alpha as it is inside a module }}, {"math", []bfsDir{ - {importPath: "math", dir: filepath.Join(abs, "math")}, + {importPath: "math", dir: filepath.Join(td, "dirs/math")}, }}, {"ath", []bfsDir{}}, {"/math", []bfsDir{}}, @@ -47,22 +118,23 @@ func TestDirs_findPackage(t *testing.T) { } func TestDirs_findDir(t *testing.T) { - abs, d := tNewDirs(t) + td, d := tNewDirs(t) tt := []struct { name string in string res []bfsDir }{ - {"rand", filepath.Join(abs, "rand"), []bfsDir{ - {importPath: "rand", dir: filepath.Join(abs, "rand")}, + {"rand", filepath.Join(td, "dirs/rand"), []bfsDir{ + {importPath: "rand", dir: filepath.Join(td, "dirs/rand")}, }}, - {"crypto/rand", filepath.Join(abs, "crypto/rand"), []bfsDir{ - {importPath: "crypto/rand", dir: filepath.Join(abs, "crypto/rand")}, + {"crypto/rand", filepath.Join(td, "dirs/crypto/rand"), []bfsDir{ + {importPath: "crypto/rand", dir: filepath.Join(td, "dirs/crypto/rand")}, }}, // ignored (dir name testdata), so should not return anything. - {"crypto/testdata/rand", filepath.Join(abs, "crypto/testdata/rand"), nil}, - {"xx", filepath.Join(abs, "xx"), nil}, + {"crypto/testdata/rand", filepath.Join(td, "dirs/crypto/testdata/rand"), nil}, + {"xx", filepath.Join(td, "dirs/xx"), nil}, {"xx2", "/xx2", nil}, + {"2xx", "/2xx", nil}, } for _, tc := range tt { tc := tc diff --git a/gnovm/pkg/doc/doc.go b/gnovm/pkg/doc/doc.go index cecd97f53d9..c77ded6db7f 100644 --- a/gnovm/pkg/doc/doc.go +++ b/gnovm/pkg/doc/doc.go @@ -159,8 +159,12 @@ var fpAbs = filepath.Abs // the same as the go doc command). // An error may be returned even if documentation was resolved in case some // packages in dirs could not be parsed correctly. -func ResolveDocumentable(dirs []string, args []string, unexported bool) (Documentable, error) { - d := newDirs(dirs...) +// +// dirs specifies the gno system directories to scan which specify full import paths +// in their directories, such as @/examples and @/gnovm/stdlibs; modDirs specifies +// directories which contain a gno.mod file. +func ResolveDocumentable(dirs, modDirs, args []string, unexported bool) (Documentable, error) { + d := newDirs(dirs, modDirs) parsed, ok := parseArgs(args) if !ok { @@ -197,7 +201,7 @@ func resolveDocumentable(dirs *bfsDirs, parsed docArgs, unexported bool) (Docume // there are no candidates. // if this is ambiguous, remove ambiguity and try parsing args using pkg as the symbol. if !parsed.pkgAmbiguous { - return nil, fmt.Errorf("commands/doc: package not found: %q (note: local packages are not yet supported)", parsed.pkg) + return nil, fmt.Errorf("commands/doc: package not found: %q", parsed.pkg) } parsed = docArgs{pkg: ".", sym: parsed.pkg, acc: parsed.sym} return resolveDocumentable(dirs, parsed, unexported) diff --git a/gnovm/pkg/doc/doc_test.go b/gnovm/pkg/doc/doc_test.go index 1cccb4106f7..8a959e142d0 100644 --- a/gnovm/pkg/doc/doc_test.go +++ b/gnovm/pkg/doc/doc_test.go @@ -15,7 +15,7 @@ func TestResolveDocumentable(t *testing.T) { p, err := os.Getwd() require.NoError(t, err) path := func(s string) string { return filepath.Join(p, "testdata/integ", s) } - dirs := newDirs(path("")) + dirs := newDirs([]string{path("")}, []string{path("mod")}) getDir := func(p string) bfsDir { return dirs.findDir(path(p))[0] } pdata := func(p string, unexp bool) *pkgData { pd, err := newPkgData(getDir(p), unexp) @@ -31,7 +31,9 @@ func TestResolveDocumentable(t *testing.T) { errContains string }{ {"package", []string{"crypto/rand"}, false, &documentable{bfsDir: getDir("crypto/rand")}, ""}, + {"packageMod", []string{"gno.land/mod"}, false, &documentable{bfsDir: getDir("mod")}, ""}, {"dir", []string{"./testdata/integ/crypto/rand"}, false, &documentable{bfsDir: getDir("crypto/rand")}, ""}, + {"dirMod", []string{"./testdata/integ/mod"}, false, &documentable{bfsDir: getDir("mod")}, ""}, {"dirAbs", []string{path("crypto/rand")}, false, &documentable{bfsDir: getDir("crypto/rand")}, ""}, // test_notapkg exists in local dir and also path("test_notapkg"). // ResolveDocumentable should first try local dir, and seeing as it is not a valid dir, try searching it as a package. @@ -93,9 +95,11 @@ func TestResolveDocumentable(t *testing.T) { {"errInvalidArgs", []string{"1", "2", "3"}, false, nil, "invalid arguments: [1 2 3]"}, {"errNoCandidates", []string{"math", "Big"}, false, nil, `package not found: "math"`}, - {"errNoCandidates2", []string{"LocalSymbol"}, false, nil, `local packages are not yet supported`}, - {"errNoCandidates3", []string{"Symbol.Accessible"}, false, nil, `local packages are not yet supported`}, + {"errNoCandidates2", []string{"LocalSymbol"}, false, nil, `package not found`}, + {"errNoCandidates3", []string{"Symbol.Accessible"}, false, nil, `package not found`}, {"errNonExisting", []string{"rand.NotExisting"}, false, nil, `could not resolve arguments`}, + {"errIgnoredMod", []string{"modignored"}, false, nil, `package not found`}, + {"errIgnoredMod2", []string{"./testdata/integ/modignored"}, false, nil, `package not found`}, {"errUnexp", []string{"crypto/rand.unexp"}, false, nil, "could not resolve arguments"}, {"errDirNotapkg", []string{"./test_notapkg"}, false, nil, `package not found: "./test_notapkg"`}, } @@ -110,7 +114,10 @@ func TestResolveDocumentable(t *testing.T) { fpAbs = func(s string) (string, error) { return filepath.Clean(filepath.Join(path("wd"), s)), nil } defer func() { fpAbs = filepath.Abs }() } - result, err := ResolveDocumentable([]string{path("")}, tc.args, tc.unexp) + result, err := ResolveDocumentable( + []string{path("")}, []string{path("mod")}, + tc.args, tc.unexp, + ) // we use stripFset because d.pkgData.fset contains sync/atomic values, // which in turn makes reflect.DeepEqual compare the two sync.Atomic values. assert.Equal(t, stripFset(tc.expect), stripFset(result), "documentables should match") diff --git a/gnovm/pkg/doc/testdata/dirs/module/alpha/a.gno b/gnovm/pkg/doc/testdata/dirs/module/alpha/a.gno new file mode 100644 index 00000000000..36373e5cc5b --- /dev/null +++ b/gnovm/pkg/doc/testdata/dirs/module/alpha/a.gno @@ -0,0 +1,2 @@ +// This directory should not be inspected, as it is inside a module +// and not explicitly added as a rootDir. diff --git a/gnovm/pkg/doc/testdata/dirs/module/gno.mod b/gnovm/pkg/doc/testdata/dirs/module/gno.mod new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gnovm/pkg/doc/testdata/dirsdep/pkg/mod/dirs.mod/dep/a.gno b/gnovm/pkg/doc/testdata/dirsdep/pkg/mod/dirs.mod/dep/a.gno new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gnovm/pkg/doc/testdata/dirsdep/pkg/mod/dirs.mod/dep/alpha/a.gno b/gnovm/pkg/doc/testdata/dirsdep/pkg/mod/dirs.mod/dep/alpha/a.gno new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gnovm/pkg/doc/testdata/dirsdep/pkg/mod/dirs.mod/dep/gno.mod b/gnovm/pkg/doc/testdata/dirsdep/pkg/mod/dirs.mod/dep/gno.mod new file mode 100644 index 00000000000..f8dabd6225a --- /dev/null +++ b/gnovm/pkg/doc/testdata/dirsdep/pkg/mod/dirs.mod/dep/gno.mod @@ -0,0 +1 @@ +module dirs.mod/dep diff --git a/gnovm/pkg/doc/testdata/dirsmod/gno.mod b/gnovm/pkg/doc/testdata/dirsmod/gno.mod new file mode 100644 index 00000000000..6c8008b958c --- /dev/null +++ b/gnovm/pkg/doc/testdata/dirsmod/gno.mod @@ -0,0 +1,5 @@ +module dirs.mod/prefix + +require ( + dirs.mod/dep v0.0.0 +) diff --git a/gnovm/pkg/doc/testdata/dirsmod/math/rand/a.gno b/gnovm/pkg/doc/testdata/dirsmod/math/rand/a.gno new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gnovm/pkg/doc/testdata/integ/mod/gno.mod b/gnovm/pkg/doc/testdata/integ/mod/gno.mod new file mode 100644 index 00000000000..248c7a8010c --- /dev/null +++ b/gnovm/pkg/doc/testdata/integ/mod/gno.mod @@ -0,0 +1 @@ +module gno.land/mod diff --git a/gnovm/pkg/doc/testdata/integ/mod/mod.gno b/gnovm/pkg/doc/testdata/integ/mod/mod.gno new file mode 100644 index 00000000000..6ebcefcdcba --- /dev/null +++ b/gnovm/pkg/doc/testdata/integ/mod/mod.gno @@ -0,0 +1,5 @@ +package mod + +// ModHello greets you warmly! +func ModHello() { +} diff --git a/gnovm/pkg/doc/testdata/integ/modignored/gno.mod b/gnovm/pkg/doc/testdata/integ/modignored/gno.mod new file mode 100644 index 00000000000..d3b3c774e2e --- /dev/null +++ b/gnovm/pkg/doc/testdata/integ/modignored/gno.mod @@ -0,0 +1 @@ +module gno.land/modignored diff --git a/gnovm/pkg/doc/testdata/integ/modignored/mod.gno b/gnovm/pkg/doc/testdata/integ/modignored/mod.gno new file mode 100644 index 00000000000..6ebcefcdcba --- /dev/null +++ b/gnovm/pkg/doc/testdata/integ/modignored/mod.gno @@ -0,0 +1,5 @@ +package mod + +// ModHello greets you warmly! +func ModHello() { +} diff --git a/gnovm/pkg/gnomod/file.go b/gnovm/pkg/gnomod/file.go index 473e60850ca..7f8879a35ed 100644 --- a/gnovm/pkg/gnomod/file.go +++ b/gnovm/pkg/gnomod/file.go @@ -33,33 +33,42 @@ func (f *File) Validate() error { return nil } +// Resolve takes a Require directive from File and returns any adequate replacement +// following the Replace directives. +func (f *File) Resolve(r *modfile.Require) module.Version { + mod, replaced := isReplaced(r.Mod, f.Replace) + if replaced { + return mod + } + return r.Mod +} + // FetchDeps fetches and writes gno.mod packages // in GOPATH/pkg/gnomod/ func (f *File) FetchDeps(path string, remote string, verbose bool) error { for _, r := range f.Require { - mod, replaced := isReplaced(r.Mod, f.Replace) - if replaced { + mod := f.Resolve(r) + if r.Mod.Path != mod.Path { if modfile.IsDirectoryPath(mod.Path) { continue } - r.Mod = *mod } indirect := "" if r.Indirect { indirect = "// indirect" } - _, err := os.Stat(filepath.Join(path, r.Mod.Path)) + _, err := os.Stat(PackageDir(path, mod)) if !os.IsNotExist(err) { if verbose { - log.Println("cached", r.Mod.Path, indirect) + log.Println("cached", mod.Path, indirect) } continue } if verbose { - log.Println("fetching", r.Mod.Path, indirect) + log.Println("fetching", mod.Path, indirect) } - requirements, err := writePackage(remote, path, r.Mod.Path) + requirements, err := writePackage(remote, path, mod.Path) if err != nil { return fmt.Errorf("writepackage: %w", err) } @@ -67,7 +76,7 @@ func (f *File) FetchDeps(path string, remote string, verbose bool) error { modFile := &File{ Module: &modfile.Module{ Mod: module.Version{ - Path: r.Mod.Path, + Path: mod.Path, }, }, } @@ -101,7 +110,7 @@ func (f *File) FetchDeps(path string, remote string, verbose bool) error { if err != nil { return err } - err = goMod.WriteToPath(filepath.Join(path, r.Mod.Path)) + err = goMod.WriteToPath(PackageDir(path, mod)) if err != nil { return err } diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index c198adc1ca8..0fd22d659a3 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -20,6 +20,19 @@ func GetGnoModPath() string { return filepath.Join(client.HomeDir(), "pkg", "mod") } +// PackageDir resolves a given module.Version to the path on the filesystem. +// If root is dir, it is defaulted to the value of [GetGnoModPath]. +func PackageDir(root string, v module.Version) string { + // This is also used internally exactly like filepath.Join; but we'll keep + // the calls centralized to make sure we can change the path centrally should + // we start including the module version in the path. + + if root == "" { + root = GetGnoModPath() + } + return filepath.Join(root, v.Path) +} + func writePackage(remote, basePath, pkgPath string) (requirements []string, err error) { res, err := queryChain(remote, queryPathFile, []byte(pkgPath)) if err != nil { @@ -150,15 +163,15 @@ func GnoToGoMod(f File) (*File, error) { return &f, nil } -func isReplaced(module module.Version, repl []*modfile.Replace) (*module.Version, bool) { +func isReplaced(mod module.Version, repl []*modfile.Replace) (module.Version, bool) { for _, r := range repl { - hasNoVersion := r.Old.Path == module.Path && r.Old.Version == "" - hasExactVersion := r.Old == module + hasNoVersion := r.Old.Path == mod.Path && r.Old.Version == "" + hasExactVersion := r.Old == mod if hasNoVersion || hasExactVersion { - return &r.New, true + return r.New, true } } - return nil, false + return module.Version{}, false } func removeDuplicateStr(str []string) (res []string) { diff --git a/gnovm/pkg/gnomod/utils.go b/gnovm/pkg/gnomod/utils.go index a0c02de8d1f..fe1d0ed79be 100644 --- a/gnovm/pkg/gnomod/utils.go +++ b/gnovm/pkg/gnomod/utils.go @@ -6,6 +6,13 @@ import ( "path/filepath" ) +// ErrGnoModNotFound is returned by [FindRootDir] when, even after traversing +// up to the root directory, a gno.mod file could not be found. +var ErrGnoModNotFound = errors.New("gno.mod file not found in current or any parent directory") + +// FindRootDir determines the root directory of the project which contains the +// gno.mod file. If no gno.mod file is found, [ErrGnoModNotFound] is returned. +// The given path must be absolute. func FindRootDir(absPath string) (string, error) { if !filepath.IsAbs(absPath) { return "", errors.New("requires absolute path") @@ -25,5 +32,5 @@ func FindRootDir(absPath string) (string, error) { return absPath, nil } - return "", errors.New("gno.mod file not found in current or any parent directory") + return "", ErrGnoModNotFound }