github.com/vugu/vugu@v0.3.5/gen/parser-go-pkg.go (about)

     1  package gen
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"go/ast"
     8  	"go/parser"
     9  	"go/token"
    10  	"io/ioutil"
    11  	"log"
    12  	"os"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strings"
    16  	"text/template"
    17  	"time"
    18  
    19  	"github.com/vugu/xxhash"
    20  )
    21  
    22  // ParserGoPkg knows how to perform source file generation in relation to a package folder.
    23  // Whereas ParserGo handles converting a single template, ParserGoPkg is a higher level interface
    24  // and provides the functionality of the vugugen command line tool.  It will scan a package
    25  // folder for .vugu files and convert them to .go, with the appropriate defaults and logic.
    26  type ParserGoPkg struct {
    27  	pkgPath string
    28  	opts    ParserGoPkgOpts
    29  }
    30  
    31  // ParserGoPkgOpts is the options for ParserGoPkg.
    32  type ParserGoPkgOpts struct {
    33  	SkipGoMod        bool    // do not try and create go.mod if it doesn't exist
    34  	SkipMainGo       bool    // do not try and create main_wasm.go if it doesn't exist in a main package
    35  	TinyGo           bool    // emit code intended for TinyGo compilation
    36  	GoFileNameAppend *string // suffix to append to file names, after base name plus .go, if nil then "_vgen" is used
    37  	MergeSingle      bool    // merge all output files into a single one
    38  	MergeSingleName  string  // name of merged output file, only used if MergeSingle is true, defaults to "0_components_vgen.go"
    39  }
    40  
    41  // TODO: CallVuguSetup bool // always call vuguSetup instead of trying to auto-detect it's existence
    42  
    43  var errNoVuguFile = errors.New("no .vugu file(s) found")
    44  
    45  // RunRecursive will create a new ParserGoPkg and call Run on it recursively for each
    46  // directory under pkgPath.  The opts will be modified for subfolders to disable go.mod and main.go
    47  // logic.  If pkgPath does not contain a .vugu file this function will return an error.
    48  func RunRecursive(pkgPath string, opts *ParserGoPkgOpts) error {
    49  
    50  	if opts == nil {
    51  		opts = &ParserGoPkgOpts{}
    52  	}
    53  
    54  	dirf, err := os.Open(pkgPath)
    55  	if err != nil {
    56  		return err
    57  	}
    58  
    59  	fis, err := dirf.Readdir(-1)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	hasVugu := false
    64  	var subDirList []string
    65  	for _, fi := range fis {
    66  		if fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") {
    67  			subDirList = append(subDirList, fi.Name())
    68  			continue
    69  		}
    70  		if filepath.Ext(fi.Name()) == ".vugu" {
    71  			hasVugu = true
    72  		}
    73  	}
    74  	if !hasVugu {
    75  		return errNoVuguFile
    76  	}
    77  
    78  	p := NewParserGoPkg(pkgPath, opts)
    79  	err = p.Run()
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	for _, subDir := range subDirList {
    85  		subPath := filepath.Join(pkgPath, subDir)
    86  		opts2 := *opts
    87  		// sub folders should never get these behaviors
    88  		opts2.SkipGoMod = true
    89  		opts2.SkipMainGo = true
    90  		err := RunRecursive(subPath, &opts2)
    91  		if err == errNoVuguFile {
    92  			continue
    93  		}
    94  		if err != nil {
    95  			return err
    96  		}
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  // Run will create a new ParserGoPkg and call Run on it.
   103  func Run(pkgPath string, opts *ParserGoPkgOpts) error {
   104  	p := NewParserGoPkg(pkgPath, opts)
   105  	return p.Run()
   106  }
   107  
   108  // NewParserGoPkg returns a new ParserGoPkg with the specified options or default if nil.  The pkgPath is required and must be an absolute path.
   109  func NewParserGoPkg(pkgPath string, opts *ParserGoPkgOpts) *ParserGoPkg {
   110  	ret := &ParserGoPkg{
   111  		pkgPath: pkgPath,
   112  	}
   113  	if opts != nil {
   114  		ret.opts = *opts
   115  	}
   116  	return ret
   117  }
   118  
   119  // Opts returns the options.
   120  func (p *ParserGoPkg) Opts() ParserGoPkgOpts {
   121  	return p.opts
   122  }
   123  
   124  // Run does the work and generates the appropriate .go files from .vugu files.
   125  // It will also create a go.mod file if not present and not SkipGoMod.  Same for main.go and SkipMainGo (will also skip
   126  // if package already has file with package name something other than main).
   127  // Per-file code generation is performed by ParserGo.
   128  func (p *ParserGoPkg) Run() error {
   129  
   130  	// record the times of existing files, so we can restore after if the same
   131  	hashTimes, err := fileHashTimes(p.pkgPath)
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	pkgF, err := os.Open(p.pkgPath)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	defer pkgF.Close()
   141  
   142  	allFileNames, err := pkgF.Readdirnames(-1)
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	var vuguFileNames []string
   148  	for _, fn := range allFileNames {
   149  		if filepath.Ext(fn) == ".vugu" {
   150  			vuguFileNames = append(vuguFileNames, fn)
   151  		}
   152  	}
   153  
   154  	if len(vuguFileNames) == 0 {
   155  		return fmt.Errorf("no .vugu files found, please create one and try again")
   156  	}
   157  
   158  	pkgName := goGuessPkgName(p.pkgPath)
   159  
   160  	namesToCheck := []string{"main"}
   161  
   162  	goFnameAppend := "_vgen"
   163  	if p.opts.GoFileNameAppend != nil {
   164  		goFnameAppend = *p.opts.GoFileNameAppend
   165  	}
   166  
   167  	var mergeFiles []string
   168  
   169  	mergeSingleName := "0_components_vgen.go"
   170  	if p.opts.MergeSingleName != "" {
   171  		mergeSingleName = p.opts.MergeSingleName
   172  	}
   173  
   174  	missingFmap := make(map[string]string, len(vuguFileNames))
   175  
   176  	// run ParserGo on each file to generate the .go files
   177  	for _, fn := range vuguFileNames {
   178  
   179  		baseFileName := strings.TrimSuffix(fn, ".vugu")
   180  		goFileName := baseFileName + goFnameAppend + ".go"
   181  		compTypeName := fnameToGoTypeName(baseFileName)
   182  
   183  		// keep track of which files to scan for missing structs
   184  		missingFmap[fn] = goFileName
   185  
   186  		mergeFiles = append(mergeFiles, goFileName)
   187  
   188  		pg := &ParserGo{}
   189  
   190  		pg.PackageName = pkgName
   191  		// pg.ComponentType = compTypeName
   192  		pg.StructType = compTypeName
   193  		// pg.DataType = pg.ComponentType + "Data"
   194  		pg.OutDir = p.pkgPath
   195  		pg.OutFile = goFileName
   196  		pg.TinyGo = p.opts.TinyGo
   197  
   198  		// add to our list of names to check after
   199  		namesToCheck = append(namesToCheck, pg.StructType)
   200  		// namesToCheck = append(namesToCheck, pg.ComponentType+".NewData")
   201  		// namesToCheck = append(namesToCheck, pg.DataType)
   202  		namesToCheck = append(namesToCheck, "vuguSetup")
   203  
   204  		// read in source
   205  		b, err := ioutil.ReadFile(filepath.Join(p.pkgPath, fn))
   206  		if err != nil {
   207  			return err
   208  		}
   209  
   210  		// parse it
   211  		err = pg.Parse(bytes.NewReader(b), fn)
   212  		if err != nil {
   213  			return fmt.Errorf("error parsing %q: %v", fn, err)
   214  		}
   215  
   216  	}
   217  
   218  	// after the code generation is done, check the package for the various names in question to see
   219  	// what we need to generate
   220  	namesFound, err := goPkgCheckNames(p.pkgPath, namesToCheck)
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	// if main package, generate main_wasm.go with default stuff if no main func in the package and no main_wasm.go
   226  	if (!p.opts.SkipMainGo) && pkgName == "main" {
   227  
   228  		mainGoPath := filepath.Join(p.pkgPath, "main_wasm.go")
   229  		// log.Printf("namesFound: %#v", namesFound)
   230  		// log.Printf("maingo found: %v", fileExists(mainGoPath))
   231  		// if _, ok := namesFound["main"]; (!ok) && !fileExists(mainGoPath) {
   232  
   233  		// NOTE: For now we're disabling the "main" symbol name check, because in single-dir cases
   234  		// it's picking up the main_wasm.go in server.go (even though it's excluded via build tag).  This
   235  		// needs some more thought but for now this will work for the common cases.
   236  		if !fileExists(mainGoPath) {
   237  
   238  			// log.Printf("WRITING TO main_wasm.go STUFF")
   239  			var buf bytes.Buffer
   240  			t, err := template.New("_main_").Parse(`// +build wasm
   241  {{$opts := .Parser.Opts}}
   242  package main
   243  
   244  import (
   245  	"fmt"
   246  {{if not $opts.TinyGo}}
   247  	"flag"
   248  {{end}}
   249  
   250  	"github.com/vugu/vugu"
   251  	"github.com/vugu/vugu/domrender"
   252  )
   253  
   254  func main() {
   255  
   256  {{if $opts.TinyGo}}
   257  	var mountPoint *string
   258  	{
   259  		mp := "#vugu_mount_point"
   260  		mountPoint = &mp
   261  	}
   262  {{else}}
   263  	mountPoint := flag.String("mount-point", "#vugu_mount_point", "The query selector for the mount point for the root component, if it is not a full HTML component")
   264  	flag.Parse()
   265  {{end}}
   266  
   267  	fmt.Printf("Entering main(), -mount-point=%q\n", *mountPoint)
   268  	{{if not $opts.TinyGo}}defer fmt.Printf("Exiting main()\n")
   269  {{end}}
   270  
   271  	renderer, err := domrender.New(*mountPoint)
   272  	if err != nil {
   273  		panic(err)
   274  	}
   275  	{{if not $opts.TinyGo}}defer renderer.Release()
   276  {{end}}
   277  
   278  	buildEnv, err := vugu.NewBuildEnv(renderer.EventEnv())
   279  	if err != nil {
   280  		panic(err)
   281  	}
   282  
   283  {{if (index .NamesFound "vuguSetup")}}
   284  	rootBuilder := vuguSetup(buildEnv, renderer.EventEnv())
   285  {{else}}
   286  	rootBuilder := &Root{}
   287  {{end}}
   288  
   289  
   290  	for ok := true; ok; ok = renderer.EventWait() {
   291  
   292  		buildResults := buildEnv.RunBuild(rootBuilder)
   293  		
   294  		err = renderer.Render(buildResults)
   295  		if err != nil {
   296  			panic(err)
   297  		}
   298  	}
   299  	
   300  }
   301  `)
   302  			if err != nil {
   303  				return err
   304  			}
   305  			err = t.Execute(&buf, map[string]interface{}{
   306  				"Parser":     p,
   307  				"NamesFound": namesFound,
   308  			})
   309  			if err != nil {
   310  				return err
   311  			}
   312  
   313  			bufstr := buf.String()
   314  			bufstr, err = gofmt(bufstr)
   315  			if err != nil {
   316  				log.Printf("WARNING: gofmt on main_wasm.go failed: %v", err)
   317  			}
   318  
   319  			err = ioutil.WriteFile(mainGoPath, []byte(bufstr), 0644)
   320  			if err != nil {
   321  				return err
   322  			}
   323  
   324  		}
   325  
   326  	}
   327  
   328  	// write go.mod if it doesn't exist and not disabled - actually this really only makes sense for main,
   329  	// otherwise we really don't know what the right module name is
   330  	goModPath := filepath.Join(p.pkgPath, "go.mod")
   331  	if pkgName == "main" && !p.opts.SkipGoMod && !fileExists(goModPath) {
   332  		err := ioutil.WriteFile(goModPath, []byte(`module `+pkgName+"\n"), 0644)
   333  		if err != nil {
   334  			return err
   335  		}
   336  	}
   337  
   338  	// remove the merged file so it doesn't mess with detection
   339  	if p.opts.MergeSingle {
   340  		os.Remove(filepath.Join(p.pkgPath, mergeSingleName))
   341  	}
   342  
   343  	// for _, fn := range vuguFileNames {
   344  
   345  	// 	goFileName := strings.TrimSuffix(fn, ".vugu") + goFnameAppend + ".go"
   346  	// 	goFilePath := filepath.Join(p.pkgPath, goFileName)
   347  
   348  	// 	err := func() error {
   349  	// 		// get ready to append to file
   350  	// 		f, err := os.OpenFile(goFilePath, os.O_WRONLY|os.O_APPEND, 0644)
   351  	// 		if err != nil {
   352  	// 			return err
   353  	// 		}
   354  	// 		defer f.Close()
   355  
   356  	// 		// TODO: would be nice to clean this up and get a better grip on how we do this filename -> struct name mapping, but this works for now
   357  	// 		compTypeName := fnameToGoTypeName(strings.TrimSuffix(goFileName, goFnameAppend+".go"))
   358  
   359  	// 		// create CompName struct if it doesn't exist in the package
   360  	// 		if _, ok := namesFound[compTypeName]; !ok {
   361  	// 			fmt.Fprintf(f, "\ntype %s struct {}\n", compTypeName)
   362  	// 		}
   363  
   364  	// 		// // create CompNameData struct if it doesn't exist in the package
   365  	// 		// if _, ok := namesFound[compTypeName+"Data"]; !ok {
   366  	// 		// 	fmt.Fprintf(f, "\ntype %s struct {}\n", compTypeName+"Data")
   367  	// 		// }
   368  
   369  	// 		// create CompName.NewData with defaults if it doesn't exist in the package
   370  	// 		// if _, ok := namesFound[compTypeName+".NewData"]; !ok {
   371  	// 		// 	fmt.Fprintf(f, "\nfunc (ct *%s) NewData(props vugu.Props) (interface{}, error) { return &%s{}, nil }\n",
   372  	// 		// 		compTypeName, compTypeName+"Data")
   373  	// 		// }
   374  
   375  	// 		// // register component unless disabled - nope, no more component registry
   376  	// 		// if !p.opts.SkipRegisterComponentTypes && !fileHasInitFunc(goFilePath) {
   377  	// 		// 	fmt.Fprintf(f, "\nfunc init() { vugu.RegisterComponentType(%q, &%s{}) }\n", strings.TrimSuffix(goFileName, ".go"), compTypeName)
   378  	// 		// }
   379  
   380  	// 		return nil
   381  	// 	}()
   382  	// 	if err != nil {
   383  	// 		return err
   384  	// 	}
   385  
   386  	// }
   387  
   388  	// generate anything missing and process vugugen comments
   389  	mf := newMissingFixer(p.pkgPath, pkgName, missingFmap)
   390  	err = mf.run()
   391  	if err != nil {
   392  		return fmt.Errorf("missing fixer error: %w", err)
   393  	}
   394  
   395  	// if requested, do merge
   396  	if p.opts.MergeSingle {
   397  
   398  		// if a missing fix file was produced include it in the list to be merged
   399  		_, err := os.Stat(filepath.Join(p.pkgPath, "0_missing_vgen.go"))
   400  		if err == nil {
   401  			mergeFiles = append(mergeFiles, "0_missing_vgen.go")
   402  		}
   403  
   404  		err = mergeGoFiles(p.pkgPath, mergeSingleName, mergeFiles...)
   405  		if err != nil {
   406  			return err
   407  		}
   408  		// remove files if merge worked
   409  		for _, mf := range mergeFiles {
   410  			err := os.Remove(filepath.Join(p.pkgPath, mf))
   411  			if err != nil {
   412  				return err
   413  			}
   414  		}
   415  
   416  	}
   417  
   418  	err = restoreFileHashTimes(p.pkgPath, hashTimes)
   419  	if err != nil {
   420  		return err
   421  	}
   422  
   423  	return nil
   424  
   425  }
   426  
   427  func fileHasInitFunc(p string) bool {
   428  	b, err := ioutil.ReadFile(p)
   429  	if err != nil {
   430  		return false
   431  	}
   432  	// hacky but workable for now
   433  	return regexp.MustCompile(`^func init\(`).Match(b)
   434  }
   435  
   436  func fileExists(p string) bool {
   437  	_, err := os.Stat(p)
   438  	return !os.IsNotExist(err)
   439  }
   440  
   441  func fnameToGoTypeName(s string) string {
   442  	s = strings.Split(s, ".")[0] // remove file extension if present
   443  	parts := strings.Split(s, "-")
   444  	for i := range parts {
   445  		p := parts[i]
   446  		if len(p) > 0 {
   447  			p = strings.ToUpper(p[:1]) + p[1:]
   448  		}
   449  		parts[i] = p
   450  	}
   451  	return strings.Join(parts, "")
   452  }
   453  
   454  func goGuessPkgName(pkgPath string) (ret string) {
   455  
   456  	// defer func() { log.Printf("goGuessPkgName returning %q", ret) }()
   457  
   458  	// see if the package already has a name and use it if so
   459  	fset := token.NewFileSet()
   460  	pkgs, err := parser.ParseDir(fset, pkgPath, nil, parser.PackageClauseOnly) // just get the package name
   461  	if err != nil {
   462  		goto checkMore
   463  	}
   464  	if len(pkgs) != 1 {
   465  		goto checkMore
   466  	}
   467  	{
   468  		var pkg *ast.Package
   469  		for _, pkg1 := range pkgs {
   470  			pkg = pkg1
   471  		}
   472  		return pkg.Name
   473  	}
   474  
   475  checkMore:
   476  
   477  	// check for a root.vugu file, in which case we assume "main"
   478  	_, err = os.Stat(filepath.Join(pkgPath, "root.vugu"))
   479  	if err == nil {
   480  		return "main"
   481  	}
   482  
   483  	// otherwise we use the name of the folder...
   484  	dirBase := filepath.Base(pkgPath)
   485  	if regexp.MustCompile(`^[a-z0-9]+$`).MatchString(dirBase) {
   486  		return dirBase
   487  	}
   488  
   489  	// ...unless it makes no sense in which case we use "main"
   490  
   491  	return "main"
   492  
   493  }
   494  
   495  // goPkgCheckNames parses a package dir and looks for names, returning a map of what was
   496  // found.  Names like "A.B" mean a method of name "B" with receiver of type "*A"
   497  func goPkgCheckNames(pkgPath string, names []string) (map[string]interface{}, error) {
   498  
   499  	ret := make(map[string]interface{})
   500  
   501  	fset := token.NewFileSet()
   502  	pkgs, err := parser.ParseDir(fset, pkgPath, nil, 0)
   503  	if err != nil {
   504  		return ret, err
   505  	}
   506  
   507  	if len(pkgs) != 1 {
   508  		return ret, fmt.Errorf("unexpected package count after parsing, expected 1 and got this: %#v", pkgs)
   509  	}
   510  
   511  	var pkg *ast.Package
   512  	for _, pkg1 := range pkgs {
   513  		pkg = pkg1
   514  	}
   515  
   516  	for _, file := range pkg.Files {
   517  
   518  		if file.Scope != nil {
   519  			for _, n := range names {
   520  				if v, ok := file.Scope.Objects[n]; ok {
   521  					ret[n] = v
   522  				}
   523  			}
   524  		}
   525  
   526  		// log.Printf("file: %#v", file)
   527  		// log.Printf("file.Scope.Objects: %#v", file.Scope.Objects)
   528  		// log.Printf("next: %#v", file.Scope.Objects["Example1"])
   529  		// e1 := file.Scope.Objects["Example1"]
   530  		// if e1.Kind == ast.Typ {
   531  		// e1.Decl
   532  		// }
   533  		for _, d := range file.Decls {
   534  			if fd, ok := d.(*ast.FuncDecl); ok {
   535  
   536  				var drecv, dmethod string
   537  				if fd.Recv != nil {
   538  					for _, f := range fd.Recv.List {
   539  						// log.Printf("f.Type: %#v", f.Type)
   540  						if tstar, ok := f.Type.(*ast.StarExpr); ok {
   541  							// log.Printf("tstar.X: %#v", tstar.X)
   542  							if tstarXi, ok := tstar.X.(*ast.Ident); ok && tstarXi != nil {
   543  								// log.Printf("namenamenamename: %#v", tstarXi.Name)
   544  								drecv = tstarXi.Name
   545  							}
   546  						}
   547  						// log.Printf("f.Names: %#v", f.Names)
   548  						// for _, fn := range f.Names {
   549  						// 	if fn != nil {
   550  						// 		log.Printf("NAMENAME: %#v", fn.Name)
   551  						// 		if fni, ok := fn.Name.(*ast.Ident); ok && fni != nil {
   552  						// 		}
   553  						// 	}
   554  						// }
   555  
   556  					}
   557  				} else {
   558  					continue // don't care methods with no receiver - found them already above as single (no period) names
   559  				}
   560  
   561  				// log.Printf("fd.Name: %#v", fd.Name)
   562  				if fd.Name != nil {
   563  					dmethod = fd.Name.Name
   564  				}
   565  
   566  				for _, n := range names {
   567  					recv, method := nameParts(n)
   568  					if drecv == recv && dmethod == method {
   569  						ret[n] = d
   570  					}
   571  				}
   572  			}
   573  		}
   574  	}
   575  	// log.Printf("Objects: %#v", pkg.Scope.Objects)
   576  
   577  	return ret, nil
   578  }
   579  
   580  func nameParts(n string) (recv, method string) {
   581  
   582  	ret := strings.SplitN(n, ".", 2)
   583  	if len(ret) < 2 {
   584  		method = n
   585  		return
   586  	}
   587  	recv = ret[0]
   588  	method = ret[1]
   589  	return
   590  }
   591  
   592  // fileHashTimes will scan a directory and return a map of hashes and corresponding mod times
   593  func fileHashTimes(dir string) (map[uint64]time.Time, error) {
   594  
   595  	ret := make(map[uint64]time.Time)
   596  
   597  	f, err := os.Open(dir)
   598  	if err != nil {
   599  		return nil, err
   600  	}
   601  	defer f.Close()
   602  
   603  	fis, err := f.Readdir(-1)
   604  	if err != nil {
   605  		return nil, err
   606  	}
   607  	for _, fi := range fis {
   608  		if fi.IsDir() {
   609  			continue
   610  		}
   611  		h := xxhash.New()
   612  		fmt.Fprint(h, fi.Name()) // hash the name too so we don't confuse different files with the same contents
   613  		b, err := ioutil.ReadFile(filepath.Join(dir, fi.Name()))
   614  		if err != nil {
   615  			return nil, err
   616  		}
   617  		h.Write(b)
   618  		ret[h.Sum64()] = fi.ModTime()
   619  	}
   620  
   621  	return ret, nil
   622  }
   623  
   624  // restoreFileHashTimes takes the map returned by fileHashTimes and for any files where the hash
   625  // matches we restore the mod time - this way we can clobber files during code generation but
   626  // then if the resulting output is byte for byte the same we can just change the mod time back and
   627  // things that look at timestamps will see the file as unchanged; somewhat hacky, but simple and
   628  // workable for now - it's important for the developer experince we don't do unnecessary builds
   629  // in cases where things don't change
   630  func restoreFileHashTimes(dir string, hashTimes map[uint64]time.Time) error {
   631  
   632  	f, err := os.Open(dir)
   633  	if err != nil {
   634  		return err
   635  	}
   636  	defer f.Close()
   637  
   638  	fis, err := f.Readdir(-1)
   639  	if err != nil {
   640  		return err
   641  	}
   642  	for _, fi := range fis {
   643  		if fi.IsDir() {
   644  			continue
   645  		}
   646  		fiPath := filepath.Join(dir, fi.Name())
   647  		h := xxhash.New()
   648  		fmt.Fprint(h, fi.Name()) // hash the name too so we don't confuse different files with the same contents
   649  		b, err := ioutil.ReadFile(fiPath)
   650  		if err != nil {
   651  			return err
   652  		}
   653  		h.Write(b)
   654  		if t, ok := hashTimes[h.Sum64()]; ok {
   655  			err := os.Chtimes(fiPath, time.Now(), t)
   656  			if err != nil {
   657  				log.Printf("Error in os.Chtimes(%q, now, %q): %v", fiPath, t, err)
   658  			}
   659  		}
   660  	}
   661  
   662  	return nil
   663  }