github.com/arnodel/golua@v0.0.0-20230215163904-e0b5347eaaa1/lib/golib/goimports/goimports.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  package goimports
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/parser"
    12  	"go/token"
    13  	"io"
    14  	"log"
    15  	"os"
    16  	"os/exec"
    17  	"path"
    18  	"plugin"
    19  	"strings"
    20  	"text/template"
    21  )
    22  
    23  const Supported = true
    24  
    25  // LoadGoPackage builds a plugin for a package if necessary, compiles it and loads
    26  // it.  The value of the "Exports" symbol is returned if successful.
    27  func LoadGoPackage(pkg string, pluginRoot string, forceBuild bool) (map[string]interface{}, error) {
    28  	pluginDir := path.Join(pluginRoot, pkg)
    29  	filename := path.Base(pluginDir) + ".so"
    30  	pluginPath := path.Join(pluginDir, filename)
    31  	var p *plugin.Plugin
    32  	var err error
    33  	if !forceBuild {
    34  		p, err = plugin.Open(pluginPath)
    35  		forceBuild = err != nil
    36  	}
    37  	if forceBuild {
    38  		err = buildPlugin(pkg, pluginPath)
    39  		if err != nil {
    40  			return nil, err
    41  		}
    42  		p, err = plugin.Open(pluginPath)
    43  		if err != nil {
    44  			return nil, err
    45  		}
    46  	}
    47  	exportsI, err := p.Lookup("Exports")
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	exports, ok := exportsI.(*map[string]interface{})
    52  	if !ok {
    53  		return nil, errors.New("Exports has incorrect type")
    54  	}
    55  	return *exports, nil
    56  }
    57  
    58  type libModel struct {
    59  	Package     string
    60  	PackageName string
    61  	Funcs       []string
    62  	Types       []string
    63  	vars        []string
    64  	consts      []string
    65  }
    66  
    67  func getPackagePath(pkg string) (string, string, error) {
    68  	res, err := exec.Command("go", "list", "-f", "{{.Dir}} {{.Name}}", pkg).Output()
    69  	if err != nil {
    70  		return "", "", err
    71  	}
    72  	bits := strings.Split(strings.TrimSpace(string(res)), " ")
    73  	return bits[0], bits[1], nil
    74  }
    75  
    76  func buildPlugin(pkg string, pluginPath string) error {
    77  	pluginDir := path.Dir(pluginPath)
    78  	pluginFile := path.Base(pluginPath)
    79  	os.MkdirAll(pluginDir, 0777)
    80  	f, err := os.Create(path.Join(pluginDir, "plugin.go"))
    81  	if err != nil {
    82  		return err
    83  	}
    84  	if err := buildLib(pkg, f); err != nil {
    85  		return err
    86  	}
    87  	runner := cmdRunner{dir: pluginDir}
    88  
    89  	// Since go 1.16 we need a go.mod file
    90  	runner.
    91  		run("go", "mod", "init", "github.com/arnode/goluaplugins/"+pkg).
    92  		andRun("go", "mod", "tidy").
    93  		andRun("go", "build", "-o", pluginFile, "-buildmode=plugin")
    94  
    95  	if runner.err != nil {
    96  		return errors.New(runner.errMsg())
    97  	}
    98  	return nil
    99  }
   100  
   101  type cmdRunner struct {
   102  	dir    string
   103  	err    error
   104  	errBuf bytes.Buffer
   105  }
   106  
   107  func (r *cmdRunner) run(name string, args ...string) *cmdRunner {
   108  	cmd := exec.Command(name, args...)
   109  	cmd.Dir = r.dir
   110  	r.errBuf = bytes.Buffer{}
   111  	cmd.Stderr = &r.errBuf
   112  	r.err = cmd.Run()
   113  	return r
   114  }
   115  
   116  // Run if no previous error
   117  func (r *cmdRunner) andRun(name string, args ...string) *cmdRunner {
   118  	if r.err != nil {
   119  		return r
   120  	}
   121  	return r.run(name, args...)
   122  }
   123  
   124  func (r *cmdRunner) errMsg() string {
   125  	return string(r.errBuf.Bytes())
   126  }
   127  
   128  func buildLib(pkg string, out io.Writer) error {
   129  	pkgDir, pkgName, err := getPackagePath(pkg)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	log.Printf("Creating lib for package %s at %s", pkgName, pkgDir)
   134  	fset := token.NewFileSet()
   135  	pkgs, err := parser.ParseDir(fset, pkgDir, nil, 0)
   136  	if err != nil {
   137  		return fmt.Errorf("Error parsing package: %s", err)
   138  	}
   139  	model := &libModel{
   140  		Package:     pkg,
   141  		PackageName: pkgName,
   142  	}
   143  	fillModel(model, pkgs[pkgName])
   144  	getTemplate().Execute(out, model)
   145  	return nil
   146  }
   147  
   148  func fillModel(model *libModel, pkg *ast.Package) {
   149  	fset := token.NewFileSet()
   150  	files := make(map[string]*ast.File)
   151  	for fName, f := range pkg.Files {
   152  		if !strings.HasSuffix(fName, "_test.go") {
   153  			files[fName] = f
   154  		}
   155  	}
   156  	pkg, _ = ast.NewPackage(fset, files, nil, nil)
   157  	for _, obj := range pkg.Scope.Objects {
   158  		switch obj.Kind {
   159  		case ast.Fun:
   160  			fdecl := obj.Decl.(*ast.FuncDecl)
   161  			if !fdecl.Name.IsExported() {
   162  				continue
   163  			}
   164  			name := fdecl.Name.String()
   165  			model.Funcs = append(model.Funcs, name)
   166  		case ast.Var:
   167  			// fmt.Printf("Var: %s %+v\n", obj.Name, obj.Decl)
   168  		// case ast.Con:
   169  		case ast.Typ:
   170  			tdecl := obj.Decl.(*ast.TypeSpec)
   171  			if !tdecl.Name.IsExported() {
   172  				continue
   173  			}
   174  			model.Types = append(model.Types, tdecl.Name.String())
   175  		default:
   176  		}
   177  	}
   178  }
   179  
   180  func getTemplate() *template.Template {
   181  	tpl, err := template.New("lib").Parse(templateStr)
   182  	if err != nil {
   183  		panic(err)
   184  	}
   185  	return tpl
   186  }
   187  
   188  const templateStr = `
   189  package main
   190  {{ $pkgName := .PackageName }}
   191  import "{{ .Package }}"
   192  
   193  var Exports = map[string]interface{}{
   194  
   195  	// Functions
   196  {{ range .Funcs }}
   197  	"{{ . }}": {{ $pkgName }}.{{ . }},
   198  {{- end }}
   199  
   200  	// Types
   201  {{ range .Types }}
   202  	"{{ . }}": func(x {{ $pkgName }}.{{ . }}) {{ $pkgName }}.{{ . }} { return x },
   203  	"new{{ . }}": func() *{{ $pkgName }}.{{ . }} { return new({{ $pkgName }}.{{ . }}) },
   204  {{- end }}
   205  }
   206  `