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 }`