Skip to content

Commit 1055f75

Browse files
committed
Simplify plugin support code
Also fix the global option processing so that they are not passed on to the plugin commands. Signed-off-by: Jan Dubois <jan.dubois@suse.com>
1 parent 7dcdf6a commit 1055f75

File tree

3 files changed

+93
-116
lines changed

3 files changed

+93
-116
lines changed

cmd/limactl/main.go

Lines changed: 74 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
"github.com/lima-vm/lima/v2/pkg/fsutil"
2222
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
2323
"github.com/lima-vm/lima/v2/pkg/osutil"
24-
"github.com/lima-vm/lima/v2/pkg/plugin"
24+
"github.com/lima-vm/lima/v2/pkg/plugins"
2525
"github.com/lima-vm/lima/v2/pkg/version"
2626
)
2727

@@ -47,15 +47,49 @@ func main() {
4747
}
4848
}
4949
}
50-
rootCmd := newApp()
51-
err := executeWithPluginSupport(rootCmd, os.Args[1:])
50+
err := newApp().Execute()
5251
server.StopAllExternalDrivers()
5352
osutil.HandleExitError(err)
5453
if err != nil {
5554
logrus.Fatal(err)
5655
}
5756
}
5857

58+
func processGlobalFlags(rootCmd *cobra.Command) error {
59+
// --log-level will override --debug, but will not reset debugutil.Debug
60+
if debug, _ := rootCmd.Flags().GetBool("debug"); debug {
61+
logrus.SetLevel(logrus.DebugLevel)
62+
debugutil.Debug = true
63+
}
64+
65+
l, _ := rootCmd.Flags().GetString("log-level")
66+
if l != "" {
67+
lvl, err := logrus.ParseLevel(l)
68+
if err != nil {
69+
return err
70+
}
71+
logrus.SetLevel(lvl)
72+
}
73+
74+
logFormat, _ := rootCmd.Flags().GetString("log-format")
75+
switch logFormat {
76+
case "json":
77+
formatter := new(logrus.JSONFormatter)
78+
logrus.StandardLogger().SetFormatter(formatter)
79+
case "text":
80+
// logrus use text format by default.
81+
if runtime.GOOS == "windows" && isatty.IsCygwinTerminal(os.Stderr.Fd()) {
82+
formatter := new(logrus.TextFormatter)
83+
// the default setting does not recognize cygwin on windows
84+
formatter.ForceColors = true
85+
logrus.StandardLogger().SetFormatter(formatter)
86+
}
87+
default:
88+
return fmt.Errorf("unsupported log-format: %q", logFormat)
89+
}
90+
return nil
91+
}
92+
5993
func newApp() *cobra.Command {
6094
templatesDir := "$PREFIX/share/lima/templates"
6195
if exe, err := os.Executable(); err == nil {
@@ -92,30 +126,8 @@ func newApp() *cobra.Command {
92126
rootCmd.PersistentFlags().Bool("tty", isatty.IsTerminal(os.Stdout.Fd()), "Enable TUI interactions such as opening an editor. Defaults to true when stdout is a terminal. Set to false for automation.")
93127
rootCmd.PersistentFlags().BoolP("yes", "y", false, "Alias of --tty=false")
94128
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
95-
l, _ := cmd.Flags().GetString("log-level")
96-
if l != "" {
97-
lvl, err := logrus.ParseLevel(l)
98-
if err != nil {
99-
return err
100-
}
101-
logrus.SetLevel(lvl)
102-
}
103-
104-
logFormat, _ := cmd.Flags().GetString("log-format")
105-
switch logFormat {
106-
case "json":
107-
formatter := new(logrus.JSONFormatter)
108-
logrus.StandardLogger().SetFormatter(formatter)
109-
case "text":
110-
// logrus use text format by default.
111-
if runtime.GOOS == "windows" && isatty.IsCygwinTerminal(os.Stderr.Fd()) {
112-
formatter := new(logrus.TextFormatter)
113-
// the default setting does not recognize cygwin on windows
114-
formatter.ForceColors = true
115-
logrus.StandardLogger().SetFormatter(formatter)
116-
}
117-
default:
118-
return fmt.Errorf("unsupported log-format: %q", logFormat)
129+
if err := processGlobalFlags(rootCmd); err != nil {
130+
return err
119131
}
120132

121133
if osutil.IsBeingRosettaTranslated() && cmd.Parent().Name() != "completion" && cmd.Name() != "generate-doc" && cmd.Name() != "validate" {
@@ -191,47 +203,58 @@ func newApp() *cobra.Command {
191203
newNetworkCommand(),
192204
newCloneCommand(),
193205
)
206+
addPluginCommands(rootCmd)
194207

195208
return rootCmd
196209
}
197210

198-
func executeWithPluginSupport(rootCmd *cobra.Command, args []string) error {
199-
rootCmd.SetArgs(args)
200-
201-
if err := rootCmd.ParseFlags(args); err == nil {
202-
if debug, _ := rootCmd.Flags().GetBool("debug"); debug {
203-
logrus.SetLevel(logrus.DebugLevel)
204-
debugutil.Debug = true
205-
}
211+
func addPluginCommands(rootCmd *cobra.Command) {
212+
// The global options are only processed when rootCmd.Execute() is called.
213+
// Let's take a sneak peek here to help debug the plugin discovery code.
214+
if len(os.Args) > 1 && os.Args[1] == "--debug" {
215+
logrus.SetLevel(logrus.DebugLevel)
206216
}
207217

208-
addPluginCommands(rootCmd)
209-
210-
return rootCmd.Execute()
211-
}
212-
213-
func addPluginCommands(rootCmd *cobra.Command) {
214-
plugins, err := plugin.DiscoverPlugins()
218+
allPlugins, err := plugins.Discover()
215219
if err != nil {
216220
logrus.Warnf("Failed to discover plugins: %v", err)
217221
return
218222
}
219223

220-
for _, p := range plugins {
221-
pluginName := p.Name
224+
for _, plugin := range allPlugins {
222225
pluginCmd := &cobra.Command{
223-
Use: pluginName,
224-
Short: p.Description,
226+
Use: plugin.Name,
227+
Short: plugin.Description,
225228
GroupID: "plugin",
226229
DisableFlagParsing: true,
227-
Run: func(cmd *cobra.Command, args []string) {
228-
plugin.RunExternalPlugin(cmd.Context(), pluginName, args)
230+
SilenceErrors: true,
231+
SilenceUsage: true,
232+
PreRunE: func(*cobra.Command, []string) error {
233+
for i, arg := range os.Args {
234+
if arg == plugin.Name {
235+
// parse global options but ignore plugin options
236+
err := rootCmd.ParseFlags(os.Args[1:i])
237+
if err == nil {
238+
err = processGlobalFlags(rootCmd)
239+
}
240+
return err
241+
}
242+
}
243+
// unreachable
244+
return nil
245+
},
246+
Run: func(cmd *cobra.Command, _ []string) {
247+
for i, arg := range os.Args {
248+
if arg == plugin.Name {
249+
// ignore global options
250+
plugin.Run(cmd.Context(), os.Args[i+1:])
251+
// plugin.Run() never returns because it calls os.Exit()
252+
}
253+
}
254+
// unreachable
229255
},
230256
}
231257

232-
pluginCmd.SilenceUsage = true
233-
pluginCmd.SilenceErrors = true
234-
235258
rootCmd.AddCommand(pluginCmd)
236259
}
237260
}

pkg/limainfo/limainfo.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
1818
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
1919
"github.com/lima-vm/lima/v2/pkg/limayaml"
20-
"github.com/lima-vm/lima/v2/pkg/plugin"
20+
"github.com/lima-vm/lima/v2/pkg/plugins"
2121
"github.com/lima-vm/lima/v2/pkg/registry"
2222
"github.com/lima-vm/lima/v2/pkg/templatestore"
2323
"github.com/lima-vm/lima/v2/pkg/usrlocalsharelima"
@@ -36,7 +36,7 @@ type LimaInfo struct {
3636
HostOS string `json:"hostOS"` // since Lima v2.0.0
3737
HostArch string `json:"hostArch"` // since Lima v2.0.0
3838
IdentityFile string `json:"identityFile"` // since Lima v2.0.0
39-
Plugins []plugin.Plugin `json:"plugins"` // since Lima v2.0.0
39+
Plugins []plugins.Plugin `json:"plugins"` // since Lima v2.0.0
4040
}
4141

4242
type DriverExt struct {
@@ -111,13 +111,10 @@ func New(ctx context.Context) (*LimaInfo, error) {
111111
}
112112
}
113113

114-
plugins, err := plugin.DiscoverPlugins()
114+
info.Plugins, err = plugins.Discover()
115115
if err != nil {
116-
logrus.WithError(err).Warn("Failed to discover plugins")
117116
// Don't fail the entire info command if plugin discovery fails.
118-
info.Plugins = []plugin.Plugin{}
119-
} else {
120-
info.Plugins = plugins
117+
logrus.WithError(err).Warn("Failed to discover plugins")
121118
}
122119

123120
return info, nil
Lines changed: 15 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// SPDX-FileCopyrightText: Copyright The Lima Authors
22
// SPDX-License-Identifier: Apache-2.0
33

4-
package plugin
4+
package plugins
55

66
import (
77
"cmp"
@@ -13,6 +13,7 @@ import (
1313
"runtime"
1414
"slices"
1515
"strings"
16+
"sync"
1617

1718
"github.com/sirupsen/logrus"
1819

@@ -28,7 +29,7 @@ type Plugin struct {
2829
Description string `json:"description,omitempty"`
2930
}
3031

31-
func DiscoverPlugins() ([]Plugin, error) {
32+
var Discover = sync.OnceValues(func() ([]Plugin, error) {
3233
var plugins []Plugin
3334
seen := make(map[string]bool)
3435

@@ -47,9 +48,9 @@ func DiscoverPlugins() ([]Plugin, error) {
4748
})
4849

4950
return plugins, nil
50-
}
51+
})
5152

52-
func getPluginDirectories() []string {
53+
var getPluginDirectories = sync.OnceValue(func() []string {
5354
dirs := usrlocalsharelima.SelfDirs()
5455

5556
pathEnv := os.Getenv("PATH")
@@ -65,7 +66,7 @@ func getPluginDirectories() []string {
6566
}
6667

6768
return dirs
68-
}
69+
})
6970

7071
// isWindowsExecutableExt checks if the given extension is a valid Windows executable extension
7172
// according to PATHEXT environment variable.
@@ -74,20 +75,9 @@ func isWindowsExecutableExt(ext string) bool {
7475
return false
7576
}
7677

77-
pathExt := os.Getenv("PATHEXT")
78-
if pathExt == "" {
79-
pathExt = defaultPathExt
80-
}
81-
78+
pathExt := cmp.Or(os.Getenv("PATHEXT"), defaultPathExt)
8279
extensions := strings.Split(strings.ToUpper(pathExt), ";")
83-
extUpper := strings.ToUpper(ext)
84-
85-
for _, validExt := range extensions {
86-
if validExt == extUpper {
87-
return true
88-
}
89-
}
90-
return false
80+
return slices.Contains(extensions, strings.ToUpper(ext))
9181
}
9282

9383
func isExecutable(path string) bool {
@@ -104,18 +94,7 @@ func isExecutable(path string) bool {
10494
return info.Mode()&0o111 != 0
10595
}
10696

107-
ext := strings.ToLower(filepath.Ext(path))
108-
pathExt := os.Getenv("PATHEXT")
109-
if pathExt == "" {
110-
pathExt = defaultPathExt
111-
}
112-
113-
for _, e := range strings.Split(strings.ToLower(pathExt), ";") {
114-
if e == ext {
115-
return true
116-
}
117-
}
118-
return false
97+
return isWindowsExecutableExt(filepath.Ext(path))
11998
}
12099

121100
func scanDirectory(dir string) []Plugin {
@@ -168,46 +147,24 @@ func scanDirectory(dir string) []Plugin {
168147
return plugins
169148
}
170149

171-
func RunExternalPlugin(ctx context.Context, name string, args []string) {
172-
if ctx == nil {
173-
ctx = context.Background()
174-
}
175-
176-
if err := UpdatePathForPlugins(); err != nil {
150+
func (plugin *Plugin) Run(ctx context.Context, args []string) {
151+
if err := UpdatePath(); err != nil {
177152
logrus.Warnf("failed to update PATH environment: %v", err)
178153
// PATH update failure shouldn't prevent plugin execution
179154
}
180155

181-
plugins, err := DiscoverPlugins()
182-
if err != nil {
183-
logrus.Warnf("failed to discover plugins: %v", err)
184-
return
185-
}
186-
187-
var execPath string
188-
for _, plugin := range plugins {
189-
if plugin.Name == name {
190-
execPath = plugin.Path
191-
break
192-
}
193-
}
194-
195-
if execPath == "" {
196-
return
197-
}
198-
199-
cmd := exec.CommandContext(ctx, execPath, args...)
156+
cmd := exec.CommandContext(ctx, plugin.Path, args...)
200157
cmd.Stdin = os.Stdin
201158
cmd.Stdout = os.Stdout
202159
cmd.Stderr = os.Stderr
203160
cmd.Env = os.Environ()
204161

205-
err = cmd.Run()
162+
err := cmd.Run()
206163
osutil.HandleExitError(err)
207164
if err == nil {
208165
os.Exit(0) //nolint:revive // it's intentional to call os.Exit in this function
209166
}
210-
logrus.Fatalf("external command %q failed: %v", execPath, err)
167+
logrus.Fatalf("external command %q failed: %v", plugin.Path, err)
211168
}
212169

213170
var descRegex = regexp.MustCompile(`<limactl-desc>(.*?)</limactl-desc>`)
@@ -235,7 +192,7 @@ func extractDescFromScript(path string) string {
235192
return desc
236193
}
237194

238-
func UpdatePathForPlugins() error {
195+
func UpdatePath() error {
239196
pluginDirs := getPluginDirectories()
240197
newPath := strings.Join(pluginDirs, string(filepath.ListSeparator))
241198
return os.Setenv("PATH", newPath)

0 commit comments

Comments
 (0)