diff --git a/cmd/micro/lua/lua.go b/cmd/micro/lua/lua.go index 7fa44a80..853f10fa 100644 --- a/cmd/micro/lua/lua.go +++ b/cmd/micro/lua/lua.go @@ -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) diff --git a/cmd/micro/lua/plugin.go b/cmd/micro/lua/plugin.go new file mode 100644 index 00000000..d68003c4 --- /dev/null +++ b/cmd/micro/lua/plugin.go @@ -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 +} diff --git a/cmd/micro/manager/fetch.go b/cmd/micro/manager/fetch.go new file mode 100644 index 00000000..97245676 --- /dev/null +++ b/cmd/micro/manager/fetch.go @@ -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 +} diff --git a/cmd/micro/manager/manager_test.go b/cmd/micro/manager/manager_test.go new file mode 100644 index 00000000..d39e1f12 --- /dev/null +++ b/cmd/micro/manager/manager_test.go @@ -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) + } +} diff --git a/cmd/micro/manager/parser.go b/cmd/micro/manager/parser.go new file mode 100644 index 00000000..d4eb63b0 --- /dev/null +++ b/cmd/micro/manager/parser.go @@ -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 +}