16 "github.com/blang/semver"
17 "github.com/yosuke-furukawa/json5/encoding/json5"
18 "github.com/yuin/gopher-lua"
22 pluginChannels PluginChannels = PluginChannels{
23 PluginChannel("https://www.boombuler.de/channel.json"),
26 allPluginPackages PluginPackages = nil
29 // CorePluginName is a plugin dependency name for the micro core.
30 const CorePluginName = "micro"
32 // PluginChannel contains an url to a json list of PluginRepository
33 type PluginChannel string
35 // PluginChannels is a slice of PluginChannel
36 type PluginChannels []PluginChannel
38 // PluginRepository contains an url to json file containing PluginPackages
39 type PluginRepository string
41 // PluginPackage contains the meta-data of a plugin and all available versions
42 type PluginPackage struct {
47 Versions PluginVersions
50 // PluginPackages is a list of PluginPackage instances.
51 type PluginPackages []*PluginPackage
53 // PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
54 type PluginVersion struct {
56 Version semver.Version
58 Require PluginDependencies
61 // PluginVersions is a slice of PluginVersion
62 type PluginVersions []*PluginVersion
64 // PluginDenendency descripes a dependency to another plugin or micro itself.
65 type PluginDependency struct {
70 // PluginDependencies is a slice of PluginDependency
71 type PluginDependencies []*PluginDependency
73 func (pp *PluginPackage) String() string {
74 buf := new(bytes.Buffer)
75 buf.WriteString("Plugin: ")
76 buf.WriteString(pp.Name)
79 buf.WriteString("Author: ")
80 buf.WriteString(pp.Author)
83 if pp.Description != "" {
84 buf.WriteString(pp.Description)
89 func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
90 wgQuery := new(sync.WaitGroup)
93 results := make(chan PluginPackages)
95 wgDone := new(sync.WaitGroup)
97 var packages PluginPackages
98 for i := 0; i < count; i++ {
100 results <- fetcher(i)
105 packages = make(PluginPackages, 0)
106 for res := range results {
107 packages = append(packages, res...)
117 // Fetch retrieves all available PluginPackages from the given channels
118 func (pc PluginChannels) Fetch() PluginPackages {
119 return fetchAllSources(len(pc), func(i int) PluginPackages {
124 // Fetch retrieves all available PluginPackages from the given channel
125 func (pc PluginChannel) Fetch() PluginPackages {
126 resp, err := http.Get(string(pc))
128 TermMessage("Failed to query plugin channel:\n", err)
129 return PluginPackages{}
131 defer resp.Body.Close()
132 decoder := json5.NewDecoder(resp.Body)
134 var repositories []PluginRepository
135 if err := decoder.Decode(&repositories); err != nil {
136 TermMessage("Failed to decode channel data:\n", err)
137 return PluginPackages{}
139 return fetchAllSources(len(repositories), func(i int) PluginPackages {
140 return repositories[i].Fetch()
144 // Fetch retrieves all available PluginPackages from the given repository
145 func (pr PluginRepository) Fetch() PluginPackages {
146 resp, err := http.Get(string(pr))
148 TermMessage("Failed to query plugin repository:\n", err)
149 return PluginPackages{}
151 defer resp.Body.Close()
152 decoder := json5.NewDecoder(resp.Body)
154 var plugins PluginPackages
155 if err := decoder.Decode(&plugins); err != nil {
156 TermMessage("Failed to decode repository data:\n", err)
157 return PluginPackages{}
162 // UnmarshalJSON unmarshals raw json to a PluginVersion
163 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
165 Version semver.Version
167 Require map[string]string
170 if err := json5.Unmarshal(data, &values); err != nil {
173 pv.Version = values.Version
175 pv.Require = make(PluginDependencies, 0)
177 for k, v := range values.Require {
178 if vRange, err := semver.ParseRange(v); err == nil {
179 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
185 // UnmarshalJSON unmarshals raw json to a PluginPackage
186 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
192 Versions PluginVersions
194 if err := json5.Unmarshal(data, &values); err != nil {
197 pp.Name = values.Name
198 pp.Description = values.Description
199 pp.Author = values.Author
200 pp.Tags = values.Tags
201 pp.Versions = values.Versions
202 for _, v := range pp.Versions {
208 // GetAllPluginPackages gets all PluginPackages which may be available.
209 func GetAllPluginPackages() PluginPackages {
210 if allPluginPackages == nil {
211 allPluginPackages = pluginChannels.Fetch()
213 return allPluginPackages
216 func (pv PluginVersions) find(ppName string) *PluginVersion {
217 for _, v := range pv {
218 if v.pack.Name == ppName {
225 // Len returns the number of pluginversions in this slice
226 func (pv PluginVersions) Len() int {
230 // Swap two entries of the slice
231 func (pv PluginVersions) Swap(i, j int) {
232 pv[i], pv[j] = pv[j], pv[i]
235 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
236 func (s PluginVersions) Less(i, j int) bool {
237 return s[i].Version.GT(s[j].Version)
240 // Match returns true if the package matches a given search text
241 func (pp PluginPackage) Match(text string) bool {
242 text = strings.ToLower(text)
243 for _, t := range pp.Tags {
244 if strings.ToLower(t) == text {
248 if strings.Contains(strings.ToLower(pp.Name), text) {
252 if strings.Contains(strings.ToLower(pp.Description), text) {
259 // IsInstallable returns true if the package can be installed.
260 func (pp PluginPackage) IsInstallable() bool {
261 _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
264 Range: semver.Range(func(v semver.Version) bool { return true }),
269 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
270 // could be or are already installed
271 func SearchPlugin(text string) (plugins PluginPackages) {
272 plugins = make(PluginPackages, 0)
273 for _, pp := range GetAllPluginPackages() {
274 if pp.Match(text) && pp.IsInstallable() {
275 plugins = append(plugins, pp)
281 func newStaticPluginVersion(name, version string) *PluginVersion {
282 vers, err := semver.ParseTolerant(version)
285 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
286 vers = semver.MustParse("0.0.0-unknown")
289 pl := &PluginPackage{
292 pv := &PluginVersion{
296 pl.Versions = PluginVersions{pv}
300 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
301 // micro itself. This can be used to resolve dependencies.
302 func GetInstalledVersions() PluginVersions {
303 result := PluginVersions{
304 newStaticPluginVersion(CorePluginName, Version),
307 for _, name := range loadedPlugins {
308 version := GetInstalledPluginVersion(name)
309 if pv := newStaticPluginVersion(name, version); pv != nil {
310 result = append(result, pv)
317 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
318 func GetInstalledPluginVersion(name string) string {
319 plugin := L.GetGlobal(name)
320 if plugin != lua.LNil {
321 version := L.GetField(plugin, "VERSION")
322 if str, ok := version.(lua.LString); ok {
330 func (pv *PluginVersion) DownloadAndInstall() error {
331 messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
332 resp, err := http.Get(pv.Url)
336 defer resp.Body.Close()
337 data, err := ioutil.ReadAll(resp.Body)
341 zipbuf := bytes.NewReader(data)
342 z, err := zip.NewReader(zipbuf, zipbuf.Size())
346 targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
347 dirPerm := os.FileMode(0755)
348 if err = os.MkdirAll(targetDir, dirPerm); err != nil {
351 for _, f := range z.File {
352 targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...))
353 if f.FileInfo().IsDir() {
354 if err := os.MkdirAll(targetName, dirPerm); err != nil {
358 content, err := f.Open()
362 defer content.Close()
363 if target, err := os.Create(targetName); err != nil {
367 if _, err = io.Copy(target, content); err != nil {
376 func (pl PluginPackages) Get(name string) *PluginPackage {
377 for _, p := range pl {
385 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
386 result := make(PluginVersions, 0)
389 for _, v := range p.Versions {
390 result = append(result, v)
396 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
397 m := make(map[string]*PluginDependency)
398 for _, r := range req {
401 for _, o := range other {
404 m[o.Name] = &PluginDependency{
406 o.Range.AND(cur.Range),
412 result := make(PluginDependencies, 0, len(m))
413 for _, v := range m {
414 result = append(result, v)
419 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
421 return selectedVersions, nil
423 currentRequirement, stillOpen := open[0], open[1:]
424 if currentRequirement != nil {
425 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
426 if currentRequirement.Range(selVersion.Version) {
427 return all.Resolve(selectedVersions, stillOpen)
429 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
431 availableVersions := all.GetAllVersions(currentRequirement.Name)
432 sort.Sort(availableVersions)
434 for _, version := range availableVersions {
435 if currentRequirement.Range(version.Version) {
436 resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
443 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
446 return selectedVersions, nil
450 func (versions PluginVersions) install() {
451 anyInstalled := false
452 currentlyInstalled := GetInstalledVersions()
454 for _, sel := range versions {
455 if sel.pack.Name != CorePluginName {
456 shouldInstall := true
457 if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
458 if pv.Version.NE(sel.Version) {
459 messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
460 UninstallPlugin(sel.pack.Name)
462 shouldInstall = false
467 if err := sel.DownloadAndInstall(); err != nil {
476 messenger.Message("One or more plugins installed. Please restart micro.")
480 // UninstallPlugin deletes the plugin folder of the given plugin
481 func UninstallPlugin(name string) {
482 if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
487 func (pl PluginPackage) Install() {
488 selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
491 Range: semver.Range(func(v semver.Version) bool { return true }),
500 func UpdatePlugins() {
501 microVersion := PluginVersions{
502 newStaticPluginVersion(CorePluginName, Version),
505 var updates = make(PluginDependencies, 0)
506 for _, name := range loadedPlugins {
507 pv := GetInstalledPluginVersion(name)
508 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
510 updates = append(updates, &PluginDependency{
517 selected, err := GetAllPluginPackages().Resolve(microVersion, updates)