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

     1  package gnomod
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  )
    10  
    11  type Pkg struct {
    12  	Dir      string   // absolute path to package dir
    13  	Name     string   // package name
    14  	Requires []string // dependencies
    15  	Draft    bool     // whether the package is a draft
    16  }
    17  
    18  type SubPkg struct {
    19  	Dir        string   // absolute path to package dir
    20  	ImportPath string   // import path of package
    21  	Root       string   // Root dir containing this package, i.e dir containing gno.mod file
    22  	Imports    []string // imports used by this package
    23  
    24  	GnoFiles         []string // .gno source files (excluding TestGnoFiles, FiletestGnoFiles)
    25  	TestGnoFiles     []string // _test.gno source files
    26  	FiletestGnoFiles []string // _filetest.gno source files
    27  }
    28  
    29  type (
    30  	PkgList       []Pkg
    31  	SortedPkgList []Pkg
    32  )
    33  
    34  // sortPkgs sorts the given packages by their dependencies.
    35  func (pl PkgList) Sort() (SortedPkgList, error) {
    36  	visited := make(map[string]bool)
    37  	onStack := make(map[string]bool)
    38  	sortedPkgs := make([]Pkg, 0, len(pl))
    39  
    40  	// Visit all packages
    41  	for _, p := range pl {
    42  		if err := visitPackage(p, pl, visited, onStack, &sortedPkgs); err != nil {
    43  			return nil, err
    44  		}
    45  	}
    46  
    47  	return sortedPkgs, nil
    48  }
    49  
    50  // visitNode visits a package's and its dependencies dependencies and adds them to the sorted list.
    51  func visitPackage(pkg Pkg, pkgs []Pkg, visited, onStack map[string]bool, sortedPkgs *[]Pkg) error {
    52  	if onStack[pkg.Name] {
    53  		return fmt.Errorf("cycle detected: %s", pkg.Name)
    54  	}
    55  	if visited[pkg.Name] {
    56  		return nil
    57  	}
    58  
    59  	visited[pkg.Name] = true
    60  	onStack[pkg.Name] = true
    61  
    62  	// Visit package's dependencies
    63  	for _, req := range pkg.Requires {
    64  		found := false
    65  		for _, p := range pkgs {
    66  			if p.Name != req {
    67  				continue
    68  			}
    69  			if err := visitPackage(p, pkgs, visited, onStack, sortedPkgs); err != nil {
    70  				return err
    71  			}
    72  			found = true
    73  			break
    74  		}
    75  		if !found {
    76  			return fmt.Errorf("missing dependency '%s' for package '%s'", req, pkg.Name)
    77  		}
    78  	}
    79  
    80  	onStack[pkg.Name] = false
    81  	*sortedPkgs = append(*sortedPkgs, pkg)
    82  	return nil
    83  }
    84  
    85  // ListPkgs lists all gno packages in the given root directory.
    86  func ListPkgs(root string) (PkgList, error) {
    87  	var pkgs []Pkg
    88  
    89  	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
    90  		if err != nil {
    91  			return err
    92  		}
    93  		if !d.IsDir() {
    94  			return nil
    95  		}
    96  		gnoModPath := filepath.Join(path, "gno.mod")
    97  		data, err := os.ReadFile(gnoModPath)
    98  		if os.IsNotExist(err) {
    99  			return nil
   100  		}
   101  		if err != nil {
   102  			return err
   103  		}
   104  
   105  		gnoMod, err := Parse(gnoModPath, data)
   106  		if err != nil {
   107  			return fmt.Errorf("parse: %w", err)
   108  		}
   109  		gnoMod.Sanitize()
   110  		if err := gnoMod.Validate(); err != nil {
   111  			return fmt.Errorf("validate: %w", err)
   112  		}
   113  
   114  		pkgs = append(pkgs, Pkg{
   115  			Dir:   path,
   116  			Name:  gnoMod.Module.Mod.Path,
   117  			Draft: gnoMod.Draft,
   118  			Requires: func() []string {
   119  				var reqs []string
   120  				for _, req := range gnoMod.Require {
   121  					reqs = append(reqs, req.Mod.Path)
   122  				}
   123  				return reqs
   124  			}(),
   125  		})
   126  		return nil
   127  	})
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	return pkgs, nil
   133  }
   134  
   135  // GetNonDraftPkgs returns packages that are not draft
   136  // and have no direct or indirect draft dependencies.
   137  func (sp SortedPkgList) GetNonDraftPkgs() SortedPkgList {
   138  	res := make([]Pkg, 0, len(sp))
   139  	draft := make(map[string]bool)
   140  
   141  	for _, pkg := range sp {
   142  		if pkg.Draft {
   143  			draft[pkg.Name] = true
   144  			continue
   145  		}
   146  		dependsOnDraft := false
   147  		for _, req := range pkg.Requires {
   148  			if draft[req] {
   149  				dependsOnDraft = true
   150  				draft[pkg.Name] = true
   151  				break
   152  			}
   153  		}
   154  		if !dependsOnDraft {
   155  			res = append(res, pkg)
   156  		}
   157  	}
   158  	return res
   159  }
   160  
   161  // SubPkgsFromPaths returns a list of subpackages from the given paths.
   162  func SubPkgsFromPaths(paths []string) ([]*SubPkg, error) {
   163  	for _, path := range paths {
   164  		fi, err := os.Stat(path)
   165  		if err != nil {
   166  			return nil, err
   167  		}
   168  		if fi.IsDir() {
   169  			continue
   170  		}
   171  		if filepath.Ext(path) != ".gno" {
   172  			return nil, fmt.Errorf("files must be .gno files: %s", path)
   173  		}
   174  
   175  		subPkg, err := GnoFileSubPkg(paths)
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  		return []*SubPkg{subPkg}, nil
   180  	}
   181  
   182  	subPkgs := make([]*SubPkg, 0, len(paths))
   183  	for _, path := range paths {
   184  		subPkg := SubPkg{}
   185  
   186  		matches, err := filepath.Glob(filepath.Join(path, "*.gno"))
   187  		if err != nil {
   188  			return nil, fmt.Errorf("failed to match pattern: %w", err)
   189  		}
   190  
   191  		subPkg.Dir = path
   192  		for _, match := range matches {
   193  			if strings.HasSuffix(match, "_test.gno") {
   194  				subPkg.TestGnoFiles = append(subPkg.TestGnoFiles, match)
   195  				continue
   196  			}
   197  
   198  			if strings.HasSuffix(match, "_filetest.gno") {
   199  				subPkg.FiletestGnoFiles = append(subPkg.FiletestGnoFiles, match)
   200  				continue
   201  			}
   202  			subPkg.GnoFiles = append(subPkg.GnoFiles, match)
   203  		}
   204  
   205  		subPkgs = append(subPkgs, &subPkg)
   206  	}
   207  
   208  	return subPkgs, nil
   209  }
   210  
   211  // GnoFileSubPkg returns a subpackage from the given .gno files.
   212  func GnoFileSubPkg(files []string) (*SubPkg, error) {
   213  	subPkg := SubPkg{}
   214  	firstDir := ""
   215  	for _, file := range files {
   216  		if filepath.Ext(file) != ".gno" {
   217  			return nil, fmt.Errorf("files must be .gno files: %s", file)
   218  		}
   219  
   220  		fi, err := os.Stat(file)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		if fi.IsDir() {
   225  			return nil, fmt.Errorf("%s is a directory, should be a Gno file", file)
   226  		}
   227  
   228  		dir := filepath.Dir(file)
   229  		if firstDir == "" {
   230  			firstDir = dir
   231  		}
   232  		if dir != firstDir {
   233  			return nil, fmt.Errorf("all files must be in one directory; have %s and %s", firstDir, dir)
   234  		}
   235  
   236  		if strings.HasSuffix(file, "_test.gno") {
   237  			subPkg.TestGnoFiles = append(subPkg.TestGnoFiles, file)
   238  			continue
   239  		}
   240  
   241  		if strings.HasSuffix(file, "_filetest.gno") {
   242  			subPkg.FiletestGnoFiles = append(subPkg.FiletestGnoFiles, file)
   243  			continue
   244  		}
   245  		subPkg.GnoFiles = append(subPkg.GnoFiles, file)
   246  	}
   247  	subPkg.Dir = firstDir
   248  
   249  	return &subPkg, nil
   250  }