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

     1  package gnomod
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/gnolang/gno/gnovm/pkg/gnoenv"
    11  	"github.com/gnolang/gno/gnovm/pkg/gnolang"
    12  	"github.com/gnolang/gno/gnovm/pkg/transpiler"
    13  	"github.com/gnolang/gno/tm2/pkg/std"
    14  	"golang.org/x/mod/modfile"
    15  	"golang.org/x/mod/module"
    16  )
    17  
    18  const queryPathFile = "vm/qfile"
    19  
    20  // GetGnoModPath returns the path for gno modules
    21  func GetGnoModPath() string {
    22  	return filepath.Join(gnoenv.HomeDir(), "pkg", "mod")
    23  }
    24  
    25  // PackageDir resolves a given module.Version to the path on the filesystem.
    26  // If root is dir, it is defaulted to the value of [GetGnoModPath].
    27  func PackageDir(root string, v module.Version) string {
    28  	// This is also used internally exactly like filepath.Join; but we'll keep
    29  	// the calls centralized to make sure we can change the path centrally should
    30  	// we start including the module version in the path.
    31  
    32  	if root == "" {
    33  		root = GetGnoModPath()
    34  	}
    35  	return filepath.Join(root, v.Path)
    36  }
    37  
    38  func writePackage(remote, basePath, pkgPath string) (requirements []string, err error) {
    39  	res, err := queryChain(remote, queryPathFile, []byte(pkgPath))
    40  	if err != nil {
    41  		return nil, fmt.Errorf("querychain (%s): %w", pkgPath, err)
    42  	}
    43  
    44  	dirPath, fileName := std.SplitFilepath(pkgPath)
    45  	if fileName == "" {
    46  		// Is Dir
    47  		// Create Dir if not exists
    48  		dirPath := filepath.Join(basePath, dirPath)
    49  		if _, err = os.Stat(dirPath); os.IsNotExist(err) {
    50  			if err = os.MkdirAll(dirPath, 0o755); err != nil {
    51  				return nil, fmt.Errorf("mkdir %q: %w", dirPath, err)
    52  			}
    53  		}
    54  
    55  		files := strings.Split(string(res.Data), "\n")
    56  		for _, file := range files {
    57  			reqs, err := writePackage(remote, basePath, filepath.Join(pkgPath, file))
    58  			if err != nil {
    59  				return nil, fmt.Errorf("writepackage: %w", err)
    60  			}
    61  			requirements = append(requirements, reqs...)
    62  		}
    63  	} else {
    64  		// Is File
    65  		// Transpile and write generated go file
    66  		if strings.HasSuffix(fileName, ".gno") {
    67  			filePath := filepath.Join(basePath, pkgPath)
    68  			targetFilename, _ := transpiler.GetTranspileFilenameAndTags(filePath)
    69  			transpileRes, err := transpiler.Transpile(string(res.Data), "", fileName)
    70  			if err != nil {
    71  				return nil, fmt.Errorf("transpile: %w", err)
    72  			}
    73  
    74  			for _, i := range transpileRes.Imports {
    75  				requirements = append(requirements, i.Path.Value)
    76  			}
    77  
    78  			targetFileNameWithPath := filepath.Join(basePath, dirPath, targetFilename)
    79  			err = os.WriteFile(targetFileNameWithPath, []byte(transpileRes.Translated), 0o644)
    80  			if err != nil {
    81  				return nil, fmt.Errorf("writefile %q: %w", targetFileNameWithPath, err)
    82  			}
    83  		}
    84  
    85  		// Write file
    86  		fileNameWithPath := filepath.Join(basePath, dirPath, fileName)
    87  		err = os.WriteFile(fileNameWithPath, res.Data, 0o644)
    88  		if err != nil {
    89  			return nil, fmt.Errorf("writefile %q: %w", fileNameWithPath, err)
    90  		}
    91  	}
    92  
    93  	return removeDuplicateStr(requirements), nil
    94  }
    95  
    96  // GnoToGoMod make necessary modifications in the gno.mod
    97  // and return go.mod file.
    98  func GnoToGoMod(f File) (*File, error) {
    99  	gnoModPath := GetGnoModPath()
   100  
   101  	if strings.HasPrefix(f.Module.Mod.Path, transpiler.GnoRealmPkgsPrefixBefore) ||
   102  		strings.HasPrefix(f.Module.Mod.Path, transpiler.GnoPackagePrefixBefore) {
   103  		f.AddModuleStmt(transpiler.ImportPrefix + "/examples/" + f.Module.Mod.Path)
   104  	}
   105  
   106  	for i := range f.Require {
   107  		mod, replaced := isReplaced(f.Require[i].Mod, f.Replace)
   108  		if replaced {
   109  			if modfile.IsDirectoryPath(mod.Path) {
   110  				continue
   111  			}
   112  		}
   113  		path := f.Require[i].Mod.Path
   114  		if strings.HasPrefix(f.Require[i].Mod.Path, transpiler.GnoRealmPkgsPrefixBefore) ||
   115  			strings.HasPrefix(f.Require[i].Mod.Path, transpiler.GnoPackagePrefixBefore) {
   116  			// Add dependency with a modified import path
   117  			f.AddRequire(transpiler.ImportPrefix+"/examples/"+f.Require[i].Mod.Path, f.Require[i].Mod.Version)
   118  		}
   119  		f.AddReplace(f.Require[i].Mod.Path, f.Require[i].Mod.Version, filepath.Join(gnoModPath, path), "")
   120  		// Remove the old require since the new dependency was added above
   121  		f.DropRequire(f.Require[i].Mod.Path)
   122  	}
   123  
   124  	// Remove replacements that are not replaced by directories.
   125  	//
   126  	// Explanation:
   127  	// By this stage every replacement should be replace by dir.
   128  	// If not replaced by dir, remove it.
   129  	//
   130  	// e.g:
   131  	//
   132  	// ```
   133  	// require (
   134  	//	gno.land/p/demo/avl v1.2.3
   135  	// )
   136  	//
   137  	// replace (
   138  	//	gno.land/p/demo/avl v1.2.3  => gno.land/p/demo/avl v3.2.1
   139  	// )
   140  	// ```
   141  	//
   142  	// In above case we will fetch `gno.land/p/demo/avl v3.2.1` and
   143  	// replace will look something like:
   144  	//
   145  	// ```
   146  	// replace (
   147  	//	gno.land/p/demo/avl v1.2.3  => gno.land/p/demo/avl v3.2.1
   148  	//	gno.land/p/demo/avl v3.2.1  => /path/to/avl/version/v3.2.1
   149  	// )
   150  	// ```
   151  	//
   152  	// Remove `gno.land/p/demo/avl v1.2.3  => gno.land/p/demo/avl v3.2.1`.
   153  	for _, r := range f.Replace {
   154  		if !modfile.IsDirectoryPath(r.New.Path) {
   155  			f.DropReplace(r.Old.Path, r.Old.Version)
   156  		}
   157  	}
   158  
   159  	return &f, nil
   160  }
   161  
   162  func CreateGnoModFile(rootDir, modPath string) error {
   163  	if !filepath.IsAbs(rootDir) {
   164  		return fmt.Errorf("dir %q is not absolute", rootDir)
   165  	}
   166  
   167  	modFilePath := filepath.Join(rootDir, "gno.mod")
   168  	if _, err := os.Stat(modFilePath); err == nil {
   169  		return errors.New("gno.mod file already exists")
   170  	}
   171  
   172  	if modPath == "" {
   173  		// Check .gno files for package name
   174  		// and use it as modPath
   175  		files, err := os.ReadDir(rootDir)
   176  		if err != nil {
   177  			return fmt.Errorf("read dir %q: %w", rootDir, err)
   178  		}
   179  
   180  		var pkgName gnolang.Name
   181  		for _, file := range files {
   182  			if file.IsDir() || !strings.HasSuffix(file.Name(), ".gno") || strings.HasSuffix(file.Name(), "_filetest.gno") {
   183  				continue
   184  			}
   185  
   186  			fpath := filepath.Join(rootDir, file.Name())
   187  			bz, err := os.ReadFile(fpath)
   188  			if err != nil {
   189  				return fmt.Errorf("read file %q: %w", fpath, err)
   190  			}
   191  
   192  			pn := gnolang.PackageNameFromFileBody(file.Name(), string(bz))
   193  			if strings.HasSuffix(string(pkgName), "_test") {
   194  				pkgName = pkgName[:len(pkgName)-len("_test")]
   195  			}
   196  			if pkgName == "" {
   197  				pkgName = pn
   198  			}
   199  			if pkgName != pn {
   200  				return fmt.Errorf("package name mismatch: [%q] and [%q]", pkgName, pn)
   201  			}
   202  		}
   203  		if pkgName == "" {
   204  			return errors.New("cannot determine package name")
   205  		}
   206  		modPath = string(pkgName)
   207  	}
   208  	if err := module.CheckImportPath(modPath); err != nil {
   209  		return err
   210  	}
   211  
   212  	modfile := new(File)
   213  	modfile.AddModuleStmt(modPath)
   214  	modfile.Write(filepath.Join(rootDir, "gno.mod"))
   215  
   216  	return nil
   217  }
   218  
   219  func isReplaced(mod module.Version, repl []*modfile.Replace) (module.Version, bool) {
   220  	for _, r := range repl {
   221  		hasNoVersion := r.Old.Path == mod.Path && r.Old.Version == ""
   222  		hasExactVersion := r.Old == mod
   223  		if hasNoVersion || hasExactVersion {
   224  			return r.New, true
   225  		}
   226  	}
   227  	return module.Version{}, false
   228  }
   229  
   230  func removeDuplicateStr(str []string) (res []string) {
   231  	m := make(map[string]struct{}, len(str))
   232  	for _, s := range str {
   233  		if _, ok := m[s]; !ok {
   234  			m[s] = struct{}{}
   235  			res = append(res, s)
   236  		}
   237  	}
   238  	return
   239  }