commit
a1a307d858
8 changed files with 825 additions and 1 deletions
2
Makefile
2
Makefile
|
@ -1,6 +1,6 @@
|
|||
.PHONY: runtime
|
||||
|
||||
VERSION = $(shell git describe --tags --abbrev=0)
|
||||
VERSION = $(shell go run tools/build-version.go)
|
||||
HASH = $(shell git rev-parse --short HEAD)
|
||||
DATE = $(shell go run tools/build-date.go)
|
||||
|
||||
|
|
|
@ -149,3 +149,29 @@ func PluginComplete(complete Completion, input string) (chosen string, suggestio
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func PluginCmdComplete(input string) (chosen string, suggestions []string) {
|
||||
for _, cmd := range []string{"install", "remove", "search", "update", "list"} {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
func PluginNameComplete(input string) (chosen string, suggestions []string) {
|
||||
for _, pp := range GetAllPluginPackages() {
|
||||
if strings.HasPrefix(pp.Name, input) {
|
||||
suggestions = append(suggestions, pp.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
@ -40,6 +41,7 @@ var commandActions = map[string]func([]string){
|
|||
"Help": Help,
|
||||
"Eval": Eval,
|
||||
"ToggleLog": ToggleLog,
|
||||
"Plugin": PluginCmd,
|
||||
}
|
||||
|
||||
// InitCommands initializes the default commands
|
||||
|
@ -86,6 +88,66 @@ func DefaultCommands() map[string]StrCommand {
|
|||
"help": {"Help", []Completion{HelpCompletion, NoCompletion}},
|
||||
"eval": {"Eval", []Completion{NoCompletion}},
|
||||
"log": {"ToggleLog", []Completion{NoCompletion}},
|
||||
"plugin": {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}},
|
||||
}
|
||||
}
|
||||
|
||||
// InstallPlugin installs the given plugin by exact name match
|
||||
func PluginCmd(args []string) {
|
||||
if len(args) >= 1 {
|
||||
switch args[0] {
|
||||
case "install":
|
||||
for _, plugin := range args[1:] {
|
||||
pp := GetAllPluginPackages().Get(plugin)
|
||||
if pp == nil {
|
||||
messenger.Error("Unknown plugin \"" + plugin + "\"")
|
||||
} else if !pp.IsInstallable() {
|
||||
messenger.Error("Plugin \"" + plugin + "\" can not be installed.")
|
||||
} else {
|
||||
pp.Install()
|
||||
}
|
||||
}
|
||||
case "remove":
|
||||
for _, plugin := range args[1:] {
|
||||
// check if the plugin exists.
|
||||
for _, lp := range loadedPlugins {
|
||||
if lp == plugin {
|
||||
UninstallPlugin(plugin)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
case "update":
|
||||
UpdatePlugins(args[1:])
|
||||
case "list":
|
||||
plugins := GetInstalledVersions(false)
|
||||
messenger.AddLog("----------------")
|
||||
messenger.AddLog("The following plugins are currently installed:\n")
|
||||
for _, p := range plugins {
|
||||
messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version))
|
||||
}
|
||||
messenger.AddLog("----------------")
|
||||
if len(plugins) > 0 {
|
||||
if CurView().Type != vtLog {
|
||||
ToggleLog([]string{})
|
||||
}
|
||||
}
|
||||
case "search":
|
||||
plugins := SearchPlugin(args[1:])
|
||||
messenger.Message(len(plugins), " plugins found")
|
||||
for _, p := range plugins {
|
||||
messenger.AddLog("----------------")
|
||||
messenger.AddLog(p.String())
|
||||
}
|
||||
messenger.AddLog("----------------")
|
||||
if len(plugins) > 0 {
|
||||
if CurView().Type != vtLog {
|
||||
ToggleLog([]string{})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messenger.Error("Not enough arguments")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -192,6 +192,8 @@ const (
|
|||
CommandCompletion
|
||||
HelpCompletion
|
||||
OptionCompletion
|
||||
PluginCmdCompletion
|
||||
PluginNameCompletion
|
||||
)
|
||||
|
||||
// Prompt sends the user a message and waits for a response to be typed in
|
||||
|
@ -255,6 +257,10 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
|
|||
chosen, suggestions = HelpComplete(currentArg)
|
||||
} else if completionType == OptionCompletion {
|
||||
chosen, suggestions = OptionComplete(currentArg)
|
||||
} else if completionType == PluginCmdCompletion {
|
||||
chosen, suggestions = PluginCmdComplete(currentArg)
|
||||
} else if completionType == PluginNameCompletion {
|
||||
chosen, suggestions = PluginNameComplete(currentArg)
|
||||
} else if completionType < NoCompletion {
|
||||
chosen, suggestions = PluginComplete(completionType, currentArg)
|
||||
}
|
||||
|
|
606
cmd/micro/pluginmanager.go
Normal file
606
cmd/micro/pluginmanager.go
Normal file
|
@ -0,0 +1,606 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/json5/encoding/json5"
|
||||
)
|
||||
|
||||
var (
|
||||
allPluginPackages PluginPackages = nil
|
||||
)
|
||||
|
||||
// CorePluginName is a plugin dependency name for the micro core.
|
||||
const CorePluginName = "micro"
|
||||
|
||||
// PluginChannel contains an url to a json list of PluginRepository
|
||||
type PluginChannel string
|
||||
|
||||
// PluginChannels is a slice of PluginChannel
|
||||
type PluginChannels []PluginChannel
|
||||
|
||||
// PluginRepository contains an url to json file containing PluginPackages
|
||||
type PluginRepository string
|
||||
|
||||
// PluginPackage contains the meta-data of a plugin and all available versions
|
||||
type PluginPackage struct {
|
||||
Name string
|
||||
Description string
|
||||
Author string
|
||||
Tags []string
|
||||
Versions PluginVersions
|
||||
}
|
||||
|
||||
// PluginPackages is a list of PluginPackage instances.
|
||||
type PluginPackages []*PluginPackage
|
||||
|
||||
// PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
|
||||
type PluginVersion struct {
|
||||
pack *PluginPackage
|
||||
Version semver.Version
|
||||
Url string
|
||||
Require PluginDependencies
|
||||
}
|
||||
|
||||
// PluginVersions is a slice of PluginVersion
|
||||
type PluginVersions []*PluginVersion
|
||||
|
||||
// PluginDenendency descripes a dependency to another plugin or micro itself.
|
||||
type PluginDependency struct {
|
||||
Name string
|
||||
Range semver.Range
|
||||
}
|
||||
|
||||
// PluginDependencies is a slice of PluginDependency
|
||||
type PluginDependencies []*PluginDependency
|
||||
|
||||
func (pp *PluginPackage) String() string {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.WriteString("Plugin: ")
|
||||
buf.WriteString(pp.Name)
|
||||
buf.WriteRune('\n')
|
||||
if pp.Author != "" {
|
||||
buf.WriteString("Author: ")
|
||||
buf.WriteString(pp.Author)
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
if pp.Description != "" {
|
||||
buf.WriteRune('\n')
|
||||
buf.WriteString(pp.Description)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
|
||||
wgQuery := new(sync.WaitGroup)
|
||||
wgQuery.Add(count)
|
||||
|
||||
results := make(chan PluginPackages)
|
||||
|
||||
wgDone := new(sync.WaitGroup)
|
||||
wgDone.Add(1)
|
||||
var packages PluginPackages
|
||||
for i := 0; i < count; i++ {
|
||||
go func(i int) {
|
||||
results <- fetcher(i)
|
||||
wgQuery.Done()
|
||||
}(i)
|
||||
}
|
||||
go func() {
|
||||
packages = make(PluginPackages, 0)
|
||||
for res := range results {
|
||||
packages = append(packages, res...)
|
||||
}
|
||||
wgDone.Done()
|
||||
}()
|
||||
wgQuery.Wait()
|
||||
close(results)
|
||||
wgDone.Wait()
|
||||
return packages
|
||||
}
|
||||
|
||||
// Fetch retrieves all available PluginPackages from the given channels
|
||||
func (pc PluginChannels) Fetch() PluginPackages {
|
||||
return fetchAllSources(len(pc), func(i int) PluginPackages {
|
||||
return pc[i].Fetch()
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch retrieves all available PluginPackages from the given channel
|
||||
func (pc PluginChannel) Fetch() PluginPackages {
|
||||
messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc)))
|
||||
resp, err := http.Get(string(pc))
|
||||
if err != nil {
|
||||
TermMessage("Failed to query plugin channel:\n", err)
|
||||
return PluginPackages{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
decoder := json5.NewDecoder(resp.Body)
|
||||
|
||||
var repositories []PluginRepository
|
||||
if err := decoder.Decode(&repositories); err != nil {
|
||||
TermMessage("Failed to decode channel data:\n", err)
|
||||
return PluginPackages{}
|
||||
}
|
||||
return fetchAllSources(len(repositories), func(i int) PluginPackages {
|
||||
return repositories[i].Fetch()
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch retrieves all available PluginPackages from the given repository
|
||||
func (pr PluginRepository) Fetch() PluginPackages {
|
||||
messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr)))
|
||||
resp, err := http.Get(string(pr))
|
||||
if err != nil {
|
||||
TermMessage("Failed to query plugin repository:\n", err)
|
||||
return PluginPackages{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
decoder := json5.NewDecoder(resp.Body)
|
||||
|
||||
var plugins PluginPackages
|
||||
if err := decoder.Decode(&plugins); err != nil {
|
||||
TermMessage("Failed to decode repository data:\n", err)
|
||||
return PluginPackages{}
|
||||
}
|
||||
return plugins
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals raw json to a PluginVersion
|
||||
func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
|
||||
var values struct {
|
||||
Version semver.Version
|
||||
Url string
|
||||
Require map[string]string
|
||||
}
|
||||
|
||||
if err := json5.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
pv.Version = values.Version
|
||||
pv.Url = values.Url
|
||||
pv.Require = make(PluginDependencies, 0)
|
||||
|
||||
for k, v := range values.Require {
|
||||
// don't add the dependency if it's the core and
|
||||
// we have a unknown version number.
|
||||
// in that case just accept that dependency (which equals to not adding it.)
|
||||
if k != CorePluginName || !isUnknownCoreVersion() {
|
||||
if vRange, err := semver.ParseRange(v); err == nil {
|
||||
pv.Require = append(pv.Require, &PluginDependency{k, vRange})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals raw json to a PluginPackage
|
||||
func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
|
||||
var values struct {
|
||||
Name string
|
||||
Description string
|
||||
Author string
|
||||
Tags []string
|
||||
Versions PluginVersions
|
||||
}
|
||||
if err := json5.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
pp.Name = values.Name
|
||||
pp.Description = values.Description
|
||||
pp.Author = values.Author
|
||||
pp.Tags = values.Tags
|
||||
pp.Versions = values.Versions
|
||||
for _, v := range pp.Versions {
|
||||
v.pack = pp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllPluginPackages gets all PluginPackages which may be available.
|
||||
func GetAllPluginPackages() PluginPackages {
|
||||
if allPluginPackages == nil {
|
||||
getOption := func(name string) []string {
|
||||
data := GetOption(name)
|
||||
if strs, ok := data.([]string); ok {
|
||||
return strs
|
||||
}
|
||||
if ifs, ok := data.([]interface{}); ok {
|
||||
result := make([]string, len(ifs))
|
||||
for i, urlIf := range ifs {
|
||||
if url, ok := urlIf.(string); ok {
|
||||
result[i] = url
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
channels := PluginChannels{}
|
||||
for _, url := range getOption("pluginchannels") {
|
||||
channels = append(channels, PluginChannel(url))
|
||||
}
|
||||
repos := []PluginRepository{}
|
||||
for _, url := range getOption("pluginrepos") {
|
||||
repos = append(repos, PluginRepository(url))
|
||||
}
|
||||
allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
|
||||
if i == 0 {
|
||||
return channels.Fetch()
|
||||
} else {
|
||||
return repos[i-1].Fetch()
|
||||
}
|
||||
})
|
||||
}
|
||||
return allPluginPackages
|
||||
}
|
||||
|
||||
func (pv PluginVersions) find(ppName string) *PluginVersion {
|
||||
for _, v := range pv {
|
||||
if v.pack.Name == ppName {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns the number of pluginversions in this slice
|
||||
func (pv PluginVersions) Len() int {
|
||||
return len(pv)
|
||||
}
|
||||
|
||||
// Swap two entries of the slice
|
||||
func (pv PluginVersions) Swap(i, j int) {
|
||||
pv[i], pv[j] = pv[j], pv[i]
|
||||
}
|
||||
|
||||
// Less returns true if the version at position i is greater then the version at position j (used for sorting)
|
||||
func (s PluginVersions) Less(i, j int) bool {
|
||||
return s[i].Version.GT(s[j].Version)
|
||||
}
|
||||
|
||||
// Match returns true if the package matches a given search text
|
||||
func (pp PluginPackage) Match(text string) bool {
|
||||
text = strings.ToLower(text)
|
||||
for _, t := range pp.Tags {
|
||||
if strings.ToLower(t) == text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(pp.Name), text) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(pp.Description), text) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsInstallable returns true if the package can be installed.
|
||||
func (pp PluginPackage) IsInstallable() bool {
|
||||
_, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
|
||||
&PluginDependency{
|
||||
Name: pp.Name,
|
||||
Range: semver.Range(func(v semver.Version) bool { return true }),
|
||||
}})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SearchPlugin retrieves a list of all PluginPackages which match the given search text and
|
||||
// could be or are already installed
|
||||
func SearchPlugin(texts []string) (plugins PluginPackages) {
|
||||
plugins = make(PluginPackages, 0)
|
||||
|
||||
pluginLoop:
|
||||
for _, pp := range GetAllPluginPackages() {
|
||||
for _, text := range texts {
|
||||
if !pp.Match(text) {
|
||||
continue pluginLoop
|
||||
}
|
||||
}
|
||||
|
||||
if pp.IsInstallable() {
|
||||
plugins = append(plugins, pp)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isUnknownCoreVersion() bool {
|
||||
_, err := semver.ParseTolerant(Version)
|
||||
return err != nil
|
||||
}
|
||||
|
||||
func newStaticPluginVersion(name, version string) *PluginVersion {
|
||||
vers, err := semver.ParseTolerant(version)
|
||||
|
||||
if err != nil {
|
||||
if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
|
||||
vers = semver.MustParse("0.0.0-unknown")
|
||||
}
|
||||
}
|
||||
pl := &PluginPackage{
|
||||
Name: name,
|
||||
}
|
||||
pv := &PluginVersion{
|
||||
pack: pl,
|
||||
Version: vers,
|
||||
}
|
||||
pl.Versions = PluginVersions{pv}
|
||||
return pv
|
||||
}
|
||||
|
||||
// GetInstalledVersions returns a list of all currently installed plugins including an entry for
|
||||
// micro itself. This can be used to resolve dependencies.
|
||||
func GetInstalledVersions(withCore bool) PluginVersions {
|
||||
result := PluginVersions{}
|
||||
if withCore {
|
||||
result = append(result, newStaticPluginVersion(CorePluginName, Version))
|
||||
}
|
||||
|
||||
for _, name := range loadedPlugins {
|
||||
version := GetInstalledPluginVersion(name)
|
||||
if pv := newStaticPluginVersion(name, version); pv != nil {
|
||||
result = append(result, pv)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
|
||||
func GetInstalledPluginVersion(name string) string {
|
||||
plugin := L.GetGlobal(name)
|
||||
if plugin != lua.LNil {
|
||||
version := L.GetField(plugin, "VERSION")
|
||||
if str, ok := version.(lua.LString); ok {
|
||||
return string(str)
|
||||
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (pv *PluginVersion) DownloadAndInstall() error {
|
||||
messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
|
||||
resp, err := http.Get(pv.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zipbuf := bytes.NewReader(data)
|
||||
z, err := zip.NewReader(zipbuf, zipbuf.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
|
||||
dirPerm := os.FileMode(0755)
|
||||
if err = os.MkdirAll(targetDir, dirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if all files in zip are in the same directory.
|
||||
// this might be the case if the plugin zip contains the whole plugin dir
|
||||
// instead of its content.
|
||||
var prefix string
|
||||
allPrefixed := false
|
||||
for i, f := range z.File {
|
||||
parts := strings.Split(f.Name, "/")
|
||||
if i == 0 {
|
||||
prefix = parts[0]
|
||||
} else if parts[0] != prefix {
|
||||
allPrefixed = false
|
||||
break
|
||||
} else {
|
||||
// switch to true since we have at least a second file
|
||||
allPrefixed = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range z.File {
|
||||
parts := strings.Split(f.Name, "/")
|
||||
if allPrefixed {
|
||||
parts = parts[1:]
|
||||
}
|
||||
|
||||
targetName := filepath.Join(targetDir, filepath.Join(parts...))
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(targetName, dirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
content, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer content.Close()
|
||||
if target, err := os.Create(targetName); err != nil {
|
||||
return err
|
||||
} else {
|
||||
defer target.Close()
|
||||
if _, err = io.Copy(target, content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl PluginPackages) Get(name string) *PluginPackage {
|
||||
for _, p := range pl {
|
||||
if p.Name == name {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
|
||||
result := make(PluginVersions, 0)
|
||||
p := pl.Get(name)
|
||||
if p != nil {
|
||||
for _, v := range p.Versions {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
|
||||
m := make(map[string]*PluginDependency)
|
||||
for _, r := range req {
|
||||
m[r.Name] = r
|
||||
}
|
||||
for _, o := range other {
|
||||
cur, ok := m[o.Name]
|
||||
if ok {
|
||||
m[o.Name] = &PluginDependency{
|
||||
o.Name,
|
||||
o.Range.AND(cur.Range),
|
||||
}
|
||||
} else {
|
||||
m[o.Name] = o
|
||||
}
|
||||
}
|
||||
result := make(PluginDependencies, 0, len(m))
|
||||
for _, v := range m {
|
||||
result = append(result, v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
|
||||
if len(open) == 0 {
|
||||
return selectedVersions, nil
|
||||
}
|
||||
currentRequirement, stillOpen := open[0], open[1:]
|
||||
if currentRequirement != nil {
|
||||
if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
|
||||
if currentRequirement.Range(selVersion.Version) {
|
||||
return all.Resolve(selectedVersions, stillOpen)
|
||||
}
|
||||
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
|
||||
} else {
|
||||
availableVersions := all.GetAllVersions(currentRequirement.Name)
|
||||
sort.Sort(availableVersions)
|
||||
|
||||
for _, version := range availableVersions {
|
||||
if currentRequirement.Range(version.Version) {
|
||||
resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
|
||||
|
||||
if err == nil {
|
||||
return resolved, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
|
||||
}
|
||||
} else {
|
||||
return selectedVersions, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (versions PluginVersions) install() {
|
||||
anyInstalled := false
|
||||
currentlyInstalled := GetInstalledVersions(true)
|
||||
|
||||
for _, sel := range versions {
|
||||
if sel.pack.Name != CorePluginName {
|
||||
shouldInstall := true
|
||||
if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
|
||||
if pv.Version.NE(sel.Version) {
|
||||
messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
|
||||
UninstallPlugin(sel.pack.Name)
|
||||
} else {
|
||||
shouldInstall = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldInstall {
|
||||
if err := sel.DownloadAndInstall(); err != nil {
|
||||
messenger.Error(err)
|
||||
return
|
||||
}
|
||||
anyInstalled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if anyInstalled {
|
||||
messenger.Message("One or more plugins installed. Please restart micro.")
|
||||
} else {
|
||||
messenger.AddLog("Nothing to install / update")
|
||||
}
|
||||
}
|
||||
|
||||
// UninstallPlugin deletes the plugin folder of the given plugin
|
||||
func UninstallPlugin(name string) {
|
||||
if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
|
||||
messenger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (pl PluginPackage) Install() {
|
||||
selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
|
||||
&PluginDependency{
|
||||
Name: pl.Name,
|
||||
Range: semver.Range(func(v semver.Version) bool { return true }),
|
||||
}})
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
return
|
||||
}
|
||||
selected.install()
|
||||
}
|
||||
|
||||
func UpdatePlugins(plugins []string) {
|
||||
// if no plugins are specified, update all installed plugins.
|
||||
if len(plugins) == 0 {
|
||||
plugins = loadedPlugins
|
||||
}
|
||||
|
||||
messenger.AddLog("Checking for plugin updates")
|
||||
microVersion := PluginVersions{
|
||||
newStaticPluginVersion(CorePluginName, Version),
|
||||
}
|
||||
|
||||
var updates = make(PluginDependencies, 0)
|
||||
for _, name := range plugins {
|
||||
pv := GetInstalledPluginVersion(name)
|
||||
r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
|
||||
if err == nil {
|
||||
updates = append(updates, &PluginDependency{
|
||||
Name: name,
|
||||
Range: r,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
return
|
||||
}
|
||||
selected.install()
|
||||
}
|
55
cmd/micro/pluginmanager_test.go
Normal file
55
cmd/micro/pluginmanager_test.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/blang/semver"
|
||||
"testing"
|
||||
|
||||
"github.com/zyedidia/json5/encoding/json5"
|
||||
)
|
||||
|
||||
func TestDependencyResolving(t *testing.T) {
|
||||
js := `
|
||||
[{
|
||||
"Name": "Foo",
|
||||
"Versions": [{ "Version": "1.0.0" }, { "Version": "1.5.0" },{ "Version": "2.0.0" }]
|
||||
}, {
|
||||
"Name": "Bar",
|
||||
"Versions": [{ "Version": "1.0.0", "Require": {"Foo": ">1.0.0 <2.0.0"} }]
|
||||
}, {
|
||||
"Name": "Unresolvable",
|
||||
"Versions": [{ "Version": "1.0.0", "Require": {"Foo": "<=1.0.0", "Bar": ">0.0.0"} }]
|
||||
}]
|
||||
`
|
||||
var all PluginPackages
|
||||
err := json5.Unmarshal([]byte(js), &all)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
selected, err := all.Resolve(PluginVersions{}, PluginDependencies{
|
||||
&PluginDependency{"Bar", semver.MustParseRange(">=1.0.0")},
|
||||
})
|
||||
|
||||
check := func(name, version string) {
|
||||
v := selected.find(name)
|
||||
expected := semver.MustParse(version)
|
||||
if v == nil {
|
||||
t.Errorf("Failed to resolve %s", name)
|
||||
} else if expected.NE(v.Version) {
|
||||
t.Errorf("%s resolved in wrong version got %s", name, v)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
check("Foo", "1.5.0")
|
||||
check("Bar", "1.0.0")
|
||||
}
|
||||
|
||||
selected, err = all.Resolve(PluginVersions{}, PluginDependencies{
|
||||
&PluginDependency{"Unresolvable", semver.MustParseRange(">0.0.0")},
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("Unresolvable package resolved:", selected)
|
||||
}
|
||||
}
|
|
@ -192,6 +192,10 @@ func DefaultGlobalSettings() map[string]interface{} {
|
|||
"syntax": true,
|
||||
"tabsize": float64(4),
|
||||
"tabstospaces": false,
|
||||
"pluginchannels": []string{
|
||||
"https://www.boombuler.de/channel.json",
|
||||
},
|
||||
"pluginrepos": []string{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
65
tools/build-version.go
Normal file
65
tools/build-version.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
)
|
||||
|
||||
func getTag(match ...string) (string, *semver.PRVersion) {
|
||||
args := append([]string{
|
||||
"describe", "--tags",
|
||||
}, match...)
|
||||
if tag, err := exec.Command("git", args...).Output(); err != nil {
|
||||
return "", nil
|
||||
} else {
|
||||
tagParts := strings.Split(string(tag), "-")
|
||||
if len(tagParts) == 3 {
|
||||
if ahead, err := semver.NewPRVersion(tagParts[1]); err == nil {
|
||||
return tagParts[0], &ahead
|
||||
}
|
||||
}
|
||||
|
||||
return tagParts[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Find the last vX.X.X Tag and get how many builds we are ahead of it.
|
||||
versionStr, ahead := getTag("--match", "v*")
|
||||
version, err := semver.ParseTolerant(versionStr)
|
||||
if err != nil {
|
||||
// no version tag found so just return what ever we can find.
|
||||
fmt.Println(getTag())
|
||||
return
|
||||
}
|
||||
// Get the tag of the current revision.
|
||||
tag, _ := getTag("--exact-match")
|
||||
if tag == versionStr {
|
||||
// Seems that we are going to build a release.
|
||||
// So the version number should already be correct.
|
||||
fmt.Println(version.String())
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't have any tag assume "dev"
|
||||
if tag == "" {
|
||||
tag = "dev"
|
||||
}
|
||||
// Get the most likely next version:
|
||||
version.Patch = version.Patch + 1
|
||||
|
||||
if pr, err := semver.NewPRVersion(tag); err == nil {
|
||||
// append the tag as pre-release name
|
||||
version.Pre = append(version.Pre, pr)
|
||||
}
|
||||
|
||||
if ahead != nil {
|
||||
// if we know how many commits we are ahead of the last release, append that too.
|
||||
version.Pre = append(version.Pre, *ahead)
|
||||
}
|
||||
|
||||
fmt.Println(version.String())
|
||||
}
|
Loading…
Reference in a new issue