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

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"io"
     7  	"io/fs"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/gnolang/gno/gnovm/pkg/gnoenv"
    15  	"github.com/gnolang/gno/gnovm/pkg/transpiler"
    16  )
    17  
    18  func isGnoFile(f fs.DirEntry) bool {
    19  	name := f.Name()
    20  	return !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".gno") && !f.IsDir()
    21  }
    22  
    23  func isFileExist(path string) bool {
    24  	_, err := os.Stat(path)
    25  	return err == nil
    26  }
    27  
    28  func gnoFilesFromArgs(args []string) ([]string, error) {
    29  	paths := []string{}
    30  	for _, arg := range args {
    31  		info, err := os.Stat(arg)
    32  		if err != nil {
    33  			return nil, fmt.Errorf("invalid file or package path: %w", err)
    34  		}
    35  		if !info.IsDir() {
    36  			curpath := arg
    37  			paths = append(paths, curpath)
    38  		} else {
    39  			err = filepath.WalkDir(arg, func(curpath string, f fs.DirEntry, err error) error {
    40  				if err != nil {
    41  					return fmt.Errorf("%s: walk dir: %w", arg, err)
    42  				}
    43  
    44  				if !isGnoFile(f) {
    45  					return nil // skip
    46  				}
    47  				paths = append(paths, curpath)
    48  				return nil
    49  			})
    50  			if err != nil {
    51  				return nil, err
    52  			}
    53  		}
    54  	}
    55  	return paths, nil
    56  }
    57  
    58  func gnoPackagesFromArgs(args []string) ([]string, error) {
    59  	paths := []string{}
    60  	for _, arg := range args {
    61  		info, err := os.Stat(arg)
    62  		if err != nil {
    63  			return nil, fmt.Errorf("invalid file or package path: %w", err)
    64  		}
    65  		if !info.IsDir() {
    66  			paths = append(paths, arg)
    67  		} else {
    68  			// if the passed arg is a dir, then we'll recursively walk the dir
    69  			// and look for directories containing at least one .gno file.
    70  
    71  			visited := map[string]bool{} // used to run the builder only once per folder.
    72  			err = filepath.WalkDir(arg, func(curpath string, f fs.DirEntry, err error) error {
    73  				if err != nil {
    74  					return fmt.Errorf("%s: walk dir: %w", arg, err)
    75  				}
    76  				if f.IsDir() {
    77  					return nil // skip
    78  				}
    79  				if !isGnoFile(f) {
    80  					return nil // skip
    81  				}
    82  
    83  				parentDir := filepath.Dir(curpath)
    84  				if _, found := visited[parentDir]; found {
    85  					return nil
    86  				}
    87  				visited[parentDir] = true
    88  
    89  				pkg := parentDir
    90  				if !filepath.IsAbs(parentDir) {
    91  					// cannot use path.Join or filepath.Join, because we need
    92  					// to ensure that ./ is the prefix to pass to go build.
    93  					// if not absolute.
    94  					pkg = "./" + parentDir
    95  				}
    96  
    97  				paths = append(paths, pkg)
    98  				return nil
    99  			})
   100  			if err != nil {
   101  				return nil, err
   102  			}
   103  		}
   104  	}
   105  	return paths, nil
   106  }
   107  
   108  // targetsFromPatterns returns a list of target paths that match the patterns.
   109  // Each pattern can represent a file or a directory, and if the pattern
   110  // includes "/...", the "..." is treated as a wildcard, matching any string.
   111  // Intended to be used by gno commands such as `gno test`.
   112  func targetsFromPatterns(patterns []string) ([]string, error) {
   113  	paths := []string{}
   114  	for _, p := range patterns {
   115  		var match func(string) bool
   116  		patternLookup := false
   117  		dirToSearch := p
   118  
   119  		// Check if the pattern includes `/...`
   120  		if strings.Contains(p, "/...") {
   121  			index := strings.Index(p, "/...")
   122  			if index != -1 {
   123  				dirToSearch = p[:index] // Extract the directory path to search
   124  			}
   125  			match = matchPattern(strings.TrimPrefix(p, "./"))
   126  			patternLookup = true
   127  		}
   128  
   129  		info, err := os.Stat(dirToSearch)
   130  		if err != nil {
   131  			return nil, fmt.Errorf("invalid file or package path: %w", err)
   132  		}
   133  
   134  		// If the pattern is a file or a directory
   135  		// without `/...`, add it to the list.
   136  		if !info.IsDir() || !patternLookup {
   137  			paths = append(paths, p)
   138  			continue
   139  		}
   140  
   141  		// the pattern is a dir containing `/...`, walk the dir recursively and
   142  		// look for directories containing at least one .gno file and match pattern.
   143  		visited := map[string]bool{} // used to run the builder only once per folder.
   144  		err = filepath.WalkDir(dirToSearch, func(curpath string, f fs.DirEntry, err error) error {
   145  			if err != nil {
   146  				return fmt.Errorf("%s: walk dir: %w", dirToSearch, err)
   147  			}
   148  			// Skip directories and non ".gno" files.
   149  			if f.IsDir() || !isGnoFile(f) {
   150  				return nil
   151  			}
   152  
   153  			parentDir := filepath.Dir(curpath)
   154  			if _, found := visited[parentDir]; found {
   155  				return nil
   156  			}
   157  
   158  			visited[parentDir] = true
   159  			if match(parentDir) {
   160  				paths = append(paths, parentDir)
   161  			}
   162  
   163  			return nil
   164  		})
   165  		if err != nil {
   166  			return nil, err
   167  		}
   168  	}
   169  	return paths, nil
   170  }
   171  
   172  // matchPattern(pattern)(name) reports whether
   173  // name matches pattern.  Pattern is a limited glob
   174  // pattern in which '...' means 'any string' and there
   175  // is no other special syntax.
   176  // Simplified version of go source's matchPatternInternal
   177  // (see $GOROOT/src/cmd/internal/pkgpattern)
   178  func matchPattern(pattern string) func(name string) bool {
   179  	re := regexp.QuoteMeta(pattern)
   180  	re = strings.Replace(re, `\.\.\.`, `.*`, -1)
   181  	// Special case: foo/... matches foo too.
   182  	if strings.HasSuffix(re, `/.*`) {
   183  		re = re[:len(re)-len(`/.*`)] + `(/.*)?`
   184  	}
   185  	reg := regexp.MustCompile(`^` + re + `$`)
   186  	return func(name string) bool {
   187  		return reg.MatchString(name)
   188  	}
   189  }
   190  
   191  func fmtDuration(d time.Duration) string {
   192  	return fmt.Sprintf("%.2fs", d.Seconds())
   193  }
   194  
   195  // makeTestGoMod creates the temporary go.mod for test
   196  func makeTestGoMod(path string, packageName string, goversion string) error {
   197  	content := fmt.Sprintf("module %s\n\ngo %s\n", packageName, goversion)
   198  	return os.WriteFile(path, []byte(content), 0o644)
   199  }
   200  
   201  // getPathsFromImportSpec derive and returns ImportPaths
   202  // without ImportPrefix from *ast.ImportSpec
   203  func getPathsFromImportSpec(importSpec []*ast.ImportSpec) (importPaths []importPath) {
   204  	for _, i := range importSpec {
   205  		path := i.Path.Value[1 : len(i.Path.Value)-1] // trim leading and trailing `"`
   206  		if strings.HasPrefix(path, transpiler.ImportPrefix) {
   207  			res := strings.TrimPrefix(path, transpiler.ImportPrefix)
   208  			importPaths = append(importPaths, importPath("."+res))
   209  		}
   210  	}
   211  	return
   212  }
   213  
   214  // ResolvePath joins the output dir with relative pkg path
   215  // e.g
   216  // Output Dir: Temp/gno-transpile
   217  // Pkg Path: ../example/gno.land/p/pkg
   218  // Returns -> Temp/gno-transpile/example/gno.land/p/pkg
   219  func ResolvePath(output string, path importPath) (string, error) {
   220  	absOutput, err := filepath.Abs(output)
   221  	if err != nil {
   222  		return "", err
   223  	}
   224  	absPkgPath, err := filepath.Abs(string(path))
   225  	if err != nil {
   226  		return "", err
   227  	}
   228  	pkgPath := strings.TrimPrefix(absPkgPath, gnoenv.RootDir())
   229  
   230  	return filepath.Join(absOutput, pkgPath), nil
   231  }
   232  
   233  // WriteDirFile write file to the path and also create
   234  // directory if needed. with:
   235  // Dir perm -> 0755; File perm -> 0o644
   236  func WriteDirFile(pathWithName string, data []byte) error {
   237  	path := filepath.Dir(pathWithName)
   238  
   239  	// Create Dir if not exists
   240  	if _, err := os.Stat(path); os.IsNotExist(err) {
   241  		os.MkdirAll(path, 0o755)
   242  	}
   243  
   244  	return os.WriteFile(pathWithName, data, 0o644)
   245  }
   246  
   247  // copyDir copies the dir from src to dst, the paths have to be
   248  // absolute to ensure consistent behavior.
   249  func copyDir(src, dst string) error {
   250  	if !filepath.IsAbs(src) || !filepath.IsAbs(dst) {
   251  		return fmt.Errorf("src or dst path not absolute, src: %s dst: %s", src, dst)
   252  	}
   253  
   254  	entries, err := os.ReadDir(src)
   255  	if err != nil {
   256  		return fmt.Errorf("cannot read dir: %s", src)
   257  	}
   258  
   259  	if err := os.MkdirAll(dst, 0o755); err != nil {
   260  		return fmt.Errorf("failed to create directory: '%s', error: '%w'", dst, err)
   261  	}
   262  
   263  	for _, entry := range entries {
   264  		srcPath := filepath.Join(src, entry.Name())
   265  		dstPath := filepath.Join(dst, entry.Name())
   266  
   267  		if entry.Type().IsDir() {
   268  			copyDir(srcPath, dstPath)
   269  		} else if entry.Type().IsRegular() {
   270  			copyFile(srcPath, dstPath)
   271  		}
   272  	}
   273  
   274  	return nil
   275  }
   276  
   277  // copyFile copies the file from src to dst, the paths have
   278  // to be absolute to ensure consistent behavior.
   279  func copyFile(src, dst string) error {
   280  	if !filepath.IsAbs(src) || !filepath.IsAbs(dst) {
   281  		return fmt.Errorf("src or dst path not absolute, src: %s dst: %s", src, dst)
   282  	}
   283  
   284  	// verify if it's regular flile
   285  	srcStat, err := os.Stat(src)
   286  	if err != nil {
   287  		return fmt.Errorf("cannot copy file: %w", err)
   288  	}
   289  	if !srcStat.Mode().IsRegular() {
   290  		return fmt.Errorf("%s not a regular file", src)
   291  	}
   292  
   293  	// create dst file
   294  	dstFile, err := os.Create(dst)
   295  	if err != nil {
   296  		return err
   297  	}
   298  	defer dstFile.Close()
   299  
   300  	// open src file
   301  	srcFile, err := os.Open(src)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	defer srcFile.Close()
   306  
   307  	// copy srcFile -> dstFile
   308  	_, err = io.Copy(dstFile, srcFile)
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	return nil
   314  }
   315  
   316  // Adapted from https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
   317  func prettySize(nb int64) string {
   318  	const unit = 1000
   319  	if nb < unit {
   320  		return fmt.Sprintf("%d", nb)
   321  	}
   322  	div, exp := int64(unit), 0
   323  	for n := nb / unit; n >= unit; n /= unit {
   324  		div *= unit
   325  		exp++
   326  	}
   327  	return fmt.Sprintf("%.1f%c", float64(nb)/float64(div), "kMGTPE"[exp])
   328  }