d2plugin: Dedup plugins array

Closes #590
This commit is contained in:
Anmol Sethi 2023-01-03 14:48:23 -08:00
parent 296b6407e9
commit 5b4a20a6c6
No known key found for this signature in database
GPG key ID: 25BC68888A99A8BA
4 changed files with 106 additions and 86 deletions

View file

@ -40,9 +40,10 @@ import (
type execPlugin struct { type execPlugin struct {
path string path string
opts map[string]string opts map[string]string
info *PluginInfo
} }
func (p execPlugin) Flags(ctx context.Context) (_ []PluginSpecificFlag, err error) { func (p *execPlugin) Flags(ctx context.Context) (_ []PluginSpecificFlag, err error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*10) ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, p.path, "flags") cmd := exec.CommandContext(ctx, p.path, "flags")
@ -92,7 +93,11 @@ func (p *execPlugin) HydrateOpts(opts []byte) error {
return nil return nil
} }
func (p execPlugin) Info(ctx context.Context) (_ *PluginInfo, err error) { func (p *execPlugin) Info(ctx context.Context) (_ *PluginInfo, err error) {
if p.info != nil {
return p.info, nil
}
ctx, cancel := context.WithTimeout(ctx, time.Second*10) ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel() defer cancel()
cmd := exec.CommandContext(ctx, p.path, "info") cmd := exec.CommandContext(ctx, p.path, "info")
@ -114,10 +119,11 @@ func (p execPlugin) Info(ctx context.Context) (_ *PluginInfo, err error) {
return nil, fmt.Errorf("failed to unmarshal json: %w", err) return nil, fmt.Errorf("failed to unmarshal json: %w", err)
} }
p.info = &info
return &info, nil return &info, nil
} }
func (p execPlugin) Layout(ctx context.Context, g *d2graph.Graph) error { func (p *execPlugin) Layout(ctx context.Context, g *d2graph.Graph) error {
ctx, cancel := context.WithTimeout(ctx, time.Minute) ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel() defer cancel()
@ -152,7 +158,7 @@ func (p execPlugin) Layout(ctx context.Context, g *d2graph.Graph) error {
return nil return nil
} }
func (p execPlugin) PostProcess(ctx context.Context, in []byte) ([]byte, error) { func (p *execPlugin) PostProcess(ctx context.Context, in []byte) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, time.Minute) ctx, cancel := context.WithTimeout(ctx, time.Minute)
defer cancel() defer cancel()

View file

@ -80,36 +80,54 @@ type PluginInfo struct {
const binaryPrefix = "d2plugin-" const binaryPrefix = "d2plugin-"
func ListPlugins(ctx context.Context) ([]*PluginInfo, error) { func ListPlugins(ctx context.Context) ([]Plugin, error) {
// 1. Run Info on all bundled plugins in the global plugins array. // 1. Run Info on all bundled plugins in the global plugins array.
// - set Type for each bundled plugin to "bundled". // - set Type for each bundled plugin to "bundled".
// 2. Iterate through directories in $PATH and look for executables within these // 2. Iterate through directories in $PATH and look for executables within these
// directories with the prefix d2plugin-* // directories with the prefix d2plugin-*
// 3. Run each plugin binary with the argument info. e.g. d2plugin-dagre info // 3. Run each plugin binary with the argument info. e.g. d2plugin-dagre info
var infoSlice []*PluginInfo var ps []Plugin
ps = append(ps, plugins...)
for _, p := range plugins {
info, err := p.Info(ctx)
if err != nil {
return nil, err
}
info.Type = "bundled"
infoSlice = append(infoSlice, info)
}
matches, err := xexec.SearchPath(binaryPrefix) matches, err := xexec.SearchPath(binaryPrefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
BINARY_PLUGINS_LOOP:
for _, path := range matches { for _, path := range matches {
p := &execPlugin{path: path} p := &execPlugin{path: path}
info, err := p.Info(ctx) info, err := p.Info(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
info.Type = "binary" for _, p2 := range ps {
info.Path = path info2, err := p2.Info(ctx)
if err != nil {
return nil, err
}
if info.Name == info2.Name {
continue BINARY_PLUGINS_LOOP
}
}
ps = append(ps, p)
}
return ps, nil
}
func ListPluginInfos(ctx context.Context, ps []Plugin) ([]*PluginInfo, error) {
var infoSlice []*PluginInfo
for _, p := range ps {
info, err := p.Info(ctx)
if err != nil {
return nil, err
}
if ep, ok := p.(*execPlugin); ok {
info.Type = "binary"
info.Path = ep.path
} else {
info.Type = "bundled"
}
infoSlice = append(infoSlice, info) infoSlice = append(infoSlice, info)
} }
@ -123,41 +141,22 @@ func ListPlugins(ctx context.Context) ([]*PluginInfo, error) {
// **NOTE** When D2 upgrades to go 1.19, remember to ignore exec.ErrDot // **NOTE** When D2 upgrades to go 1.19, remember to ignore exec.ErrDot
// 3. If such a binary is found, it builds an execPlugin in exec.go // 3. If such a binary is found, it builds an execPlugin in exec.go
// to get a plugin implementation around the binary and returns it. // to get a plugin implementation around the binary and returns it.
func FindPlugin(ctx context.Context, name string) (Plugin, string, error) { func FindPlugin(ctx context.Context, ps []Plugin, name string) (Plugin, error) {
for _, p := range plugins { for _, p := range ps {
info, err := p.Info(ctx) info, err := p.Info(ctx)
if err != nil {
return nil, "", err
}
if info.Name == name {
return p, "", nil
}
}
path, err := exec.LookPath(binaryPrefix + name)
if err != nil {
return nil, "", err
}
return &execPlugin{path: path}, path, nil
}
func ListPluginFlags(ctx context.Context) ([]PluginSpecificFlag, error) {
var out []PluginSpecificFlag
for _, p := range plugins {
flags, err := p.Flags(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
out = append(out, flags...) if info.Name == name {
return p, nil
}
} }
return nil, exec.ErrNotFound
}
matches, err := xexec.SearchPath(binaryPrefix) func ListPluginFlags(ctx context.Context, ps []Plugin) ([]PluginSpecificFlag, error) {
if err != nil { var out []PluginSpecificFlag
return nil, err for _, p := range ps {
}
for _, path := range matches {
p := &execPlugin{path: path}
flags, err := p.Flags(ctx) flags, err := p.Flags(ctx)
if err != nil { if err != nil {
return nil, err return nil, err

62
help.go
View file

@ -39,23 +39,23 @@ See more docs and the source code at https://oss.terrastruct.com/d2
`, filepath.Base(ms.Name), ms.Opts.Defaults()) `, filepath.Base(ms.Name), ms.Opts.Defaults())
} }
func layoutCmd(ctx context.Context, ms *xmain.State) error { func layoutCmd(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
if len(ms.Opts.Flags.Args()) == 1 { if len(ms.Opts.Flags.Args()) == 1 {
return shortLayoutHelp(ctx, ms) return shortLayoutHelp(ctx, ms, ps)
} else if len(ms.Opts.Flags.Args()) == 2 { } else if len(ms.Opts.Flags.Args()) == 2 {
return longLayoutHelp(ctx, ms) return longLayoutHelp(ctx, ms, ps)
} else { } else {
return pluginSubcommand(ctx, ms) return pluginSubcommand(ctx, ms, ps)
} }
} }
func shortLayoutHelp(ctx context.Context, ms *xmain.State) error { func shortLayoutHelp(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
var pluginLines []string var pluginLines []string
plugins, err := d2plugin.ListPlugins(ctx) pinfos, err := d2plugin.ListPluginInfos(ctx, ps)
if err != nil { if err != nil {
return err return err
} }
for _, p := range plugins { for _, p := range pinfos {
var l string var l string
if p.Type == "bundled" { if p.Type == "bundled" {
l = fmt.Sprintf("%s (bundled) - %s", p.Name, p.ShortHelp) l = fmt.Sprintf("%s (bundled) - %s", p.Name, p.ShortHelp)
@ -82,40 +82,43 @@ See more docs at https://oss.terrastruct.com/d2
return nil return nil
} }
func longLayoutHelp(ctx context.Context, ms *xmain.State) error { func longLayoutHelp(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
layout := ms.Opts.Flags.Arg(1) layout := ms.Opts.Flags.Arg(1)
plugin, path, err := d2plugin.FindPlugin(ctx, layout) plugin, err := d2plugin.FindPlugin(ctx, ps, layout)
if errors.Is(err, exec.ErrNotFound) { if err != nil {
return layoutNotFound(ctx, layout) if errors.Is(err, exec.ErrNotFound) {
return layoutNotFound(ctx, ps, layout)
}
return err
} }
pluginLocation := "bundled" pinfo, err := plugin.Info(ctx)
if path != "" {
pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(path))
}
pluginInfo, err := plugin.Info(ctx)
if err != nil { if err != nil {
return err return err
} }
if !strings.HasSuffix(pluginInfo.LongHelp, "\n") { plocation := pinfo.Type
pluginInfo.LongHelp += "\n" if pinfo.Type == "binary" {
plocation = fmt.Sprintf("executable plugin at %s", humanPath(pinfo.Path))
}
if !strings.HasSuffix(pinfo.LongHelp, "\n") {
pinfo.LongHelp += "\n"
} }
fmt.Fprintf(ms.Stdout, `%s (%s): fmt.Fprintf(ms.Stdout, `%s (%s):
%s`, pluginInfo.Name, pluginLocation, pluginInfo.LongHelp) %s`, pinfo.Name, plocation, pinfo.LongHelp)
return nil return nil
} }
func layoutNotFound(ctx context.Context, layout string) error { func layoutNotFound(ctx context.Context, ps []d2plugin.Plugin, layout string) error {
var names []string pinfos, err := d2plugin.ListPluginInfos(ctx, ps)
plugins, err := d2plugin.ListPlugins(ctx)
if err != nil { if err != nil {
return err return err
} }
for _, p := range plugins { var names []string
for _, p := range pinfos {
names = append(names, p.Name) names = append(names, p.Name)
} }
@ -126,11 +129,14 @@ For more information on setup, please visit https://github.com/terrastruct/d2.`,
layout, strings.Join(names, ", ")) layout, strings.Join(names, ", "))
} }
func pluginSubcommand(ctx context.Context, ms *xmain.State) error { func pluginSubcommand(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
layout := ms.Opts.Flags.Arg(1) layout := ms.Opts.Flags.Arg(1)
plugin, _, err := d2plugin.FindPlugin(ctx, layout) plugin, err := d2plugin.FindPlugin(ctx, ps, layout)
if errors.Is(err, exec.ErrNotFound) { if err != nil {
return layoutNotFound(ctx, layout) if errors.Is(err, exec.ErrNotFound) {
return layoutNotFound(ctx, ps, layout)
}
return err
} }
ms.Opts.Args = ms.Opts.Flags.Args()[2:] ms.Opts.Args = ms.Opts.Flags.Args()[2:]

33
main.go
View file

@ -75,7 +75,11 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
return err return err
} }
err = populateLayoutOpts(ctx, ms) ps, err := d2plugin.ListPlugins(ctx)
if err != nil {
return err
}
err = populateLayoutOpts(ctx, ms, ps)
if err != nil { if err != nil {
return err return err
} }
@ -93,7 +97,7 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
if len(ms.Opts.Flags.Args()) > 0 { if len(ms.Opts.Flags.Args()) > 0 {
switch ms.Opts.Flags.Arg(0) { switch ms.Opts.Flags.Arg(0) {
case "layout": case "layout":
return layoutCmd(ctx, ms) return layoutCmd(ctx, ms, ps)
case "fmt": case "fmt":
return fmtCmd(ctx, ms) return fmtCmd(ctx, ms)
case "version": case "version":
@ -142,10 +146,11 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
} }
ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag) ms.Log.Debug.Printf("using theme %s (ID: %d)", match.Name, *themeFlag)
plugin, path, err := d2plugin.FindPlugin(ctx, *layoutFlag) plugin, err := d2plugin.FindPlugin(ctx, ps, *layoutFlag)
if errors.Is(err, exec.ErrNotFound) { if err != nil {
return layoutNotFound(ctx, *layoutFlag) if errors.Is(err, exec.ErrNotFound) {
} else if err != nil { return layoutNotFound(ctx, ps, *layoutFlag)
}
return err return err
} }
@ -154,11 +159,15 @@ func run(ctx context.Context, ms *xmain.State) (err error) {
return err return err
} }
pluginLocation := "bundled" pinfo, err := plugin.Info(ctx)
if path != "" { if err != nil {
pluginLocation = fmt.Sprintf("executable plugin at %s", humanPath(path)) return err
} }
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, pluginLocation) plocation := pinfo.Type
if pinfo.Type == "binary" {
plocation = fmt.Sprintf("executable plugin at %s", humanPath(pinfo.Path))
}
ms.Log.Debug.Printf("using layout plugin %s (%s)", *layoutFlag, plocation)
var pw png.Playwright var pw png.Playwright
if filepath.Ext(outputPath) == ".png" { if filepath.Ext(outputPath) == ".png" {
@ -299,8 +308,8 @@ func DiscardSlog(ctx context.Context) context.Context {
return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard))) return ctxlog.With(ctx, slog.Make(sloghuman.Sink(io.Discard)))
} }
func populateLayoutOpts(ctx context.Context, ms *xmain.State) error { func populateLayoutOpts(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
pluginFlags, err := d2plugin.ListPluginFlags(ctx) pluginFlags, err := d2plugin.ListPluginFlags(ctx, ps)
if err != nil { if err != nil {
return err return err
} }