// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Gonew starts a new Go module by copying a template module. // // Usage: // // gonew srcmod[@version] [dstmod [dir]] // // Gonew makes a copy of the srcmod module, changing its module path to dstmod. // It writes that new module to a new directory named by dir. // If dir already exists, it must be an empty directory. // If dir is omitted, gonew uses ./elem where elem is the final path element of dstmod. // // This command is highly experimental and subject to change. // // # Example // // To install gonew: // // go install golang.org/x/tools/cmd/gonew@latest // // To clone the basic command-line program template golang.org/x/example/hello // as your.domain/myprog, in the directory ./myprog: // // gonew golang.org/x/example/hello your.domain/myprog // // To clone the latest copy of the rsc.io/quote module, keeping that module path, // into ./quote: // // gonew rsc.io/quote package main import ( "bytes" "encoding/json" "flag" "fmt" "go/parser" "go/token" "io/fs" "log" "os" "os/exec" "path" "path/filepath" "strconv" "strings" "golang.org/x/mod/modfile" "golang.org/x/mod/module" "golang.org/x/tools/internal/edit" ) func usage() { fmt.Fprintf(os.Stderr, "usage: gonew srcmod[@version] [dstmod [dir]]\n") fmt.Fprintf(os.Stderr, "See https://pkg.go.dev/golang.org/x/tools/cmd/gonew.\n") os.Exit(2) } func main() { log.SetPrefix("gonew: ") log.SetFlags(0) flag.Usage = usage flag.Parse() args := flag.Args() if len(args) < 1 || len(args) > 3 { usage() } srcMod := args[0] srcModVers := srcMod if !strings.Contains(srcModVers, "@") { srcModVers += "@latest" } srcMod, _, _ = strings.Cut(srcMod, "@") if err := module.CheckPath(srcMod); err != nil { log.Fatalf("invalid source module name: %v", err) } dstMod := srcMod if len(args) >= 2 { dstMod = args[1] if err := module.CheckPath(dstMod); err != nil { log.Fatalf("invalid destination module name: %v", err) } } var dir string if len(args) == 3 { dir = args[2] } else { dir = "." + string(filepath.Separator) + path.Base(dstMod) } // Dir must not exist or must be an empty directory. de, err := os.ReadDir(dir) if err == nil && len(de) > 0 { log.Fatalf("target directory %s exists and is non-empty", dir) } needMkdir := err != nil var stdout, stderr bytes.Buffer cmd := exec.Command("go", "mod", "download", "-json", srcModVers) cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { log.Fatalf("go mod download -json %s: %v\n%s%s", srcModVers, err, stderr.Bytes(), stdout.Bytes()) } var info struct { Dir string } if err := json.Unmarshal(stdout.Bytes(), &info); err != nil { log.Fatalf("go mod download -json %s: invalid JSON output: %v\n%s%s", srcMod, err, stderr.Bytes(), stdout.Bytes()) } if needMkdir { if err := os.MkdirAll(dir, 0777); err != nil { log.Fatal(err) } } // Copy from module cache into new directory, making edits as needed. filepath.WalkDir(info.Dir, func(src string, d fs.DirEntry, err error) error { if err != nil { log.Fatal(err) } rel, err := filepath.Rel(info.Dir, src) if err != nil { log.Fatal(err) } dst := filepath.Join(dir, rel) if d.IsDir() { if err := os.MkdirAll(dst, 0777); err != nil { log.Fatal(err) } return nil } data, err := os.ReadFile(src) if err != nil { log.Fatal(err) } isRoot := !strings.Contains(rel, string(filepath.Separator)) if strings.HasSuffix(rel, ".go") { data = fixGo(data, rel, srcMod, dstMod, isRoot) } if rel == "go.mod" { data = fixGoMod(data, srcMod, dstMod) } if err := os.WriteFile(dst, data, 0666); err != nil { log.Fatal(err) } return nil }) log.Printf("initialized %s in %s", dstMod, dir) } // fixGo rewrites the Go source in data to replace srcMod with dstMod. // isRoot indicates whether the file is in the root directory of the module, // in which case we also update the package name. func fixGo(data []byte, file string, srcMod, dstMod string, isRoot bool) []byte { fset := token.NewFileSet() f, err := parser.ParseFile(fset, file, data, parser.ImportsOnly) if err != nil { log.Fatalf("parsing source module:\n%s", err) } buf := edit.NewBuffer(data) at := func(p token.Pos) int { return fset.File(p).Offset(p) } srcName := path.Base(srcMod) dstName := path.Base(dstMod) if isRoot { if name := f.Name.Name; name == srcName || name == srcName+"_test" { dname := dstName + strings.TrimPrefix(name, srcName) if !token.IsIdentifier(dname) { log.Fatalf("%s: cannot rename package %s to package %s: invalid package name", file, name, dname) } buf.Replace(at(f.Name.Pos()), at(f.Name.End()), dname) } } for _, spec := range f.Imports { path, err := strconv.Unquote(spec.Path.Value) if err != nil { continue } if path == srcMod { if srcName != dstName && spec.Name == nil { // Add package rename because source code uses original name. // The renaming looks strange, but template authors are unlikely to // create a template where the root package is imported by packages // in subdirectories, and the renaming at least keeps the code working. // A more sophisticated approach would be to rename the uses of // the package identifier in the file too, but then you have to worry about // name collisions, and given how unlikely this is, it doesn't seem worth // trying to clean up the file that way. buf.Insert(at(spec.Path.Pos()), srcName+" ") } // Change import path to dstMod buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(dstMod)) } if strings.HasPrefix(path, srcMod+"/") { // Change import path to begin with dstMod buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(strings.Replace(path, srcMod, dstMod, 1))) } } return buf.Bytes() } // fixGoMod rewrites the go.mod content in data to replace srcMod with dstMod // in the module path. func fixGoMod(data []byte, srcMod, dstMod string) []byte { f, err := modfile.ParseLax("go.mod", data, nil) if err != nil { log.Fatalf("parsing source module:\n%s", err) } f.AddModuleStmt(dstMod) new, err := f.Format() if err != nil { return data } return new }