diff --git a/gno.land/cmd/bumpkg/main.go b/gno.land/cmd/bumpkg/main.go new file mode 100644 index 00000000000..578cac8bd8b --- /dev/null +++ b/gno.land/cmd/bumpkg/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + + "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") + ) + + 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 + + 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{} + + roots := []string{targetPkgPath} + seen := map[string]struct{}{} + for len(roots) > 0 { + root := roots[0] + roots = roots[1:] + if _, ok := seen[root]; ok { + continue + } + seen[root] = struct{}{} + roots = append(roots, requiredBy[root]...) + + vR := regexp.MustCompile(`_v(\d+)$`) + submatches := vR.FindAllStringSubmatch(root, -1) + version := uint64(1) + unversioned := true + if len(submatches) >= 1 && len(submatches[0]) >= 2 { + vMatch := submatches[0][1] + v, err := strconv.ParseUint(vMatch, 10, 64) + if err != nil { + panic("failed to parse version") + } + version = v + unversioned = false + } + nextVersion := version + 1 + basePkgPath := root + if !unversioned { + basePkgPath = strings.TrimSuffix(basePkgPath, "_v"+strconv.FormatUint(version, 10)) + } + newPkgPath := fmt.Sprintf("%s_v%d", basePkgPath, nextVersion) + upgrades[root] = newPkgPath + } + + 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)) + } + } +} + +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 +}