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  }