Skip to content

Commit e849a36

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

File tree

4 files changed

+357
-1
lines changed

4 files changed

+357
-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/gnodeploy/main.go

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

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

0 commit comments

Comments
 (0)