From 304e5d5f07d1f69c88902799d10741c27b4d72c9 Mon Sep 17 00:00:00 2001 From: Alex Johnson Date: Wed, 14 Dec 2022 09:22:44 -0500 Subject: [PATCH] feat(`config`): global plugins config (#3214) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * base * base refactor * refactor * rename * rename * fix comment * rename file * format * refactor base * format * refactor some imports * imports refactor * fix * fix import * refactor: * changelog * changelog * Update ignite/services/network/networkchain/init.go Co-authored-by: Jerónimo Albi * base -> baseconfig * refactoring for clarity * v12 -> v1 * fix tests * fix integration * fix name * fix integration * format * Update ignite/cmd/cmd.go Co-authored-by: Jerónimo Albi * imports * LocateDefault logic * lint fix * refactor and test * finish refactor * revert * move * move * rename * fix tests * fix test * rename * add global * functionality * format * fix error statement for global plugins * typo * chainconfig * imports * fix imports * address review * testdata * better import * fix test * changelog * refactor * simplify * RemoveDuplicates * refactor LoadPlugins * lint * add global flag * add info * add error prints * simplify return * fix test * fix global parse * add integration test * modify test * fix returns * use ignite example plugin * refactor global flag * Fix plugin integration test Add more assertion and use an alternate `.ignite` folder to avoid conflicts with user home. Also fix `plugin add -g` when `.ignite/plugins` doesn't exist. * add removed TODO by mistake * move flags * return err * Update ignite/cmd/plugin.go Co-authored-by: Thomas Bruyelle * format * fix test * remove yaml tag * Update ignite/cmd/plugin.go Co-authored-by: Thomas Bruyelle * fix test * test: add key/value pairs to plugin add * add detailed plugin Co-authored-by: Jerónimo Albi Co-authored-by: Thomas Bruyelle Co-authored-by: Thomas Bruyelle --- changelog.md | 1 + ignite/cmd/plugin.go | 127 ++++++++++++++++++++---- ignite/config/config.go | 15 +-- ignite/config/plugins/config.go | 3 + ignite/pkg/env/env.go | 30 +++++- ignite/pkg/xfilepath/xfilepath.go | 54 ++++++---- ignite/pkg/xfilepath/xfilepath_test.go | 14 +++ ignite/services/plugin/plugin.go | 44 +++++--- ignite/services/plugin/plugin_test.go | 61 ++++++++++++ integration/account/cmd_account_test.go | 2 +- integration/env.go | 6 ++ integration/plugin/plugin_test.go | 109 ++++++++++++++++++++ 12 files changed, 395 insertions(+), 71 deletions(-) create mode 100644 integration/plugin/plugin_test.go diff --git a/changelog.md b/changelog.md index 0eab745caa..04779c5134 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ### Features +- [#3214](https://github.com/ignite/cli/pull/3214) Global plugins config. - [#3142](https://github.com/ignite/cli/pull/3142) Add `ignite network request param-change` command. - [#3181](https://github.com/ignite/cli/pull/3181) Addition of `add` `remove` commands for `plugins` - [#3184](https://github.com/ignite/cli/pull/3184) Separate `plugins.yml` config file. diff --git a/ignite/cmd/plugin.go b/ignite/cmd/plugin.go index c324716437..91e2a10c39 100644 --- a/ignite/cmd/plugin.go +++ b/ignite/cmd/plugin.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + flag "github.com/spf13/pflag" pluginsconfig "github.com/ignite/cli/ignite/config/plugins" "github.com/ignite/cli/ignite/pkg/clictx" @@ -19,29 +20,44 @@ import ( "github.com/ignite/cli/ignite/services/plugin" ) +const ( + igniteCmdPrefix = "ignite " + flagPluginsGlobal = "global" +) + // plugins hold the list of plugin declared in the config. // A global variable is used so the list is accessible to the plugin commands. var plugins []*plugin.Plugin -const ( - igniteCmdPrefix = "ignite " -) - -// LoadPlugins tries to load all the plugins found in configuration. -// If no configuration found, it returns w/o error. +// LoadPlugins tries to load all the plugins found in configurations. +// If no configurations found, it returns w/o error. func LoadPlugins(ctx context.Context, rootCmd *cobra.Command) error { - cfg, err := parseLocalPlugins(rootCmd) - if err != nil { - // if binary is run where there is no plugins.yml, don't load - return nil + pluginsConfigs := make([]pluginsconfig.Plugin, 0) + + localCfg, err := parseLocalPlugins(rootCmd) + if err != nil && !errors.As(err, &cosmosanalysis.ErrPathNotChain{}) { + return err + } else if err == nil { + pluginsConfigs = append(pluginsConfigs, localCfg.Plugins...) + } + + globalCfg, err := parseGlobalPlugins() + if err == nil { + pluginsConfigs = append(pluginsConfigs, globalCfg.Plugins...) } - // TODO: parse global config + if len(pluginsConfigs) == 0 { + return nil + } - plugins, err = plugin.Load(ctx, cfg) + uniquePlugins := plugin.RemoveDuplicates(pluginsConfigs) + plugins, err = plugin.Load(ctx, uniquePlugins) if err != nil { return err + } else if len(plugins) == 0 { + return nil } + return loadPlugins(rootCmd, plugins) } @@ -62,6 +78,25 @@ func parseLocalPlugins(cmd *cobra.Command) (*pluginsconfig.Config, error) { return pluginsconfig.ParseDir(wd) } +func parseGlobalPlugins() (cfg *pluginsconfig.Config, err error) { + globalDir, err := plugin.PluginsPath() + if err != nil { + return cfg, err + } + + cfg, err = pluginsconfig.ParseDir(globalDir) + // if there is error parsing, return empty config and continue execution to load + // local plugins if they exist. + if err != nil { + return &pluginsconfig.Config{}, nil + } + + for i := range cfg.Plugins { + cfg.Plugins[i].Global = true + } + return +} + func loadPlugins(rootCmd *cobra.Command, plugins []*plugin.Plugin) error { // Link plugins to related commands var loadErrors []string @@ -373,8 +408,8 @@ func NewPluginUpdate() *cobra.Command { func NewPluginAdd() *cobra.Command { cmdPluginAdd := &cobra.Command{ Use: "add [path] [key=value]...", - Short: "Adds a plugin declaration to a chain's plugin configuration", - Long: `Adds a plugin declaration to a chain's plugin configuration. + Short: "Adds a plugin declaration to a plugin configuration", + Long: `Adds a plugin declaration to a plugin configuration. Respects key value pairs declared after the plugin path to be added to the generated configuration definition. Example: @@ -384,7 +419,17 @@ Example: session := cliui.New(cliui.WithStdout(os.Stdout)) defer session.End() - conf, err := parseLocalPlugins(cmd) + var ( + conf *pluginsconfig.Config + err error + ) + + global := flagGetPluginsGlobal(cmd) + if global { + conf, err = parseGlobalPlugins() + } else { + conf, err = parseLocalPlugins(cmd) + } if err != nil { return err } @@ -396,8 +441,9 @@ Example: } p := pluginsconfig.Plugin{ - Path: args[0], - With: make(map[string]string), + Path: args[0], + With: make(map[string]string), + Global: global, } var pluginArgs []string @@ -432,6 +478,9 @@ Example: return nil }, } + + cmdPluginAdd.Flags().AddFlagSet(flagSetPluginsGlobal()) + return cmdPluginAdd } @@ -444,7 +493,17 @@ func NewPluginRemove() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { s := cliui.New(cliui.WithStdout(os.Stdout)) - conf, err := parseLocalPlugins(cmd) + var ( + conf *pluginsconfig.Config + err error + ) + + global := flagGetPluginsGlobal(cmd) + if global { + conf, err = parseGlobalPlugins() + } else { + conf, err = parseLocalPlugins(cmd) + } if err != nil { return err } @@ -461,10 +520,14 @@ func NewPluginRemove() *cobra.Command { } s.Printf("%s %s removed\n", icons.OK, args[0]) + s.Printf("\t%s updated\n", conf.Path()) return nil }, } + + cmdPluginRemove.Flags().AddFlagSet(flagSetPluginsGlobal()) + return cmdPluginRemove } @@ -552,12 +615,22 @@ func printPlugins(session *cliui.Session) error { hookCount = len(manifest.Hooks) cmdCount = len(manifest.Commands) ) - return fmt.Sprintf("%s Loaded 🪝%d 💻%d", icons.OK, hookCount, cmdCount) + + return fmt.Sprintf("%s Loaded: 🪝%d 💻%d", icons.OK, hookCount, cmdCount) + } + + installedStatus := func(p *plugin.Plugin) string { + installed := "local" + if p.IsGlobal() { + installed = "global" + } + return installed } + for _, p := range plugins { - entries = append(entries, []string{p.Path, buildStatus(p)}) + entries = append(entries, []string{p.Path, buildStatus(p), installedStatus(p)}) } - if err := session.PrintTable([]string{"Path", "Status"}, entries...); err != nil { + if err := session.PrintTable([]string{"Path", "Status", "Config"}, entries...); err != nil { return fmt.Errorf("error while printing plugins: %w", err) } return nil @@ -613,3 +686,15 @@ func printPluginHooks(hooks []plugin.Hook, session *cliui.Session) error { } return nil } + +func flagSetPluginsGlobal() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.BoolP(flagPluginsGlobal, "g", false, "use global plugins configuration"+ + " ($HOME/.ignite/plugins/plugins.yml)") + return fs +} + +func flagGetPluginsGlobal(cmd *cobra.Command) bool { + global, _ := cmd.Flags().GetBool(flagPluginsGlobal) + return global +} diff --git a/ignite/config/config.go b/ignite/config/config.go index 2217fa486a..44a5c4ff2b 100644 --- a/ignite/config/config.go +++ b/ignite/config/config.go @@ -1,20 +1,9 @@ package config import ( - "os" - + "github.com/ignite/cli/ignite/pkg/env" "github.com/ignite/cli/ignite/pkg/xfilepath" ) // DirPath returns the path of configuration directory of Ignite. -var DirPath = xfilepath.JoinFromHome(xfilepath.Path(".ignite")) - -// CreateConfigDir creates config directory if it is not created yet. -func CreateConfigDir() error { - path, err := DirPath() - if err != nil { - return err - } - - return os.MkdirAll(path, 0o755) -} +var DirPath = xfilepath.Mkdir(env.ConfigDir()) diff --git a/ignite/config/plugins/config.go b/ignite/config/plugins/config.go index 8b3be1598a..22afc75627 100644 --- a/ignite/config/plugins/config.go +++ b/ignite/config/plugins/config.go @@ -36,6 +36,9 @@ type Plugin struct { Path string `yaml:"path"` // With holds arguments passed to the plugin interface With map[string]string `yaml:"with,omitempty"` + // Global holds whether the plugin is installed globally + // (default: $HOME/.ignite/plugins/plugins.yml) or locally for a chain. + Global bool `yaml:"-"` } // Path return the path of the config file. diff --git a/ignite/pkg/env/env.go b/ignite/pkg/env/env.go index 09ee2de779..25c66fe152 100644 --- a/ignite/pkg/env/env.go +++ b/ignite/pkg/env/env.go @@ -1,11 +1,37 @@ package env -import "os" +import ( + "fmt" + "os" + "path" + + "github.com/ignite/cli/ignite/pkg/xfilepath" +) const ( - debug = "IGNT_DEBUG" + debug = "IGNT_DEBUG" + configDir = "IGNT_CONFIG_DIR" ) func DebugEnabled() bool { return os.Getenv(debug) == "1" } + +func ConfigDir() xfilepath.PathRetriever { + return func() (string, error) { + if dir := os.Getenv(configDir); dir != "" { + if !path.IsAbs(dir) { + panic(fmt.Sprintf("%s must be an absolute path", configDir)) + } + return dir, nil + } + return xfilepath.JoinFromHome(xfilepath.Path(".ignite"))() + } +} + +func SetConfigDir(dir string) { + err := os.Setenv(configDir, dir) + if err != nil { + panic(fmt.Sprintf("set config dir env: %v", err)) + } +} diff --git a/ignite/pkg/xfilepath/xfilepath.go b/ignite/pkg/xfilepath/xfilepath.go index 1707a7409a..ce6c09712d 100644 --- a/ignite/pkg/xfilepath/xfilepath.go +++ b/ignite/pkg/xfilepath/xfilepath.go @@ -26,19 +26,18 @@ func PathWithError(path string, err error) PathRetriever { // The returned path retriever eventually returns the error from the first provided path retrievers // that returns a non-nil error. func Join(paths ...PathRetriever) PathRetriever { - var components []string - var err error - for _, path := range paths { - var component string - component, err = path() - if err != nil { - break - } - components = append(components, component) - } - path := filepath.Join(components...) - return func() (string, error) { + var components []string + var err error + for _, path := range paths { + var component string + component, err = path() + if err != nil { + break + } + components = append(components, component) + } + path := filepath.Join(components...) return path, err } } @@ -52,18 +51,29 @@ func JoinFromHome(paths ...PathRetriever) PathRetriever { // List returns a paths retriever from a list of path retrievers. // The returned paths retriever eventually returns the error from the first provided path retrievers that returns a non-nil error. func List(paths ...PathRetriever) PathsRetriever { - var list []string - var err error - for _, path := range paths { - var resolved string - resolved, err = path() - if err != nil { - break + return func() ([]string, error) { + var list []string + var err error + for _, path := range paths { + var resolved string + resolved, err = path() + if err != nil { + break + } + list = append(list, resolved) } - list = append(list, resolved) - } - return func() ([]string, error) { return list, err } } + +// Mkdir ensure path exists before returning it. +func Mkdir(path PathRetriever) PathRetriever { + return func() (string, error) { + p, err := path() + if err != nil { + return "", err + } + return p, os.MkdirAll(p, 0o755) + } +} diff --git a/ignite/pkg/xfilepath/xfilepath_test.go b/ignite/pkg/xfilepath/xfilepath_test.go index 5e459daf81..9b5216666d 100644 --- a/ignite/pkg/xfilepath/xfilepath_test.go +++ b/ignite/pkg/xfilepath/xfilepath_test.go @@ -3,9 +3,11 @@ package xfilepath_test import ( "errors" "os" + "path" "path/filepath" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ignite/cli/ignite/pkg/xfilepath" @@ -88,3 +90,15 @@ func TestList(t *testing.T) { _, err = retriever() require.Error(t, err) } + +func TestMkdir(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + newdir := path.Join(t.TempDir(), "hey") + + dir, err := xfilepath.Mkdir(xfilepath.Path(newdir))() + + require.NoError(err) + assert.Equal(newdir, dir) + assert.DirExists(dir) +} diff --git a/ignite/services/plugin/plugin.go b/ignite/services/plugin/plugin.go index 8e8212824b..68280dc560 100644 --- a/ignite/services/plugin/plugin.go +++ b/ignite/services/plugin/plugin.go @@ -30,11 +30,11 @@ import ( "github.com/ignite/cli/ignite/pkg/xurl" ) -// pluginsPath holds the plugin cache directory. -var pluginsPath = xfilepath.Join( +// PluginsPath holds the plugin cache directory. +var PluginsPath = xfilepath.Mkdir(xfilepath.Join( config.DirPath, xfilepath.Path("plugins"), -) +)) // Plugin represents a ignite plugin. type Plugin struct { @@ -67,24 +67,23 @@ type Plugin struct { // If an error occurs during a plugin load, it's not returned but rather stored // in the Plugin.Error field. This prevents the loading of other plugins to be // interrupted. -func Load(ctx context.Context, cfg *pluginsconfig.Config) ([]*Plugin, error) { - pluginsDir, err := pluginsPath() +func Load(ctx context.Context, plugins []pluginsconfig.Plugin) ([]*Plugin, error) { + pluginsDir, err := PluginsPath() if err != nil { return nil, errors.WithStack(err) } - var plugins []*Plugin - for _, cp := range cfg.Plugins { + var loaded []*Plugin + for _, cp := range plugins { p := newPlugin(pluginsDir, cp) p.load(ctx) - // TODO: override global plugins with locally defined ones - plugins = append(plugins, p) + loaded = append(loaded, p) } - return plugins, nil + return loaded, nil } func LoadSingle(ctx context.Context, pluginCfg *pluginsconfig.Plugin) (*Plugin, error) { - pluginsDir, err := pluginsPath() + pluginsDir, err := PluginsPath() if err != nil { return nil, errors.WithStack(err) } @@ -110,7 +109,9 @@ func Update(plugins ...*Plugin) error { // newPlugin creates a Plugin from configuration. func newPlugin(pluginsDir string, cp pluginsconfig.Plugin) *Plugin { var ( - p = &Plugin{Plugin: cp} + p = &Plugin{ + Plugin: cp, + } pluginPath = cp.Path ) if pluginPath == "" { @@ -151,15 +152,34 @@ func newPlugin(pluginsDir string, cp pluginsconfig.Plugin) *Plugin { p.cloneDir = path.Join(pluginsDir, p.repoPath) p.srcPath = path.Join(pluginsDir, p.repoPath, path.Join(parts[3:]...)) p.binaryName = path.Base(pluginPath) + return p } +// RemoveDuplicates takes a list of pluginsconfig.Plugins and returns a new list with only unique values. +func RemoveDuplicates(plugins []pluginsconfig.Plugin) (unique []pluginsconfig.Plugin) { + keys := make(map[string]bool) + for _, plugin := range plugins { + if _, value := keys[plugin.Path]; !value { + keys[plugin.Path] = true + unique = append(unique, plugin) + } + } + return unique +} + +// KillClient kills the running plugin client. func (p *Plugin) KillClient() { if p.client != nil { p.client.Kill() } } +// IsGlobal returns whether the plugin is installed globally or locally for a chain. +func (p *Plugin) IsGlobal() bool { + return p.Plugin.Global +} + func (p *Plugin) isLocal() bool { return p.cloneURL == "" } diff --git a/ignite/services/plugin/plugin_test.go b/ignite/services/plugin/plugin_test.go index 420269b210..55878f95b6 100644 --- a/ignite/services/plugin/plugin_test.go +++ b/ignite/services/plugin/plugin_test.go @@ -394,6 +394,67 @@ func TestPluginClean(t *testing.T) { } } +func TestRemoveDuplicates(t *testing.T) { + tests := []struct { + name string + configs []pluginsconfig.Plugin + expected []pluginsconfig.Plugin + }{ + { + name: "do nothing for empty list", + configs: []pluginsconfig.Plugin(nil), + expected: []pluginsconfig.Plugin(nil), + }, + { + name: "remove duplicates", + configs: []pluginsconfig.Plugin{ + { + Path: "foo/bar", + }, + { + Path: "foo/bar", + }, + { + Path: "bar/foo", + }, + }, + expected: []pluginsconfig.Plugin{ + { + Path: "foo/bar", + }, + { + Path: "bar/foo", + }, + }, + }, + { + name: "do nothing for no duplicates", + configs: []pluginsconfig.Plugin{ + { + Path: "foo/bar", + }, + { + Path: "bar/foo", + }, + }, + expected: []pluginsconfig.Plugin{ + { + Path: "foo/bar", + }, + { + Path: "bar/foo", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + unique := RemoveDuplicates(tt.configs) + require.EqualValues(t, tt.expected, unique) + }) + } +} + func assertPlugin(t *testing.T, want, have Plugin) { if want.Error != nil { require.Error(t, have.Error) diff --git a/integration/account/cmd_account_test.go b/integration/account/cmd_account_test.go index 9679220832..0ec564ef1b 100644 --- a/integration/account/cmd_account_test.go +++ b/integration/account/cmd_account_test.go @@ -49,7 +49,7 @@ func TestAccount(t *testing.T) { )), envtest.ExecStdout(listOutputAfterDeleteBuffer), )) - require.Equal(t, listOutputAfterDeleteBuffer.String(), "Name \tAddress Public Key \t\n\n") + require.Equal(t, "Name \tAddress Public Key \t\n\n", listOutputAfterDeleteBuffer.String()) env.Must(env.Exec("import account with mnemonic", step.NewSteps(step.New( diff --git a/integration/env.go b/integration/env.go index 39cb47ab4b..f653bbdbd5 100644 --- a/integration/env.go +++ b/integration/env.go @@ -18,6 +18,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ignite/cli/ignite/pkg/cosmosfaucet" + "github.com/ignite/cli/ignite/pkg/env" "github.com/ignite/cli/ignite/pkg/gocmd" "github.com/ignite/cli/ignite/pkg/gomodulepath" "github.com/ignite/cli/ignite/pkg/httpstatuschecker" @@ -53,6 +54,11 @@ func New(t *testing.T) Env { t: t, ctx: ctx, } + // To avoid conflicts with the default config folder located in $HOME, we + // set an other one thanks to env var. + cfgDir := path.Join(t.TempDir(), ".ignite") + env.SetConfigDir(cfgDir) + t.Cleanup(cancel) compileBinaryOnce.Do(func() { compileBinary(ctx) diff --git a/integration/plugin/plugin_test.go b/integration/plugin/plugin_test.go new file mode 100644 index 0000000000..fa67287c5d --- /dev/null +++ b/integration/plugin/plugin_test.go @@ -0,0 +1,109 @@ +package plugin_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + pluginsconfig "github.com/ignite/cli/ignite/config/plugins" + "github.com/ignite/cli/ignite/pkg/cmdrunner/step" + "github.com/ignite/cli/ignite/services/plugin" + envtest "github.com/ignite/cli/integration" +) + +func TestAddRemovePlugin(t *testing.T) { + var ( + require = require.New(t) + assert = assert.New(t) + env = envtest.New(t) + app = env.Scaffold("github.com/test/blog") + pluginRepo = "github.com/ignite/example-plugin" + + assertPlugins = func(expectedLocalPlugins, expectedGlobalPlugins []pluginsconfig.Plugin) { + localCfg, err := pluginsconfig.ParseDir(app.SourcePath()) + require.NoError(err) + assert.ElementsMatch(expectedLocalPlugins, localCfg.Plugins, "unexpected local plugins") + + globalCfgPath, err := plugin.PluginsPath() + require.NoError(err) + globalCfg, err := pluginsconfig.ParseDir(globalCfgPath) + require.NoError(err) + assert.ElementsMatch(expectedGlobalPlugins, globalCfg.Plugins, "unexpected global plugins") + } + ) + + // no plugins expected + assertPlugins(nil, nil) + + env.Must(env.Exec("add plugin locally", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, "plugin", "add", pluginRepo, "k1=v1", "k2=v2"), + step.Workdir(app.SourcePath()), + )), + )) + + // one local plugin expected + assertPlugins( + []pluginsconfig.Plugin{ + { + Path: pluginRepo, + With: map[string]string{ + "k1": "v1", + "k2": "v2", + }, + }, + }, + nil, + ) + + env.Must(env.Exec("remove plugin locally", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, "plugin", "remove", pluginRepo), + step.Workdir(app.SourcePath()), + )), + )) + + // no plugins expected + assertPlugins(nil, nil) + + env.Must(env.Exec("add plugin globally", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, "plugin", "add", pluginRepo, "-g"), + step.Workdir(app.SourcePath()), + )), + )) + + // one global plugins expected + assertPlugins( + nil, + []pluginsconfig.Plugin{ + { + Path: pluginRepo, + }, + }, + ) + + env.Must(env.Exec("remove plugin globally", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, "plugin", "remove", pluginRepo, "-g"), + step.Workdir(app.SourcePath()), + )), + )) + + // no plugins expected + assertPlugins(nil, nil) +} + +// TODO install network plugin test + +func TestPluginScaffold(t *testing.T) { + env := envtest.New(t) + + env.Must(env.Exec("add a plugin", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, "plugin", "scaffold", "test"), + step.Workdir(env.TmpDir()), + )), + )) +}