16 "github.com/blang/semver"
17 "github.com/yuin/gopher-lua"
18 "github.com/zyedidia/json5/encoding/json5"
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 // don't add the dependency if it's the core and
178 // we have a unknown version number.
179 // in that case just accept that dependency (which equals to not adding it.)
180 if k != CorePluginName || !isUnknownCoreVersion() {
181 if vRange, err := semver.ParseRange(v); err == nil {
182 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
189 // UnmarshalJSON unmarshals raw json to a PluginPackage
190 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
196 Versions PluginVersions
198 if err := json5.Unmarshal(data, &values); err != nil {
201 pp.Name = values.Name
202 pp.Description = values.Description
203 pp.Author = values.Author
204 pp.Tags = values.Tags
205 pp.Versions = values.Versions
206 for _, v := range pp.Versions {
212 // GetAllPluginPackages gets all PluginPackages which may be available.
213 func GetAllPluginPackages() PluginPackages {
214 if allPluginPackages == nil {
215 getOption := func(name string) []string {
216 data := GetOption(name)
217 if strs, ok := data.([]string); ok {
220 if ifs, ok := data.([]interface{}); ok {
221 result := make([]string, len(ifs))
222 for i, urlIf := range ifs {
223 if url, ok := urlIf.(string); ok {
234 channels := PluginChannels{}
235 for _, url := range getOption("pluginchannels") {
236 channels = append(channels, PluginChannel(url))
238 repos := []PluginRepository{}
239 for _, url := range getOption("pluginrepos") {
240 repos = append(repos, PluginRepository(url))
242 allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
244 return channels.Fetch()
246 return repos[i-1].Fetch()
250 return allPluginPackages
253 func (pv PluginVersions) find(ppName string) *PluginVersion {
254 for _, v := range pv {
255 if v.pack.Name == ppName {
262 // Len returns the number of pluginversions in this slice
263 func (pv PluginVersions) Len() int {
267 // Swap two entries of the slice
268 func (pv PluginVersions) Swap(i, j int) {
269 pv[i], pv[j] = pv[j], pv[i]
272 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
273 func (s PluginVersions) Less(i, j int) bool {
274 return s[i].Version.GT(s[j].Version)
277 // Match returns true if the package matches a given search text
278 func (pp PluginPackage) Match(text string) bool {
279 text = strings.ToLower(text)
280 for _, t := range pp.Tags {
281 if strings.ToLower(t) == text {
285 if strings.Contains(strings.ToLower(pp.Name), text) {
289 if strings.Contains(strings.ToLower(pp.Description), text) {
296 // IsInstallable returns true if the package can be installed.
297 func (pp PluginPackage) IsInstallable() bool {
298 _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
301 Range: semver.Range(func(v semver.Version) bool { return true }),
306 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
307 // could be or are already installed
308 func SearchPlugin(texts []string) (plugins PluginPackages) {
309 plugins = make(PluginPackages, 0)
312 for _, pp := range GetAllPluginPackages() {
313 for _, text := range texts {
319 if pp.IsInstallable() {
320 plugins = append(plugins, pp)
326 func isUnknownCoreVersion() bool {
327 _, err := semver.ParseTolerant(Version)
331 func newStaticPluginVersion(name, version string) *PluginVersion {
332 vers, err := semver.ParseTolerant(version)
335 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
336 vers = semver.MustParse("0.0.0-unknown")
339 pl := &PluginPackage{
342 pv := &PluginVersion{
346 pl.Versions = PluginVersions{pv}
350 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
351 // micro itself. This can be used to resolve dependencies.
352 func GetInstalledVersions(withCore bool) PluginVersions {
353 result := PluginVersions{}
355 result = append(result, newStaticPluginVersion(CorePluginName, Version))
358 for _, name := range loadedPlugins {
359 version := GetInstalledPluginVersion(name)
360 if pv := newStaticPluginVersion(name, version); pv != nil {
361 result = append(result, pv)
368 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
369 func GetInstalledPluginVersion(name string) string {
370 plugin := L.GetGlobal(name)
371 if plugin != lua.LNil {
372 version := L.GetField(plugin, "VERSION")
373 if str, ok := version.(lua.LString); ok {
381 func (pv *PluginVersion) DownloadAndInstall() error {
382 messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
383 resp, err := http.Get(pv.Url)
387 defer resp.Body.Close()
388 data, err := ioutil.ReadAll(resp.Body)
392 zipbuf := bytes.NewReader(data)
393 z, err := zip.NewReader(zipbuf, zipbuf.Size())
397 targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
398 dirPerm := os.FileMode(0755)
399 if err = os.MkdirAll(targetDir, dirPerm); err != nil {
403 // Check if all files in zip are in the same directory.
404 // this might be the case if the plugin zip contains the whole plugin dir
405 // instead of its content.
408 for i, f := range z.File {
409 parts := strings.Split(f.Name, "/")
412 } else if parts[0] != prefix {
416 // switch to true since we have at least a second file
421 for _, f := range z.File {
422 parts := strings.Split(f.Name, "/")
427 targetName := filepath.Join(targetDir, filepath.Join(parts...))
428 if f.FileInfo().IsDir() {
429 if err := os.MkdirAll(targetName, dirPerm); err != nil {
433 content, err := f.Open()
437 defer content.Close()
438 if target, err := os.Create(targetName); err != nil {
442 if _, err = io.Copy(target, content); err != nil {
451 func (pl PluginPackages) Get(name string) *PluginPackage {
452 for _, p := range pl {
460 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
461 result := make(PluginVersions, 0)
464 for _, v := range p.Versions {
465 result = append(result, v)
471 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
472 m := make(map[string]*PluginDependency)
473 for _, r := range req {
476 for _, o := range other {
479 m[o.Name] = &PluginDependency{
481 o.Range.AND(cur.Range),
487 result := make(PluginDependencies, 0, len(m))
488 for _, v := range m {
489 result = append(result, v)
494 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
496 return selectedVersions, nil
498 currentRequirement, stillOpen := open[0], open[1:]
499 if currentRequirement != nil {
500 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
501 if currentRequirement.Range(selVersion.Version) {
502 return all.Resolve(selectedVersions, stillOpen)
504 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
506 availableVersions := all.GetAllVersions(currentRequirement.Name)
507 sort.Sort(availableVersions)
509 for _, version := range availableVersions {
510 if currentRequirement.Range(version.Version) {
511 resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
518 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
521 return selectedVersions, nil
525 func (versions PluginVersions) install() {
526 anyInstalled := false
527 currentlyInstalled := GetInstalledVersions(true)
529 for _, sel := range versions {
530 if sel.pack.Name != CorePluginName {
531 shouldInstall := true
532 if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
533 if pv.Version.NE(sel.Version) {
534 messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
535 UninstallPlugin(sel.pack.Name)
537 shouldInstall = false
542 if err := sel.DownloadAndInstall(); err != nil {
551 messenger.Message("One or more plugins installed. Please restart micro.")
553 messenger.AddLog("Nothing to install / update")
557 // UninstallPlugin deletes the plugin folder of the given plugin
558 func UninstallPlugin(name string) {
559 if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
564 func (pl PluginPackage) Install() {
565 selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
568 Range: semver.Range(func(v semver.Version) bool { return true }),
577 func UpdatePlugins(plugins []string) {
578 // if no plugins are specified, update all installed plugins.
579 if len(plugins) == 0 {
580 plugins = loadedPlugins
583 messenger.AddLog("Checking for plugin updates")
584 microVersion := PluginVersions{
585 newStaticPluginVersion(CorePluginName, Version),
588 var updates = make(PluginDependencies, 0)
589 for _, name := range plugins {
590 pv := GetInstalledPluginVersion(name)
591 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
593 updates = append(updates, &PluginDependency{
600 selected, err := GetAllPluginPackages().Resolve(microVersion, updates)