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 }