github.com/shogo82148/goa-v1@v1.6.2/goagen/meta/generator.go (about)

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