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  }