diff --git a/.gitignore b/.gitignore index d7e6d7eb9a4..6f6c5ec254d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ pbbindings.go *# cover.out coverage.out +/.deploy/ \ No newline at end of file diff --git a/gno.land/cmd/bumpkg/main.go b/gno.land/cmd/bumpkg/main.go new file mode 100644 index 00000000000..5b1cf634e51 --- /dev/null +++ b/gno.land/cmd/bumpkg/main.go @@ -0,0 +1,338 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + + expect "github.com/Netflix/go-expect" + "github.com/peterbourgon/ff/v3" + "github.com/pkg/errors" +) + +func main() { + fs := flag.NewFlagSet("bumpkg", flag.ContinueOnError) + var ( + packagesRootFlag = fs.String("root", "examples", "root directory of packages") + targetPkgPathFlag = fs.String("target", "", "target package path") + remoteGnowebFlag = fs.String("remote-gnoweb", "https://testnet.gno.teritori.com", "remote gnoweb node") + remoteGnoFlag = fs.String("remote-gno", "testnet.gno.teritori.com:26657", "remote gno node") + chainIdFlag = fs.String("chain-id", "teritori-1", "remote chain id") + walletNameFlag = fs.String("wallet", "tester", "wallet name") + depositFlag = fs.String("deposit", "1ugnot", "deposit") + gasFeeFlag = fs.String("gas-fee", "1ugnot", "gas fee") + gasWantedFlag = fs.String("gas-wanted", "10000000", "gas wanted") + ) + + err := ff.Parse(fs, os.Args[1:]) + if err != nil { + panic(err) + } + + if targetPkgPathFlag == nil || *targetPkgPathFlag == "" { + panic("target package path is required") + } + targetPkgPath := *targetPkgPathFlag + + if packagesRootFlag == nil || *packagesRootFlag == "" { + panic("packages root is required") + } + packagesRoot := *packagesRootFlag + + if remoteGnowebFlag == nil || *remoteGnowebFlag == "" { + panic("remote gnoweb node is required") + } + remoteGnoweb := *remoteGnowebFlag + + if remoteGnoFlag == nil || *remoteGnoFlag == "" { + panic("remote gno node is required") + } + remoteGno := *remoteGnoFlag + + if chainIdFlag == nil || *chainIdFlag == "" { + panic("chain id is required") + } + chainId := *chainIdFlag + + if walletNameFlag == nil || *walletNameFlag == "" { + panic("wallet name is required") + } + walletName := *walletNameFlag + + if depositFlag == nil || *depositFlag == "" { + panic("deposit is required") + } + deposit := *depositFlag + + if gasFeeFlag == nil || *gasFeeFlag == "" { + panic("gas fee is required") + } + gasFee := *gasFeeFlag + + if gasWantedFlag == nil || *gasWantedFlag == "" { + panic("gas wanted is required") + } + gasWanted := *gasWantedFlag + + targetPackageFSPath := filepath.Join(packagesRoot, targetPkgPath) + targetPackageGnoModPath := filepath.Join(targetPackageFSPath, "gno.mod") + fmt.Println("Target package:\n\n" + targetPkgPath) + + allGnoMods := map[string]struct{}{} + if err := filepath.Walk(packagesRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println("error during walk:", err) + return nil + } + if info.IsDir() || info.Name() != "gno.mod" { + return nil + } + + allGnoMods[path] = struct{}{} + + return nil + }); err != nil { + panic(errors.Wrap(err, "failed to walk packages")) + } + if _, ok := allGnoMods[targetPackageGnoModPath]; !ok { + panic("target package not found") + } + + requires := map[string][]string{} + requiredBy := map[string][]string{} + for gnoModPath := range allGnoMods { + deps, err := gnoModDeps(gnoModPath) + if err != nil { + panic(errors.Wrap(err, "failed to parse "+gnoModPath)) + } + + pkgPath := strings.TrimSuffix(strings.TrimPrefix(gnoModPath, packagesRoot+"/"), "/gno.mod") // FIXME: brittle, not cross-platform + + requires[pkgPath] = deps + for _, dep := range deps { + requiredBy[dep] = append(requiredBy[dep], pkgPath) + } + } + + upgrades := map[string]string{} + + fmt.Println("\nFetching versions from remote...") + + roots := []string{targetPkgPath} + seen := map[string]struct{}{} + for len(roots) > 0 { + pkgPath := roots[0] + roots = roots[1:] + if _, ok := seen[pkgPath]; ok { + continue + } + seen[pkgPath] = struct{}{} + roots = append(roots, requiredBy[pkgPath]...) + + // find highest version on remote + nextVersion := 2 + for { + resp, err := http.Get(fmt.Sprintf("%s/%s_v%d/", remoteGnoweb, strings.TrimPrefix(pkgPath, "gno.land/"), nextVersion)) // last slash is important so we query sources and don't run into problems with render errors in realms + if err != nil { + panic(errors.Wrap(err, "failed to get "+pkgPath)) + } + if resp.StatusCode == 500 { + break + } + if resp.StatusCode != 200 { + panic("unexpected status code: " + strconv.Itoa(resp.StatusCode)) + } + nextVersion++ + } + + newPkgPath := fmt.Sprintf("%s_v%d", pkgPath, nextVersion) + upgrades[pkgPath] = newPkgPath + } + + fmt.Print("Copying root to temporary directory...\n") + tmpDir := ".deploy" + if err := os.RemoveAll(tmpDir); err != nil { + panic(errors.Wrap(err, "failed to remove "+tmpDir)) + } + cmd := exec.Command("cp", "-r", packagesRoot, tmpDir) + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + panic(errors.Wrap(err, "failed to copy "+packagesRoot)) + } + + // preversedPackagesRoot := packagesRoot + packagesRoot = tmpDir + + fmt.Print("\nBumping:\n\n") + + for oldPkgPath, newPkgPath := range upgrades { + fmt.Println(oldPkgPath, "->", newPkgPath) + + r := regexp.MustCompile(oldPkgPath) + + // change module name in gno.mod + gnoModPath := filepath.Join(packagesRoot, oldPkgPath, "gno.mod") + data, err := os.ReadFile(gnoModPath) + if err != nil { + panic(errors.Wrap(err, "failed to read "+gnoModPath)) + } + edited := r.ReplaceAll(data, []byte(newPkgPath)) + if err := os.WriteFile(gnoModPath, edited, 0644); err != nil { + panic(errors.Wrap(err, "failed to write "+gnoModPath)) + } + + for _, child := range requiredBy[oldPkgPath] { + // change import paths in dependent .gno files + if err := filepath.Walk(filepath.Join(packagesRoot, child), func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println("error during walk:", err) + return nil + } + + if info.IsDir() || !strings.HasSuffix(path, ".gno") { + return nil + } + + // replace oldPkgPath with newPkgPath in file + data, err := os.ReadFile(path) + if err != nil { + return errors.Wrap(err, "failed to read "+path) + } + edited := r.ReplaceAll(data, []byte(newPkgPath)) + if err := os.WriteFile(path, edited, 0644); err != nil { + return errors.Wrap(err, "failed to write "+path) + } + + return nil + }); err != nil { + panic(errors.Wrap(err, "failed to walk packages")) + } + + // change import paths in dependent gno.mod files + gnoModPath := filepath.Join(packagesRoot, child, "gno.mod") + data, err := os.ReadFile(gnoModPath) + if err != nil { + panic(errors.Wrap(err, "failed to read "+gnoModPath)) + } + edited := r.ReplaceAll(data, []byte(newPkgPath)) + if err := os.WriteFile(gnoModPath, edited, 0644); err != nil { + panic(errors.Wrap(err, "failed to write "+gnoModPath)) + } + } + } + + for oldPkgPath, newPkgPath := range upgrades { + // rename directory + if err := os.Rename(filepath.Join(packagesRoot, oldPkgPath), filepath.Join(packagesRoot, newPkgPath)); err != nil { + panic(errors.Wrap(err, "failed to rename "+oldPkgPath)) + } + } + + fmt.Print("\nDeploying:\n\n") + + // deploy packages in dependency order + deployed := map[string]struct{}{} + remaining := map[string]struct{}{} + for pkgPath := range upgrades { + remaining[pkgPath] = struct{}{} + } + for len(remaining) > 0 { + leafs := map[string]struct{}{} + for pkgPath := range remaining { + deps := requires[pkgPath] + if len(deps) == 0 { + leafs[pkgPath] = struct{}{} + } + hasDep := false + for _, dep := range deps { + if _, ok := upgrades[dep]; ok { + if _, ok := deployed[dep]; !ok { + hasDep = true + break + } + } + } + if !hasDep { + leafs[pkgPath] = struct{}{} + } + } + + if len(leafs) == 0 { + panic("no leafs found, probably a cylic dependency") + } + + for leaf := range leafs { + fmt.Println(upgrades[leaf]) + c, err := expect.NewConsole() + if err != nil { + panic(errors.Wrap(err, "failed to create console")) + } + cmd := exec.Command("gnokey", "maketx", "addpkg", + "-deposit="+deposit, + "-gas-fee="+gasFee, + "-gas-wanted="+gasWanted, + "-broadcast=true", + "-remote="+remoteGno, + "-chainid="+chainId, + "-pkgdir="+filepath.Join(packagesRoot, upgrades[leaf]), + "-pkgpath="+upgrades[leaf], + walletName, + ) + + buf := bytes.NewBuffer(nil) + multiWriter := io.MultiWriter(c.Tty(), buf) + cmd.Stderr = multiWriter + cmd.Stdout = multiWriter + cmd.Stdin = c.Tty() + + go func() { + c.ExpectString("Enter password.") + c.SendLine("") + }() + + if err := cmd.Run(); err != nil { + fmt.Println("\n" + buf.String()) + panic(errors.Wrap(err, "failed to deploy "+upgrades[leaf])) + } + + deployed[leaf] = struct{}{} + delete(remaining, leaf) + } + } +} + +func gnoModDeps(gnoModPath string) ([]string, error) { + data, err := os.ReadFile(gnoModPath) + if err != nil { + return nil, errors.Wrap(err, "failed to read "+gnoModPath) + } + r := regexp.MustCompile(`(?s)require.+?\((.+?)\)`) + submatches := r.FindAllStringSubmatch(string(data), -1) + if len(submatches) < 1 || len(submatches[0]) < 2 { + return nil, nil + } + lines := strings.Split(submatches[0][1], "\n") + depEntries := []string{} + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + depR := regexp.MustCompile(`"(.+)"`) + submatches := depR.FindAllStringSubmatch(line, -1) + if len(submatches) < 1 || len(submatches[0]) < 2 { + return nil, fmt.Errorf("failed to parse dep line: %q", line) + } + depEntry := submatches[0][1] + depEntries = append(depEntries, depEntry) + } + return depEntries, nil +} diff --git a/go.mod b/go.mod index eb7c3b9a277..eb29c0ee45c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/gnolang/gno go 1.19 require ( + github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c github.com/btcsuite/btcd/btcutil v1.1.1 github.com/btcsuite/btcutil v1.0.2 @@ -26,6 +27,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 github.com/pelletier/go-toml v1.9.5 github.com/peterbourgon/ff/v3 v3.4.0 + github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 github.com/rogpeppe/go-internal v1.11.0 github.com/stretchr/testify v1.8.4 @@ -44,6 +46,7 @@ require ( require ( github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/creack/pty v1.1.17 // indirect github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect @@ -61,7 +64,6 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.7 // indirect github.com/lucasb-eyer/go-colorful v1.0.3 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect go.opencensus.io v0.22.5 // indirect go.uber.org/atomic v1.7.0 // indirect diff --git a/go.sum b/go.sum index c51f8d91df2..00ef5d53a2e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -35,6 +37,8 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=