diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index b5e13b8913d..09194232fec 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -30,6 +30,7 @@ func newModCmd(io *commands.IO) *commands.Command { cmd.AddSubCommands( newModDownloadCmd(io), + newModInitCmd(), ) return cmd @@ -51,6 +52,20 @@ func newModDownloadCmd(io *commands.IO) *commands.Command { ) } +func newModInitCmd() *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "init", + ShortUsage: "init [module-path]", + ShortHelp: "Initialize gno.mod file in current directory", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execModInit(args) + }, + ) +} + func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.remote, @@ -111,10 +126,29 @@ func execModDownload(cfg *modDownloadCfg, args []string, io *commands.IO) error } // write go.mod file - err = gomod.WriteToPath(path) + err = gomod.WriteToPath(filepath.Join(path, "go.mod")) if err != nil { return fmt.Errorf("write go.mod file: %w", err) } return nil } + +func execModInit(args []string) error { + if len(args) > 1 { + return flag.ErrHelp + } + var modPath string + if len(args) == 1 { + modPath = args[0] + } + dir, err := os.Getwd() + if err != nil { + return err + } + if err := gnomod.CreateGnoModFile(dir, modPath); err != nil { + return fmt.Errorf("create gno.mod file: %w", err) + } + + return nil +} diff --git a/gnovm/cmd/gno/mod_test.go b/gnovm/cmd/gno/mod_test.go index ecdc3d2516f..fdae3d12c7a 100644 --- a/gnovm/cmd/gno/mod_test.go +++ b/gnovm/cmd/gno/mod_test.go @@ -9,7 +9,7 @@ func TestModApp(t *testing.T) { errShouldBe: "flag: help requested", }, - // test gno.mod + // test gno.mod download { args: []string{"mod", "download"}, testDir: "../../tests/integ/empty-dir", @@ -72,6 +72,71 @@ func TestModApp(t *testing.T) { simulateExternalRepo: true, errShouldContain: "fetch: writepackage: querychain", }, + + // test gno.mod init with no module name + { + args: []string{"mod", "init"}, + testDir: "../../tests/integ/valid1", + simulateExternalRepo: true, + }, + { + args: []string{"mod", "init"}, + testDir: "../../tests/integ/empty-dir", + simulateExternalRepo: true, + errShouldBe: "create gno.mod file: cannot determine package name", + }, + { + args: []string{"mod", "init"}, + testDir: "../../tests/integ/empty-gno1", + simulateExternalRepo: true, + recoverShouldContain: "expected 'package', found 'EOF'", + }, + { + args: []string{"mod", "init"}, + testDir: "../../tests/integ/empty-gno2", + simulateExternalRepo: true, + recoverShouldContain: "expected 'package', found 'EOF'", + }, + { + args: []string{"mod", "init"}, + testDir: "../../tests/integ/empty-gno3", + simulateExternalRepo: true, + recoverShouldContain: "expected 'package', found 'EOF'", + }, + { + args: []string{"mod", "init"}, + testDir: "../../tests/integ/empty-gnomod", + simulateExternalRepo: true, + errShouldBe: "create gno.mod file: gno.mod file already exists", + }, + + // test gno.mod init with module name + { + args: []string{"mod", "init", "gno.land/p/demo/foo"}, + testDir: "../../tests/integ/empty-dir", + simulateExternalRepo: true, + }, + { + args: []string{"mod", "init", "gno.land/p/demo/foo"}, + testDir: "../../tests/integ/empty-gno1", + simulateExternalRepo: true, + }, + { + args: []string{"mod", "init", "gno.land/p/demo/foo"}, + testDir: "../../tests/integ/empty-gno2", + simulateExternalRepo: true, + }, + { + args: []string{"mod", "init", "gno.land/p/demo/foo"}, + testDir: "../../tests/integ/empty-gno3", + simulateExternalRepo: true, + }, + { + args: []string{"mod", "init", "gno.land/p/demo/foo"}, + testDir: "../../tests/integ/empty-gnomod", + simulateExternalRepo: true, + errShouldBe: "create gno.mod file: gno.mod file already exists", + }, } testMainCaseRun(t, tc) } diff --git a/gnovm/docs/go-gno-compatibility.md b/gnovm/docs/go-gno-compatibility.md index e35e9e5dd5d..e3616861ca1 100644 --- a/gnovm/docs/go-gno-compatibility.md +++ b/gnovm/docs/go-gno-compatibility.md @@ -361,6 +361,7 @@ Additional native types: | go list | | | | go mod | | | | + go mod download | gno mod download | same behavior | +| + go mod init | gno mod init | same behavior | | | gno precompile | | | go work | | | | | gno repl | | diff --git a/gnovm/pkg/gnomod/file.go b/gnovm/pkg/gnomod/file.go index 7f8879a35ed..71d98b2d14b 100644 --- a/gnovm/pkg/gnomod/file.go +++ b/gnovm/pkg/gnomod/file.go @@ -110,7 +110,9 @@ func (f *File) FetchDeps(path string, remote string, verbose bool) error { if err != nil { return err } - err = goMod.WriteToPath(PackageDir(path, mod)) + pkgPath := PackageDir(path, mod) + goModFilePath := filepath.Join(pkgPath, "go.mod") + err = goMod.WriteToPath(goModFilePath) if err != nil { return err } @@ -119,10 +121,10 @@ func (f *File) FetchDeps(path string, remote string, verbose bool) error { return nil } -// WriteToPath writes go.mod file in the given absolute path +// WriteToPath writes file to the given absolute file path // TODO: Find better way to do this. Try to use `modfile` // package to manage this. -func (f *File) WriteToPath(absPath string) error { +func (f *File) WriteToPath(absFilePath string) error { if f.Module == nil { return errors.New("writing go.mod: module not found") } @@ -150,10 +152,9 @@ func (f *File) WriteToPath(absPath string) error { data += ")\n" } - modPath := filepath.Join(absPath, "go.mod") - err := os.WriteFile(modPath, []byte(data), 0o644) + err := os.WriteFile(absFilePath, []byte(data), 0o644) if err != nil { - return fmt.Errorf("writefile %q: %w", modPath, err) + return fmt.Errorf("writefile %q: %w", absFilePath, err) } return nil diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index 0fd22d659a3..aa41c5aa00c 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -1,7 +1,9 @@ package gnomod import ( + "errors" "fmt" + "io/ioutil" "os" "path/filepath" "strings" @@ -163,6 +165,68 @@ func GnoToGoMod(f File) (*File, error) { return &f, nil } +func CreateGnoModFile(rootDir, modPath string) error { + if !filepath.IsAbs(rootDir) { + return fmt.Errorf("dir %q is not absolute", rootDir) + } + + modFilePath := filepath.Join(rootDir, "gno.mod") + if _, err := os.Stat(modFilePath); err == nil { + return errors.New("gno.mod file already exists") + } + + if modPath == "" { + // Check .gno files for package name + // and use it as modPath + files, err := ioutil.ReadDir(rootDir) + if err != nil { + fmt.Errorf("read dir %q: %w", rootDir, err) + } + + var pkgName gnolang.Name + for _, file := range files { + if file.IsDir() || !strings.HasSuffix(file.Name(), ".gno") || strings.HasSuffix(file.Name(), "_filetest.gno") { + continue + } + + fpath := filepath.Join(rootDir, file.Name()) + bz, err := os.ReadFile(fpath) + if err != nil { + return fmt.Errorf("read file %q: %w", fpath, err) + } + + pn := gnolang.PackageNameFromFileBody(file.Name(), string(bz)) + if strings.HasSuffix(string(pkgName), "_test") { + pkgName = pkgName[:len(pkgName)-len("_test")] + } + if pkgName == "" { + pkgName = pn + } + if pkgName != pn { + return fmt.Errorf("package name mismatch: [%q] and [%q]", pkgName, pn) + } + } + if pkgName == "" { + return errors.New("cannot determine package name") + } + modPath = string(pkgName) + } + if err := module.CheckImportPath(modPath); err != nil { + return err + } + + modFile := &File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: modPath, + }, + }, + } + modFile.WriteToPath(filepath.Join(rootDir, "gno.mod")) + + return nil +} + func isReplaced(mod module.Version, repl []*modfile.Replace) (module.Version, bool) { for _, r := range repl { hasNoVersion := r.Old.Path == mod.Path && r.Old.Version == "" diff --git a/gnovm/pkg/gnomod/gnomod_test.go b/gnovm/pkg/gnomod/gnomod_test.go new file mode 100644 index 00000000000..76c9a96c4e8 --- /dev/null +++ b/gnovm/pkg/gnomod/gnomod_test.go @@ -0,0 +1,151 @@ +package gnomod + +import ( + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateGnoModFile(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + desc string + in []struct{ filename, content string } + inModPath string + out string + errShouldContain string + }{ + { + desc: "empty directory", + inModPath: "gno.land/p/demo/foo", + out: "module gno.land/p/demo/foo\n", + }, + { + desc: "empty directory (without modPath)", + errShouldContain: "cannot determine package name", + }, + { + desc: "invalid modPath 1", + inModPath: " ", + errShouldContain: "malformed import path", + }, + { + desc: "invalid modPath 2", + inModPath: "\"", + errShouldContain: "malformed import path", + }, + { + desc: "valid package", + in: []struct{ filename, content string }{ + { + "foo.gno", + `package foo`, + }, + }, + inModPath: "gno.land/p/demo/foo", + out: "module gno.land/p/demo/foo\n", + }, + { + desc: "valid package (without modPath)", + in: []struct{ filename, content string }{ + { + "foo.gno", + `package foo`, + }, + }, + out: "module foo\n", + }, + { + desc: "ambigious package names", + in: []struct{ filename, content string }{ + { + "foo.gno", + `package foo`, + }, + { + "bar.gno", + `package bar`, + }, + }, + inModPath: "gno.land/p/demo/foo", + out: "module gno.land/p/demo/foo\n", + }, + { + desc: "ambigious package names (without modPath)", + in: []struct{ filename, content string }{ + { + "foo.gno", + `package foo`, + }, + { + "bar.gno", + `package bar`, + }, + }, + errShouldContain: "package name mismatch:", + }, + { + desc: "valid package with gno.mod file", + in: []struct{ filename, content string }{ + { + "foo.gno", + `package foo`, + }, + { + "gno.mod", + `module gno.land/p/demo/foo`, + }, + }, + inModPath: "gno.land/p/demo/foo", + errShouldContain: "gno.mod file already exists", + }, + { + desc: "valid package with gno.mod file (without modPath)", + in: []struct{ filename, content string }{ + { + "foo.gno", + `package foo`, + }, + { + "gno.mod", + `module gno.land/p/demo/foo`, + }, + }, + errShouldContain: "gno.mod file already exists", + }, + } { + tc := tc + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + // Create test dir + dirPath, cleanUpFn := testutils.NewTestCaseDir(t) + require.NotNil(t, dirPath) + defer cleanUpFn() + + // Create files + for _, f := range tc.in { + err := os.WriteFile(filepath.Join(dirPath, f.filename), []byte(f.content), 0o644) + require.NoError(t, err) + } + + err := CreateGnoModFile(dirPath, tc.inModPath) + if tc.errShouldContain != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.errShouldContain) + return + } + assert.NoError(t, err) + + // Verify gno.mod file + bz, err := os.ReadFile(filepath.Join(dirPath, "gno.mod")) + assert.NoError(t, err) + assert.Equal(t, tc.out, string(bz)) + }) + } +}