From 556d8c104c83d8573e052063b7ab8201c5b6a365 Mon Sep 17 00:00:00 2001 From: Reece Williams <31943163+Reecepbcups@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:33:18 -0600 Subject: [PATCH] feat: plugin support (#71) * example plugin * plugins * simplify * simplify * use `*cobra.Command` instead of a func --- cmd/spawn/main.go | 84 +++++++++++++++++++++++++++++++ plugins/example/build.sh | 4 ++ plugins/example/example-plugin.go | 61 ++++++++++++++++++++++ plugins/plugin.go | 25 +++++++++ 4 files changed, 174 insertions(+) create mode 100644 plugins/example/build.sh create mode 100644 plugins/example/example-plugin.go create mode 100644 plugins/plugin.go diff --git a/cmd/spawn/main.go b/cmd/spawn/main.go index 83b88664..8959b95d 100644 --- a/cmd/spawn/main.go +++ b/cmd/spawn/main.go @@ -2,15 +2,19 @@ package main import ( "fmt" + "io/fs" "log" "log/slog" "os" + "path" + "plugin" "strings" "time" "github.com/lmittmann/tint" "github.com/mattn/go-isatty" "github.com/spf13/cobra" + "gitub.com/strangelove-ventures/spawn/plugins" ) // Set in the makefile ld_flags on compile @@ -19,11 +23,14 @@ var SpawnVersion = "" var LogLevelFlag = "log-level" func main() { + rootCmd.AddCommand(newChain) rootCmd.AddCommand(LocalICCmd) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(ModuleCmd()) + applyPluginCmds() + rootCmd.PersistentFlags().String(LogLevelFlag, "info", "log level (debug, info, warn, error)") if err := rootCmd.Execute(); err != nil { @@ -50,6 +57,83 @@ func GetLogger() *slog.Logger { return slog.Default() } +func applyPluginCmds() { + plugins := &cobra.Command{ + Use: "plugins", + Short: "Manage plugins", + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + log.Fatal(err) + } + }, + } + + for _, plugin := range loadPlugins() { + plugins.AddCommand(plugin.Cmd()) + } + + rootCmd.AddCommand(plugins) +} + +func loadPlugins() map[string]*plugins.SpawnPluginBase { + p := make(map[string]*plugins.SpawnPluginBase) + + homeDir, err := os.UserHomeDir() + if err != nil { + panic(err) + } + + pluginsDir := path.Join(homeDir, ".spawn", "plugins") + + d := os.DirFS(pluginsDir) + if _, err := d.Open("."); err != nil { + if os.IsNotExist(err) { + if err := os.MkdirAll(pluginsDir, 0755); err != nil { + panic(err) + } + } else { + panic(err) + } + } + + err = fs.WalkDir(d, ".", func(relPath string, d fs.DirEntry, e error) error { + if d.IsDir() { + return nil + } + + if !strings.Contains(relPath, ".so") { + return nil + } + + absPath := path.Join(pluginsDir, relPath) + + // read the absolute path + plug, err := plugin.Open(absPath) + if err != nil { + return fmt.Errorf("error opening plugin %s: %w", absPath, err) + } + + base, err := plug.Lookup("Plugin") + if err != nil { + return fmt.Errorf("error looking up symbol: %w", err) + } + + pluginInstance, ok := base.(plugins.SpawnPlugin) + if !ok { + log.Fatal("Symbol 'Plugin' does not implement the SpawnPlugin interface") + } + + p[relPath] = plugins.NewSpawnPluginBase(pluginInstance.Cmd()) + + return nil + }) + if err != nil { + panic(err) + } + + return p +} + var rootCmd = &cobra.Command{ Use: "spawn", Short: "Entry into the Interchain", diff --git a/plugins/example/build.sh b/plugins/example/build.sh new file mode 100644 index 00000000..75749e6d --- /dev/null +++ b/plugins/example/build.sh @@ -0,0 +1,4 @@ +EXPORT_LOC=$HOME/.spawn/plugins +mkdir -p $EXPORT_LOC + +go build -buildmode=plugin -o $EXPORT_LOC/example.so plugins/example/example-plugin.go \ No newline at end of file diff --git a/plugins/example/example-plugin.go b/plugins/example/example-plugin.go new file mode 100644 index 00000000..f9f2d390 --- /dev/null +++ b/plugins/example/example-plugin.go @@ -0,0 +1,61 @@ +package main + +import ( + "log" + "os" + "path" + + "github.com/spf13/cobra" + plugins "gitub.com/strangelove-ventures/spawn/plugins" +) + +// Make the plugin public +var Plugin SpawnMainExamplePlugin + +var _ plugins.SpawnPlugin = &SpawnMainExamplePlugin{} + +const ( + cmdName = "example" +) + +type SpawnMainExamplePlugin struct { + Impl plugins.SpawnPluginBase +} + +// Cmd implements plugins.SpawnPlugin. +func (e *SpawnMainExamplePlugin) Cmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: cmdName, + Short: cmdName + " plugin command", + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + log.Fatal(err) + } + }, + } + + rootCmd.AddCommand(&cobra.Command{ + Use: "touch-file [name]", + Short: "An example plugin sub command", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + + fileName := args[0] + + filePath := path.Join(cwd, fileName) + file, err := os.Create(filePath) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + cmd.Printf("Created file: %s\n", filePath) + }, + }) + + return rootCmd +} diff --git a/plugins/plugin.go b/plugins/plugin.go new file mode 100644 index 00000000..3e585b8f --- /dev/null +++ b/plugins/plugin.go @@ -0,0 +1,25 @@ +package plugins + +import "github.com/spf13/cobra" + +// SpawnPlugin is the interface that we're exposing as a plugin. +type SpawnPlugin interface { + Cmd() *cobra.Command +} + +var _ SpawnPlugin = &SpawnPluginBase{} + +type SpawnPluginBase struct { + cmd *cobra.Command +} + +func NewSpawnPluginBase(cmd *cobra.Command) *SpawnPluginBase { + return &SpawnPluginBase{ + cmd: cmd, + } +} + +// Cmd implements SpawnPlugin. +func (s *SpawnPluginBase) Cmd() *cobra.Command { + return s.cmd +}