golang.org/x/tools@v0.21.0/cmd/gonew/main.go (about) 1 // Copyright 2023 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Gonew starts a new Go module by copying a template module. 6 // 7 // Usage: 8 // 9 // gonew srcmod[@version] [dstmod [dir]] 10 // 11 // Gonew makes a copy of the srcmod module, changing its module path to dstmod. 12 // It writes that new module to a new directory named by dir. 13 // If dir already exists, it must be an empty directory. 14 // If dir is omitted, gonew uses ./elem where elem is the final path element of dstmod. 15 // 16 // This command is highly experimental and subject to change. 17 // 18 // # Example 19 // 20 // To install gonew: 21 // 22 // go install golang.org/x/tools/cmd/gonew@latest 23 // 24 // To clone the basic command-line program template golang.org/x/example/hello 25 // as your.domain/myprog, in the directory ./myprog: 26 // 27 // gonew golang.org/x/example/hello your.domain/myprog 28 // 29 // To clone the latest copy of the rsc.io/quote module, keeping that module path, 30 // into ./quote: 31 // 32 // gonew rsc.io/quote 33 package main 34 35 import ( 36 "bytes" 37 "encoding/json" 38 "flag" 39 "fmt" 40 "go/parser" 41 "go/token" 42 "io/fs" 43 "log" 44 "os" 45 "os/exec" 46 "path" 47 "path/filepath" 48 "strconv" 49 "strings" 50 51 "golang.org/x/mod/modfile" 52 "golang.org/x/mod/module" 53 "golang.org/x/tools/internal/edit" 54 ) 55 56 func usage() { 57 fmt.Fprintf(os.Stderr, "usage: gonew srcmod[@version] [dstmod [dir]]\n") 58 fmt.Fprintf(os.Stderr, "See https://pkg.go.dev/golang.org/x/tools/cmd/gonew.\n") 59 os.Exit(2) 60 } 61 62 func main() { 63 log.SetPrefix("gonew: ") 64 log.SetFlags(0) 65 flag.Usage = usage 66 flag.Parse() 67 args := flag.Args() 68 69 if len(args) < 1 || len(args) > 3 { 70 usage() 71 } 72 73 srcMod := args[0] 74 srcModVers := srcMod 75 if !strings.Contains(srcModVers, "@") { 76 srcModVers += "@latest" 77 } 78 srcMod, _, _ = strings.Cut(srcMod, "@") 79 if err := module.CheckPath(srcMod); err != nil { 80 log.Fatalf("invalid source module name: %v", err) 81 } 82 83 dstMod := srcMod 84 if len(args) >= 2 { 85 dstMod = args[1] 86 if err := module.CheckPath(dstMod); err != nil { 87 log.Fatalf("invalid destination module name: %v", err) 88 } 89 } 90 91 var dir string 92 if len(args) == 3 { 93 dir = args[2] 94 } else { 95 dir = "." + string(filepath.Separator) + path.Base(dstMod) 96 } 97 98 // Dir must not exist or must be an empty directory. 99 de, err := os.ReadDir(dir) 100 if err == nil && len(de) > 0 { 101 log.Fatalf("target directory %s exists and is non-empty", dir) 102 } 103 needMkdir := err != nil 104 105 var stdout, stderr bytes.Buffer 106 cmd := exec.Command("go", "mod", "download", "-json", srcModVers) 107 cmd.Stdout = &stdout 108 cmd.Stderr = &stderr 109 if err := cmd.Run(); err != nil { 110 log.Fatalf("go mod download -json %s: %v\n%s%s", srcModVers, err, stderr.Bytes(), stdout.Bytes()) 111 } 112 113 var info struct { 114 Dir string 115 } 116 if err := json.Unmarshal(stdout.Bytes(), &info); err != nil { 117 log.Fatalf("go mod download -json %s: invalid JSON output: %v\n%s%s", srcMod, err, stderr.Bytes(), stdout.Bytes()) 118 } 119 120 if needMkdir { 121 if err := os.MkdirAll(dir, 0777); err != nil { 122 log.Fatal(err) 123 } 124 } 125 126 // Copy from module cache into new directory, making edits as needed. 127 filepath.WalkDir(info.Dir, func(src string, d fs.DirEntry, err error) error { 128 if err != nil { 129 log.Fatal(err) 130 } 131 rel, err := filepath.Rel(info.Dir, src) 132 if err != nil { 133 log.Fatal(err) 134 } 135 dst := filepath.Join(dir, rel) 136 if d.IsDir() { 137 if err := os.MkdirAll(dst, 0777); err != nil { 138 log.Fatal(err) 139 } 140 return nil 141 } 142 143 data, err := os.ReadFile(src) 144 if err != nil { 145 log.Fatal(err) 146 } 147 148 isRoot := !strings.Contains(rel, string(filepath.Separator)) 149 if strings.HasSuffix(rel, ".go") { 150 data = fixGo(data, rel, srcMod, dstMod, isRoot) 151 } 152 if rel == "go.mod" { 153 data = fixGoMod(data, srcMod, dstMod) 154 } 155 156 if err := os.WriteFile(dst, data, 0666); err != nil { 157 log.Fatal(err) 158 } 159 return nil 160 }) 161 162 log.Printf("initialized %s in %s", dstMod, dir) 163 } 164 165 // fixGo rewrites the Go source in data to replace srcMod with dstMod. 166 // isRoot indicates whether the file is in the root directory of the module, 167 // in which case we also update the package name. 168 func fixGo(data []byte, file string, srcMod, dstMod string, isRoot bool) []byte { 169 fset := token.NewFileSet() 170 f, err := parser.ParseFile(fset, file, data, parser.ImportsOnly) 171 if err != nil { 172 log.Fatalf("parsing source module:\n%s", err) 173 } 174 175 buf := edit.NewBuffer(data) 176 at := func(p token.Pos) int { 177 return fset.File(p).Offset(p) 178 } 179 180 srcName := path.Base(srcMod) 181 dstName := path.Base(dstMod) 182 if isRoot { 183 if name := f.Name.Name; name == srcName || name == srcName+"_test" { 184 dname := dstName + strings.TrimPrefix(name, srcName) 185 if !token.IsIdentifier(dname) { 186 log.Fatalf("%s: cannot rename package %s to package %s: invalid package name", file, name, dname) 187 } 188 buf.Replace(at(f.Name.Pos()), at(f.Name.End()), dname) 189 } 190 } 191 192 for _, spec := range f.Imports { 193 path, err := strconv.Unquote(spec.Path.Value) 194 if err != nil { 195 continue 196 } 197 if path == srcMod { 198 if srcName != dstName && spec.Name == nil { 199 // Add package rename because source code uses original name. 200 // The renaming looks strange, but template authors are unlikely to 201 // create a template where the root package is imported by packages 202 // in subdirectories, and the renaming at least keeps the code working. 203 // A more sophisticated approach would be to rename the uses of 204 // the package identifier in the file too, but then you have to worry about 205 // name collisions, and given how unlikely this is, it doesn't seem worth 206 // trying to clean up the file that way. 207 buf.Insert(at(spec.Path.Pos()), srcName+" ") 208 } 209 // Change import path to dstMod 210 buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(dstMod)) 211 } 212 if strings.HasPrefix(path, srcMod+"/") { 213 // Change import path to begin with dstMod 214 buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(strings.Replace(path, srcMod, dstMod, 1))) 215 } 216 } 217 return buf.Bytes() 218 } 219 220 // fixGoMod rewrites the go.mod content in data to replace srcMod with dstMod 221 // in the module path. 222 func fixGoMod(data []byte, srcMod, dstMod string) []byte { 223 f, err := modfile.ParseLax("go.mod", data, nil) 224 if err != nil { 225 log.Fatalf("parsing source module:\n%s", err) 226 } 227 f.AddModuleStmt(dstMod) 228 new, err := f.Format() 229 if err != nil { 230 return data 231 } 232 return new 233 }