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  }