16 "github.com/blang/semver"
17 "github.com/yosuke-furukawa/json5/encoding/json5"
18 "github.com/yuin/gopher-lua"
22 allPluginPackages PluginPackages = nil
25 // CorePluginName is a plugin dependency name for the micro core.
26 const CorePluginName = "micro"
28 // PluginChannel contains an url to a json list of PluginRepository
29 type PluginChannel string
31 // PluginChannels is a slice of PluginChannel
32 type PluginChannels []PluginChannel
34 // PluginRepository contains an url to json file containing PluginPackages
35 type PluginRepository string
37 // PluginPackage contains the meta-data of a plugin and all available versions
38 type PluginPackage struct {
43 Versions PluginVersions
46 // PluginPackages is a list of PluginPackage instances.
47 type PluginPackages []*PluginPackage
49 // PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
50 type PluginVersion struct {
52 Version semver.Version
54 Require PluginDependencies
57 // PluginVersions is a slice of PluginVersion
58 type PluginVersions []*PluginVersion
60 // PluginDenendency descripes a dependency to another plugin or micro itself.
61 type PluginDependency struct {
66 // PluginDependencies is a slice of PluginDependency
67 type PluginDependencies []*PluginDependency
69 func (pp *PluginPackage) String() string {
70 buf := new(bytes.Buffer)
71 buf.WriteString("Plugin: ")
72 buf.WriteString(pp.Name)
75 buf.WriteString("Author: ")
76 buf.WriteString(pp.Author)
79 if pp.Description != "" {
81 buf.WriteString(pp.Description)
86 func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
87 wgQuery := new(sync.WaitGroup)
90 results := make(chan PluginPackages)
92 wgDone := new(sync.WaitGroup)
94 var packages PluginPackages
95 for i := 0; i < count; i++ {
102 packages = make(PluginPackages, 0)
103 for res := range results {
104 packages = append(packages, res...)
114 // Fetch retrieves all available PluginPackages from the given channels
115 func (pc PluginChannels) Fetch() PluginPackages {
116 return fetchAllSources(len(pc), func(i int) PluginPackages {
121 // Fetch retrieves all available PluginPackages from the given channel
122 func (pc PluginChannel) Fetch() PluginPackages {
123 messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc)))
124 resp, err := http.Get(string(pc))
126 TermMessage("Failed to query plugin channel:\n", err)
127 return PluginPackages{}
129 defer resp.Body.Close()
130 decoder := json5.NewDecoder(resp.Body)
132 var repositories []PluginRepository
133 if err := decoder.Decode(&repositories); err != nil {
134 TermMessage("Failed to decode channel data:\n", err)
135 return PluginPackages{}
137 return fetchAllSources(len(repositories), func(i int) PluginPackages {
138 return repositories[i].Fetch()
142 // Fetch retrieves all available PluginPackages from the given repository
143 func (pr PluginRepository) Fetch() PluginPackages {
144 messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr)))
145 resp, err := http.Get(string(pr))
147 TermMessage("Failed to query plugin repository:\n", err)
148 return PluginPackages{}
150 defer resp.Body.Close()
151 decoder := json5.NewDecoder(resp.Body)
153 var plugins PluginPackages
154 if err := decoder.Decode(&plugins); err != nil {
155 TermMessage("Failed to decode repository data:\n", err)
156 return PluginPackages{}
161 // UnmarshalJSON unmarshals raw json to a PluginVersion
162 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
164 Version semver.Version
166 Require map[string]string
169 if err := json5.Unmarshal(data, &values); err != nil {
172 pv.Version = values.Version
174 pv.Require = make(PluginDependencies, 0)
176 for k, v := range values.Require {
177 if vRange, err := semver.ParseRange(v); err == nil {
178 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
184 // UnmarshalJSON unmarshals raw json to a PluginPackage
185 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
191 Versions PluginVersions
193 if err := json5.Unmarshal(data, &values); err != nil {
196 pp.Name = values.Name
197 pp.Description = values.Description
198 pp.Author = values.Author
199 pp.Tags = values.Tags
200 pp.Versions = values.Versions
201 for _, v := range pp.Versions {
207 // GetAllPluginPackages gets all PluginPackages which may be available.
208 func GetAllPluginPackages() PluginPackages {
209 if allPluginPackages == nil {
210 getOption := func(name string) []string {
211 data := GetOption(name)
212 if strs, ok := data.([]string); ok {
215 if ifs, ok := data.([]interface{}); ok {
216 result := make([]string, len(ifs))
217 for i, urlIf := range ifs {
218 if url, ok := urlIf.(string); ok {
229 channels := PluginChannels{}
230 for _, url := range getOption("pluginchannels") {
231 channels = append(channels, PluginChannel(url))
233 repos := []PluginRepository{}
234 for _, url := range getOption("pluginrepos") {
235 repos = append(repos, PluginRepository(url))
237 allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
239 return channels.Fetch()
241 return repos[i-1].Fetch()
245 return allPluginPackages
248 func (pv PluginVersions) find(ppName string) *PluginVersion {
249 for _, v := range pv {
250 if v.pack.Name == ppName {
257 // Len returns the number of pluginversions in this slice
258 func (pv PluginVersions) Len() int {
262 // Swap two entries of the slice
263 func (pv PluginVersions) Swap(i, j int) {
264 pv[i], pv[j] = pv[j], pv[i]
267 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
268 func (s PluginVersions) Less(i, j int) bool {
269 return s[i].Version.GT(s[j].Version)
272 // Match returns true if the package matches a given search text
273 func (pp PluginPackage) Match(text string) bool {
274 text = strings.ToLower(text)
275 for _, t := range pp.Tags {
276 if strings.ToLower(t) == text {
280 if strings.Contains(strings.ToLower(pp.Name), text) {
284 if strings.Contains(strings.ToLower(pp.Description), text) {
291 // IsInstallable returns true if the package can be installed.
292 func (pp PluginPackage) IsInstallable() bool {
293 _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
296 Range: semver.Range(func(v semver.Version) bool { return true }),
301 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
302 // could be or are already installed
303 func SearchPlugin(texts []string) (plugins PluginPackages) {
304 plugins = make(PluginPackages, 0)
307 for _, pp := range GetAllPluginPackages() {
308 for _, text := range texts {
314 if pp.IsInstallable() {
315 plugins = append(plugins, pp)
321 func newStaticPluginVersion(name, version string) *PluginVersion {
322 vers, err := semver.ParseTolerant(version)
325 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
326 vers = semver.MustParse("0.0.0-unknown")
329 pl := &PluginPackage{
332 pv := &PluginVersion{
336 pl.Versions = PluginVersions{pv}
340 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
341 // micro itself. This can be used to resolve dependencies.
342 func GetInstalledVersions(withCore bool) PluginVersions {
343 result := PluginVersions{}
345 result = append(result, newStaticPluginVersion(CorePluginName, Version))
348 for _, name := range loadedPlugins {
349 version := GetInstalledPluginVersion(name)
350 if pv := newStaticPluginVersion(name, version); pv != nil {
351 result = append(result, pv)
358 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
359 func GetInstalledPluginVersion(name string) string {
360 plugin := L.GetGlobal(name)
361 if plugin != lua.LNil {
362 version := L.GetField(plugin, "VERSION")
363 if str, ok := version.(lua.LString); ok {
371 func (pv *PluginVersion) DownloadAndInstall() error {
372 messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
373 resp, err := http.Get(pv.Url)
377 defer resp.Body.Close()
378 data, err := ioutil.ReadAll(resp.Body)
382 zipbuf := bytes.NewReader(data)
383 z, err := zip.NewReader(zipbuf, zipbuf.Size())
387 targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
388 dirPerm := os.FileMode(0755)
389 if err = os.MkdirAll(targetDir, dirPerm); err != nil {
393 // Check if all files in zip are in the same directory.
394 // this might be the case if the plugin zip contains the whole plugin dir
395 // instead of its content.
398 for i, f := range z.File {
399 parts := strings.Split(f.Name, "/")
402 } else if parts[0] != prefix {
406 // switch to true since we have at least a second file
411 for _, f := range z.File {
412 parts := strings.Split(f.Name, "/")
417 targetName := filepath.Join(targetDir, filepath.Join(parts...))
418 if f.FileInfo().IsDir() {
419 if err := os.MkdirAll(targetName, dirPerm); err != nil {
423 content, err := f.Open()
427 defer content.Close()
428 if target, err := os.Create(targetName); err != nil {
432 if _, err = io.Copy(target, content); err != nil {
441 func (pl PluginPackages) Get(name string) *PluginPackage {
442 for _, p := range pl {
450 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
451 result := make(PluginVersions, 0)
454 for _, v := range p.Versions {
455 result = append(result, v)
461 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
462 m := make(map[string]*PluginDependency)
463 for _, r := range req {
466 for _, o := range other {
469 m[o.Name] = &PluginDependency{
471 o.Range.AND(cur.Range),
477 result := make(PluginDependencies, 0, len(m))
478 for _, v := range m {
479 result = append(result, v)
484 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
486 return selectedVersions, nil
488 currentRequirement, stillOpen := open[0], open[1:]
489 if currentRequirement != nil {
490 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
491 if currentRequirement.Range(selVersion.Version) {
492 return all.Resolve(selectedVersions, stillOpen)
494 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
496 availableVersions := all.GetAllVersions(currentRequirement.Name)
497 sort.Sort(availableVersions)
499 for _, version := range availableVersions {
500 if currentRequirement.Range(version.Version) {
501 resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
508 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
511 return selectedVersions, nil
515 func (versions PluginVersions) install() {
516 anyInstalled := false
517 currentlyInstalled := GetInstalledVersions(true)
519 for _, sel := range versions {
520 if sel.pack.Name != CorePluginName {
521 shouldInstall := true
522 if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
523 if pv.Version.NE(sel.Version) {
524 messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
525 UninstallPlugin(sel.pack.Name)
527 shouldInstall = false
532 if err := sel.DownloadAndInstall(); err != nil {
541 messenger.Message("One or more plugins installed. Please restart micro.")
543 messenger.AddLog("Nothing to install / update")
547 // UninstallPlugin deletes the plugin folder of the given plugin
548 func UninstallPlugin(name string) {
549 if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
554 func (pl PluginPackage) Install() {
555 selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
558 Range: semver.Range(func(v semver.Version) bool { return true }),
567 func UpdatePlugins(plugins []string) {
568 // if no plugins are specified, update all installed plugins.
569 if len(plugins) == 0 {
570 plugins = loadedPlugins
573 messenger.AddLog("Checking for plugin updates")
574 microVersion := PluginVersions{
575 newStaticPluginVersion(CorePluginName, Version),
578 var updates = make(PluginDependencies, 0)
579 for _, name := range plugins {
580 pv := GetInstalledPluginVersion(name)
581 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
583 updates = append(updates, &PluginDependency{
590 selected, err := GetAllPluginPackages().Resolve(microVersion, updates)