github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/gnovm/cmd/gno/mod.go (about) 1 package main 2 3 import ( 4 "context" 5 "flag" 6 "fmt" 7 "go/parser" 8 "go/token" 9 "os" 10 "path/filepath" 11 "sort" 12 "strings" 13 14 "github.com/gnolang/gno/gnovm/pkg/gnomod" 15 "github.com/gnolang/gno/tm2/pkg/commands" 16 "github.com/gnolang/gno/tm2/pkg/errors" 17 ) 18 19 type modDownloadCfg struct { 20 remote string 21 verbose bool 22 } 23 24 func newModCmd(io commands.IO) *commands.Command { 25 cmd := commands.NewCommand( 26 commands.Metadata{ 27 Name: "mod", 28 ShortUsage: "mod <command>", 29 ShortHelp: "manage gno.mod", 30 }, 31 commands.NewEmptyConfig(), 32 commands.HelpExec, 33 ) 34 35 cmd.AddSubCommands( 36 newModDownloadCmd(io), 37 newModInitCmd(), 38 newModTidy(io), 39 newModWhy(io), 40 ) 41 42 return cmd 43 } 44 45 func newModDownloadCmd(io commands.IO) *commands.Command { 46 cfg := &modDownloadCfg{} 47 48 return commands.NewCommand( 49 commands.Metadata{ 50 Name: "download", 51 ShortUsage: "download [flags]", 52 ShortHelp: "download modules to local cache", 53 }, 54 cfg, 55 func(_ context.Context, args []string) error { 56 return execModDownload(cfg, args, io) 57 }, 58 ) 59 } 60 61 func newModInitCmd() *commands.Command { 62 return commands.NewCommand( 63 commands.Metadata{ 64 Name: "init", 65 ShortUsage: "init [module-path]", 66 ShortHelp: "initialize gno.mod file in current directory", 67 }, 68 commands.NewEmptyConfig(), 69 func(_ context.Context, args []string) error { 70 return execModInit(args) 71 }, 72 ) 73 } 74 75 func newModTidy(io commands.IO) *commands.Command { 76 return commands.NewCommand( 77 commands.Metadata{ 78 Name: "tidy", 79 ShortUsage: "tidy", 80 ShortHelp: "add missing and remove unused modules", 81 }, 82 commands.NewEmptyConfig(), 83 func(_ context.Context, args []string) error { 84 return execModTidy(args, io) 85 }, 86 ) 87 } 88 89 func newModWhy(io commands.IO) *commands.Command { 90 return commands.NewCommand( 91 commands.Metadata{ 92 Name: "why", 93 ShortUsage: "why <package> [<package>...]", 94 ShortHelp: "Explains why modules are needed", 95 LongHelp: `Explains why modules are needed. 96 97 gno mod why shows a list of files where specified packages or modules are 98 being used, explaining why those specified packages or modules are being 99 kept by gno mod tidy. 100 101 The output is a sequence of stanzas, one for each module/package name 102 specified, separated by blank lines. Each stanza begins with a 103 comment line "# module" giving the target module/package. Subsequent lines 104 show files that import the specified module/package, one filename per line. 105 If the package or module is not being used/needed/imported, the stanza 106 will display a single parenthesized note indicating that fact. 107 108 For example: 109 110 $ gno mod why gno.land/p/demo/avl gno.land/p/demo/users 111 # gno.land/p/demo/avl 112 [FILENAME_1.gno] 113 [FILENAME_2.gno] 114 115 # gno.land/p/demo/users 116 (module [MODULE_NAME] does not need package gno.land/p/demo/users) 117 $ 118 `, 119 }, 120 commands.NewEmptyConfig(), 121 func(_ context.Context, args []string) error { 122 return execModWhy(args, io) 123 }, 124 ) 125 } 126 127 func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { 128 fs.StringVar( 129 &c.remote, 130 "remote", 131 "test3.gno.land:36657", 132 "remote for fetching gno modules", 133 ) 134 135 fs.BoolVar( 136 &c.verbose, 137 "v", 138 false, 139 "verbose output when running", 140 ) 141 } 142 143 func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { 144 if len(args) > 0 { 145 return flag.ErrHelp 146 } 147 148 path, err := os.Getwd() 149 if err != nil { 150 return err 151 } 152 modPath := filepath.Join(path, "gno.mod") 153 if !isFileExist(modPath) { 154 return errors.New("gno.mod not found") 155 } 156 157 // read gno.mod 158 data, err := os.ReadFile(modPath) 159 if err != nil { 160 return fmt.Errorf("readfile %q: %w", modPath, err) 161 } 162 163 // parse gno.mod 164 gnoMod, err := gnomod.Parse(modPath, data) 165 if err != nil { 166 return fmt.Errorf("parse: %w", err) 167 } 168 // sanitize gno.mod 169 gnoMod.Sanitize() 170 171 // validate gno.mod 172 if err := gnoMod.Validate(); err != nil { 173 return fmt.Errorf("validate: %w", err) 174 } 175 176 // fetch dependencies 177 if err := gnoMod.FetchDeps(gnomod.GetGnoModPath(), cfg.remote, cfg.verbose); err != nil { 178 return fmt.Errorf("fetch: %w", err) 179 } 180 181 gomod, err := gnomod.GnoToGoMod(*gnoMod) 182 if err != nil { 183 return fmt.Errorf("sanitize: %w", err) 184 } 185 186 // write go.mod file 187 err = gomod.Write(filepath.Join(path, "go.mod")) 188 if err != nil { 189 return fmt.Errorf("write go.mod file: %w", err) 190 } 191 192 return nil 193 } 194 195 func execModInit(args []string) error { 196 if len(args) > 1 { 197 return flag.ErrHelp 198 } 199 var modPath string 200 if len(args) == 1 { 201 modPath = args[0] 202 } 203 dir, err := os.Getwd() 204 if err != nil { 205 return err 206 } 207 if err := gnomod.CreateGnoModFile(dir, modPath); err != nil { 208 return fmt.Errorf("create gno.mod file: %w", err) 209 } 210 211 return nil 212 } 213 214 func execModTidy(args []string, io commands.IO) error { 215 if len(args) > 0 { 216 return flag.ErrHelp 217 } 218 219 wd, err := os.Getwd() 220 if err != nil { 221 return err 222 } 223 fname := filepath.Join(wd, "gno.mod") 224 gm, err := gnomod.ParseGnoMod(fname) 225 if err != nil { 226 return err 227 } 228 229 // Drop all existing requires 230 for _, r := range gm.Require { 231 gm.DropRequire(r.Mod.Path) 232 } 233 234 imports, err := getGnoPackageImports(wd) 235 if err != nil { 236 return err 237 } 238 for _, im := range imports { 239 // skip if importpath is modulepath 240 if im == gm.Module.Mod.Path { 241 continue 242 } 243 gm.AddRequire(im, "v0.0.0-latest") 244 } 245 246 gm.Write(fname) 247 return nil 248 } 249 250 func execModWhy(args []string, io commands.IO) error { 251 if len(args) < 1 { 252 return flag.ErrHelp 253 } 254 255 wd, err := os.Getwd() 256 if err != nil { 257 return err 258 } 259 fname := filepath.Join(wd, "gno.mod") 260 gm, err := gnomod.ParseGnoMod(fname) 261 if err != nil { 262 return err 263 } 264 265 importToFilesMap, err := getImportToFilesMap(wd) 266 if err != nil { 267 return err 268 } 269 270 // Format and print `gno mod why` output stanzas 271 out := formatModWhyStanzas(gm.Module.Mod.Path, args, importToFilesMap) 272 io.Printf(out) 273 274 return nil 275 } 276 277 // formatModWhyStanzas returns a formatted output for the go mod why command. 278 // It takes three parameters: 279 // - modulePath (the path of the module) 280 // - args (input arguments) 281 // - importToFilesMap (a map of import to files). 282 func formatModWhyStanzas(modulePath string, args []string, importToFilesMap map[string][]string) (out string) { 283 for i, arg := range args { 284 out += fmt.Sprintf("# %s\n", arg) 285 files, ok := importToFilesMap[arg] 286 if !ok { 287 out += fmt.Sprintf("(module %s does not need package %s)\n", modulePath, arg) 288 } else { 289 for _, file := range files { 290 out += file + "\n" 291 } 292 } 293 if i < len(args)-1 { // Add a newline if it's not the last stanza 294 out += "\n" 295 } 296 } 297 return 298 } 299 300 // getImportToFilesMap returns a map where each key is an import path and its 301 // value is a list of files importing that package with the specified import path. 302 func getImportToFilesMap(pkgPath string) (map[string][]string, error) { 303 entries, err := os.ReadDir(pkgPath) 304 if err != nil { 305 return nil, err 306 } 307 m := make(map[string][]string) // import -> []file 308 for _, e := range entries { 309 filename := e.Name() 310 if ext := filepath.Ext(filename); ext != ".gno" { 311 continue 312 } 313 if strings.HasSuffix(filename, "_filetest.gno") { 314 continue 315 } 316 imports, err := getGnoFileImports(filepath.Join(pkgPath, filename)) 317 if err != nil { 318 return nil, err 319 } 320 321 for _, imp := range imports { 322 m[imp] = append(m[imp], filename) 323 } 324 } 325 return m, nil 326 } 327 328 // getGnoPackageImports returns the list of gno imports from a given path. 329 // Note: It ignores subdirs. Since right now we are still deciding on 330 // how to handle subdirs. 331 // See: 332 // - https://github.com/gnolang/gno/issues/1024 333 // - https://github.com/gnolang/gno/issues/852 334 // 335 // TODO: move this to better location. 336 func getGnoPackageImports(path string) ([]string, error) { 337 entries, err := os.ReadDir(path) 338 if err != nil { 339 return nil, err 340 } 341 342 allImports := make([]string, 0) 343 seen := make(map[string]struct{}) 344 for _, e := range entries { 345 filename := e.Name() 346 if ext := filepath.Ext(filename); ext != ".gno" { 347 continue 348 } 349 if strings.HasSuffix(filename, "_filetest.gno") { 350 continue 351 } 352 imports, err := getGnoFileImports(filepath.Join(path, filename)) 353 if err != nil { 354 return nil, err 355 } 356 for _, im := range imports { 357 if !strings.HasPrefix(im, "gno.land/") { 358 continue 359 } 360 if _, ok := seen[im]; ok { 361 continue 362 } 363 allImports = append(allImports, im) 364 seen[im] = struct{}{} 365 } 366 } 367 sort.Strings(allImports) 368 369 return allImports, nil 370 } 371 372 func getGnoFileImports(fname string) ([]string, error) { 373 if !strings.HasSuffix(fname, ".gno") { 374 return nil, fmt.Errorf("not a gno file: %q", fname) 375 } 376 data, err := os.ReadFile(fname) 377 if err != nil { 378 return nil, err 379 } 380 fs := token.NewFileSet() 381 f, err := parser.ParseFile(fs, fname, data, parser.ImportsOnly) 382 if err != nil { 383 return nil, err 384 } 385 res := make([]string, 0) 386 for _, im := range f.Imports { 387 importPath := strings.TrimPrefix(strings.TrimSuffix(im.Path.Value, `"`), `"`) 388 res = append(res, importPath) 389 } 390 return res, nil 391 }