github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/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  	"log"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  	"text/template"
    16  	"time"
    17  
    18  	"github.com/vugu/xxhash"
    19  )
    20  
    21  // ParserGoPkg knows how to perform source file generation in relation to a package folder.
    22  // Whereas ParserGo handles converting a single template, ParserGoPkg is a higher level interface
    23  // and provides the functionality of the vugugen command line tool.  It will scan a package
    24  // folder for .vugu files and convert them to .go, with the appropriate defaults and logic.
    25  type ParserGoPkg struct {
    26  	pkgPath string
    27  	opts    ParserGoPkgOpts
    28  }
    29  
    30  // ParserGoPkgOpts is the options for ParserGoPkg.
    31  type ParserGoPkgOpts struct {
    32  	SkipGoMod        bool    // do not try and create go.mod if it doesn't exist
    33  	SkipMainGo       bool    // do not try and create main_wasm.go if it doesn't exist in a main package
    34  	TinyGo           bool    // emit code intended for TinyGo compilation
    35  	GoFileNameAppend *string // suffix to append to file names, after base name plus .go, if nil then "_gen" is used
    36  	MergeSingle      bool    // merge all output files into a single one
    37  	MergeSingleName  string  // name of merged output file, only used if MergeSingle is true, defaults to "0_components_gen.go"
    38  }
    39  
    40  // TODO: CallVuguSetup bool // always call vuguSetup instead of trying to auto-detect it's existence
    41  
    42  var errNoVuguFile = errors.New("no .vugu file(s) found")
    43  
    44  // RunRecursive will create a new ParserGoPkg and call Run on it recursively for each
    45  // directory under pkgPath.  The opts will be modified for subfolders to disable go.mod and main.go
    46  // logic.  If pkgPath does not contain a .vugu file this function will return an error.
    47  func RunRecursive(pkgPath string, opts *ParserGoPkgOpts) error {
    48  
    49  	if opts == nil {
    50  		opts = &ParserGoPkgOpts{}
    51  	}
    52  
    53  	dirf, err := os.Open(pkgPath)
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	fis, err := dirf.Readdir(-1)
    59  	if err != nil {
    60  		return err
    61  	}
    62  	hasVugu := false
    63  	var subDirList []string
    64  	for _, fi := range fis {
    65  		if fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") {
    66  			subDirList = append(subDirList, fi.Name())
    67  			continue
    68  		}
    69  		if filepath.Ext(fi.Name()) == ".vugu" {
    70  			hasVugu = true
    71  		}
    72  	}
    73  	if !hasVugu {
    74  		return errNoVuguFile
    75  	}
    76  
    77  	p := NewParserGoPkg(pkgPath, opts)
    78  	err = p.Run()
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	for _, subDir := range subDirList {
    84  		subPath := filepath.Join(pkgPath, subDir)
    85  		opts2 := *opts
    86  		// sub folders should never get these behaviors
    87  		opts2.SkipGoMod = true
    88  		opts2.SkipMainGo = true
    89  		err := RunRecursive(subPath, &opts2)
    90  		if err == errNoVuguFile {
    91  			continue
    92  		}
    93  		if err != nil {
    94  			return err
    95  		}
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  // Run will create a new ParserGoPkg and call Run on it.
   102  func Run(pkgPath string, opts *ParserGoPkgOpts) error {
   103  	p := NewParserGoPkg(pkgPath, opts)
   104  	return p.Run()
   105  }
   106  
   107  // NewParserGoPkg returns a new ParserGoPkg with the specified options or default if nil.  The pkgPath is required and must be an absolute path.
   108  func NewParserGoPkg(pkgPath string, opts *ParserGoPkgOpts) *ParserGoPkg {
   109  	ret := &ParserGoPkg{
   110  		pkgPath: pkgPath,
   111  	}
   112  	if opts != nil {
   113  		ret.opts = *opts
   114  	}
   115  	return ret
   116  }
   117  
   118  // Opts returns the options.
   119  func (p *ParserGoPkg) Opts() ParserGoPkgOpts {
   120  	return p.opts
   121  }
   122  
   123  // Run does the work and generates the appropriate .go files from .vugu files.
   124  // It will also create a go.mod file if not present and not SkipGoMod.  Same for main.go and SkipMainGo (will also skip
   125  // if package already has file with package name something other than main).
   126  // Per-file code generation is performed by ParserGo.
   127  func (p *ParserGoPkg) Run() error {
   128  
   129  	// record the times of existing files, so we can restore after if the same
   130  	hashTimes, err := fileHashTimes(p.pkgPath)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	pkgF, err := os.Open(p.pkgPath)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	defer pkgF.Close()
   140  
   141  	allFileNames, err := pkgF.Readdirnames(-1)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	var vuguFileNames []string
   147  	for _, fn := range allFileNames {
   148  		if filepath.Ext(fn) == ".vugu" {
   149  			vuguFileNames = append(vuguFileNames, fn)
   150  		}
   151  	}
   152  
   153  	if len(vuguFileNames) == 0 {
   154  		return fmt.Errorf("no .vugu files found, please create one and try again")
   155  	}
   156  
   157  	pkgName := goGuessPkgName(p.pkgPath)
   158  
   159  	namesToCheck := []string{"main"}
   160  
   161  	goFnameAppend := "_gen"
   162  	if p.opts.GoFileNameAppend != nil {
   163  		goFnameAppend = *p.opts.GoFileNameAppend
   164  	}
   165  
   166  	var mergeFiles []string
   167  
   168  	mergeSingleName := "0_components_gen.go"
   169  	if p.opts.MergeSingleName != "" {
   170  		mergeSingleName = p.opts.MergeSingleName
   171  	}
   172  
   173  	missingFmap := make(map[string]string, len(vuguFileNames))
   174  
   175  	// run ParserGo on each file to generate the .go files
   176  	for _, fn := range vuguFileNames {
   177  
   178  		baseFileName := strings.TrimSuffix(fn, ".vugu")
   179  		goFileName := baseFileName + goFnameAppend + ".go"
   180  		compTypeName := fnameToGoTypeName(baseFileName)
   181  
   182  		// keep track of which files to scan for missing structs
   183  		missingFmap[fn] = goFileName
   184  
   185  		mergeFiles = append(mergeFiles, goFileName)
   186  
   187  		pg := &ParserGo{}
   188  
   189  		pg.PackageName = pkgName
   190  		// pg.ComponentType = compTypeName
   191  		pg.StructType = compTypeName
   192  		// pg.DataType = pg.ComponentType + "Data"
   193  		pg.OutDir = p.pkgPath
   194  		pg.OutFile = goFileName
   195  		pg.TinyGo = p.opts.TinyGo
   196  
   197  		// add to our list of names to check after
   198  		namesToCheck = append(namesToCheck, pg.StructType)
   199  		// namesToCheck = append(namesToCheck, pg.ComponentType+".NewData")
   200  		// namesToCheck = append(namesToCheck, pg.DataType)
   201  		namesToCheck = append(namesToCheck, "vuguSetup")
   202  
   203  		// read in source
   204  		b, err := os.ReadFile(filepath.Join(p.pkgPath, fn))
   205  		if err != nil {
   206  			return err
   207  		}
   208  
   209  		// parse it
   210  		err = pg.Parse(bytes.NewReader(b), fn)
   211  		if err != nil {
   212  			return fmt.Errorf("error parsing %q: %v", fn, err)
   213  		}
   214  
   215  	}
   216  
   217  	// after the code generation is done, check the package for the various names in question to see
   218  	// what we need to generate
   219  	namesFound, err := goPkgCheckNames(p.pkgPath, namesToCheck)
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	// if main package, generate main_wasm.go with default stuff if no main func in the package and no main_wasm.go
   225  	if (!p.opts.SkipMainGo) && pkgName == "main" {
   226  
   227  		mainGoPath := filepath.Join(p.pkgPath, "main_wasm.go")
   228  		// log.Printf("namesFound: %#v", namesFound)
   229  		// log.Printf("maingo found: %v", fileExists(mainGoPath))
   230  		// if _, ok := namesFound["main"]; (!ok) && !fileExists(mainGoPath) {
   231  
   232  		// NOTE: For now we're disabling the "main" symbol name check, because in single-dir cases
   233  		// it's picking up the main_wasm.go in server.go (even though it's excluded via build tag).  This
   234  		// needs some more thought but for now this will work for the common cases.
   235  		if !fileExists(mainGoPath) {
   236  
   237  			// log.Printf("WRITING TO main_wasm.go STUFF")
   238  			var buf bytes.Buffer
   239  			t, err := template.New("_main_").Parse(`// +build wasm
   240  {{$opts := .Parser.Opts}}
   241  package main
   242  
   243  import (
   244  	"fmt"
   245  {{if not $opts.TinyGo}}
   246  	"flag"
   247  {{end}}
   248  
   249  	"github.com/vugu/vugu"
   250  	"github.com/vugu/vugu/domrender"
   251  )
   252  
   253  func main() {
   254  
   255  {{if $opts.TinyGo}}
   256  	var mountPoint *string
   257  	{
   258  		mp := "#vugu_mount_point"
   259  		mountPoint = &mp
   260  	}
   261  {{else}}
   262  	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")
   263  	flag.Parse()
   264  {{end}}
   265  
   266  	fmt.Printf("Entering main(), -mount-point=%q\n", *mountPoint)
   267  	{{if not $opts.TinyGo}}defer fmt.Printf("Exiting main()\n")
   268  {{end}}
   269  
   270  	renderer, err := domrender.New(*mountPoint)
   271  	if err != nil {
   272  		panic(err)
   273  	}
   274  	{{if not $opts.TinyGo}}defer renderer.Release()
   275  {{end}}
   276  
   277  	buildEnv, err := vugu.NewBuildEnv(renderer.EventEnv())
   278  	if err != nil {
   279  		panic(err)
   280  	}
   281  
   282  {{if (index .NamesFound "vuguSetup")}}
   283  	rootBuilder := vuguSetup(buildEnv, renderer.EventEnv())
   284  {{else}}
   285  	rootBuilder := &Root{}
   286  {{end}}
   287  
   288  
   289  	for ok := true; ok; ok = renderer.EventWait() {
   290  
   291  		buildResults := buildEnv.RunBuild(rootBuilder)
   292  		
   293  		err = renderer.Render(buildResults)
   294  		if err != nil {
   295  			panic(err)
   296  		}
   297  	}
   298  	
   299  }
   300  `)
   301  			if err != nil {
   302  				return err
   303  			}
   304  			err = t.Execute(&buf, map[string]interface{}{
   305  				"Parser":     p,
   306  				"NamesFound": namesFound,
   307  			})
   308  			if err != nil {
   309  				return err
   310  			}
   311  
   312  			bufstr := buf.String()
   313  			bufstr, err = gofmt(bufstr)
   314  			if err != nil {
   315  				log.Printf("WARNING: gofmt on main_wasm.go failed: %v", err)
   316  			}
   317  
   318  			err = os.WriteFile(mainGoPath, []byte(bufstr), 0644)
   319  			if err != nil {
   320  				return err
   321  			}
   322  
   323  		}
   324  
   325  	}
   326  
   327  	// write go.mod if it doesn't exist and not disabled - actually this really only makes sense for main,
   328  	// otherwise we really don't know what the right module name is
   329  	goModPath := filepath.Join(p.pkgPath, "go.mod")
   330  	if pkgName == "main" && !p.opts.SkipGoMod && !fileExists(goModPath) {
   331  		err := os.WriteFile(goModPath, []byte(`module `+pkgName+"\n"), 0644)
   332  		if err != nil {
   333  			return err
   334  		}
   335  	}
   336  
   337  	// remove the merged file so it doesn't mess with detection
   338  	if p.opts.MergeSingle {
   339  		os.Remove(filepath.Join(p.pkgPath, mergeSingleName))
   340  	}
   341  
   342  	// for _, fn := range vuguFileNames {
   343  
   344  	// 	goFileName := strings.TrimSuffix(fn, ".vugu") + goFnameAppend + ".go"
   345  	// 	goFilePath := filepath.Join(p.pkgPath, goFileName)
   346  
   347  	// 	err := func() error {
   348  	// 		// get ready to append to file
   349  	// 		f, err := os.OpenFile(goFilePath, os.O_WRONLY|os.O_APPEND, 0644)
   350  	// 		if err != nil {
   351  	// 			return err
   352  	// 		}
   353  	// 		defer f.Close()
   354  
   355  	// 		// 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
   356  	// 		compTypeName := fnameToGoTypeName(strings.TrimSuffix(goFileName, goFnameAppend+".go"))
   357  
   358  	// 		// create CompName struct if it doesn't exist in the package
   359  	// 		if _, ok := namesFound[compTypeName]; !ok {
   360  	// 			fmt.Fprintf(f, "\ntype %s struct {}\n", compTypeName)
   361  	// 		}
   362  
   363  	// 		// // create CompNameData struct if it doesn't exist in the package
   364  	// 		// if _, ok := namesFound[compTypeName+"Data"]; !ok {
   365  	// 		// 	fmt.Fprintf(f, "\ntype %s struct {}\n", compTypeName+"Data")
   366  	// 		// }
   367  
   368  	// 		// create CompName.NewData with defaults if it doesn't exist in the package
   369  	// 		// if _, ok := namesFound[compTypeName+".NewData"]; !ok {
   370  	// 		// 	fmt.Fprintf(f, "\nfunc (ct *%s) NewData(props vugu.Props) (interface{}, error) { return &%s{}, nil }\n",
   371  	// 		// 		compTypeName, compTypeName+"Data")
   372  	// 		// }
   373  
   374  	// 		// // register component unless disabled - nope, no more component registry
   375  	// 		// if !p.opts.SkipRegisterComponentTypes && !fileHasInitFunc(goFilePath) {
   376  	// 		// 	fmt.Fprintf(f, "\nfunc init() { vugu.RegisterComponentType(%q, &%s{}) }\n", strings.TrimSuffix(goFileName, ".go"), compTypeName)
   377  	// 		// }
   378  
   379  	// 		return nil
   380  	// 	}()
   381  	// 	if err != nil {
   382  	// 		return err
   383  	// 	}
   384  
   385  	// }
   386  
   387  	// generate anything missing and process vugugen comments
   388  	mf := newMissingFixer(p.pkgPath, pkgName, missingFmap)
   389  	err = mf.run()
   390  	if err != nil {
   391  		return fmt.Errorf("missing fixer error: %w", err)
   392  	}
   393  
   394  	// if requested, do merge
   395  	if p.opts.MergeSingle {
   396  
   397  		// if a missing fix file was produced include it in the list to be merged
   398  		_, err := os.Stat(filepath.Join(p.pkgPath, "0_missing_gen.go"))
   399  		if err == nil {
   400  			mergeFiles = append(mergeFiles, "0_missing_gen.go")
   401  		}
   402  
   403  		err = mergeGoFiles(p.pkgPath, mergeSingleName, mergeFiles...)
   404  		if err != nil {
   405  			return err
   406  		}
   407  		// remove files if merge worked
   408  		for _, mf := range mergeFiles {
   409  			err := os.Remove(filepath.Join(p.pkgPath, mf))
   410  			if err != nil {
   411  				return err
   412  			}
   413  		}
   414  
   415  	}
   416  
   417  	err = restoreFileHashTimes(p.pkgPath, hashTimes)
   418  	if err != nil {
   419  		return err
   420  	}
   421  
   422  	return nil
   423  
   424  }
   425  
   426  //nolint:golint,unused
   427  func fileHasInitFunc(p string) bool {
   428  	b, err := os.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 := os.ReadFile(filepath.Join(dir, fi.Name()))
   614  		if err != nil {
   615  			return nil, err
   616  		}
   617  		_, err = h.Write(b)
   618  		if err != nil {
   619  			return nil, err
   620  		}
   621  		ret[h.Sum64()] = fi.ModTime()
   622  	}
   623  
   624  	return ret, nil
   625  }
   626  
   627  // restoreFileHashTimes takes the map returned by fileHashTimes and for any files where the hash
   628  // matches we restore the mod time - this way we can clobber files during code generation but
   629  // then if the resulting output is byte for byte the same we can just change the mod time back and
   630  // things that look at timestamps will see the file as unchanged; somewhat hacky, but simple and
   631  // workable for now - it's important for the developer experince we don't do unnecessary builds
   632  // in cases where things don't change
   633  func restoreFileHashTimes(dir string, hashTimes map[uint64]time.Time) error {
   634  
   635  	f, err := os.Open(dir)
   636  	if err != nil {
   637  		return err
   638  	}
   639  	defer f.Close()
   640  
   641  	fis, err := f.Readdir(-1)
   642  	if err != nil {
   643  		return err
   644  	}
   645  	for _, fi := range fis {
   646  		if fi.IsDir() {
   647  			continue
   648  		}
   649  		fiPath := filepath.Join(dir, fi.Name())
   650  		h := xxhash.New()
   651  		fmt.Fprint(h, fi.Name()) // hash the name too so we don't confuse different files with the same contents
   652  		b, err := os.ReadFile(fiPath)
   653  		if err != nil {
   654  			return err
   655  		}
   656  		_, err = h.Write(b)
   657  		if err != nil {
   658  			return err
   659  		}
   660  		if t, ok := hashTimes[h.Sum64()]; ok {
   661  			err := os.Chtimes(fiPath, time.Now(), t)
   662  			if err != nil {
   663  				log.Printf("Error in os.Chtimes(%q, now, %q): %v", fiPath, t, err)
   664  			}
   665  		}
   666  	}
   667  
   668  	return nil
   669  }