go-hep.org/x/hep@v0.38.1/fwk/utils/builder/builder.go (about) 1 // Copyright ©2017 The go-hep Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // package builder builds a fwk-app binary from a list of go files. 6 // 7 // builder's architecture and sources are heavily inspired from golint: 8 // 9 // https://github.com/golang/lint 10 package builder // import "go-hep.org/x/hep/fwk/utils/builder" 11 12 import ( 13 "fmt" 14 "go/ast" 15 "go/parser" 16 "go/token" 17 "go/types" 18 "io" 19 "os" 20 "os/exec" 21 "path/filepath" 22 ) 23 24 type file struct { 25 app *Builder 26 f *ast.File 27 fset *token.FileSet 28 src []byte 29 name string 30 } 31 32 // Builder generates and builds fwk-based applications. 33 type Builder struct { 34 fset *token.FileSet 35 files map[string]*file 36 37 pkg *types.Package 38 info *types.Info 39 40 funcs []string 41 42 Name string // name of resulting compiled binary 43 Usage string // usage string displayed by compiled binary (with -help) 44 } 45 46 // NewBuilder creates a Builder from a list of file names or directories 47 func NewBuilder(fnames ...string) (*Builder, error) { 48 var err error 49 50 b := &Builder{ 51 fset: token.NewFileSet(), 52 files: make(map[string]*file, len(fnames)), 53 funcs: make([]string, 0), 54 Usage: `Usage: %[1]s [options] <input> <output> 55 56 ex: 57 $ %[1]s -l=INFO -evtmax=-1 input.dat output.dat 58 59 options: 60 `, 61 } 62 63 for _, fname := range fnames { 64 fi, err := os.Stat(fname) 65 if err != nil { 66 return nil, fmt.Errorf("builder: could not stat %q: %w", fname, err) 67 } 68 fm := fi.Mode() 69 if fm.IsRegular() { 70 src, err := os.ReadFile(fname) 71 if err != nil { 72 return nil, fmt.Errorf("builder: could not read %q: %w", fname, err) 73 } 74 f, err := parser.ParseFile(b.fset, fname, src, parser.ParseComments) 75 if err != nil { 76 return nil, fmt.Errorf("builder: could not parse file %q: %w", fname, err) 77 } 78 b.files[fname] = &file{ 79 app: b, 80 f: f, 81 fset: b.fset, 82 src: src, 83 name: fname, 84 } 85 } 86 if fm.IsDir() { 87 return nil, fmt.Errorf("directories not (yet) handled (got=%q)", fname) 88 } 89 } 90 return b, err 91 } 92 93 // Build applies some type-checking, collects setup functions and generates the sources of the fwk-based application. 94 func (b *Builder) Build() error { 95 var err error 96 97 if b.Name == "" { 98 pwd, err := os.Getwd() 99 if err != nil { 100 return fmt.Errorf("builder: could not fetch current work directory: %w", err) 101 } 102 b.Name = filepath.Base(pwd) 103 } 104 105 err = b.doTypeCheck() 106 if err != nil { 107 return err 108 } 109 110 // check we build a 'main' package 111 if !b.isMain() { 112 return fmt.Errorf("not a 'main' package") 113 } 114 115 err = b.scanSetupFuncs() 116 if err != nil { 117 return err 118 } 119 120 if len(b.funcs) <= 0 { 121 return fmt.Errorf("no setup function found") 122 } 123 124 err = b.genSources() 125 if err != nil { 126 return err 127 } 128 129 return err 130 } 131 132 func (b *Builder) doTypeCheck() error { 133 var err error 134 config := &types.Config{ 135 // By setting a no-op error reporter, the type checker does 136 // as much work as possible. 137 Error: func(error) {}, 138 } 139 info := &types.Info{ 140 Types: make(map[ast.Expr]types.TypeAndValue), 141 Defs: make(map[*ast.Ident]types.Object), 142 Uses: make(map[*ast.Ident]types.Object), 143 } 144 var anyFile *file 145 var astFiles []*ast.File 146 for _, f := range b.files { 147 anyFile = f 148 astFiles = append(astFiles, f.f) 149 } 150 pkg, err := config.Check(anyFile.f.Name.Name, b.fset, astFiles, info) 151 // Remember the typechecking info, even if config.Check failed, 152 // since we will get partial information. 153 b.pkg = pkg 154 b.info = info 155 return err 156 } 157 158 func (b *Builder) typeOf(expr ast.Expr) types.Type { 159 if b.info == nil { 160 return nil 161 } 162 return b.info.TypeOf(expr) 163 } 164 165 func (b *Builder) scanSetupFuncs() error { 166 var err error 167 168 // setupfunc := types.New("func (*job.Job)") 169 // fmt.Fprintf(os.Stderr, "looking for type: %#v...\n", setupfunc) 170 171 for _, f := range b.files { 172 // fmt.Fprintf(os.Stderr, ">>> [%s]...\n", f.name) 173 f.walk(func(n ast.Node) bool { 174 fn, ok := n.(*ast.FuncDecl) 175 if !ok { 176 return true 177 } 178 179 // fmt.Fprintf(os.Stderr, "file: %q - func=%s\n", f.name, fn.Name.Name) 180 181 if fn.Recv != nil { 182 return true 183 } 184 185 if fn.Type.Results != nil && fn.Type.Results.NumFields() != 0 { 186 // fmt.Fprintf(os.Stderr, 187 // "file: %q - func=%s [results=%d]\n", 188 // f.name, fn.Name.Name, 189 // fn.Type.Results.NumFields(), 190 // ) 191 return true 192 } 193 194 if fn.Type.Params == nil { 195 // fmt.Fprintf(os.Stderr, 196 // "file: %q - func=%s [params=nil]\n", 197 // f.name, fn.Name.Name, 198 // ) 199 return true 200 } 201 202 if fn.Type.Params.NumFields() != 1 { 203 // fmt.Fprintf(os.Stderr, 204 // "file: %q - func=%s [params=%d]\n", 205 // f.name, fn.Name.Name, 206 // fn.Type.Params.NumFields(), 207 // ) 208 return true 209 } 210 211 param := b.typeOf(fn.Type.Params.List[0].Type) 212 // FIXME(sbinet) 213 // - go the extra mile and create a proper type.Type from type.New("func(*job.Job)") 214 // - compare the types 215 if param.String() != "*go-hep.org/x/hep/fwk/job.Job" { 216 // fmt.Fprintf(os.Stderr, 217 // "file: %q - func=%s [invalid type=%s]\n", 218 // f.name, fn.Name.Name, 219 // param.String(), 220 // ) 221 return true 222 } 223 224 // fmt.Fprintf(os.Stderr, "file: %q - func=%s [ok]\n", f.name, fn.Name.Name) 225 226 b.funcs = append(b.funcs, fn.Name.Name) 227 return false 228 }) 229 } 230 return err 231 } 232 233 func (b *Builder) isMain() bool { 234 for _, f := range b.files { 235 if f.isMain() { 236 return true 237 } 238 } 239 return false 240 } 241 242 func (b *Builder) genSources() error { 243 var err error 244 tmpdir, err := os.MkdirTemp("", "fwk-builder-") 245 if err != nil { 246 return fmt.Errorf("builder: could not create tmpdir: %w", err) 247 } 248 defer os.RemoveAll(tmpdir) 249 // fmt.Fprintf(os.Stderr, "tmpdir=[%s]...\n", tmpdir) 250 251 // copy sources to dst 252 for _, f := range b.files { 253 // FIXME(sbinet) 254 // only take base. watch out for duplicates! 255 fname := filepath.Base(f.name) 256 dstname := filepath.Join(tmpdir, fname) 257 dst, err := os.Create(dstname) 258 if err != nil { 259 return fmt.Errorf("builder: could not create dst: %w", err) 260 } 261 defer dst.Close() 262 263 _, err = dst.Write(f.src) 264 if err != nil { 265 return fmt.Errorf("builder: could not write dst: %w", err) 266 } 267 268 err = dst.Close() 269 if err != nil { 270 return fmt.Errorf("builder: could not close dst: %w", err) 271 } 272 } 273 274 // add main. 275 f, err := os.Create(filepath.Join(tmpdir, "main.go")) 276 if err != nil { 277 return fmt.Errorf("builder: could not create main: %w", err) 278 } 279 defer f.Close() 280 281 data := struct { 282 Usage string 283 Name string 284 SetupFuncs []string 285 }{ 286 Usage: b.Usage, 287 Name: b.Name, 288 SetupFuncs: b.funcs, 289 } 290 291 err = render(f, tmpl, data) 292 if err != nil { 293 return fmt.Errorf("builder: could not render: %w", err) 294 } 295 296 build := exec.Command( 297 "go", "build", "-o", b.Name, ".", 298 ) 299 build.Stdout = os.Stdout 300 build.Stderr = os.Stderr 301 build.Dir = tmpdir 302 err = build.Run() 303 if err != nil { 304 return fmt.Errorf("builder: could not build %q: %w", b.Name, err) 305 } 306 307 // copy final binary. 308 { 309 src, err := os.Open(filepath.Join(tmpdir, b.Name)) 310 if err != nil { 311 return fmt.Errorf("builder: could not open src %q: %w", filepath.Join(tmpdir, b.Name), err) 312 } 313 defer src.Close() 314 fi, err := src.Stat() 315 if err != nil { 316 return fmt.Errorf("builder: could not stat src %q: %w", src.Name(), err) 317 } 318 319 dst, err := os.OpenFile(b.Name, os.O_CREATE|os.O_WRONLY, fi.Mode()) 320 if err != nil { 321 return fmt.Errorf("builder: could not open dst %q: %w", b.Name, err) 322 } 323 defer dst.Close() 324 325 _, err = io.Copy(dst, src) 326 if err != nil { 327 return fmt.Errorf("builder: could not copy src to dst: %w", err) 328 } 329 330 err = dst.Close() 331 if err != nil { 332 return fmt.Errorf("builder: could not close %q: %w", dst.Name(), err) 333 } 334 } 335 336 return err 337 } 338 339 func (f *file) isMain() bool { 340 return f.f.Name.Name == "main" 341 } 342 343 func (f *file) walk(fn func(ast.Node) bool) { 344 ast.Walk(walker(fn), f.f) 345 } 346 347 // walker adapts a function to satisfy the ast.Visitor interface. 348 // The function return whether the walk should proceed into the node's children. 349 type walker func(ast.Node) bool 350 351 func (w walker) Visit(node ast.Node) ast.Visitor { 352 if w(node) { 353 return w 354 } 355 return nil 356 }