18 "github.com/blang/semver"
19 "github.com/yuin/gopher-lua"
23 pluginChannels PluginChannels = PluginChannels{
24 PluginChannel("https://www.boombuler.de/channel.json"),
27 allPluginPackages PluginPackages = nil
30 // PluginChannel contains an url to a json list of PluginRepository
31 type PluginChannel string
33 // PluginChannels is a slice of PluginChannel
34 type PluginChannels []PluginChannel
36 // PluginRepository contains an url to json file containing PluginPackages
37 type PluginRepository string
39 // PluginPackage contains the meta-data of a plugin and all available versions
40 type PluginPackage struct {
45 Versions PluginVersions
48 // PluginPackages is a list of PluginPackage instances.
49 type PluginPackages []*PluginPackage
51 // PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
52 type PluginVersion struct {
54 Version semver.Version
56 Require PluginDependencies
59 // PluginVersions is a slice of PluginVersion
60 type PluginVersions []*PluginVersion
62 // PluginDenendency descripes a dependency to another plugin or micro itself.
63 type PluginDependency struct {
68 // PluginDependencies is a slice of PluginDependency
69 type PluginDependencies []*PluginDependency
71 func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
72 wgQuery := new(sync.WaitGroup)
75 results := make(chan PluginPackages)
77 wgDone := new(sync.WaitGroup)
79 var packages PluginPackages
80 for i := 0; i < count; i++ {
87 packages = make(PluginPackages, 0)
88 for res := range results {
89 packages = append(packages, res...)
99 // Fetch retrieves all available PluginPackages from the given channels
100 func (pc PluginChannels) Fetch() PluginPackages {
101 return fetchAllSources(len(pc), func(i int) PluginPackages {
106 // Fetch retrieves all available PluginPackages from the given channel
107 func (pc PluginChannel) Fetch() PluginPackages {
108 resp, err := http.Get(string(pc))
110 TermMessage("Failed to query plugin channel:\n", err)
111 return PluginPackages{}
113 defer resp.Body.Close()
114 decoder := json.NewDecoder(resp.Body)
116 var repositories []PluginRepository
117 if err := decoder.Decode(&repositories); err != nil {
118 TermMessage("Failed to decode channel data:\n", err)
119 return PluginPackages{}
121 return fetchAllSources(len(repositories), func(i int) PluginPackages {
122 return repositories[i].Fetch()
126 // Fetch retrieves all available PluginPackages from the given repository
127 func (pr PluginRepository) Fetch() PluginPackages {
128 resp, err := http.Get(string(pr))
130 TermMessage("Failed to query plugin repository:\n", err)
131 return PluginPackages{}
133 defer resp.Body.Close()
134 decoder := json.NewDecoder(resp.Body)
136 var plugins PluginPackages
137 if err := decoder.Decode(&plugins); err != nil {
138 TermMessage("Failed to decode repository data:\n", err)
139 return PluginPackages{}
144 // UnmarshalJSON unmarshals raw json to a PluginVersion
145 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
147 Version semver.Version
149 Require map[string]string
152 if err := json.Unmarshal(data, &values); err != nil {
155 pv.Version = values.Version
157 pv.Require = make(PluginDependencies, 0)
159 for k, v := range values.Require {
160 if vRange, err := semver.ParseRange(v); err == nil {
161 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
167 // UnmarshalJSON unmarshals raw json to a PluginPackage
168 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
174 Versions PluginVersions
176 if err := json.Unmarshal(data, &values); err != nil {
179 pp.Name = values.Name
180 pp.Description = values.Description
181 pp.Author = values.Author
182 pp.Tags = values.Tags
183 pp.Versions = values.Versions
184 for _, v := range pp.Versions {
190 // GetAllPluginPackages gets all PluginPackages which may be available.
191 func GetAllPluginPackages() PluginPackages {
192 if allPluginPackages == nil {
193 allPluginPackages = pluginChannels.Fetch()
195 return allPluginPackages
198 func (pv PluginVersions) find(ppName string) *PluginVersion {
199 for _, v := range pv {
200 if v.pack.Name == ppName {
207 // Len returns the number of pluginversions in this slice
208 func (pv PluginVersions) Len() int {
212 // Swap two entries of the slice
213 func (pv PluginVersions) Swap(i, j int) {
214 pv[i], pv[j] = pv[j], pv[i]
217 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
218 func (s PluginVersions) Less(i, j int) bool {
219 return s[i].Version.GT(s[j].Version)
222 // Match returns true if the package matches a given search text
223 func (pp PluginPackage) Match(text string) bool {
224 // ToDo: improve matching.
226 if r, err := regexp.Compile(text); err == nil {
227 return r.MatchString(pp.Name)
232 // IsInstallable returns true if the package can be installed.
233 func (pp PluginPackage) IsInstallable() bool {
234 _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
237 Range: semver.Range(func(v semver.Version) bool { return true }),
242 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
243 // could be or are already installed
244 func SearchPlugin(text string) (plugins PluginPackages) {
245 plugins = make(PluginPackages, 0)
246 for _, pp := range GetAllPluginPackages() {
247 if pp.Match(text) && pp.IsInstallable() {
248 plugins = append(plugins, pp)
254 func newStaticPluginVersion(name, version string) *PluginVersion {
255 vers, err := semver.Parse(version)
259 pl := &PluginPackage{
262 pv := &PluginVersion{
266 pl.Versions = PluginVersions{pv}
270 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
271 // micro itself. This can be used to resolve dependencies.
272 func GetInstalledVersions() PluginVersions {
273 result := PluginVersions{
274 newStaticPluginVersion("micro", Version),
277 for _, name := range loadedPlugins {
278 version := GetInstalledPluginVersion(name)
279 if pv := newStaticPluginVersion(name, version); pv != nil {
280 result = append(result, pv)
287 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
288 func GetInstalledPluginVersion(name string) string {
289 plugin := L.GetGlobal(name)
290 if plugin != lua.LNil {
291 version := L.GetField(plugin, "VERSION")
292 if str, ok := version.(lua.LString); ok {
300 func (pv *PluginVersion) DownloadAndInstall() error {
301 resp, err := http.Get(pv.Url)
305 defer resp.Body.Close()
306 data, err := ioutil.ReadAll(resp.Body)
310 zipbuf := bytes.NewReader(data)
311 z, err := zip.NewReader(zipbuf, zipbuf.Size())
315 targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
316 dirPerm := os.FileMode(0755)
317 if err = os.MkdirAll(targetDir, dirPerm); err != nil {
320 for _, f := range z.File {
321 targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...))
322 if f.FileInfo().IsDir() {
323 if err := os.MkdirAll(targetName, dirPerm); err != nil {
327 content, err := f.Open()
331 defer content.Close()
332 if target, err := os.Create(targetName); err != nil {
336 if _, err = io.Copy(target, content); err != nil {
345 func (pl PluginPackages) Get(name string) *PluginPackage {
346 for _, p := range pl {
354 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
355 result := make(PluginVersions, 0)
358 for _, v := range p.Versions {
359 result = append(result, v)
365 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
366 m := make(map[string]*PluginDependency)
367 for _, r := range req {
370 for _, o := range other {
373 m[o.Name] = &PluginDependency{
375 o.Range.AND(cur.Range),
381 result := make(PluginDependencies, 0, len(m))
382 for _, v := range m {
383 result = append(result, v)
388 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
390 return selectedVersions, nil
392 currentRequirement, stillOpen := open[0], open[1:]
393 if currentRequirement != nil {
394 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
395 if currentRequirement.Range(selVersion.Version) {
396 return all.Resolve(selectedVersions, stillOpen)
398 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
400 availableVersions := all.GetAllVersions(currentRequirement.Name)
401 sort.Sort(availableVersions)
403 for _, version := range availableVersions {
404 if currentRequirement.Range(version.Version) {
405 resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
412 return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
415 return selectedVersions, nil
419 func (versions PluginVersions) install() {
420 anyInstalled := false
421 for _, sel := range versions {
422 if sel.pack.Name != "micro" {
423 installed := GetInstalledPluginVersion(sel.pack.Name)
424 if v, err := semver.Parse(installed); err != nil || v.NE(sel.Version) {
425 UninstallPlugin(sel.pack.Name)
427 if err := sel.DownloadAndInstall(); err != nil {
435 messenger.Message("One or more plugins installed. Please restart micro.")
439 // UninstallPlugin deletes the plugin folder of the given plugin
440 func UninstallPlugin(name string) {
441 if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
446 func (pl PluginPackage) Install() {
447 selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{
450 Range: semver.Range(func(v semver.Version) bool { return true }),
459 func UpdatePlugins() {
460 microVersion := PluginVersions{
461 newStaticPluginVersion("micro", Version),
464 var updates = make(PluginDependencies, 0)
465 for _, name := range loadedPlugins {
466 pv := GetInstalledPluginVersion(name)
467 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
469 updates = append(updates, &PluginDependency{
476 selected, err := GetAllPluginPackages().Resolve(microVersion, updates)