Skip to content

Commit 5b8dd43

Browse files
author
Norman Meier
committed
feat: bumpkg cmd
Signed-off-by: Norman Meier <norman@berty.tech>
1 parent b40ac9c commit 5b8dd43

File tree

4 files changed

+346
-1
lines changed

4 files changed

+346
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ pbbindings.go
2020
*#
2121
cover.out
2222
coverage.out
23+
/.deploy/

gno.land/cmd/bumpkg/main.go

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"regexp"
13+
"strconv"
14+
"strings"
15+
16+
expect "github.com/Netflix/go-expect"
17+
"github.com/peterbourgon/ff/v3"
18+
"github.com/pkg/errors"
19+
)
20+
21+
func main() {
22+
fs := flag.NewFlagSet("bumpkg", flag.ContinueOnError)
23+
var (
24+
packagesRootFlag = fs.String("root", "examples", "root directory of packages")
25+
targetPkgPathFlag = fs.String("target", "", "target package path")
26+
remoteGnowebFlag = fs.String("remote-gnoweb", "https://testnet.gno.teritori.com", "remote gnoweb node")
27+
remoteGnoFlag = fs.String("remote-gno", "testnet.gno.teritori.com:26657", "remote gno node")
28+
chainIdFlag = fs.String("chain-id", "teritori-1", "remote chain id")
29+
walletNameFlag = fs.String("wallet", "tester", "wallet name")
30+
depositFlag = fs.String("deposit", "1ugnot", "deposit")
31+
gasFeeFlag = fs.String("gas-fee", "1ugnot", "gas fee")
32+
gasWantedFlag = fs.String("gas-wanted", "10000000", "gas wanted")
33+
)
34+
35+
err := ff.Parse(fs, os.Args[1:])
36+
if err != nil {
37+
panic(err)
38+
}
39+
40+
if targetPkgPathFlag == nil || *targetPkgPathFlag == "" {
41+
panic("target package path is required")
42+
}
43+
targetPkgPath := *targetPkgPathFlag
44+
45+
if packagesRootFlag == nil || *packagesRootFlag == "" {
46+
panic("packages root is required")
47+
}
48+
packagesRoot := *packagesRootFlag
49+
50+
if remoteGnowebFlag == nil || *remoteGnowebFlag == "" {
51+
panic("remote gnoweb node is required")
52+
}
53+
remoteGnoweb := *remoteGnowebFlag
54+
55+
if remoteGnoFlag == nil || *remoteGnoFlag == "" {
56+
panic("remote gno node is required")
57+
}
58+
remoteGno := *remoteGnoFlag
59+
60+
if chainIdFlag == nil || *chainIdFlag == "" {
61+
panic("chain id is required")
62+
}
63+
chainId := *chainIdFlag
64+
65+
if walletNameFlag == nil || *walletNameFlag == "" {
66+
panic("wallet name is required")
67+
}
68+
walletName := *walletNameFlag
69+
70+
if depositFlag == nil || *depositFlag == "" {
71+
panic("deposit is required")
72+
}
73+
deposit := *depositFlag
74+
75+
if gasFeeFlag == nil || *gasFeeFlag == "" {
76+
panic("gas fee is required")
77+
}
78+
gasFee := *gasFeeFlag
79+
80+
if gasWantedFlag == nil || *gasWantedFlag == "" {
81+
panic("gas wanted is required")
82+
}
83+
gasWanted := *gasWantedFlag
84+
85+
targetPackageFSPath := filepath.Join(packagesRoot, targetPkgPath)
86+
targetPackageGnoModPath := filepath.Join(targetPackageFSPath, "gno.mod")
87+
fmt.Println("Target package:\n\n" + targetPkgPath)
88+
89+
allGnoMods := map[string]struct{}{}
90+
if err := filepath.Walk(packagesRoot, func(path string, info os.FileInfo, err error) error {
91+
if err != nil {
92+
fmt.Println("error during walk:", err)
93+
return nil
94+
}
95+
if info.IsDir() || info.Name() != "gno.mod" {
96+
return nil
97+
}
98+
99+
allGnoMods[path] = struct{}{}
100+
101+
return nil
102+
}); err != nil {
103+
panic(errors.Wrap(err, "failed to walk packages"))
104+
}
105+
if _, ok := allGnoMods[targetPackageGnoModPath]; !ok {
106+
panic("target package not found")
107+
}
108+
109+
requires := map[string][]string{}
110+
requiredBy := map[string][]string{}
111+
for gnoModPath := range allGnoMods {
112+
deps, err := gnoModDeps(gnoModPath)
113+
if err != nil {
114+
panic(errors.Wrap(err, "failed to parse "+gnoModPath))
115+
}
116+
117+
pkgPath := strings.TrimSuffix(strings.TrimPrefix(gnoModPath, packagesRoot+"/"), "/gno.mod") // FIXME: brittle, not cross-platform
118+
119+
requires[pkgPath] = deps
120+
for _, dep := range deps {
121+
requiredBy[dep] = append(requiredBy[dep], pkgPath)
122+
}
123+
}
124+
125+
upgrades := map[string]string{}
126+
127+
fmt.Println("\nFetching versions from remote...")
128+
129+
roots := []string{targetPkgPath}
130+
seen := map[string]struct{}{}
131+
for len(roots) > 0 {
132+
pkgPath := roots[0]
133+
roots = roots[1:]
134+
if _, ok := seen[pkgPath]; ok {
135+
continue
136+
}
137+
seen[pkgPath] = struct{}{}
138+
roots = append(roots, requiredBy[pkgPath]...)
139+
140+
// find highest version on remote
141+
nextVersion := 2
142+
for {
143+
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
144+
if err != nil {
145+
panic(errors.Wrap(err, "failed to get "+pkgPath))
146+
}
147+
if resp.StatusCode == 500 {
148+
break
149+
}
150+
if resp.StatusCode != 200 {
151+
panic("unexpected status code: " + strconv.Itoa(resp.StatusCode))
152+
}
153+
nextVersion++
154+
}
155+
156+
newPkgPath := fmt.Sprintf("%s_v%d", pkgPath, nextVersion)
157+
upgrades[pkgPath] = newPkgPath
158+
}
159+
160+
fmt.Print("Copying root to temporary directory...\n")
161+
tmpDir := ".deploy"
162+
if err := os.RemoveAll(tmpDir); err != nil {
163+
panic(errors.Wrap(err, "failed to remove "+tmpDir))
164+
}
165+
cmd := exec.Command("cp", "-r", packagesRoot, tmpDir)
166+
cmd.Stderr = os.Stderr
167+
if err := cmd.Run(); err != nil {
168+
panic(errors.Wrap(err, "failed to copy "+packagesRoot))
169+
}
170+
171+
// preversedPackagesRoot := packagesRoot
172+
packagesRoot = tmpDir
173+
174+
fmt.Print("\nBumping:\n\n")
175+
176+
for oldPkgPath, newPkgPath := range upgrades {
177+
fmt.Println(oldPkgPath, "->", newPkgPath)
178+
179+
r := regexp.MustCompile(oldPkgPath)
180+
181+
// change module name in gno.mod
182+
gnoModPath := filepath.Join(packagesRoot, oldPkgPath, "gno.mod")
183+
data, err := os.ReadFile(gnoModPath)
184+
if err != nil {
185+
panic(errors.Wrap(err, "failed to read "+gnoModPath))
186+
}
187+
edited := r.ReplaceAll(data, []byte(newPkgPath))
188+
if err := os.WriteFile(gnoModPath, edited, 0644); err != nil {
189+
panic(errors.Wrap(err, "failed to write "+gnoModPath))
190+
}
191+
192+
for _, child := range requiredBy[oldPkgPath] {
193+
// change import paths in dependent .gno files
194+
if err := filepath.Walk(filepath.Join(packagesRoot, child), func(path string, info os.FileInfo, err error) error {
195+
if err != nil {
196+
fmt.Println("error during walk:", err)
197+
return nil
198+
}
199+
200+
if info.IsDir() || !strings.HasSuffix(path, ".gno") {
201+
return nil
202+
}
203+
204+
// replace oldPkgPath with newPkgPath in file
205+
data, err := os.ReadFile(path)
206+
if err != nil {
207+
return errors.Wrap(err, "failed to read "+path)
208+
}
209+
edited := r.ReplaceAll(data, []byte(newPkgPath))
210+
if err := os.WriteFile(path, edited, 0644); err != nil {
211+
return errors.Wrap(err, "failed to write "+path)
212+
}
213+
214+
return nil
215+
}); err != nil {
216+
panic(errors.Wrap(err, "failed to walk packages"))
217+
}
218+
219+
// change import paths in dependent gno.mod files
220+
gnoModPath := filepath.Join(packagesRoot, child, "gno.mod")
221+
data, err := os.ReadFile(gnoModPath)
222+
if err != nil {
223+
panic(errors.Wrap(err, "failed to read "+gnoModPath))
224+
}
225+
edited := r.ReplaceAll(data, []byte(newPkgPath))
226+
if err := os.WriteFile(gnoModPath, edited, 0644); err != nil {
227+
panic(errors.Wrap(err, "failed to write "+gnoModPath))
228+
}
229+
}
230+
}
231+
232+
for oldPkgPath, newPkgPath := range upgrades {
233+
// rename directory
234+
if err := os.Rename(filepath.Join(packagesRoot, oldPkgPath), filepath.Join(packagesRoot, newPkgPath)); err != nil {
235+
panic(errors.Wrap(err, "failed to rename "+oldPkgPath))
236+
}
237+
}
238+
239+
fmt.Print("\nDeploying:\n\n")
240+
241+
// deploy packages in dependency order
242+
deployed := map[string]struct{}{}
243+
remaining := map[string]struct{}{}
244+
for pkgPath := range upgrades {
245+
remaining[pkgPath] = struct{}{}
246+
}
247+
for len(remaining) > 0 {
248+
leafs := map[string]struct{}{}
249+
for pkgPath := range remaining {
250+
deps := requires[pkgPath]
251+
if len(deps) == 0 {
252+
leafs[pkgPath] = struct{}{}
253+
}
254+
hasDep := false
255+
for _, dep := range deps {
256+
if _, ok := upgrades[dep]; ok {
257+
if _, ok := deployed[dep]; !ok {
258+
hasDep = true
259+
break
260+
}
261+
}
262+
}
263+
if !hasDep {
264+
leafs[pkgPath] = struct{}{}
265+
}
266+
}
267+
268+
if len(leafs) == 0 {
269+
panic("no leafs found, probably a cylic dependency")
270+
}
271+
272+
for leaf := range leafs {
273+
fmt.Println(upgrades[leaf])
274+
c, err := expect.NewConsole()
275+
if err != nil {
276+
panic(errors.Wrap(err, "failed to create console"))
277+
}
278+
cmd := exec.Command("gnokey", "maketx", "addpkg",
279+
"-deposit="+deposit,
280+
"-gas-fee="+gasFee,
281+
"-gas-wanted="+gasWanted,
282+
"-broadcast=true",
283+
"-remote="+remoteGno,
284+
"-chainid="+chainId,
285+
"-pkgdir="+filepath.Join(packagesRoot, upgrades[leaf]),
286+
"-pkgpath="+upgrades[leaf],
287+
walletName,
288+
)
289+
290+
buf := bytes.NewBuffer(nil)
291+
multiWriter := io.MultiWriter(c.Tty(), buf)
292+
cmd.Stderr = multiWriter
293+
cmd.Stdout = multiWriter
294+
cmd.Stdin = c.Tty()
295+
296+
go func() {
297+
c.ExpectString("Enter password.")
298+
c.SendLine("")
299+
}()
300+
301+
if err := cmd.Run(); err != nil {
302+
fmt.Println("\n" + buf.String())
303+
panic(errors.Wrap(err, "failed to deploy "+upgrades[leaf]))
304+
}
305+
306+
deployed[leaf] = struct{}{}
307+
delete(remaining, leaf)
308+
}
309+
}
310+
}
311+
312+
func gnoModDeps(gnoModPath string) ([]string, error) {
313+
data, err := os.ReadFile(gnoModPath)
314+
if err != nil {
315+
return nil, errors.Wrap(err, "failed to read "+gnoModPath)
316+
}
317+
r := regexp.MustCompile(`(?s)require.+?\((.+?)\)`)
318+
submatches := r.FindAllStringSubmatch(string(data), -1)
319+
if len(submatches) < 1 || len(submatches[0]) < 2 {
320+
return nil, nil
321+
}
322+
lines := strings.Split(submatches[0][1], "\n")
323+
depEntries := []string{}
324+
for _, line := range lines {
325+
line = strings.TrimSpace(line)
326+
if line == "" {
327+
continue
328+
}
329+
depR := regexp.MustCompile(`"(.+)"`)
330+
submatches := depR.FindAllStringSubmatch(line, -1)
331+
if len(submatches) < 1 || len(submatches[0]) < 2 {
332+
return nil, fmt.Errorf("failed to parse dep line: %q", line)
333+
}
334+
depEntry := submatches[0][1]
335+
depEntries = append(depEntries, depEntry)
336+
}
337+
return depEntries, nil
338+
}

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/gnolang/gno
33
go 1.19
44

