17 "github.com/blang/semver"
18 "github.com/yosuke-furukawa/json5/encoding/json5"
19 "github.com/yuin/gopher-lua"
23 pluginChannels PluginChannels = PluginChannels{
24 PluginChannel("https://www.boombuler.de/channel.json"),
27 allPluginPackages PluginPackages = nil
30 // CorePluginName is a plugin dependency name for the micro core.
31 const CorePluginName = "micro"
33 // PluginChannel contains an url to a json list of PluginRepository
34 type PluginChannel string
36 // PluginChannels is a slice of PluginChannel
37 type PluginChannels []PluginChannel
39 // PluginRepository contains an url to json file containing PluginPackages
40 type PluginRepository string
42 // PluginPackage contains the meta-data of a plugin and all available versions
43 type PluginPackage struct {
48 Versions PluginVersions
51 // PluginPackages is a list of PluginPackage instances.
52 type PluginPackages []*PluginPackage
54 // PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
55 type PluginVersion struct {
57 Version semver.Version
59 Require PluginDependencies
62 // PluginVersions is a slice of PluginVersion
63 type PluginVersions []*PluginVersion
65 // PluginDenendency descripes a dependency to another plugin or micro itself.
66 type PluginDependency struct {
71 // PluginDependencies is a slice of PluginDependency
72 type PluginDependencies []*PluginDependency
74 func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
75 wgQuery := new(sync.WaitGroup)
78 results := make(chan PluginPackages)
80 wgDone := new(sync.WaitGroup)
82 var packages PluginPackages
83 for i := 0; i < count; i++ {
90 packages = make(PluginPackages, 0)
91 for res := range results {
92 packages = append(packages, res...)
102 // Fetch retrieves all available PluginPackages from the given channels
103 func (pc PluginChannels) Fetch() PluginPackages {
104 return fetchAllSources(len(pc), func(i int) PluginPackages {
109 // Fetch retrieves all available PluginPackages from the given channel
110 func (pc PluginChannel) Fetch() PluginPackages {
111 resp, err := http.Get(string(pc))
113 TermMessage("Failed to query plugin channel:\n", err)
114 return PluginPackages{}
116 defer resp.Body.Close()
117 decoder := json5.NewDecoder(resp.Body)
119 var repositories []PluginRepository
120 if err := decoder.Decode(&repositories); err != nil {
121 TermMessage("Failed to decode channel data:\n", err)
122 return PluginPackages{}
124 return fetchAllSources(len(repositories), func(i int) PluginPackages {
125 return repositories[i].Fetch()
129 // Fetch retrieves all available PluginPackages from the given repository
130 func (pr PluginRepository) Fetch() PluginPackages {
131 resp, err := http.Get(string(pr))
133 TermMessage("Failed to query plugin repository:\n", err)
134 return PluginPackages{}
136 defer resp.Body.Close()
137 decoder := json5.NewDecoder(resp.Body)
139 var plugins PluginPackages
140 if err := decoder.Decode(&plugins); err != nil {
141 TermMessage("Failed to decode repository data:\n", err)
142 return PluginPackages{}
147 // UnmarshalJSON unmarshals raw json to a PluginVersion
148 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
150 Version semver.Version
152 Require map[string]string
155 if err := json5.Unmarshal(data, &values); err != nil {
158 pv.Version = values.Version
160 pv.Require = make(PluginDependencies, 0)
162 for k, v := range values.Require {
163 if vRange, err := semver.ParseRange(v); err == nil {
164 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
170 // UnmarshalJSON unmarshals raw json to a PluginPackage
171 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
177 Versions PluginVersions
179 if err := json5.Unmarshal(data, &values); err != nil {
182 pp.Name = values.Name
183 pp.Description = values.Description
184 pp.Author = values.Author
185 pp.Tags = values.Tags
186 pp.Versions = values.Versions
187 for _, v := range pp.Versions {
193 // GetAllPluginPackages gets all PluginPackages which may be available.
194 func GetAllPluginPackages() PluginPackages {
195 if allPluginPackages == nil {
196 allPluginPackages = pluginChannels.Fetch()
198 return allPluginPackages
201 func (pv PluginVersions) find(ppName string) *PluginVersion {
202 for _, v := range pv {
203 if v.pack.Name == ppName {
210 // Len returns the number of pluginversions in this slice
211 func (pv PluginVersions) Len() int {
215 // Swap two entries of the slice
216 func (pv PluginVersions) Swap(i, j int) {
217 pv[i], pv[j] = pv[j], pv[i]
220 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
221 func (s PluginVersions) Less(i, j int) bool {
222 return s[i].Version.GT(s[j].Version)
225 // Match returns true if the package matches a given search text
226 func (pp PluginPackage) Match(text string) bool {
227 // ToDo: improve matching.
229 if r, err := regexp.Compile(text); err == nil {
230 return r.MatchString(pp.Name)
235 // IsInstallable returns true if the package can be installed.
236 func (pp PluginPackage) IsInstallable() bool {
237 _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
240 Range: semver.Range(func(v semver.Version) bool { return true }),
245 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
246 // could be or are already installed
247 func SearchPlugin(text string) (plugins PluginPackages) {
248 plugins = make(PluginPackages, 0)
249 for _, pp := range GetAllPluginPackages() {
250 if pp.Match(text) && pp.IsInstallable() {
251 plugins = append(plugins, pp)
257 func newStaticPluginVersion(name, version string) *PluginVersion {
258 vers, err := semver.ParseTolerant(version)
261 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
262 vers = semver.MustParse("0.0.0-unknown")
265 pl := &PluginPackage{
268 pv := &PluginVersion{
272 pl.Versions = PluginVersions{pv}
276 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
277 // micro itself. This can be used to resolve dependencies.
278 func GetInstalledVersions() PluginVersions {
279 result := PluginVersions{
280 newStaticPluginVersion(CorePluginName, Version),
283 for _, name := range loadedPlugins {
284 version := GetInstalledPluginVersion(name)
285 if pv := newStaticPluginVersion(name, version); pv != nil {
286 result = append(result, pv)
293 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
294 func GetInstalledPluginVersion(name string) string {
295 plugin := L.GetGlobal(name)
296 if plugin != lua.LNil {
297 version := L.GetField(plugin, "VERSION")
298 if str, ok := version.(lua.LString); ok {
306 func (pv *PluginVersion) DownloadAndInstall() error {
307 resp, err := http.Get(pv.Url)
311 defer resp.Body.Close()
312 data, err := ioutil.ReadAll(resp.Body)
316 zipbuf := bytes.NewReader(data)
317 z, err := zip.NewReader(zipbuf, zipbuf.Size())
321 targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
322 dirPerm := os.FileMode(0755)
323 if err = os.MkdirAll(targetDir, dirPerm); err != nil {
326 for _, f := range z.File {
327 targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...))
328 if f.FileInfo().IsDir() {
329 if err := os.MkdirAll(targetName, dirPerm); err != nil {
333 content, err := f.Open()
337 defer content.Close()
338 if target, err := os.Create(targetName); err != nil {
342 if _, err = io.Copy(target, content); err != nil {
351 func (pl PluginPackages) Get(name string) *PluginPackage {
352 for _, p := range pl {
360 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
361 result := make(PluginVersions, 0)
364 for _, v := range p.Versions {
365 result = append(result, v)
371 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
372 m := make(map[string]*PluginDependency)
373 for _, r := range req {
376 for _, o := range other {
379 m[o.Name] = &PluginDependency{
381 o.Range.AND(cur.Range),
387 result := make(PluginDependencies, 0, len(m))
388 for _, v := range m {
389 result = append(result, v)
394 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
396 return selectedVersions, nil
398 currentRequirement, stillOpen := open[0], open[1:]
399 if currentRequirement != nil {
400 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
401 if currentRequirement.Range(selVersion.Version) {
402 return all.Resolve(selectedVersions, stillOpen)
404 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
406 availableVersions := all.GetAllVersions(currentRequirement.Name)
407 sort.Sort(availableVersions)
409 for _, version := range availableVersions {
410 if currentRequirement.Range(version.Version) {
411 resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
418 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
421 return selectedVersions, nil
425 func (versions PluginVersions) install() {
426 anyInstalled := false
427 for _, sel := range versions {
428 if sel.pack.Name != CorePluginName {
429 installed := GetInstalledPluginVersion(sel.pack.Name)
430 if v, err := semver.ParseTolerant(installed); err != nil || v.NE(sel.Version) {
431 UninstallPlugin(sel.pack.Name)
433 if err := sel.DownloadAndInstall(); err != nil {
441 messenger.Message("One or more plugins installed. Please restart micro.")
445 // UninstallPlugin deletes the plugin folder of the given plugin
446 func UninstallPlugin(name string) {
447 if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
452 func (pl PluginPackage) Install() {
453 selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
456 Range: semver.Range(func(v semver.Version) bool { return true }),
465 func UpdatePlugins() {
466 microVersion := PluginVersions{
467 newStaticPluginVersion(CorePluginName, Version),
470 var updates = make(PluginDependencies, 0)
471 for _, name := range loadedPlugins {
472 pv := GetInstalledPluginVersion(name)
473 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
475 updates = append(updates, &PluginDependency{
482 selected, err := GetAllPluginPackages().Resolve(microVersion, updates)