Skip to content

Commit 556d8c1

Browse files
authored
feat: plugin support (#71)
* example plugin * plugins * simplify * simplify * use `*cobra.Command` instead of a func
1 parent e5f27ca commit 556d8c1

File tree

4 files changed

+174
-0
lines changed

4 files changed

+174
-0
lines changed

cmd/spawn/main.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ package main
22

33
import (
44
"fmt"
5+
"io/fs"
56
"log"
67
"log/slog"
78
"os"
9+
"path"
10+
"plugin"
811
"strings"
912
"time"
1013

1114
"github.com/lmittmann/tint"
1215
"github.com/mattn/go-isatty"
1316
"github.com/spf13/cobra"
17+
"gitub.com/strangelove-ventures/spawn/plugins"
1418
)
1519

1620
// Set in the makefile ld_flags on compile
@@ -19,11 +23,14 @@ var SpawnVersion = ""
1923
var LogLevelFlag = "log-level"
2024

2125
func main() {
26+
2227
rootCmd.AddCommand(newChain)
2328
rootCmd.AddCommand(LocalICCmd)
2429
rootCmd.AddCommand(versionCmd)
2530
rootCmd.AddCommand(ModuleCmd())
2631

32+
applyPluginCmds()
33+
2734
rootCmd.PersistentFlags().String(LogLevelFlag, "info", "log level (debug, info, warn, error)")
2835

2936
if err := rootCmd.Execute(); err != nil {
@@ -50,6 +57,83 @@ func GetLogger() *slog.Logger {
5057
return slog.Default()
5158
}
5259

60+
func applyPluginCmds() {
61+
plugins := &cobra.Command{
62+
Use: "plugins",
63+
Short: "Manage plugins",
64+
Run: func(cmd *cobra.Command, args []string) {
65+
if err := cmd.Help(); err != nil {
66+
log.Fatal(err)
67+
}
68+
},
69+
}
70+
71+
for _, plugin := range loadPlugins() {
72+
plugins.AddCommand(plugin.Cmd())
73+
}
74+
75+
rootCmd.AddCommand(plugins)
76+
}
77+
78+
func loadPlugins() map[string]*plugins.SpawnPluginBase {
79+
p := make(map[string]*plugins.SpawnPluginBase)
80+
81+
homeDir, err := os.UserHomeDir()
82+
if err != nil {
83+
panic(err)
84+
}
85+
86+
pluginsDir := path.Join(homeDir, ".spawn", "plugins")
87+
88+
d := os.DirFS(pluginsDir)
89+
if _, err := d.Open("."); err != nil {
90+
if os.IsNotExist(err) {
91+
if err := os.MkdirAll(pluginsDir, 0755); err != nil {
92+
panic(err)
93+
}
94+
} else {
95+
panic(err)
96+
}
97+
}
98+
99+
err = fs.WalkDir(d, ".", func(relPath string, d fs.DirEntry, e error) error {
100+
if d.IsDir() {
101+
return nil
102+
}
103+
104+
if !strings.Contains(relPath, ".so") {
105+
return nil
106+
}
107+
108+
absPath := path.Join(pluginsDir, relPath)
109+
110+
// read the absolute path
111+
plug, err := plugin.Open(absPath)
112+
if err != nil {
113+
return fmt.Errorf("error opening plugin %s: %w", absPath, err)
114+
}
115+
116+
base, err := plug.Lookup("Plugin")
117+
if err != nil {
118+
return fmt.Errorf("error looking up symbol: %w", err)
119+
}
120+
121+
pluginInstance, ok := base.(plugins.SpawnPlugin)
122+
if !ok {
123+
log.Fatal("Symbol 'Plugin' does not implement the SpawnPlugin interface")
124+
}
125+
126+
p[relPath] = plugins.NewSpawnPluginBase(pluginInstance.Cmd())
127+
128+
return nil
129+
})
130+
if err != nil {
131+
panic(err)
132+
}
133+
134+
return p
135+
}
136+
53137
var rootCmd = &cobra.Command{
54138
Use: "spawn",
55139
Short: "Entry into the Interchain",

plugins/example/build.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
EXPORT_LOC=$HOME/.spawn/plugins
2+
mkdir -p $EXPORT_LOC
3+
4+
go build -buildmode=plugin -o $EXPORT_LOC/example.so plugins/example/example-plugin.go

plugins/example/example-plugin.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"os"
6+
"path"
7+
8+
"github.com/spf13/cobra"
9+
plugins "gitub.com/strangelove-ventures/spawn/plugins"
10+
)
11+
12+
// Make the plugin public
13+
var Plugin SpawnMainExamplePlugin
14+
15+
var _ plugins.SpawnPlugin = &SpawnMainExamplePlugin{}
16+
17+
const (
18+
cmdName = "example"
19+
)
20+
21+
type SpawnMainExamplePlugin struct {
22+
Impl plugins.SpawnPluginBase
23+
}
24+
25+
// Cmd implements plugins.SpawnPlugin.
26+
func (e *SpawnMainExamplePlugin) Cmd() *cobra.Command {
27+
rootCmd := &cobra.Command{
28+
Use: cmdName,
29+
Short: cmdName + " plugin command",
30+
Run: func(cmd *cobra.Command, args []string) {
31+
if err := cmd.Help(); err != nil {
32+
log.Fatal(err)
33+
}
34+
},
35+
}
36+
37+
rootCmd.AddCommand(&cobra.Command{
38+
Use: "touch-file [name]",
39+
Short: "An example plugin sub command",
40+
Args: cobra.ExactArgs(1),
41+
Run: func(cmd *cobra.Command, args []string) {
42+
cwd, err := os.Getwd()
43+
if err != nil {
44+
log.Fatal(err)
45+
}
46+
47+
fileName := args[0]
48+
49+
filePath := path.Join(cwd, fileName)
50+
file, err := os.Create(filePath)
51+
if err != nil {
52+
log.Fatal(err)
53+
}
54+
defer file.Close()
55+
56+
cmd.Printf("Created file: %s\n", filePath)
57+
},
58+
})
59+
60+
return rootCmd
61+
}

plugins/plugin.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package plugins
2+
3+
import "github.com/spf13/cobra"
4+
5+
// SpawnPlugin is the interface that we're exposing as a plugin.
6+
type SpawnPlugin interface {
7+
Cmd() *cobra.Command
8+
}
9+
10+
var _ SpawnPlugin = &SpawnPluginBase{}
11+
12+
type SpawnPluginBase struct {
13+
cmd *cobra.Command
14+
}
15+
16+
func NewSpawnPluginBase(cmd *cobra.Command) *SpawnPluginBase {
17+
return &SpawnPluginBase{
18+
cmd: cmd,
19+
}
20+
}
21+
22+
// Cmd implements SpawnPlugin.
23+
func (s *SpawnPluginBase) Cmd() *cobra.Command {
24+
return s.cmd
25+
}

0 commit comments

Comments
 (0)