17 "github.com/blang/semver"
18 "github.com/flynn/json5"
19 "github.com/yuin/gopher-lua"
23 allPluginPackages PluginPackages
26 // CorePluginName is a plugin dependency name for the micro core.
27 const CorePluginName = "micro"
29 // PluginChannel contains an url to a json list of PluginRepository
30 type PluginChannel string
32 // PluginChannels is a slice of PluginChannel
33 type PluginChannels []PluginChannel
35 // PluginRepository contains an url to json file containing PluginPackages
36 type PluginRepository string
38 // PluginPackage contains the meta-data of a plugin and all available versions
39 type PluginPackage struct {
44 Versions PluginVersions
47 // PluginPackages is a list of PluginPackage instances.
48 type PluginPackages []*PluginPackage
50 // PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
51 type PluginVersion struct {
53 Version semver.Version
55 Require PluginDependencies
58 // PluginVersions is a slice of PluginVersion
59 type PluginVersions []*PluginVersion
61 // PluginDependency descripes a dependency to another plugin or micro itself.
62 type PluginDependency struct {
67 // PluginDependencies is a slice of PluginDependency
68 type PluginDependencies []*PluginDependency
70 func (pp *PluginPackage) String() string {
71 buf := new(bytes.Buffer)
72 buf.WriteString("Plugin: ")
73 buf.WriteString(pp.Name)
76 buf.WriteString("Author: ")
77 buf.WriteString(pp.Author)
80 if pp.Description != "" {
82 buf.WriteString(pp.Description)
87 func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
88 wgQuery := new(sync.WaitGroup)
91 results := make(chan PluginPackages)
93 wgDone := new(sync.WaitGroup)
95 var packages PluginPackages
96 for i := 0; i < count; i++ {
103 packages = make(PluginPackages, 0)
104 for res := range results {
105 packages = append(packages, res...)
115 // Fetch retrieves all available PluginPackages from the given channels
116 func (pc PluginChannels) Fetch() PluginPackages {
117 return fetchAllSources(len(pc), func(i int) PluginPackages {
122 // Fetch retrieves all available PluginPackages from the given channel
123 func (pc PluginChannel) Fetch() PluginPackages {
124 // messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc)))
125 resp, err := http.Get(string(pc))
127 TermMessage("Failed to query plugin channel:\n", err)
128 return PluginPackages{}
130 defer resp.Body.Close()
131 decoder := json5.NewDecoder(resp.Body)
133 var repositories []PluginRepository
134 if err := decoder.Decode(&repositories); err != nil {
135 TermMessage("Failed to decode channel data:\n", err)
136 return PluginPackages{}
138 return fetchAllSources(len(repositories), func(i int) PluginPackages {
139 return repositories[i].Fetch()
143 // Fetch retrieves all available PluginPackages from the given repository
144 func (pr PluginRepository) Fetch() PluginPackages {
145 // messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr)))
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{}
159 if len(plugins) > 0 {
160 return PluginPackages{plugins[0]}
166 // UnmarshalJSON unmarshals raw json to a PluginVersion
167 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
169 Version semver.Version
171 Require map[string]string
174 if err := json5.Unmarshal(data, &values); err != nil {
177 pv.Version = values.Version
179 pv.Require = make(PluginDependencies, 0)
181 for k, v := range values.Require {
182 // don't add the dependency if it's the core and
183 // we have a unknown version number.
184 // in that case just accept that dependency (which equals to not adding it.)
185 if k != CorePluginName || !isUnknownCoreVersion() {
186 if vRange, err := semver.ParseRange(v); err == nil {
187 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
194 // UnmarshalJSON unmarshals raw json to a PluginPackage
195 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
201 Versions PluginVersions
203 if err := json5.Unmarshal(data, &values); err != nil {
206 pp.Name = values.Name
207 pp.Description = values.Description
208 pp.Author = values.Author
209 pp.Tags = values.Tags
210 pp.Versions = values.Versions
211 for _, v := range pp.Versions {
217 // GetAllPluginPackages gets all PluginPackages which may be available.
218 func GetAllPluginPackages() PluginPackages {
219 if allPluginPackages == nil {
220 getOption := func(name string) []string {
221 data := GetOption(name)
222 if strs, ok := data.([]string); ok {
225 if ifs, ok := data.([]interface{}); ok {
226 result := make([]string, len(ifs))
227 for i, urlIf := range ifs {
228 if url, ok := urlIf.(string); ok {
239 channels := PluginChannels{}
240 for _, url := range getOption("pluginchannels") {
241 channels = append(channels, PluginChannel(url))
243 repos := []PluginRepository{}
244 for _, url := range getOption("pluginrepos") {
245 repos = append(repos, PluginRepository(url))
247 allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
249 return channels.Fetch()
251 return repos[i-1].Fetch()
254 return allPluginPackages
257 func (pv PluginVersions) find(ppName string) *PluginVersion {
258 for _, v := range pv {
259 if v.pack.Name == ppName {
266 // Len returns the number of pluginversions in this slice
267 func (pv PluginVersions) Len() int {
271 // Swap two entries of the slice
272 func (pv PluginVersions) Swap(i, j int) {
273 pv[i], pv[j] = pv[j], pv[i]
276 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
277 func (pv PluginVersions) Less(i, j int) bool {
278 return pv[i].Version.GT(pv[j].Version)
281 // Match returns true if the package matches a given search text
282 func (pp PluginPackage) Match(text string) bool {
283 text = strings.ToLower(text)
284 for _, t := range pp.Tags {
285 if strings.ToLower(t) == text {
289 if strings.Contains(strings.ToLower(pp.Name), text) {
293 if strings.Contains(strings.ToLower(pp.Description), text) {
300 // IsInstallable returns true if the package can be installed.
301 func (pp PluginPackage) IsInstallable() error {
302 _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
305 Range: semver.Range(func(v semver.Version) bool { return true }),
310 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
311 // could be or are already installed
312 func SearchPlugin(texts []string) (plugins PluginPackages) {
313 plugins = make(PluginPackages, 0)
316 for _, pp := range GetAllPluginPackages() {
317 for _, text := range texts {
323 if err := pp.IsInstallable(); err == nil {
324 plugins = append(plugins, pp)
330 func isUnknownCoreVersion() bool {
331 _, err := semver.ParseTolerant(Version)
335 func newStaticPluginVersion(name, version string) *PluginVersion {
336 vers, err := semver.ParseTolerant(version)
339 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
340 vers = semver.MustParse("0.0.0-unknown")
343 pl := &PluginPackage{
346 pv := &PluginVersion{
350 pl.Versions = PluginVersions{pv}
354 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
355 // micro itself. This can be used to resolve dependencies.
356 func GetInstalledVersions(withCore bool) PluginVersions {
357 result := PluginVersions{}
359 result = append(result, newStaticPluginVersion(CorePluginName, Version))
362 for name, lpname := range loadedPlugins {
363 version := GetInstalledPluginVersion(lpname)
364 if pv := newStaticPluginVersion(name, version); pv != nil {
365 result = append(result, pv)
372 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
373 func GetInstalledPluginVersion(name string) string {
374 plugin := L.GetGlobal(name)
375 if plugin != lua.LNil {
376 version := L.GetField(plugin, "VERSION")
377 if str, ok := version.(lua.LString); ok {
385 // DownloadAndInstall downloads and installs the given plugin and version
386 func (pv *PluginVersion) DownloadAndInstall() error {
387 messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
388 resp, err := http.Get(pv.Url)
392 defer resp.Body.Close()
393 data, err := ioutil.ReadAll(resp.Body)
397 zipbuf := bytes.NewReader(data)
398 z, err := zip.NewReader(zipbuf, zipbuf.Size())
402 targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
403 dirPerm := os.FileMode(0755)
404 if err = os.MkdirAll(targetDir, dirPerm); err != nil {
408 // Check if all files in zip are in the same directory.
409 // this might be the case if the plugin zip contains the whole plugin dir
410 // instead of its content.
413 for i, f := range z.File {
414 parts := strings.Split(f.Name, "/")
417 } else if parts[0] != prefix {
421 // switch to true since we have at least a second file
426 for _, f := range z.File {
427 parts := strings.Split(f.Name, "/")
432 targetName := filepath.Join(targetDir, filepath.Join(parts...))
433 if f.FileInfo().IsDir() {
434 if err := os.MkdirAll(targetName, dirPerm); err != nil {
438 basepath := path.Dir(targetName)
440 if err := os.MkdirAll(basepath, dirPerm); err != nil {
444 content, err := f.Open()
448 defer content.Close()
449 target, err := os.Create(targetName)
454 if _, err = io.Copy(target, content); err != nil {
462 func (pl PluginPackages) Get(name string) *PluginPackage {
463 for _, p := range pl {
471 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
472 result := make(PluginVersions, 0)
475 for _, v := range p.Versions {
476 result = append(result, v)
482 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
483 m := make(map[string]*PluginDependency)
484 for _, r := range req {
487 for _, o := range other {
490 m[o.Name] = &PluginDependency{
492 o.Range.AND(cur.Range),
498 result := make(PluginDependencies, 0, len(m))
499 for _, v := range m {
500 result = append(result, v)
505 // Resolve resolves dependencies between different plugins
506 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
508 return selectedVersions, nil
510 currentRequirement, stillOpen := open[0], open[1:]
511 if currentRequirement != nil {
512 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
513 if currentRequirement.Range(selVersion.Version) {
514 return all.Resolve(selectedVersions, stillOpen)
516 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
518 availableVersions := all.GetAllVersions(currentRequirement.Name)
519 sort.Sort(availableVersions)
521 for _, version := range availableVersions {
522 if currentRequirement.Range(version.Version) {
523 resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
530 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
532 return selectedVersions, nil
535 func (pv PluginVersions) install() {
536 anyInstalled := false
537 currentlyInstalled := GetInstalledVersions(true)
539 for _, sel := range pv {
540 if sel.pack.Name != CorePluginName {
541 shouldInstall := true
542 if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
543 if pv.Version.NE(sel.Version) {
544 messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
545 UninstallPlugin(sel.pack.Name)
547 shouldInstall = false
552 if err := sel.DownloadAndInstall(); err != nil {
561 messenger.Message("One or more plugins installed. Please restart micro.")
563 messenger.AddLog("Nothing to install / update")
567 // UninstallPlugin deletes the plugin folder of the given plugin
568 func UninstallPlugin(name string) {
569 if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
573 delete(loadedPlugins, name)
576 // Install installs the plugin
577 func (pl PluginPackage) Install() {
578 selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
581 Range: semver.Range(func(v semver.Version) bool { return true }),
590 // UpdatePlugins updates the given plugins
591 func UpdatePlugins(plugins []string) {
592 // if no plugins are specified, update all installed plugins.
593 if len(plugins) == 0 {
594 for name := range loadedPlugins {
595 plugins = append(plugins, name)
599 messenger.AddLog("Checking for plugin updates")
600 microVersion := PluginVersions{
601 newStaticPluginVersion(CorePluginName, Version),
604 var updates = make(PluginDependencies, 0)
605 for _, name := range plugins {
606 pv := GetInstalledPluginVersion(name)
607 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
609 updates = append(updates, &PluginDependency{
616 selected, err := GetAllPluginPackages().Resolve(microVersion, updates)