Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/plugins/plugins.go
2611 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package plugins
5
6
import (
7
"cmp"
8
"context"
9
"os"
10
"os/exec"
11
"path/filepath"
12
"regexp"
13
"runtime"
14
"slices"
15
"strings"
16
"sync"
17
18
"github.com/sirupsen/logrus"
19
20
"github.com/lima-vm/lima/v2/pkg/osutil"
21
"github.com/lima-vm/lima/v2/pkg/usrlocal"
22
)
23
24
const defaultPathExt = ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL"
25
26
type Plugin struct {
27
Name string `json:"name"`
28
Path string `json:"path"`
29
Description string `json:"description,omitempty"`
30
}
31
32
var Discover = sync.OnceValues(func() ([]Plugin, error) {
33
var plugins []Plugin
34
seen := make(map[string]bool)
35
36
for _, dir := range getPluginDirectories() {
37
for _, plugin := range scanDirectory(dir) {
38
if !seen[plugin.Name] {
39
plugins = append(plugins, plugin)
40
seen[plugin.Name] = true
41
}
42
}
43
}
44
45
slices.SortFunc(plugins,
46
func(i, j Plugin) int {
47
return cmp.Compare(i.Name, j.Name)
48
})
49
50
return plugins, nil
51
})
52
53
var getPluginDirectories = sync.OnceValue(func() []string {
54
dirs := usrlocal.SelfDirs()
55
56
pathEnv := os.Getenv("PATH")
57
if pathEnv != "" {
58
pathDirs := filepath.SplitList(pathEnv)
59
dirs = append(dirs, pathDirs...)
60
}
61
62
libexecDirs, err := usrlocal.LibexecLima()
63
if err == nil {
64
dirs = append(dirs, libexecDirs...)
65
}
66
67
return dirs
68
})
69
70
// isWindowsExecutableExt checks if the given extension is a valid Windows executable extension
71
// according to PATHEXT environment variable.
72
func isWindowsExecutableExt(ext string) bool {
73
if runtime.GOOS != "windows" {
74
return false
75
}
76
77
pathExt := cmp.Or(os.Getenv("PATHEXT"), defaultPathExt)
78
extensions := strings.Split(strings.ToUpper(pathExt), ";")
79
return slices.Contains(extensions, strings.ToUpper(ext))
80
}
81
82
func isExecutable(path string) bool {
83
info, err := os.Stat(path)
84
if err != nil {
85
return false
86
}
87
88
if !info.Mode().IsRegular() {
89
return false
90
}
91
92
if runtime.GOOS != "windows" {
93
return info.Mode()&0o111 != 0
94
}
95
96
return isWindowsExecutableExt(filepath.Ext(path))
97
}
98
99
func scanDirectory(dir string) []Plugin {
100
var plugins []Plugin
101
102
entries, err := os.ReadDir(dir)
103
if err != nil {
104
logrus.Debugf("Plugin discovery: failed to scan directory %s: %v", dir, err)
105
106
return plugins
107
}
108
109
for _, entry := range entries {
110
if entry.IsDir() {
111
continue
112
}
113
114
name := entry.Name()
115
if !strings.HasPrefix(name, "limactl-") {
116
continue
117
}
118
119
pluginName := strings.TrimPrefix(name, "limactl-")
120
121
if runtime.GOOS == "windows" {
122
ext := filepath.Ext(pluginName)
123
if isWindowsExecutableExt(ext) {
124
pluginName = strings.TrimSuffix(pluginName, ext)
125
}
126
}
127
128
fullPath := filepath.Join(dir, name)
129
130
if !isExecutable(fullPath) {
131
continue
132
}
133
134
plugin := Plugin{
135
Name: pluginName,
136
Path: fullPath,
137
}
138
139
if desc := extractDescFromScript(fullPath); desc != "" {
140
plugin.Description = desc
141
}
142
143
plugins = append(plugins, plugin)
144
}
145
146
return plugins
147
}
148
149
func (plugin *Plugin) Run(ctx context.Context, args []string) {
150
if err := UpdatePath(); err != nil {
151
logrus.Warnf("failed to update PATH environment: %v", err)
152
// PATH update failure shouldn't prevent plugin execution
153
}
154
155
cmd := exec.CommandContext(ctx, plugin.Path, args...)
156
cmd.Stdin = os.Stdin
157
cmd.Stdout = os.Stdout
158
cmd.Stderr = os.Stderr
159
cmd.Env = os.Environ()
160
161
err := cmd.Run()
162
osutil.HandleExitError(err)
163
if err == nil {
164
os.Exit(0) //nolint:revive // it's intentional to call os.Exit in this function
165
}
166
logrus.Fatalf("external command %q failed: %v", plugin.Path, err)
167
}
168
169
var descRegex = regexp.MustCompile(`<limactl-desc>(.*?)</limactl-desc>`)
170
171
func extractDescFromScript(path string) string {
172
content, err := os.ReadFile(path)
173
if err != nil {
174
logrus.Debugf("Failed to read plugin script %s: %v", path, err)
175
return ""
176
}
177
178
if !strings.HasPrefix(string(content), "#!") {
179
logrus.Debugf("Plugin %s: not a script file, skipping description extraction", path)
180
return ""
181
}
182
183
matches := descRegex.FindStringSubmatch(string(content))
184
if len(matches) < 2 {
185
logrus.Debugf("Plugin %s: no <limactl-desc> found in script", filepath.Base(path))
186
return ""
187
}
188
189
desc := strings.Trim(matches[1], " ")
190
logrus.Debugf("Plugin %s: extracted description: %q", filepath.Base(path), desc)
191
return desc
192
}
193
194
// Find locates a plugin by name and returns a pointer to a copy.
195
func Find(name string) (*Plugin, error) {
196
allPlugins, err := Discover()
197
if err != nil {
198
return nil, err
199
}
200
for _, plugin := range allPlugins {
201
if name == plugin.Name {
202
pluginCopy := plugin
203
return &pluginCopy, nil
204
}
205
}
206
return nil, nil
207
}
208
209
func UpdatePath() error {
210
pluginDirs := getPluginDirectories()
211
newPath := strings.Join(pluginDirs, string(filepath.ListSeparator))
212
return os.Setenv("PATH", newPath)
213
}
214
215