| // 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, 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 add a module | |
| // statement for dstMod. | |
| func fixGoMod(data []byte, 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 | |
| } |