github.com/neugram/ng@v0.0.0-20180309130942-d472ff93d872/gotool/gotool.go (about) 1 // Copyright 2017 The Neugram Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package gotool manages access to the Go tool for building packages, 6 // plugins, and export data for feeding the go/types package. 7 // 8 // It maintains a process-wide temporary directory that is used as a 9 // GOPATH for building ephemeral packages as part of executing the 10 // Neugram interpreter. It is process-wide because plugins are 11 // necessarily so, and so maintaining any finer-grained GOPATHs just 12 // lead to confusion and bugs. 13 package gotool 14 15 import ( 16 "fmt" 17 goimporter "go/importer" 18 gotypes "go/types" 19 "io" 20 "io/ioutil" 21 "os" 22 "os/exec" 23 "path/filepath" 24 "plugin" 25 "runtime" 26 "strings" 27 "sync" 28 ) 29 30 var M = new(Manager) 31 32 // Manager is a process-global manager of an ephemeral GOPATH 33 // used to generate plugins. 34 // 35 // Completely independent *Program objects co-ordinate the 36 // plugins they generate to avoid multiple attempts at loading 37 // the same plugin. 38 type Manager struct { 39 mu sync.Mutex 40 tempdir string 41 importer gotypes.Importer 42 importerIsGlobal bool // means we are pre Go 1.10 43 } 44 45 func (m *Manager) gocmd(args ...string) error { 46 cmd := exec.Command("go", args...) 47 cmd.Dir = m.tempdir 48 cmd.Env = append(os.Environ(), "GOPATH="+m.gopath()) 49 out, err := cmd.CombinedOutput() 50 if err != nil { 51 return fmt.Errorf("gotool: %v: %v\n%s", args, err, out) 52 } 53 return nil 54 } 55 56 func (m *Manager) gopath() string { 57 usr := os.Getenv("GOPATH") 58 if usr == "" { 59 usr = filepath.Join(os.Getenv("HOME"), "go") 60 } 61 return fmt.Sprintf("%s%c%s", m.tempdir, filepath.ListSeparator, usr) 62 } 63 64 func (m *Manager) init() error { 65 if m.tempdir != "" { 66 return nil 67 } 68 var err error 69 m.tempdir, err = ioutil.TempDir("", "ng-tmp-") 70 if err != nil { 71 return err 72 } 73 if err := os.MkdirAll(filepath.Join(m.tempdir, "src"), 0775); err != nil { 74 return err 75 } 76 77 defer func() { 78 if r := recover(); r != nil { 79 // Prior to Go 1.10, importer.For did not 80 // support having a lookup function passed 81 // to it, and instead paniced. In that case, 82 // we use the default and always "go install". 83 m.importer = goimporter.For(runtime.Compiler, nil) 84 m.importerIsGlobal = true 85 } 86 }() 87 88 m.importer = goimporter.For(runtime.Compiler, m.importerLookup) 89 90 return nil 91 } 92 93 func (m *Manager) importerLookup(path string) (io.ReadCloser, error) { 94 filename := filepath.Join(m.tempdir, path+".a") 95 f, err := os.Open(filename) 96 if os.IsNotExist(err) { 97 os.MkdirAll(filepath.Dir(filename), 0775) 98 if err := m.gocmd("build", "-o="+filename, path); err != nil { 99 return nil, err 100 } 101 f, err = os.Open(filename) 102 } 103 if err != nil { 104 return nil, fmt.Errorf("gotool: %v", err) 105 } 106 os.Remove(filename) 107 return f, nil 108 } 109 110 func (m *Manager) Cleanup() { 111 m.mu.Lock() 112 defer m.mu.Unlock() 113 os.RemoveAll(m.tempdir) 114 m.tempdir = "" 115 } 116 117 func (m *Manager) ImportGo(path string) (*gotypes.Package, error) { 118 m.mu.Lock() 119 defer m.mu.Unlock() 120 121 if err := m.init(); err != nil { 122 return nil, err 123 } 124 if m.importerIsGlobal { 125 // Make sure our source '.a' files are fresh. 126 if err := m.gocmd("install", path); err != nil { 127 return nil, err 128 } 129 } 130 return m.importer.Import(path) 131 } 132 133 // Create creates and loads a single-file plugin outside 134 // of the process-temporary plugin GOPATH. 135 func (m *Manager) Create(name string, contents []byte) (*plugin.Plugin, error) { 136 m.mu.Lock() 137 defer m.mu.Unlock() 138 139 if err := m.init(); err != nil { 140 return nil, err 141 } 142 143 name = strings.Replace(name, "/", "_", -1) 144 name = strings.Replace(name, "\\", "_", -1) 145 var path string 146 for i := 0; true; i++ { 147 filename := fmt.Sprintf("ng-plugin-%s-%d.go", name, i) 148 path = filepath.Join(m.tempdir, filename) 149 f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0664) 150 if err != nil { 151 if os.IsExist(err) { 152 continue // pick a different name 153 } 154 return nil, err 155 } 156 _, err = f.Write(contents) 157 f.Close() 158 if err != nil { 159 return nil, err 160 } 161 break 162 } 163 164 name = filepath.Base(path) 165 if err := m.gocmd("build", "-buildmode=plugin", name); err != nil { 166 return nil, err 167 } 168 pluginName := name[:len(name)-3] + ".so" 169 plg, err := plugin.Open(filepath.Join(m.tempdir, pluginName)) 170 if err != nil { 171 return nil, fmt.Errorf("failed to open plugin %s: %v", name, err) 172 } 173 return plg, nil 174 } 175 176 func (m *Manager) Dir(pkgPath string) (adjPkgPath, dir string, err error) { 177 m.mu.Lock() 178 defer m.mu.Unlock() 179 180 if err := m.init(); err != nil { 181 return "", "", err 182 } 183 184 gopath := m.tempdir 185 adjPkgPath = pkgPath 186 dir = filepath.Join(gopath, "src", adjPkgPath) 187 i := 0 188 for { 189 _, err := os.Stat(dir) 190 if os.IsNotExist(err) { 191 break 192 } 193 i++ 194 adjPkgPath = filepath.Join(fmt.Sprintf("p%d", i), pkgPath) 195 dir = filepath.Join(gopath, "src", adjPkgPath) 196 } 197 if err := os.MkdirAll(dir, 0775); err != nil { 198 return "", "", err 199 } 200 return adjPkgPath, dir, nil 201 } 202 203 func (m *Manager) Open(mainPkgPath string) (*plugin.Plugin, error) { 204 m.mu.Lock() 205 defer m.mu.Unlock() 206 207 pluginName := filepath.Base(mainPkgPath) + ".so" 208 filename := filepath.Join(m.tempdir, "src", mainPkgPath, pluginName) 209 210 if err := m.gocmd("build", "-buildmode=plugin", "-o="+filename, mainPkgPath); err != nil { 211 return nil, err 212 } 213 214 plg, err := plugin.Open(filename) 215 if err != nil { 216 return nil, fmt.Errorf("failed to open plugin %s: %v", pluginName, err) 217 } 218 os.Remove(filename) 219 return plg, nil 220 }