Skip to content

Commit c16d0be

Browse files
rscgopherbot
authored andcommitted
cmd/gonew: add new tool for starting a module by copying one
This is an experimental command that perhaps would become "go new" once we have more experience with it. We want to enable people to experiment with it and write their own templates and see how it works, and for that we need to put it in a place where it's reasonable to ask users to fetch it from. That place is golang.org/x/tools/cmd/gonew. There is an earlier copy in rsc.io/tmp/gonew, but that isn't the right place for end users to be fetching something to try. Once the tool is checked in I intend to start a GitHub discussion asking for feedback and suggestions about what is missing. I hope we will be able to identify core functionality that handles a large fraction of use cases. I've been using the earlier version myself for a while, and I've found it very convenient even in other contexts, like I want the code for a given module and don't want to go look up its Git repo and so on: go new rsc.io/[email protected] cd quote Change-Id: Ifc27cbd5d87ded89bc707b087b3f08fa70b1ef07 Reviewed-on: https://go-review.googlesource.com/c/tools/+/513737 gopls-CI: kokoro <[email protected]> Run-TryBot: Russ Cox <[email protected]> Auto-Submit: Russ Cox <[email protected]> Reviewed-by: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 304e203 commit c16d0be

File tree

3 files changed

+475
-0
lines changed

3 files changed

+475
-0
lines changed

cmd/gonew/main.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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+
}

0 commit comments

Comments
 (0)