55
require (
6+
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2
67
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c
78
github.com/btcsuite/btcd/btcutil v1.1.1
89
github.com/btcsuite/btcutil v1.0.2
@@ -26,6 +27,7 @@ require (
2627
github.com/mattn/go-runewidth v0.0.15
2728
github.com/pelletier/go-toml v1.9.5
2829
github.com/peterbourgon/ff/v3 v3.4.0
30+
github.com/pkg/errors v0.9.1
2931
github.com/pmezard/go-difflib v1.0.0
3032
github.com/rogpeppe/go-internal v1.11.0
3133
github.com/stretchr/testify v1.8.4
@@ -44,6 +46,7 @@ require (
4446
require (
4547
github.com/cespare/xxhash v1.1.0 // indirect
4648
github.com/cespare/xxhash/v2 v2.1.1 // indirect
49+
github.com/creack/pty v1.1.17 // indirect
4750
github.com/dgraph-io/ristretto v0.1.1 // indirect
4851
github.com/dustin/go-humanize v1.0.0 // indirect
4952
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect
@@ -61,7 +64,6 @@ require (
6164
github.com/kr/text v0.2.0 // indirect
6265
github.com/lib/pq v1.10.7 // indirect
6366
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
64-
github.com/pkg/errors v0.9.1 // indirect
6567
github.com/rivo/uniseg v0.2.0 // indirect
6668
go.opencensus.io v0.22.5 // indirect
6769
go.uber.org/atomic v1.7.0 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)