github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/goagen/meta/generator.go (about)

     1  package meta
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  	"text/template"
    13  
    14  	"github.com/goadesign/goa/goagen/codegen"
    15  	"github.com/goadesign/goa/version"
    16  )
    17  
    18  // Generator generates the code of, compiles and runs generators.
    19  // This extra step is necessary to compile in the end user design package so
    20  // that generator code can iterate through it.
    21  type Generator struct {
    22  	// Genfunc contains the name of the generator entry point function.
    23  	// The function signature must be:
    24  	//
    25  	// func <Genfunc>([]dslengine.Root) ([]string, error)
    26  	Genfunc string
    27  
    28  	// Imports list the imports that are specific for that generator that
    29  	// should be added to the main Go file.
    30  	Imports []*codegen.ImportSpec
    31  
    32  	// Flags is the list of flags to be used when invoking the final
    33  	// generator on the command line.
    34  	Flags map[string]string
    35  
    36  	// CustomFlags is the list of arguments that appear after the -- separator.
    37  	// These arguments are appended verbatim to the final generator command line.
    38  	CustomFlags []string
    39  
    40  	// OutDir is the final output directory.
    41  	OutDir string
    42  
    43  	// DesignPkgPath is the Go import path to the design package.
    44  	DesignPkgPath string
    45  
    46  	debug bool
    47  }
    48  
    49  // NewGenerator returns a meta generator that can run an actual Generator
    50  // given its factory method and command line flags.
    51  func NewGenerator(genfunc string, imports []*codegen.ImportSpec, flags map[string]string, customflags []string) (*Generator, error) {
    52  	var (
    53  		outDir, designPkgPath string
    54  		debug                 bool
    55  	)
    56  
    57  	if o, ok := flags["out"]; ok {
    58  		outDir = o
    59  	}
    60  	if d, ok := flags["design"]; ok {
    61  		designPkgPath = d
    62  	}
    63  	if d, ok := flags["debug"]; ok {
    64  		var err error
    65  		debug, err = strconv.ParseBool(d)
    66  		if err != nil {
    67  			return nil, fmt.Errorf("failed to parse debug flag: %s", err)
    68  		}
    69  	}
    70  
    71  	return &Generator{
    72  		Genfunc:       genfunc,
    73  		Imports:       imports,
    74  		Flags:         flags,
    75  		CustomFlags:   customflags,
    76  		OutDir:        outDir,
    77  		DesignPkgPath: designPkgPath,
    78  		debug:         debug,
    79  	}, nil
    80  }
    81  
    82  // Generate compiles and runs the generator and returns the generated filenames.
    83  func (m *Generator) Generate() ([]string, error) {
    84  	// Sanity checks
    85  	if os.Getenv("GOPATH") == "" {
    86  		return nil, fmt.Errorf("GOPATH not set")
    87  	}
    88  	if m.OutDir == "" {
    89  		return nil, fmt.Errorf("missing output directory flag")
    90  	}
    91  	if m.DesignPkgPath == "" {
    92  		return nil, fmt.Errorf("missing design package flag")
    93  	}
    94  
    95  	// Create output directory
    96  	if err := os.MkdirAll(m.OutDir, 0755); err != nil {
    97  		return nil, err
    98  	}
    99  
   100  	// Create temporary workspace used for generation
   101  	wd, err := os.Getwd()
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  	tmpDir, err := ioutil.TempDir(wd, "goagen")
   106  	if err != nil {
   107  		if _, ok := err.(*os.PathError); ok {
   108  			err = fmt.Errorf(`invalid output directory path "%s"`, m.OutDir)
   109  		}
   110  		return nil, err
   111  	}
   112  	defer func() {
   113  		if !m.debug {
   114  			os.RemoveAll(tmpDir)
   115  		}
   116  	}()
   117  	if m.debug {
   118  		fmt.Printf("** Code generator source dir: %s\n", tmpDir)
   119  	}
   120  
   121  	pkgSourcePath, err := codegen.PackageSourcePath(m.DesignPkgPath)
   122  	if err != nil {
   123  		return nil, fmt.Errorf("invalid design package import path: %s", err)
   124  	}
   125  	pkgName, err := codegen.PackageName(pkgSourcePath)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	// Generate tool source code.
   131  	pkgPath := filepath.Join(tmpDir, pkgName)
   132  	p, err := codegen.PackageFor(pkgPath)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	m.generateToolSourceCode(p)
   137  
   138  	// Compile and run generated tool.
   139  	if m.debug {
   140  		fmt.Printf("** Compiling with:\n%s", strings.Join(os.Environ(), "\n"))
   141  	}
   142  	genbin, err := p.Compile("goagen")
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	return m.spawn(genbin)
   147  }
   148  
   149  func (m *Generator) generateToolSourceCode(pkg *codegen.Package) {
   150  	file := pkg.CreateSourceFile("main.go")
   151  	imports := append(m.Imports,
   152  		codegen.SimpleImport("fmt"),
   153  		codegen.SimpleImport("strings"),
   154  		codegen.SimpleImport("github.com/goadesign/goa/dslengine"),
   155  		codegen.NewImport("_", filepath.ToSlash(m.DesignPkgPath)),
   156  	)
   157  	file.WriteHeader("Code Generator", "main", imports)
   158  	tmpl, err := template.New("generator").Parse(mainTmpl)
   159  	if err != nil {
   160  		panic(err) // bug
   161  	}
   162  	pkgName, err := codegen.PackageName(pkg.Abs())
   163  	if err != nil {
   164  		panic(err)
   165  	}
   166  	context := map[string]string{
   167  		"Genfunc":       m.Genfunc,
   168  		"DesignPackage": m.DesignPkgPath,
   169  		"PkgName":       pkgName,
   170  	}
   171  	err = tmpl.Execute(file, context)
   172  	if err != nil {
   173  		panic(err) // bug
   174  	}
   175  }
   176  
   177  // spawn runs the compiled generator using the arguments initialized by Kingpin
   178  // when parsing the command line.
   179  func (m *Generator) spawn(genbin string) ([]string, error) {
   180  	var args []string
   181  	for k, v := range m.Flags {
   182  		if k == "debug" {
   183  			continue
   184  		}
   185  		args = append(args, fmt.Sprintf("--%s=%s", k, v))
   186  	}
   187  	sort.Strings(args)
   188  	args = append(args, "--version="+version.String())
   189  	args = append(args, m.CustomFlags...)
   190  	cmd := exec.Command(genbin, args...)
   191  	out, err := cmd.CombinedOutput()
   192  	if err != nil {
   193  		return nil, fmt.Errorf("%s\n%s", err, string(out))
   194  	}
   195  	res := strings.Split(string(out), "\n")
   196  	for (len(res) > 0) && (res[len(res)-1] == "") {
   197  		res = res[:len(res)-1]
   198  	}
   199  	return res, nil
   200  }
   201  
   202  const mainTmpl = `
   203  func main() {
   204  	// Check if there were errors while running the first DSL pass
   205  	dslengine.FailOnError(dslengine.Errors)
   206  
   207  	// Now run the secondary DSLs
   208  	dslengine.FailOnError(dslengine.Run())
   209  
   210  	files, err := {{.Genfunc}}()
   211  	dslengine.FailOnError(err)
   212  
   213  	// We're done
   214  	fmt.Println(strings.Join(files, "\n"))
   215  }`