1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 package gentools
26
27 import (
28 "bytes"
29 "flag"
30 "fmt"
31 "go/format"
32 "go/scanner"
33 "go/token"
34 "internal/diff"
35 "io"
36 "io/fs"
37 "os"
38 "path/filepath"
39 "strings"
40 "sync"
41 )
42
43
44 type Options struct {
45 GOROOT string
46 outDir string
47 Write bool
48 Diff bool
49 Txtar bool
50
51 Output io.Writer
52 ErrOutput io.Writer
53 }
54
55 var globalOptions *Options
56
57
58
59
60
61
62
63 func RegisterFlags(fs *flag.FlagSet) *Options {
64 o := new(Options)
65 if fs == nil {
66 fs = flag.CommandLine
67 globalOptions = o
68 }
69 defaultGOROOT := findGOROOT()
70 fs.StringVar(&o.GOROOT, "goroot", defaultGOROOT, "source Go dev tree")
71 fs.StringVar(&o.outDir, "outdir", "", "output directory (default: set to -goroot)")
72 fs.BoolVar(&o.Write, "w", false, "write generated files directly to disk under -outdir")
73 fs.BoolVar(&o.Diff, "diff", false, "compare generated files against disk and print unified diffs")
74 fs.BoolVar(&o.Txtar, "txtar", false, "output generated files as a txtar archive to stdout (default mode)")
75 return o
76 }
77
78
79
80
81 func (o *Options) InputPath(relPath string) string {
82 if o.outDir != o.GOROOT {
83 path := o.OutputPath(relPath)
84 if _, err := os.Stat(path); err == nil {
85 return path
86 }
87 }
88 return filepath.Join(o.GOROOT, "src", relPath)
89 }
90
91
92 func (o *Options) ReadFile(relPath string) ([]byte, error) {
93 return os.ReadFile(o.InputPath(relPath))
94 }
95
96
97 func (o *Options) OutputPath(relPath string) string {
98 outDir := o.outDir
99 if outDir == "" {
100 outDir = o.GOROOT
101 }
102 return filepath.Join(outDir, "src", relPath)
103 }
104
105
106 func (o *Options) WritingToInput() bool {
107 return o.Write && (o.outDir == "" || o.outDir == o.GOROOT)
108 }
109
110 type fileInfo struct {
111 relPath string
112 isGo bool
113 buf bytes.Buffer
114 }
115
116
117
118
119 type Files struct {
120
121
122 Options *Options
123
124 files []*fileInfo
125
126
127
128 tmpDirOnce sync.Once
129 tmpDir string
130 }
131
132 func (f *Files) getOptions() Options {
133 var opts Options
134 if f != nil && f.Options != nil {
135 opts = *f.Options
136 } else if globalOptions != nil {
137 opts = *globalOptions
138 }
139
140 if opts.GOROOT == "" {
141 opts.GOROOT = findGOROOT()
142 }
143 if opts.Output == nil {
144 opts.Output = os.Stdout
145 }
146 if opts.ErrOutput == nil {
147 opts.ErrOutput = os.Stderr
148 }
149 if !(opts.Write || opts.Diff || opts.Txtar) {
150 opts.Txtar = true
151 }
152
153 return opts
154 }
155
156
157
158
159 func (f *Files) NewGoFile(relPath string) *bytes.Buffer {
160 info := &fileInfo{
161 relPath: relPath,
162 isGo: true,
163 }
164 f.files = append(f.files, info)
165 return &info.buf
166 }
167
168
169
170
171 func (f *Files) NewRawFile(relPath string) *bytes.Buffer {
172 info := &fileInfo{
173 relPath: relPath,
174 isGo: false,
175 }
176 f.files = append(f.files, info)
177 return &info.buf
178 }
179
180
181
182
183
184
185
186
187
188 func (f *Files) ExecFlags() []string {
189 f.tmpDirOnce.Do(func() {
190 tmpDir, err := os.MkdirTemp("", "")
191 if err != nil {
192 panic("failed to create tmpdir: " + err.Error())
193 }
194 f.tmpDir = tmpDir
195 })
196 return []string{"-goroot", f.getOptions().GOROOT, "-w", "-outdir", f.tmpDir}
197 }
198
199
200
201
202
203
204
205 func (f *Files) Flush() error {
206 opts := f.getOptions()
207
208 if (opts.Write || opts.Diff) && opts.GOROOT == "" {
209 return fmt.Errorf("GOROOT not found; pass -goroot flag")
210 }
211
212 type preparedFile struct {
213 relPath string
214 content []byte
215 }
216
217 prepared := make([]preparedFile, len(f.files))
218
219
220 if f.tmpDir != "" {
221 root := filepath.Join(f.tmpDir, "src")
222 err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
223 if d.IsDir() {
224 return nil
225 }
226 relPath, ok := strings.CutPrefix(path, root)
227 if !ok {
228 return fmt.Errorf("expected path %q to start with root %q", path, root)
229 }
230 content, err := os.ReadFile(path)
231 if err != nil {
232 return err
233 }
234 prepared = append(prepared, preparedFile{relPath, content})
235 return nil
236 })
237 if err != nil {
238 return err
239 }
240 os.RemoveAll(f.tmpDir)
241 }
242
243 for i, fi := range f.files {
244 raw := fi.buf.Bytes()
245 var content []byte
246 if fi.isGo {
247 formatted, err := format.Source(raw)
248 if err != nil {
249 printFormattingError(opts.ErrOutput, fi.relPath, raw, err)
250 return fmt.Errorf("error formatting %s: %w", fi.relPath, err)
251 }
252 content = formatted
253 } else {
254 content = raw
255 }
256
257 prepared[i] = preparedFile{
258 relPath: fi.relPath,
259 content: content,
260 }
261 }
262 f.files = nil
263
264 if opts.Diff {
265 hasDiffs := false
266 for _, pf := range prepared {
267 onDisk, err := opts.ReadFile(pf.relPath)
268 if err != nil && !os.IsNotExist(err) {
269 return fmt.Errorf("reading %s for diff: %w", pf.relPath, err)
270 }
271 srcPath := filepath.Join("src", pf.relPath)
272 d := diff.Diff(srcPath, onDisk, srcPath, pf.content)
273 if len(d) > 0 {
274 hasDiffs = true
275 opts.Output.Write(d)
276 }
277 }
278 if hasDiffs {
279 return fmt.Errorf("generated files differ from disk")
280 }
281 }
282
283 if opts.Txtar {
284 for i, pf := range prepared {
285 if i > 0 {
286 fmt.Fprintln(opts.Output)
287 }
288 srcPath := filepath.Join("src", pf.relPath)
289 fmt.Fprintf(opts.Output, "-- %s --\n", srcPath)
290 opts.Output.Write(pf.content)
291
292 if len(pf.content) > 0 && !bytes.HasSuffix(pf.content, []byte("\n")) {
293 fmt.Fprintln(opts.Output)
294 }
295 }
296 }
297
298 if opts.Write {
299 for _, pf := range prepared {
300 path := opts.OutputPath(pf.relPath)
301 dir := filepath.Dir(path)
302 if err := os.MkdirAll(dir, 0755); err != nil {
303 return fmt.Errorf("creating directory %s: %w", dir, err)
304 }
305 if err := os.WriteFile(path, pf.content, 0644); err != nil {
306 return fmt.Errorf("writing %s: %w", path, err)
307 }
308 }
309 }
310
311 return nil
312 }
313
314
315
316
317
318 func (f *Files) FlushOrExit() {
319 if r := recover(); r != nil {
320 panic(r)
321 }
322 if err := f.Flush(); err != nil {
323 fmt.Fprintf(os.Stderr, "%v\n", err)
324 os.Exit(1)
325 }
326 }
327
328
329
330 func printFormattingError(out io.Writer, relPath string, raw []byte, err error) {
331 var pos token.Position
332 if el, ok := err.(scanner.ErrorList); ok && len(el) > 0 {
333 el.Sort()
334 pos = el[0].Pos
335 } else if e, ok := err.(*scanner.Error); ok {
336 pos = e.Pos
337 } else if e, ok := err.(scanner.Error); ok {
338 pos = e.Pos
339 }
340
341 lines := strings.Split(string(raw), "\n")
342 if len(lines) > 0 && lines[len(lines)-1] == "" {
343 lines = lines[:len(lines)-1]
344 }
345 if pos.Line <= 0 || pos.Line > len(lines) {
346 fmt.Fprintf(out, "error formatting %s: %v\n", relPath, err)
347 fmt.Fprintf(out, "%s\n", raw)
348 return
349 }
350
351 startLine := max(pos.Line-5, 1)
352 endLine := min(pos.Line+5, len(lines))
353
354 for i := startLine; i <= endLine; i++ {
355 line := lines[i-1]
356 fmt.Fprintf(out, "%s\n", line)
357 if i == pos.Line {
358 var indent strings.Builder
359 for _, ch := range line {
360 pos.Column--
361 if pos.Column == 0 {
362 break
363 }
364 if ch == '\t' {
365 indent.WriteByte('\t')
366 } else {
367 indent.WriteByte(' ')
368 }
369 }
370 fmt.Fprintf(out, "%s^\n", indent.String())
371 fmt.Fprintf(out, "%s\n", strings.TrimRight(err.Error(), "\n"))
372 }
373 }
374 }
375
376 func findGOROOT() string {
377 cwd, err := os.Getwd()
378 if err != nil {
379 return ""
380 }
381 dir := cwd
382 for {
383 parent := filepath.Dir(dir)
384 if parent == dir {
385 return ""
386 }
387 if filepath.Base(dir) == "src" {
388 if b, err := os.ReadFile(filepath.Join(dir, "go.mod")); err == nil {
389 for line := range strings.SplitSeq(string(b), "\n") {
390 fields := strings.Fields(line)
391 if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
392 return parent
393 }
394 }
395 }
396 }
397 dir = parent
398 }
399 }
400
401 func resolvePath(goroot, relPath string) string {
402 clean := cleanRelPath(relPath)
403 if goroot == "" {
404 return clean
405 }
406 return filepath.Join(goroot, clean)
407 }
408
409 func cleanRelPath(p string) string {
410 p = strings.ReplaceAll(p, "\\", "/")
411 return filepath.Join("src", p)
412 }
413
View as plain text