Start plugin support and plugin manager

This commit is contained in:
Zachary Yedidia
2019-01-25 22:33:45 -05:00
parent 453e96358a
commit f4a3465a08
5 changed files with 338 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
package lua
import (
"bytes"
"errors"
"fmt"
"io"
@@ -28,10 +29,10 @@ func init() {
}
// LoadFile loads a lua file
func LoadFile(module string, file string, data string) error {
pluginDef := "local P = {};" + module + " = P;setmetatable(" + module + ", {__index = _G});setfenv(1, P);"
func LoadFile(module string, file string, data []byte) error {
pluginDef := []byte("module(\"" + module + "\", package.seeall)")
if fn, err := L.Load(strings.NewReader(pluginDef+data), file); err != nil {
if fn, err := L.Load(bytes.NewReader(append(pluginDef, data...)), file); err != nil {
return err
} else {
L.Push(fn)

67
cmd/micro/lua/plugin.go Normal file
View File

@@ -0,0 +1,67 @@
package lua
import (
"errors"
"io/ioutil"
"strings"
lua "github.com/yuin/gopher-lua"
)
var ErrNoSuchFunction = errors.New("No such function exists")
type Plugin struct {
name string
files []string
}
func NewPluginFromDir(name string, dir string) (*Plugin, error) {
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
p := new(Plugin)
p.name = name
for _, f := range files {
if strings.HasSuffix(f.Name(), ".lua") {
p.files = append(p.files, dir+f.Name())
}
}
return p, nil
}
func (p *Plugin) Load() error {
for _, f := range p.files {
dat, err := ioutil.ReadFile(f)
if err != nil {
return err
}
err = LoadFile(p.name, f, dat)
if err != nil {
return err
}
}
return nil
}
func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) {
plug := L.GetGlobal(p.name)
luafn := L.GetField(plug, fn)
if luafn == lua.LNil {
return nil, ErrNoSuchFunction
}
err := L.CallByParam(lua.P{
Fn: luafn,
NRet: 1,
Protect: true,
}, args...)
if err != nil {
return nil, err
}
ret := L.Get(-1)
L.Pop(1)
return ret, nil
}

View File

@@ -0,0 +1,46 @@
package manager
import (
"io/ioutil"
"net/http"
"path"
"github.com/zyedidia/micro/cmd/micro/config"
git "gopkg.in/src-d/go-git.v4"
)
// NewPluginInfoFromUrl creates a new PluginInfo from a URL by fetching
// the data at that URL and parsing the JSON (running a GET request at
// the URL should return the JSON for a plugin info)
func NewPluginInfoFromUrl(url string) (*PluginInfo, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
dat, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return NewPluginInfo(dat)
}
// FetchRepo downloads this plugin's git repository
func (i *PluginInfo) FetchRepo() error {
_, err := git.PlainClone(path.Join(config.ConfigDir, "plugin", i.Name), false, &git.CloneOptions{
URL: i.Repo,
Progress: nil,
})
return err
}
func (i *PluginInfo) FetchDeps() error {
return nil
}
func (i *PluginInfo) PostInstallHooks() error {
return nil
}

View File

@@ -0,0 +1,72 @@
package manager
import (
"fmt"
"testing"
"github.com/zyedidia/micro/cmd/micro/config"
)
func init() {
config.InitConfigDir("./")
}
var sampleJson = []byte(`{
"name": "comment",
"description": "Plugin to auto comment or uncomment lines",
"website": "https://github.com/micro-editor/comment-plugin",
"repository": "https://github.com/micro-editor/comment-plugin",
"versions": [
{
"version": "1.0.6",
"tag": "v1.0.6",
"require": {
"micro": ">=1.1.0"
}
},
{
"version": "1.0.5",
"tag": "v1.0.5",
"require": {
"micro": ">=1.0.0"
}
},
{
"version": "1.0.6-dev",
"tag": "nightly",
"require": {
"micro": ">=1.3.1"
}
}
]
}`)
func TestParse(t *testing.T) {
_, err := NewPluginInfo(sampleJson)
if err != nil {
t.Error(err)
}
}
// func TestFetch(t *testing.T) {
// i, err := NewPluginInfoFromUrl("http://zbyedidia.webfactional.com/micro/test.json")
// if err != nil {
// t.Error(err)
// }
//
// err = i.FetchRepo()
// if err != nil {
// t.Error(err)
// }
// }
func TestList(t *testing.T) {
is, err := ListInstalledPlugins()
if err != nil {
t.Error(err)
}
for _, i := range is {
fmt.Println(i.Name)
}
}

149
cmd/micro/manager/parser.go Normal file
View File

@@ -0,0 +1,149 @@
package manager
import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"path"
"github.com/blang/semver"
"github.com/zyedidia/micro/cmd/micro/config"
git "gopkg.in/src-d/go-git.v4"
)
var (
ErrMissingName = errors.New("Missing or empty name field")
ErrMissingDesc = errors.New("Missing or empty description field")
ErrMissingSite = errors.New("Missing or empty website field")
ErrMissingRepo = errors.New("Missing or empty repository field")
ErrMissingVersions = errors.New("Missing or empty versions field")
ErrMissingTag = errors.New("Missing or empty tag field")
ErrMissingRequire = errors.New("Missing or empty require field")
)
type Plugin struct {
info *PluginInfo
dir string
repo *git.Repository
// Index into info.Versions showing the current version of this plugin
Version int
}
// PluginVersion describes a version for a plugin as well as any dependencies that
// version might have
// This marks a tag that corresponds to the version in the git repo
type PluginVersion struct {
Vers semver.Version
Vstr string `json:"version"`
Tag string `json:"tag"`
Require map[string]string `json:"require"`
}
// PluginInfo contains all the needed info about a plugin
type PluginInfo struct {
Name string `json:"name"`
Desc string `json:"description"`
Site string `json:"website"`
Repo string `json:"repository"`
Versions []PluginVersion `json:"versions"`
}
// NewPluginInfo parses a JSON input into a valid PluginInfo struct
// Returns an error if there are any missing fields or any invalid fields
// There are no optional fields in a plugin info json file
func NewPluginInfo(data []byte) (*PluginInfo, error) {
var info PluginInfo
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields() // Force errors
if err := dec.Decode(&info); err != nil {
return nil, err
}
if len(info.Name) == 0 {
return nil, ErrMissingName
} else if len(info.Desc) == 0 {
return nil, ErrMissingDesc
} else if len(info.Site) == 0 {
return nil, ErrMissingSite
} else if len(info.Repo) == 0 {
return nil, ErrMissingRepo
} else if err := info.makeVersions(); err != nil {
return nil, err
}
return &info, nil
}
func (i *PluginInfo) makeVersions() error {
if len(i.Versions) == 0 {
return ErrMissingVersions
}
for _, v := range i.Versions {
sv, err := semver.Make(v.Vstr)
if err != nil {
return err
}
v.Vers = sv
if len(v.Tag) == 0 {
return ErrMissingTag
} else if v.Require == nil {
return ErrMissingRequire
}
}
return nil
}
// InstalledPlugins searches the config directory for all installed plugins
// and returns the list of plugin infos corresponding to them
func ListInstalledPlugins() ([]*Plugin, error) {
pdir := path.Join(config.ConfigDir, "plugin")
files, err := ioutil.ReadDir(pdir)
if err != nil {
return nil, err
}
var plugins []*Plugin
for _, dir := range files {
if dir.IsDir() {
files, err := ioutil.ReadDir(path.Join(pdir, dir.Name()))
if err != nil {
return nil, err
}
for _, f := range files {
if f.Name() == "repo.json" {
dat, err := ioutil.ReadFile(path.Join(pdir, dir.Name(), "repo.json"))
if err != nil {
return nil, err
}
info, err := NewPluginInfo(dat)
if err != nil {
return nil, err
}
dirname := path.Join(pdir, dir.Name())
r, err := git.PlainOpen(dirname)
if err != nil {
return nil, err
}
p := &Plugin{
info: info,
dir: dirname,
repo: r,
}
plugins = append(plugins, p)
}
}
}
}
return plugins, nil
}