1
2
3
4
5
6
7
8 package modindex
9
10 import (
11 "bytes"
12 "cmd/go/internal/fsys"
13 "errors"
14 "fmt"
15 "go/ast"
16 "go/build"
17 "go/build/constraint"
18 "go/token"
19 "internal/syslist"
20 "io"
21 "io/fs"
22 "path/filepath"
23 "slices"
24 "sort"
25 "strings"
26 "unicode"
27 "unicode/utf8"
28 )
29
30
31 type Context struct {
32 GOARCH string
33 GOOS string
34 GOROOT string
35 GOPATH string
36
37
38
39
40
41
42
43 Dir string
44
45 CgoEnabled bool
46 UseAllFiles bool
47 Compiler string
48
49
50
51
52
53
54
55
56
57
58
59 BuildTags []string
60 ToolTags []string
61 ReleaseTags []string
62
63
64
65
66
67
68
69 InstallSuffix string
70
71
72
73
74
75
76
77
78
79 JoinPath func(elem ...string) string
80
81
82
83 SplitPathList func(list string) []string
84
85
86
87 IsAbsPath func(path string) bool
88
89
90
91 IsDir func(path string) bool
92
93
94
95
96
97
98
99
100 HasSubdir func(root, dir string) (rel string, ok bool)
101
102
103
104
105 ReadDir func(dir string) ([]fs.FileInfo, error)
106
107
108
109 OpenFile func(path string) (io.ReadCloser, error)
110 }
111
112
113 func (ctxt *Context) joinPath(elem ...string) string {
114 if f := ctxt.JoinPath; f != nil {
115 return f(elem...)
116 }
117 return filepath.Join(elem...)
118 }
119
120
121 func isDir(path string) bool {
122 fi, err := fsys.Stat(path)
123 return err == nil && fi.IsDir()
124 }
125
126 var defaultToolTags, defaultReleaseTags []string
127
128
129
130
131 type NoGoError struct {
132 Dir string
133 }
134
135 func (e *NoGoError) Error() string {
136 return "no buildable Go source files in " + e.Dir
137 }
138
139
140
141 type MultiplePackageError struct {
142 Dir string
143 Packages []string
144 Files []string
145 }
146
147 func (e *MultiplePackageError) Error() string {
148
149 return fmt.Sprintf("found packages %s (%s) and %s (%s) in %s", e.Packages[0], e.Files[0], e.Packages[1], e.Files[1], e.Dir)
150 }
151
152 func nameExt(name string) string {
153 i := strings.LastIndex(name, ".")
154 if i < 0 {
155 return ""
156 }
157 return name[i:]
158 }
159
160 func fileListForExt(p *build.Package, ext string) *[]string {
161 switch ext {
162 case ".c":
163 return &p.CFiles
164 case ".cc", ".cpp", ".cxx":
165 return &p.CXXFiles
166 case ".m":
167 return &p.MFiles
168 case ".h", ".hh", ".hpp", ".hxx":
169 return &p.HFiles
170 case ".f", ".F", ".for", ".f90":
171 return &p.FFiles
172 case ".s", ".S", ".sx":
173 return &p.SFiles
174 case ".swig":
175 return &p.SwigFiles
176 case ".swigcxx":
177 return &p.SwigCXXFiles
178 case ".syso":
179 return &p.SysoFiles
180 }
181 return nil
182 }
183
184 var (
185 slashSlash = []byte("//")
186 slashStar = []byte("/*")
187 starSlash = []byte("*/")
188 )
189
190 var dummyPkg build.Package
191
192
193 type fileInfo struct {
194 name string
195 header []byte
196 fset *token.FileSet
197 parsed *ast.File
198 parseErr error
199 imports []fileImport
200 embeds []fileEmbed
201 directives []build.Directive
202
203
204 binaryOnly bool
205 goBuildConstraint string
206 plusBuildConstraints []string
207 }
208
209 type fileImport struct {
210 path string
211 pos token.Pos
212 doc *ast.CommentGroup
213 }
214
215 type fileEmbed struct {
216 pattern string
217 pos token.Position
218 }
219
220 var errNonSource = errors.New("non source file")
221
222
223
224
225
226
227
228
229
230
231
232 func getFileInfo(dir, name string, fset *token.FileSet) (*fileInfo, error) {
233 if strings.HasPrefix(name, "_") ||
234 strings.HasPrefix(name, ".") {
235 return nil, nil
236 }
237
238 i := strings.LastIndex(name, ".")
239 if i < 0 {
240 i = len(name)
241 }
242 ext := name[i:]
243
244 if ext != ".go" && fileListForExt(&dummyPkg, ext) == nil {
245
246 return nil, errNonSource
247 }
248
249 info := &fileInfo{name: filepath.Join(dir, name), fset: fset}
250 if ext == ".syso" {
251
252 return info, nil
253 }
254
255 f, err := fsys.Open(info.name)
256 if err != nil {
257 return nil, err
258 }
259
260
261
262 var ignoreBinaryOnly bool
263 if strings.HasSuffix(name, ".go") {
264 err = readGoInfo(f, info)
265 if strings.HasSuffix(name, "_test.go") {
266 ignoreBinaryOnly = true
267 }
268 } else {
269 info.header, err = readComments(f)
270 }
271 f.Close()
272 if err != nil {
273 return nil, fmt.Errorf("read %s: %v", info.name, err)
274 }
275
276
277 info.goBuildConstraint, info.plusBuildConstraints, info.binaryOnly, err = getConstraints(info.header)
278 if err != nil {
279 return nil, fmt.Errorf("%s: %v", name, err)
280 }
281
282 if ignoreBinaryOnly && info.binaryOnly {
283 info.binaryOnly = false
284 }
285
286 return info, nil
287 }
288
289 func cleanDecls(m map[string][]token.Position) ([]string, map[string][]token.Position) {
290 all := make([]string, 0, len(m))
291 for path := range m {
292 all = append(all, path)
293 }
294 sort.Strings(all)
295 return all, m
296 }
297
298 var (
299 bSlashSlash = []byte(slashSlash)
300 bStarSlash = []byte(starSlash)
301 bSlashStar = []byte(slashStar)
302 bPlusBuild = []byte("+build")
303
304 goBuildComment = []byte("//go:build")
305
306 errMultipleGoBuild = errors.New("multiple //go:build comments")
307 )
308
309 func isGoBuildComment(line []byte) bool {
310 if !bytes.HasPrefix(line, goBuildComment) {
311 return false
312 }
313 line = bytes.TrimSpace(line)
314 rest := line[len(goBuildComment):]
315 return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest)
316 }
317
318
319
320
321 var binaryOnlyComment = []byte("//go:binary-only-package")
322
323 func getConstraints(content []byte) (goBuild string, plusBuild []string, binaryOnly bool, err error) {
324
325
326
327 content, goBuildBytes, sawBinaryOnly, err := parseFileHeader(content)
328 if err != nil {
329 return "", nil, false, err
330 }
331
332
333
334 if goBuildBytes == nil {
335 p := content
336 for len(p) > 0 {
337 line := p
338 if i := bytes.IndexByte(line, '\n'); i >= 0 {
339 line, p = line[:i], p[i+1:]
340 } else {
341 p = p[len(p):]
342 }
343 line = bytes.TrimSpace(line)
344 if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) {
345 continue
346 }
347 text := string(line)
348 if !constraint.IsPlusBuild(text) {
349 continue
350 }
351 plusBuild = append(plusBuild, text)
352 }
353 }
354
355 return string(goBuildBytes), plusBuild, sawBinaryOnly, nil
356 }
357
358 func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) {
359 end := 0
360 p := content
361 ended := false
362 inSlashStar := false
363
364 Lines:
365 for len(p) > 0 {
366 line := p
367 if i := bytes.IndexByte(line, '\n'); i >= 0 {
368 line, p = line[:i], p[i+1:]
369 } else {
370 p = p[len(p):]
371 }
372 line = bytes.TrimSpace(line)
373 if len(line) == 0 && !ended {
374
375
376
377
378
379
380
381
382 end = len(content) - len(p)
383 continue Lines
384 }
385 if !bytes.HasPrefix(line, slashSlash) {
386 ended = true
387 }
388
389 if !inSlashStar && isGoBuildComment(line) {
390 if goBuild != nil {
391 return nil, nil, false, errMultipleGoBuild
392 }
393 goBuild = line
394 }
395 if !inSlashStar && bytes.Equal(line, binaryOnlyComment) {
396 sawBinaryOnly = true
397 }
398
399 Comments:
400 for len(line) > 0 {
401 if inSlashStar {
402 if i := bytes.Index(line, starSlash); i >= 0 {
403 inSlashStar = false
404 line = bytes.TrimSpace(line[i+len(starSlash):])
405 continue Comments
406 }
407 continue Lines
408 }
409 if bytes.HasPrefix(line, bSlashSlash) {
410 continue Lines
411 }
412 if bytes.HasPrefix(line, bSlashStar) {
413 inSlashStar = true
414 line = bytes.TrimSpace(line[len(bSlashStar):])
415 continue Comments
416 }
417
418 break Lines
419 }
420 }
421
422 return content[:end], goBuild, sawBinaryOnly, nil
423 }
424
425
426
427
428 func (ctxt *Context) saveCgo(filename string, di *build.Package, text string) error {
429 for _, line := range strings.Split(text, "\n") {
430 orig := line
431
432
433
434
435 line = strings.TrimSpace(line)
436 if len(line) < 5 || line[:4] != "#cgo" || (line[4] != ' ' && line[4] != '\t') {
437 continue
438 }
439
440
441 if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") {
442 continue
443 }
444
445
446 line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
447 if !ok {
448 return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
449 }
450
451
452 f := strings.Fields(line)
453 if len(f) < 1 {
454 return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
455 }
456
457 cond, verb := f[:len(f)-1], f[len(f)-1]
458 if len(cond) > 0 {
459 ok := false
460 for _, c := range cond {
461 if ctxt.matchAuto(c, nil) {
462 ok = true
463 break
464 }
465 }
466 if !ok {
467 continue
468 }
469 }
470
471 args, err := splitQuoted(argstr)
472 if err != nil {
473 return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
474 }
475 for i, arg := range args {
476 if arg, ok = expandSrcDir(arg, di.Dir); !ok {
477 return fmt.Errorf("%s: malformed #cgo argument: %s", filename, arg)
478 }
479 args[i] = arg
480 }
481
482 switch verb {
483 case "CFLAGS", "CPPFLAGS", "CXXFLAGS", "FFLAGS", "LDFLAGS":
484
485 ctxt.makePathsAbsolute(args, di.Dir)
486 }
487
488 switch verb {
489 case "CFLAGS":
490 di.CgoCFLAGS = append(di.CgoCFLAGS, args...)
491 case "CPPFLAGS":
492 di.CgoCPPFLAGS = append(di.CgoCPPFLAGS, args...)
493 case "CXXFLAGS":
494 di.CgoCXXFLAGS = append(di.CgoCXXFLAGS, args...)
495 case "FFLAGS":
496 di.CgoFFLAGS = append(di.CgoFFLAGS, args...)
497 case "LDFLAGS":
498 di.CgoLDFLAGS = append(di.CgoLDFLAGS, args...)
499 case "pkg-config":
500 di.CgoPkgConfig = append(di.CgoPkgConfig, args...)
501 default:
502 return fmt.Errorf("%s: invalid #cgo verb: %s", filename, orig)
503 }
504 }
505 return nil
506 }
507
508
509
510 func expandSrcDir(str string, srcdir string) (string, bool) {
511
512
513
514 srcdir = filepath.ToSlash(srcdir)
515
516 chunks := strings.Split(str, "${SRCDIR}")
517 if len(chunks) < 2 {
518 return str, safeCgoName(str)
519 }
520 ok := true
521 for _, chunk := range chunks {
522 ok = ok && (chunk == "" || safeCgoName(chunk))
523 }
524 ok = ok && (srcdir == "" || safeCgoName(srcdir))
525 res := strings.Join(chunks, srcdir)
526 return res, ok && res != ""
527 }
528
529
530
531
532
533
534
535
536
537
538
539
540 func (ctxt *Context) makePathsAbsolute(args []string, srcDir string) {
541 nextPath := false
542 for i, arg := range args {
543 if nextPath {
544 if !filepath.IsAbs(arg) {
545 args[i] = filepath.Join(srcDir, arg)
546 }
547 nextPath = false
548 } else if strings.HasPrefix(arg, "-I") || strings.HasPrefix(arg, "-L") {
549 if len(arg) == 2 {
550 nextPath = true
551 } else {
552 if !filepath.IsAbs(arg[2:]) {
553 args[i] = arg[:2] + filepath.Join(srcDir, arg[2:])
554 }
555 }
556 }
557 }
558 }
559
560
561
562
563
564
565
566
567 const safeString = "+-.,/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz:$@%! ~^"
568
569 func safeCgoName(s string) bool {
570 if s == "" {
571 return false
572 }
573 for i := 0; i < len(s); i++ {
574 if c := s[i]; c < utf8.RuneSelf && strings.IndexByte(safeString, c) < 0 {
575 return false
576 }
577 }
578 return true
579 }
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596 func splitQuoted(s string) (r []string, err error) {
597 var args []string
598 arg := make([]rune, len(s))
599 escaped := false
600 quoted := false
601 quote := '\x00'
602 i := 0
603 for _, rune := range s {
604 switch {
605 case escaped:
606 escaped = false
607 case rune == '\\':
608 escaped = true
609 continue
610 case quote != '\x00':
611 if rune == quote {
612 quote = '\x00'
613 continue
614 }
615 case rune == '"' || rune == '\'':
616 quoted = true
617 quote = rune
618 continue
619 case unicode.IsSpace(rune):
620 if quoted || i > 0 {
621 quoted = false
622 args = append(args, string(arg[:i]))
623 i = 0
624 }
625 continue
626 }
627 arg[i] = rune
628 i++
629 }
630 if quoted || i > 0 {
631 args = append(args, string(arg[:i]))
632 }
633 if quote != 0 {
634 err = errors.New("unclosed quote")
635 } else if escaped {
636 err = errors.New("unfinished escaping")
637 }
638 return args, err
639 }
640
641
642
643
644
645
646 func (ctxt *Context) matchAuto(text string, allTags map[string]bool) bool {
647 if strings.ContainsAny(text, "&|()") {
648 text = "//go:build " + text
649 } else {
650 text = "// +build " + text
651 }
652 x, err := constraint.Parse(text)
653 if err != nil {
654 return false
655 }
656 return ctxt.eval(x, allTags)
657 }
658
659 func (ctxt *Context) eval(x constraint.Expr, allTags map[string]bool) bool {
660 return x.Eval(func(tag string) bool { return ctxt.matchTag(tag, allTags) })
661 }
662
663
664
665
666
667
668
669
670
671
672
673
674
675 func (ctxt *Context) matchTag(name string, allTags map[string]bool) bool {
676 if allTags != nil {
677 allTags[name] = true
678 }
679
680
681 if ctxt.CgoEnabled && name == "cgo" {
682 return true
683 }
684 if name == ctxt.GOOS || name == ctxt.GOARCH || name == ctxt.Compiler {
685 return true
686 }
687 if ctxt.GOOS == "android" && name == "linux" {
688 return true
689 }
690 if ctxt.GOOS == "illumos" && name == "solaris" {
691 return true
692 }
693 if ctxt.GOOS == "ios" && name == "darwin" {
694 return true
695 }
696 if name == "unix" && syslist.UnixOS[ctxt.GOOS] {
697 return true
698 }
699 if name == "boringcrypto" {
700 name = "goexperiment.boringcrypto"
701 }
702
703
704 return slices.Contains(ctxt.BuildTags, name) || slices.Contains(ctxt.ToolTags, name) ||
705 slices.Contains(ctxt.ReleaseTags, name)
706 }
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723 func (ctxt *Context) goodOSArchFile(name string, allTags map[string]bool) bool {
724 name, _, _ = strings.Cut(name, ".")
725
726
727
728
729
730
731
732
733 i := strings.Index(name, "_")
734 if i < 0 {
735 return true
736 }
737 name = name[i:]
738
739 l := strings.Split(name, "_")
740 if n := len(l); n > 0 && l[n-1] == "test" {
741 l = l[:n-1]
742 }
743 n := len(l)
744 if n >= 2 && syslist.KnownOS[l[n-2]] && syslist.KnownArch[l[n-1]] {
745 if allTags != nil {
746
747 allTags[l[n-2]] = true
748 }
749 return ctxt.matchTag(l[n-1], allTags) && ctxt.matchTag(l[n-2], allTags)
750 }
751 if n >= 1 && (syslist.KnownOS[l[n-1]] || syslist.KnownArch[l[n-1]]) {
752 return ctxt.matchTag(l[n-1], allTags)
753 }
754 return true
755 }
756
View as plain text