github.com/blp1526/goa@v1.4.0/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, err := pkg.CreateSourceFile("main.go")
   151  	if err != nil {
   152  		panic(err) // bug
   153  	}
   154  	defer file.Close()
   155  	imports := append(m.Imports,
   156  		codegen.SimpleImport("fmt"),
   157  		codegen.SimpleImport("strings"),
   158  		codegen.SimpleImport("github.com/goadesign/goa/dslengine"),
   159  		codegen.NewImport("_", filepath.ToSlash(m.DesignPkgPath)),
   160  	)
   161  	file.WriteHeader("Code Generator", "main", imports)
   162  	tmpl, err := template.New("generator").Parse(mainTmpl)
   163  	if err != nil {
   164  		panic(err) // bug
   165  	}
   166  	pkgName, err := codegen.PackageName(pkg.Abs())
   167  	if err != nil {
   168  		panic(err)
   169  	}
   170  	context := map[string]string{
   171  		"Genfunc":       m.Genfunc,
   172  		"DesignPackage": m.DesignPkgPath,
   173  		"PkgName":       pkgName,
   174  	}
   175  	if err := tmpl.Execute(file, context); err != nil {
   176  		panic(err) // bug
   177  	}
   178  }
   179  
   180  // spawn runs the compiled generator using the arguments initialized by Kingpin
   181  // when parsing the command line.
   182  func (m *Generator) spawn(genbin string) ([]string, error) {
   183  	var args []string
   184  	for k, v := range m.Flags {
   185  		if k == "debug" {
   186  			continue
   187  		}
   188  		args = append(args, fmt.Sprintf("--%s=%s", k, v))
   189  	}
   190  	sort.Strings(args)
   191  	args = append(args, "--version="+version.String())
   192  	args = append(args, m.CustomFlags...)
   193  	cmd := exec.Command(genbin, args...)
   194  	out, err := cmd.CombinedOutput()
   195  	if err != nil {
   196  		return nil, fmt.Errorf("%s\n%s", err, string(out))
   197  	}
   198  	res := strings.Split(string(out), "\n")
   199  	for (len(res) > 0) && (res[len(res)-1] == "") {
   200  		res = res[:len(res)-1]
   201  	}
   202  	return res, nil
   203  }
   204  
   205  const mainTmpl = `
   206  func main() {
   207  	// Check if there were errors while running the first DSL pass
   208  	dslengine.FailOnError(dslengine.Errors)
   209  
   210  	// Now run the secondary DSLs
   211  	dslengine.FailOnError(dslengine.Run())
   212  
   213  	files, err := {{.Genfunc}}()
   214  	dslengine.FailOnError(err)
   215  
   216  	// We're done
   217  	fmt.Println(strings.Join(files, "\n"))
   218  }`