From 759cdc706f0b1980e1458418ebc0ea041a873554 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:50:10 +0100 Subject: [PATCH] feat: add 'gno env' subcommand (#1233) --- gno.land/cmd/gnokey/main.go | 7 +- gno.land/cmd/gnoland/start.go | 3 +- gno.land/cmd/gnoweb/main_test.go | 6 +- gno.land/pkg/gnoland/app.go | 50 +------- .../pkg/integration/testing_integration.go | 4 +- gnovm/Makefile | 9 +- gnovm/cmd/gno/clean.go | 4 +- gnovm/cmd/gno/doc.go | 3 +- gnovm/cmd/gno/env.go | 113 ++++++++++++++++++ gnovm/cmd/gno/env_test.go | 43 +++++++ gnovm/cmd/gno/lint.go | 3 +- gnovm/cmd/gno/main.go | 1 + gnovm/cmd/gno/repl.go | 3 +- gnovm/cmd/gno/run.go | 3 +- gnovm/cmd/gno/test.go | 3 +- gnovm/cmd/gno/util.go | 21 +--- gnovm/pkg/gnoenv/gnohome.go | 35 ++++++ gnovm/pkg/gnoenv/gnohome_test.go | 43 +++++++ gnovm/pkg/gnoenv/gnoroot.go | 92 ++++++++++++++ gnovm/pkg/gnoenv/gnoroot_test.go | 75 ++++++++++++ .../client => gnovm/pkg/gnoenv}/migration.go | 2 +- gnovm/pkg/gnoenv/migration_test.go | 29 +++++ gnovm/pkg/gnomod/gnomod.go | 4 +- gnovm/pkg/integration/gno.go | 12 +- tm2/pkg/crypto/keys/client/common.go | 26 +--- tm2/pkg/crypto/keys/client/root.go | 18 ++- 26 files changed, 485 insertions(+), 127 deletions(-) create mode 100644 gnovm/cmd/gno/env.go create mode 100644 gnovm/cmd/gno/env_test.go create mode 100644 gnovm/pkg/gnoenv/gnohome.go create mode 100644 gnovm/pkg/gnoenv/gnohome_test.go create mode 100644 gnovm/pkg/gnoenv/gnoroot.go create mode 100644 gnovm/pkg/gnoenv/gnoroot_test.go rename {tm2/pkg/crypto/keys/client => gnovm/pkg/gnoenv}/migration.go (98%) create mode 100644 gnovm/pkg/gnoenv/migration_test.go diff --git a/gno.land/cmd/gnokey/main.go b/gno.land/cmd/gnokey/main.go index 28cb665eac1..57a58bfee9c 100644 --- a/gno.land/cmd/gnokey/main.go +++ b/gno.land/cmd/gnokey/main.go @@ -5,13 +5,18 @@ import ( "fmt" "os" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" ) func main() { - cmd := client.NewRootCmd(commands.NewDefaultIO()) + baseCfg := client.BaseOptions{ + Home: gnoenv.HomeDir(), + Remote: "127.0.0.1:26657", + } + cmd := client.NewRootCmdWithBaseConfig(commands.NewDefaultIO(), baseCfg) if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 77d6eb4ed51..9ca2369c6eb 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -10,6 +10,7 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -59,7 +60,7 @@ func newStartCmd(io commands.IO) *commands.Command { } func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { - gnoroot := gnoland.MustGuessGnoRootDir() + gnoroot := gnoenv.RootDir() defaultGenesisBalancesFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt") defaultGenesisTxsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.txt") diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/cmd/gnoweb/main_test.go index 61650563405..2bec0a9ac37 100644 --- a/gno.land/cmd/gnoweb/main_test.go +++ b/gno.land/cmd/gnoweb/main_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gotuna/gotuna/test/assert" ) @@ -44,7 +44,7 @@ func TestRoutes(t *testing.T) { {"/404-not-found", notFound, "/404-not-found"}, } - config, _ := integration.TestingNodeConfig(t, gnoland.MustGuessGnoRootDir()) + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) defer node.Stop() @@ -92,7 +92,7 @@ func TestAnalytics(t *testing.T) { "/404-not-found", } - config, _ := integration.TestingNodeConfig(t, gnoland.MustGuessGnoRootDir()) + config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) defer node.Stop() diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 0610cd4a93f..a1e917dd8d1 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -1,15 +1,11 @@ package gnoland import ( - "errors" "fmt" - "os" - "os/exec" "path/filepath" - "runtime" - "strings" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" dbm "github.com/gnolang/gno/tm2/pkg/db" @@ -37,7 +33,7 @@ func NewAppOptions() *AppOptions { return &AppOptions{ Logger: log.NewNopLogger(), DB: dbm.NewMemDB(), - GnoRootDir: MustGuessGnoRootDir(), + GnoRootDir: gnoenv.RootDir(), } } @@ -180,45 +176,3 @@ func EndBlocker(vmk vm.VMKeeperI) func(ctx sdk.Context, req abci.RequestEndBlock return abci.ResponseEndBlock{} } } - -// XXX: all the method bellow should be removed in favor of -// https://github.com/gnolang/gno/pull/1233 -func MustGuessGnoRootDir() string { - root, err := GuessGnoRootDir() - if err != nil { - panic(err) - } - - return root -} - -func GuessGnoRootDir() (string, error) { - // First try to get the root directory from the GNOROOT environment variable. - if rootdir := os.Getenv("GNOROOT"); rootdir != "" { - return filepath.Clean(rootdir), nil - } - - // Try to guess GNOROOT using the nearest go.mod. - if gobin, err := exec.LookPath("go"); err == nil { - // If GNOROOT is not set, try to guess the root directory using the `go list` command. - cmd := exec.Command(gobin, "list", "-m", "-mod=mod", "-f", "{{.Dir}}", "github.com/gnolang/gno") - out, err := cmd.CombinedOutput() - if err == nil { - return strings.TrimSpace(string(out)), nil - } - } - - // Try to guess GNOROOT using caller stack. - if _, filename, _, ok := runtime.Caller(1); ok && filepath.IsAbs(filename) { - if currentDir := filepath.Dir(filename); currentDir != "" { - // Gno root directory relative from `app.go` path: - // gno/ .. /gno.land/ .. /pkg/ .. /gnoland/app.go - rootdir, err := filepath.Abs(filepath.Join(currentDir, "..", "..", "..")) - if err == nil { - return rootdir, nil - } - } - } - - return "", errors.New("unable to guess gno's root-directory") -} diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index c3e117a0e9c..0d19237eab3 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys" @@ -63,7 +63,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // `gnoRootDir` should point to the local location of the gno repository. // It serves as the gno equivalent of GOROOT. - gnoRootDir := gnoland.MustGuessGnoRootDir() + gnoRootDir := gnoenv.RootDir() // `gnoHomeDir` should be the local directory where gnokey stores keys. gnoHomeDir := filepath.Join(tmpdir, "gno") diff --git a/gnovm/Makefile b/gnovm/Makefile index 34e94f88633..29510e9a1da 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -1,3 +1,5 @@ +GNOROOT_DIR ?= $(abspath $(lastword $(MAKEFILE_LIST))/../../) + .PHONY: help help: @echo "Available make commands:" @@ -5,13 +7,16 @@ help: rundep=go run -modfile ../misc/devdeps/go.mod +# We can't use '-trimpath' yet as amino use absolute path from call stack +# to find some directory: see #1236 +GOBUILD_FLAGS := -ldflags "-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" .PHONY: build build: - go build -o build/gno ./cmd/gno + go build $(GOBUILD_FLAGS) -o build/gno ./cmd/gno .PHONY: install install: - go install ./cmd/gno + go install $(GOBUILD_FLAGS) ./cmd/gno .PHONY: clean clean: diff --git a/gnovm/cmd/gno/clean.go b/gnovm/cmd/gno/clean.go index dad5332c4fa..444131986cb 100644 --- a/gnovm/cmd/gno/clean.go +++ b/gnovm/cmd/gno/clean.go @@ -9,9 +9,9 @@ import ( "path/filepath" "strings" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" ) type cleanCfg struct { @@ -82,7 +82,7 @@ func execClean(cfg *cleanCfg, args []string, io commands.IO) error { } if cfg.modCache { - modCacheDir := filepath.Join(client.HomeDir(), "pkg", "mod") + modCacheDir := filepath.Join(gnoenv.HomeDir(), "pkg", "mod") if !cfg.dryRun { if err := os.RemoveAll(modCacheDir); err != nil { return err diff --git a/gnovm/cmd/gno/doc.go b/gnovm/cmd/gno/doc.go index 4617c31741e..1661abd9b84 100644 --- a/gnovm/cmd/gno/doc.go +++ b/gnovm/cmd/gno/doc.go @@ -10,6 +10,7 @@ import ( "path/filepath" "github.com/gnolang/gno/gnovm/pkg/doc" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -77,7 +78,7 @@ func (c *docCfg) RegisterFlags(fs *flag.FlagSet) { func execDoc(cfg *docCfg, args []string, io commands.IO) error { // guess opts.RootDir if cfg.rootDir == "" { - cfg.rootDir = guessRootDir() + cfg.rootDir = gnoenv.RootDir() } wd, err := os.Getwd() diff --git a/gnovm/cmd/gno/env.go b/gnovm/cmd/gno/env.go new file mode 100644 index 00000000000..4ccd1873c3f --- /dev/null +++ b/gnovm/cmd/gno/env.go @@ -0,0 +1,113 @@ +package main + +import ( + "context" + "flag" + + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type envCfg struct { + json bool +} + +func newEnvCmd(io commands.IO) *commands.Command { + c := &envCfg{} + return commands.NewCommand( + commands.Metadata{ + Name: "env", + ShortUsage: "env [flags] ", + ShortHelp: "`env` prints Gno environment information", + }, + c, + func(_ context.Context, args []string) error { + return execEnv(c, args, io) + }, + ) +} + +func (c *envCfg) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.json, + "json", + false, + "Prints the environment in JSON format instead of as a shell script.", + ) +} + +type envVar struct { + Key string + Value string +} + +func findEnv(env []envVar, name string) string { + for _, e := range env { + if e.Key == name { + return e.Value + } + } + return "" +} + +type envPrinter func(vars []envVar, io commands.IO) + +func execEnv(cfg *envCfg, args []string, io commands.IO) error { + envs := []envVar{ + // GNOROOT Should point to the local location of the GNO repository. + // It serves as the gno equivalent of `GOROOT`. + {Key: "GNOROOT", Value: gnoenv.RootDir()}, + // GNOHOME Should point to the user local configuration. + // The most common place for this should be $HOME/gno. + {Key: "GNOHOME", Value: gnoenv.HomeDir()}, + } + + // Setup filters + filters := make([]envVar, len(args)) + for i, arg := range args { + filters[i] = envVar{Key: arg, Value: findEnv(envs, arg)} + } + + // Setup printer + var printerEnv envPrinter + if cfg.json { + printerEnv = printJSON + } else { + printerEnv = getPrinterShell(len(args) == 0) + } + + // Print environements + if len(filters) > 0 { + printerEnv(filters, io) + } else { + printerEnv(envs, io) + } + + return nil +} + +func getPrinterShell(printkeys bool) envPrinter { + return func(vars []envVar, io commands.IO) { + for _, env := range vars { + if printkeys { + io.Printf("%s=%q\n", env.Key, env.Value) + } else { + io.Printf("%s\n", env.Value) + } + } + } +} + +func printJSON(vars []envVar, io commands.IO) { + io.Println("{") + for i, env := range vars { + io.Printf("\t%q: %q", env.Key, env.Value) + if i != len(vars)-1 { + io.Printf(",") + } + + // Jump to next line + io.Printf("\n") + } + io.Println("}") +} diff --git a/gnovm/cmd/gno/env_test.go b/gnovm/cmd/gno/env_test.go new file mode 100644 index 00000000000..8aeb84ab2cc --- /dev/null +++ b/gnovm/cmd/gno/env_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "testing" +) + +func TestEnvApp(t *testing.T) { + const ( + testGnoRootEnv = "/faster/better/stronger" + testGnoHomeEnv = "/around/the/world" + ) + + t.Setenv("GNOROOT", testGnoRootEnv) + t.Setenv("GNOHOME", testGnoHomeEnv) + tc := []testMainCase{ + // shell + {args: []string{"env", "foo"}, stdoutShouldBe: "\n"}, + {args: []string{"env", "foo", "bar"}, stdoutShouldBe: "\n\n"}, + {args: []string{"env", "GNOROOT"}, stdoutShouldBe: testGnoRootEnv + "\n"}, + {args: []string{"env", "GNOHOME", "storm"}, stdoutShouldBe: testGnoHomeEnv + "\n\n"}, + {args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOROOT=%q", testGnoRootEnv)}, + {args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOHOME=%q", testGnoHomeEnv)}, + + // json + {args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOROOT\": %q", testGnoRootEnv)}, + {args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOHOME\": %q", testGnoHomeEnv)}, + { + args: []string{"env", "-json", "GNOROOT"}, + stdoutShouldBe: fmt.Sprintf("{\n\t\"GNOROOT\": %q\n}\n", testGnoRootEnv), + }, + { + args: []string{"env", "-json", "GNOROOT", "storm"}, + stdoutShouldBe: fmt.Sprintf("{\n\t\"GNOROOT\": %q,\n\t\"storm\": \"\"\n}\n", testGnoRootEnv), + }, + { + args: []string{"env", "-json", "storm"}, + stdoutShouldBe: "{\n\t\"storm\": \"\"\n}\n", + }, + } + + testMainCaseRun(t, tc) +} diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index ad223afa3b0..a90f432b7c9 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" osm "github.com/gnolang/gno/tm2/pkg/os" ) @@ -51,7 +52,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { rootDir = cfg.rootDir ) if rootDir == "" { - rootDir = guessRootDir() + rootDir = gnoenv.RootDir() } pkgPaths, err := gnoPackagesFromArgs(args) diff --git a/gnovm/cmd/gno/main.go b/gnovm/cmd/gno/main.go index 751a78748f4..cbe204f9700 100644 --- a/gnovm/cmd/gno/main.go +++ b/gnovm/cmd/gno/main.go @@ -37,6 +37,7 @@ func newGnocliCmd(io commands.IO) *commands.Command { newCleanCmd(io), newReplCmd(), newDocCmd(io), + newEnvCmd(io), // fmt -- gofmt // graph // vendor -- download deps from the chain in vendor/ diff --git a/gnovm/cmd/gno/repl.go b/gnovm/cmd/gno/repl.go index 0a9d4934ce3..18a7db16568 100644 --- a/gnovm/cmd/gno/repl.go +++ b/gnovm/cmd/gno/repl.go @@ -10,6 +10,7 @@ import ( "os" "strings" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/repl" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -81,7 +82,7 @@ func execRepl(cfg *replCfg, args []string) error { } if cfg.rootDir == "" { - cfg.rootDir = guessRootDir() + cfg.rootDir = gnoenv.RootDir() } if !cfg.skipUsage { diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index d37c86df502..4291d5fe411 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/tests" "github.com/gnolang/gno/tm2/pkg/commands" @@ -65,7 +66,7 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { } if cfg.rootDir == "" { - cfg.rootDir = guessRootDir() + cfg.rootDir = gnoenv.RootDir() } stdin := io.In() diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index edfd36efed3..bd3284ce84a 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -16,6 +16,7 @@ import ( "go.uber.org/multierr" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/gnovm/tests" @@ -179,7 +180,7 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error { // guess opts.RootDir if cfg.rootDir == "" { - cfg.rootDir = guessRootDir() + cfg.rootDir = gnoenv.RootDir() } paths, err := targetsFromPatterns(args) diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go index 73ee0f0323b..a8e835d4759 100644 --- a/gnovm/cmd/gno/util.go +++ b/gnovm/cmd/gno/util.go @@ -5,14 +5,13 @@ import ( "go/ast" "io" "io/fs" - "log" "os" - "os/exec" "path/filepath" "regexp" "strings" "time" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" ) @@ -193,22 +192,6 @@ func fmtDuration(d time.Duration) string { return fmt.Sprintf("%.2fs", d.Seconds()) } -func guessRootDir() string { - // try to get the root directory from the GNOROOT environment variable. - if rootdir := os.Getenv("GNOROOT"); rootdir != "" { - return filepath.Clean(rootdir) - } - - // if GNOROOT is not set, try to guess the root directory using the `go list` command. - cmd := exec.Command("go", "list", "-m", "-mod=mod", "-f", "{{.Dir}}", "github.com/gnolang/gno") - out, err := cmd.CombinedOutput() - if err != nil { - log.Fatal("can't guess --root-dir, please fill it manually or define the GNOROOT environment variable globally.") - } - rootDir := strings.TrimSpace(string(out)) - return rootDir -} - // makeTestGoMod creates the temporary go.mod for test func makeTestGoMod(path string, packageName string, goversion string) error { content := fmt.Sprintf("module %s\n\ngo %s\n", packageName, goversion) @@ -242,7 +225,7 @@ func ResolvePath(output string, path importPath) (string, error) { if err != nil { return "", err } - pkgPath := strings.TrimPrefix(absPkgPath, guessRootDir()) + pkgPath := strings.TrimPrefix(absPkgPath, gnoenv.RootDir()) return filepath.Join(absOutput, pkgPath), nil } diff --git a/gnovm/pkg/gnoenv/gnohome.go b/gnovm/pkg/gnoenv/gnohome.go new file mode 100644 index 00000000000..52dd5e6adb4 --- /dev/null +++ b/gnovm/pkg/gnoenv/gnohome.go @@ -0,0 +1,35 @@ +package gnoenv + +import ( + "fmt" + "os" + "path/filepath" +) + +func HomeDir() string { + // if environment variable is set, always use that. + // otherwise, use config dir (varies depending on OS) + "gno" + + dir := os.Getenv("GNOHOME") + if dir != "" { + return dir + } + + // XXX: `GNO_HOME` is deprecated and should be replaced by `GNOHOME` + // keeping for compatibility support + dir = os.Getenv("GNO_HOME") + if dir != "" { + return dir + } + + var err error + dir, err = os.UserConfigDir() + if err != nil { + panic(fmt.Errorf("couldn't get user config dir: %w", err)) + } + gnoHome := filepath.Join(dir, "gno") + + // XXX: added april 2023 as a transitory measure - remove after test4 + fixOldDefaultGnoHome(gnoHome) + return gnoHome +} diff --git a/gnovm/pkg/gnoenv/gnohome_test.go b/gnovm/pkg/gnoenv/gnohome_test.go new file mode 100644 index 00000000000..827d87774bb --- /dev/null +++ b/gnovm/pkg/gnoenv/gnohome_test.go @@ -0,0 +1,43 @@ +package gnoenv + +import ( + "os" + "path/filepath" + "testing" + + "github.com/jaekwon/testify/require" +) + +func TestHomeDir(t *testing.T) { + t.Run("use GNOHOME if set", func(t *testing.T) { + // Backup any related environment variables + t.Setenv("GNOHOME", "") + t.Setenv("GNO_HOME", "") + + expected := "/test/gno_home" + os.Setenv("GNOHOME", expected) + require.Equal(t, expected, HomeDir()) + }) + + t.Run("fallback to GNO_HOME if set", func(t *testing.T) { + // Backup any related environment variables + t.Setenv("GNOHOME", "") + t.Setenv("GNO_HOME", "") + t.Log("`GNO_HOME` is deprecated, use `GNOHOME` instead") + + expected := "/test/gnohome" + os.Setenv("GNO_HOME", expected) + require.Equal(t, expected, HomeDir()) + }) + + t.Run("use UserConfigDir with gno", func(t *testing.T) { + // Backup any related environment variables + t.Setenv("GNOHOME", "") + t.Setenv("GNO_HOME", "") + + dir, err := os.UserConfigDir() + require.NoError(t, err) + expected := filepath.Join(dir, "gno") + require.Equal(t, expected, HomeDir()) + }) +} diff --git a/gnovm/pkg/gnoenv/gnoroot.go b/gnovm/pkg/gnoenv/gnoroot.go new file mode 100644 index 00000000000..ad66cd93b57 --- /dev/null +++ b/gnovm/pkg/gnoenv/gnoroot.go @@ -0,0 +1,92 @@ +package gnoenv + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "sync" +) + +var ErrUnableToGuessGnoRoot = errors.New("gno was unable to determine GNOROOT. Please set the GNOROOT environment variable") + +// Can be set manually at build time using: +// -ldflags="-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT" +var _GNOROOT string + +// RootDir guesses the Gno root directory and panics if it fails. +func RootDir() string { + root, err := GuessRootDir() + if err != nil { + panic(err) + } + + return root +} + +var muGnoRoot sync.Mutex + +// GuessRootDir attempts to determine the Gno root directory using various strategies: +// 1. First, It tries to obtain it from the `GNOROOT` environment variable. +// 2. If the env variable isn't set, It checks if `_GNOROOT` has been previously determined or set with -ldflags. +// 3. If not, it uses the `go list` command to infer from go.mod. +// 4. As a last resort, it determines `GNOROOT` based on the caller stack's file path. +func GuessRootDir() (string, error) { + muGnoRoot.Lock() + defer muGnoRoot.Unlock() + + // First try to get the root directory from the `GNOROOT` environment variable. + if rootdir := os.Getenv("GNOROOT"); rootdir != "" { + return strings.TrimSpace(rootdir), nil + } + + var err error + if _GNOROOT == "" { + // Try to guess `GNOROOT` using various strategies + _GNOROOT, err = guessRootDir() + } + + return _GNOROOT, err +} + +func guessRootDir() (string, error) { + // Attempt to guess `GNOROOT` from go.mod by using the `go list` command. + if rootdir, err := inferRootFromGoMod(); err == nil { + return filepath.Clean(rootdir), nil + } + + // If the above method fails, ultimately try to determine `GNOROOT` based + // on the caller stack's file path. + // Path need to be absolute here, that mostly mean that if `-trimpath` + // as been passed this method will not works. + if _, filename, _, ok := runtime.Caller(1); ok && filepath.IsAbs(filename) { + if currentDir := filepath.Dir(filename); currentDir != "" { + // Deduce Gno root directory relative from the current file's path. + // gno/ .. /gnovm/ .. /pkg/ .. /gnoenv/gnoenv.go + rootdir, err := filepath.Abs(filepath.Join(currentDir, "..", "..", "..")) + if err == nil { + return rootdir, nil + } + } + } + + return "", ErrUnableToGuessGnoRoot +} + +func inferRootFromGoMod() (string, error) { + gobin, err := exec.LookPath("go") + if err != nil { + return "", fmt.Errorf("unable to find `go` binary: %w", err) + } + + cmd := exec.Command(gobin, "list", "-m", "-mod=mod", "-f", "{{.Dir}}", "github.com/gnolang/gno") + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("unable to infer GnoRoot from go.mod: %w", err) + } + + return strings.TrimSpace(string(out)), nil +} diff --git a/gnovm/pkg/gnoenv/gnoroot_test.go b/gnovm/pkg/gnoenv/gnoroot_test.go new file mode 100644 index 00000000000..300ed8727b3 --- /dev/null +++ b/gnovm/pkg/gnoenv/gnoroot_test.go @@ -0,0 +1,75 @@ +package gnoenv + +import ( + "os/exec" + "path/filepath" + "testing" + + "github.com/jaekwon/testify/require" +) + +func TestGuessGnoRootDir_WithSetGnoRoot(t *testing.T) { + originalGnoRoot := _GNOROOT + defer func() { _GNOROOT = originalGnoRoot }() // Restore after test + + t.Setenv("GNOROOT", "") + + const testPath = "/path/to/gnoRoot" + + _GNOROOT = testPath + root, err := GuessRootDir() + require.NoError(t, err) + require.Equal(t, root, testPath) +} + +func TestGuessGnoRootDir_UsingCallerStack(t *testing.T) { + originalGnoRoot := _GNOROOT + defer func() { _GNOROOT = originalGnoRoot }() + + // Unset PATH should prevent InferGnoRootFromGoMod to works + t.Setenv("GNOROOT", "") + t.Setenv("PATH", "") + + _, err := exec.LookPath("go") + require.Error(t, err) + + // gno/ .. /gnovm/ .. /pkg/ .. /gnoenv/gnoroot.go + testPath, _ := filepath.Abs(filepath.Join(".", "..", "..", "..")) + root, err := GuessRootDir() + require.NoError(t, err) + require.Equal(t, root, testPath) +} + +func TestGuessGnoRootDir_Error(t *testing.T) { + // XXX: Determine a method to test the GuessGnoRoot final error. + // One approach might be to use `txtar` to build a test binary with -trimpath, + // avoiding absolute paths in the call stack. + t.Skip("not implemented; refer to the inline comment for more details.") +} + +func TestGuessGnoRootDir_WithGoModList(t *testing.T) { + // XXX: find a way to test `go mod list` phase. + // One solution is to use txtar with embed go.mod file. + // For now only `inferGnoRootFromGoMod` is tested. + t.Skip("not implemented; refer to the inline comment for more details.") +} + +func TestInferGnoRootFromGoMod(t *testing.T) { + // gno/ .. /gnovm/ .. /pkg/ .. /gnoenv/gnoroot.go + testPath, _ := filepath.Abs(filepath.Join(".", "..", "..", "..")) + + t.Run("go is present", func(t *testing.T) { + root, err := inferRootFromGoMod() + require.NoError(t, err) + require.Equal(t, root, testPath) + }) + + t.Run("go is not present", func(t *testing.T) { + // Unset PATH should prevent `inferGnoRootFromGoMod` to works + t.Setenv("PATH", "") + + root, err := inferRootFromGoMod() + require.Error(t, err) + require.Empty(t, root) + }) +} diff --git a/tm2/pkg/crypto/keys/client/migration.go b/gnovm/pkg/gnoenv/migration.go similarity index 98% rename from tm2/pkg/crypto/keys/client/migration.go rename to gnovm/pkg/gnoenv/migration.go index a3b1f029ba2..5b1d1fd1fa0 100644 --- a/tm2/pkg/crypto/keys/client/migration.go +++ b/gnovm/pkg/gnoenv/migration.go @@ -1,4 +1,4 @@ -package client +package gnoenv import ( "log" diff --git a/gnovm/pkg/gnoenv/migration_test.go b/gnovm/pkg/gnoenv/migration_test.go new file mode 100644 index 00000000000..7ab12b59b67 --- /dev/null +++ b/gnovm/pkg/gnoenv/migration_test.go @@ -0,0 +1,29 @@ +package gnoenv + +import ( + "os" + "path/filepath" + "testing" + + "github.com/jaekwon/testify/require" +) + +func TestFixOldDefaultGnoHome(t *testing.T) { + tempHomeDir := t.TempDir() + t.Setenv("HOME", tempHomeDir) + + oldGnoHome := filepath.Join(tempHomeDir, ".gno") + newGnoHome := filepath.Join(tempHomeDir, "gno") + + // Create a dummy old GNO_HOME + os.Mkdir(oldGnoHome, 0o755) + + // Test migration + fixOldDefaultGnoHome(newGnoHome) + + _, errOld := os.Stat(oldGnoHome) + require.NotNil(t, errOld) + _, errNew := os.Stat(newGnoHome) + require.True(t, os.IsNotExist(errOld), "invalid errors", errOld) + require.NoError(t, errNew) +} diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index 3c224bafb87..d750b7c9f29 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -7,8 +7,8 @@ import ( "path/filepath" "strings" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" "github.com/gnolang/gno/tm2/pkg/std" "golang.org/x/mod/modfile" "golang.org/x/mod/module" @@ -18,7 +18,7 @@ const queryPathFile = "vm/qfile" // GetGnoModPath returns the path for gno modules func GetGnoModPath() string { - return filepath.Join(client.HomeDir(), "pkg", "mod") + return filepath.Join(gnoenv.HomeDir(), "pkg", "mod") } // PackageDir resolves a given module.Version to the path on the filesystem. diff --git a/gnovm/pkg/integration/gno.go b/gnovm/pkg/integration/gno.go index 7358c93ae75..ee0216fa9e8 100644 --- a/gnovm/pkg/integration/gno.go +++ b/gnovm/pkg/integration/gno.go @@ -8,6 +8,7 @@ import ( "path/filepath" "testing" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/rogpeppe/go-internal/testscript" ) @@ -18,16 +19,7 @@ import ( // to correctly set up the environment variables needed for the `gno` command. func SetupGno(p *testscript.Params, buildDir string) error { // Try to fetch `GNOROOT` from the environment variables - gnoroot := os.Getenv("GNOROOT") - if gnoroot == "" { - // If `GNOROOT` isn't set, determine the root directory of github.com/gnolang/gno - goModPath, err := exec.Command("go", "env", "GOMOD").CombinedOutput() - if err != nil { - return fmt.Errorf("unable to determine gno root directory") - } - - gnoroot = filepath.Dir(string(goModPath)) - } + gnoroot := gnoenv.RootDir() if !osm.DirExists(buildDir) { return fmt.Errorf("%q does not exist or is not a directory", buildDir) diff --git a/tm2/pkg/crypto/keys/client/common.go b/tm2/pkg/crypto/keys/client/common.go index cfadd05120b..a6b52b6cad3 100644 --- a/tm2/pkg/crypto/keys/client/common.go +++ b/tm2/pkg/crypto/keys/client/common.go @@ -1,11 +1,5 @@ package client -import ( - "fmt" - "os" - "path/filepath" -) - type BaseOptions struct { Home string Remote string @@ -15,27 +9,9 @@ type BaseOptions struct { } var DefaultBaseOptions = BaseOptions{ - Home: HomeDir(), + Home: "", Remote: "127.0.0.1:26657", Quiet: false, InsecurePasswordStdin: false, Config: "", } - -func HomeDir() string { - // if environment variable is set, always use that. - // otherwise, use config dir (varies depending on OS) + "gno" - var err error - dir := os.Getenv("GNO_HOME") - if dir != "" { - return dir - } - dir, err = os.UserConfigDir() - if err != nil { - panic(fmt.Errorf("couldn't get user config dir: %w", err)) - } - gnoHome := filepath.Join(dir, "gno") - // XXX: added april 2023 as a transitory measure - remove after test4 - fixOldDefaultGnoHome(gnoHome) - return gnoHome -} diff --git a/tm2/pkg/crypto/keys/client/root.go b/tm2/pkg/crypto/keys/client/root.go index 61fd7d077b5..e09b31d45dd 100644 --- a/tm2/pkg/crypto/keys/client/root.go +++ b/tm2/pkg/crypto/keys/client/root.go @@ -19,7 +19,13 @@ type baseCfg struct { } func NewRootCmd(io commands.IO) *commands.Command { - cfg := &baseCfg{} + return NewRootCmdWithBaseConfig(io, DefaultBaseOptions) +} + +func NewRootCmdWithBaseConfig(io commands.IO, base BaseOptions) *commands.Command { + cfg := &baseCfg{ + BaseOptions: base, + } cmd := commands.NewCommand( commands.Metadata{ @@ -56,35 +62,35 @@ func (c *baseCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.Home, "home", - DefaultBaseOptions.Home, + c.Home, "home directory", ) fs.StringVar( &c.Remote, "remote", - DefaultBaseOptions.Remote, + c.Remote, "remote node URL", ) fs.BoolVar( &c.Quiet, "quiet", - DefaultBaseOptions.Quiet, + c.Quiet, "suppress output during execution", ) fs.BoolVar( &c.InsecurePasswordStdin, "insecure-password-stdin", - DefaultBaseOptions.Quiet, + c.Quiet, "WARNING! take password from stdin", ) fs.StringVar( &c.Config, "config", - DefaultBaseOptions.Config, + c.Config, "config file (optional)", ) }