github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/cmd/gno/transpile.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"go/scanner"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  
    13  	"github.com/gnolang/gno/gnovm/pkg/transpiler"
    14  	"github.com/gnolang/gno/tm2/pkg/commands"
    15  )
    16  
    17  type importPath string
    18  
    19  type transpileCfg struct {
    20  	verbose     bool
    21  	skipFmt     bool
    22  	skipImports bool
    23  	gobuild     bool
    24  	goBinary    string
    25  	gofmtBinary string
    26  	output      string
    27  }
    28  
    29  type transpileOptions struct {
    30  	cfg *transpileCfg
    31  	// transpiled is the set of packages already
    32  	// transpiled from .gno to .go.
    33  	transpiled map[importPath]struct{}
    34  }
    35  
    36  var defaultTranspileCfg = &transpileCfg{
    37  	verbose:  false,
    38  	goBinary: "go",
    39  }
    40  
    41  func newTranspileOptions(cfg *transpileCfg) *transpileOptions {
    42  	return &transpileOptions{cfg, map[importPath]struct{}{}}
    43  }
    44  
    45  func (p *transpileOptions) getFlags() *transpileCfg {
    46  	return p.cfg
    47  }
    48  
    49  func (p *transpileOptions) isTranspiled(pkg importPath) bool {
    50  	_, transpiled := p.transpiled[pkg]
    51  	return transpiled
    52  }
    53  
    54  func (p *transpileOptions) markAsTranspiled(pkg importPath) {
    55  	p.transpiled[pkg] = struct{}{}
    56  }
    57  
    58  func newTranspileCmd(io commands.IO) *commands.Command {
    59  	cfg := &transpileCfg{}
    60  
    61  	return commands.NewCommand(
    62  		commands.Metadata{
    63  			Name:       "transpile",
    64  			ShortUsage: "transpile [flags] <package> [<package>...]",
    65  			ShortHelp:  "transpiles .gno files to .go",
    66  		},
    67  		cfg,
    68  		func(_ context.Context, args []string) error {
    69  			return execTranspile(cfg, args, io)
    70  		},
    71  	)
    72  }
    73  
    74  func (c *transpileCfg) RegisterFlags(fs *flag.FlagSet) {
    75  	fs.BoolVar(
    76  		&c.verbose,
    77  		"v",
    78  		false,
    79  		"verbose output when running",
    80  	)
    81  
    82  	fs.BoolVar(
    83  		&c.skipFmt,
    84  		"skip-fmt",
    85  		false,
    86  		"do not check syntax of generated .go files",
    87  	)
    88  
    89  	fs.BoolVar(
    90  		&c.skipImports,
    91  		"skip-imports",
    92  		false,
    93  		"do not transpile imports recursively",
    94  	)
    95  
    96  	fs.BoolVar(
    97  		&c.gobuild,
    98  		"gobuild",
    99  		false,
   100  		"run go build on generated go files, ignoring test files",
   101  	)
   102  
   103  	fs.StringVar(
   104  		&c.goBinary,
   105  		"go-binary",
   106  		"go",
   107  		"go binary to use for building",
   108  	)
   109  
   110  	fs.StringVar(
   111  		&c.gofmtBinary,
   112  		"go-fmt-binary",
   113  		"gofmt",
   114  		"gofmt binary to use for syntax checking",
   115  	)
   116  
   117  	fs.StringVar(
   118  		&c.output,
   119  		"output",
   120  		".",
   121  		"output directory",
   122  	)
   123  }
   124  
   125  func execTranspile(cfg *transpileCfg, args []string, io commands.IO) error {
   126  	if len(args) < 1 {
   127  		return flag.ErrHelp
   128  	}
   129  
   130  	// transpile .gno files.
   131  	paths, err := gnoFilesFromArgs(args)
   132  	if err != nil {
   133  		return fmt.Errorf("list paths: %w", err)
   134  	}
   135  
   136  	opts := newTranspileOptions(cfg)
   137  	var errlist scanner.ErrorList
   138  	for _, filepath := range paths {
   139  		if err := transpileFile(filepath, opts); err != nil {
   140  			var fileErrlist scanner.ErrorList
   141  			if !errors.As(err, &fileErrlist) {
   142  				// Not an scanner.ErrorList: return immediately.
   143  				return fmt.Errorf("%s: transpile: %w", filepath, err)
   144  			}
   145  			errlist = append(errlist, fileErrlist...)
   146  		}
   147  	}
   148  
   149  	if errlist.Len() == 0 && cfg.gobuild {
   150  		paths, err := gnoPackagesFromArgs(args)
   151  		if err != nil {
   152  			return fmt.Errorf("list packages: %w", err)
   153  		}
   154  
   155  		for _, pkgPath := range paths {
   156  			err := goBuildFileOrPkg(pkgPath, cfg)
   157  			if err != nil {
   158  				var fileErrlist scanner.ErrorList
   159  				if !errors.As(err, &fileErrlist) {
   160  					// Not an scanner.ErrorList: return immediately.
   161  					return fmt.Errorf("%s: build: %w", pkgPath, err)
   162  				}
   163  				errlist = append(errlist, fileErrlist...)
   164  			}
   165  		}
   166  	}
   167  
   168  	if errlist.Len() > 0 {
   169  		for _, err := range errlist {
   170  			io.ErrPrintfln(err.Error())
   171  		}
   172  		return fmt.Errorf("%d transpile error(s)", errlist.Len())
   173  	}
   174  	return nil
   175  }
   176  
   177  func transpilePkg(pkgPath importPath, opts *transpileOptions) error {
   178  	if opts.isTranspiled(pkgPath) {
   179  		return nil
   180  	}
   181  	opts.markAsTranspiled(pkgPath)
   182  
   183  	files, err := filepath.Glob(filepath.Join(string(pkgPath), "*.gno"))
   184  	if err != nil {
   185  		log.Fatal(err)
   186  	}
   187  
   188  	for _, file := range files {
   189  		if err = transpileFile(file, opts); err != nil {
   190  			return fmt.Errorf("%s: %w", file, err)
   191  		}
   192  	}
   193  
   194  	return nil
   195  }
   196  
   197  func transpileFile(srcPath string, opts *transpileOptions) error {
   198  	flags := opts.getFlags()
   199  	gofmt := flags.gofmtBinary
   200  	if gofmt == "" {
   201  		gofmt = "gofmt"
   202  	}
   203  
   204  	if flags.verbose {
   205  		fmt.Fprintf(os.Stderr, "%s\n", srcPath)
   206  	}
   207  
   208  	// parse .gno.
   209  	source, err := os.ReadFile(srcPath)
   210  	if err != nil {
   211  		return fmt.Errorf("read: %w", err)
   212  	}
   213  
   214  	// compute attributes based on filename.
   215  	targetFilename, tags := transpiler.GetTranspileFilenameAndTags(srcPath)
   216  
   217  	// preprocess.
   218  	transpileRes, err := transpiler.Transpile(string(source), tags, srcPath)
   219  	if err != nil {
   220  		return fmt.Errorf("transpile: %w", err)
   221  	}
   222  
   223  	// resolve target path
   224  	var targetPath string
   225  	if flags.output != "." {
   226  		path, err := ResolvePath(flags.output, importPath(filepath.Dir(srcPath)))
   227  		if err != nil {
   228  			return fmt.Errorf("resolve output path: %w", err)
   229  		}
   230  		targetPath = filepath.Join(path, targetFilename)
   231  	} else {
   232  		targetPath = filepath.Join(filepath.Dir(srcPath), targetFilename)
   233  	}
   234  
   235  	// write .go file.
   236  	err = WriteDirFile(targetPath, []byte(transpileRes.Translated))
   237  	if err != nil {
   238  		return fmt.Errorf("write .go file: %w", err)
   239  	}
   240  
   241  	// check .go fmt, if `SkipFmt` sets to false.
   242  	if !flags.skipFmt {
   243  		err = transpiler.TranspileVerifyFile(targetPath, gofmt)
   244  		if err != nil {
   245  			return fmt.Errorf("check .go file: %w", err)
   246  		}
   247  	}
   248  
   249  	// transpile imported packages, if `SkipImports` sets to false
   250  	if !flags.skipImports {
   251  		importPaths := getPathsFromImportSpec(transpileRes.Imports)
   252  		for _, path := range importPaths {
   253  			transpilePkg(path, opts)
   254  		}
   255  	}
   256  
   257  	return nil
   258  }
   259  
   260  func goBuildFileOrPkg(fileOrPkg string, cfg *transpileCfg) error {
   261  	verbose := cfg.verbose
   262  	goBinary := cfg.goBinary
   263  
   264  	if verbose {
   265  		fmt.Fprintf(os.Stderr, "%s\n", fileOrPkg)
   266  	}
   267  
   268  	return transpiler.TranspileBuildPackage(fileOrPkg, goBinary)
   269  }