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 bSlashSlash = []byte("//")
186 bSlashStar = []byte("/*")
187 bStarSlash = []byte("*/")
188 bPlusBuild = []byte("+build")
189 )
190
191 var dummyPkg build.Package
192
193
194 type fileInfo struct {
195 name string
196 header []byte
197 fset *token.FileSet
198 parsed *ast.File
199 parseErr error
200 imports []fileImport
201 embeds []fileEmbed
202 directives []build.Directive
203
204
205 binaryOnly bool
206 goBuildConstraint string
207 plusBuildConstraints []string
208 }
209
210 type fileImport struct {
211 path string
212 pos token.Pos
213 doc *ast.CommentGroup
214 }
215
216 type fileEmbed struct {
217 pattern string
218 pos token.Position
219 }
220
221 var errNonSource = errors.New("non source file")
222
223
224
225
226
227
228
229
230
231
232
233 func getFileInfo(dir, name string, fset *token.FileSet) (*fileInfo, error) {
234 if strings.HasPrefix(name, "_") ||
235 strings.HasPrefix(name, ".") {
236 return nil, nil
237 }
238
239 i := strings.LastIndex(name, ".")
240 if i < 0 {
241 i = len(name)
242 }
243 ext := name[i:]
244
245 if ext != ".go" && fileListForExt(&dummyPkg, ext) == nil {
246
247 return nil, errNonSource
248 }
249
250 info := &fileInfo{name: filepath.Join(dir, name), fset: fset}
251 if ext == ".syso" {
252
253 return info, nil
254 }
255
256 f, err := fsys.Open(info.name)
257 if err != nil {
258 return nil, err
259 }
260
261
262
263 var ignoreBinaryOnly bool
264 if strings.HasSuffix(name, ".go") {
265 err = readGoInfo(f, info)
266 if strings.HasSuffix(name, "_test.go") {
267 ignoreBinaryOnly = true
268 }
269 } else {
270 info.header, err = readComments(f)
271 }
272 f.Close()
273 if err != nil {
274 return nil, fmt.Errorf("read %s: %v", info.name, err)
275 }
276
277
278 info.goBuildConstraint, info.plusBuildConstraints, info.binaryOnly, err = getConstraints(info.header)
279 if err != nil {
280 return nil, fmt.Errorf("%s: %v", name, err)
281 }
282
283 if ignoreBinaryOnly && info.binaryOnly {
284 info.binaryOnly = false
285 }
286
287 return info, nil
288 }
289
290 func cleanDecls(m map[string][]token.Position) ([]string, map[string][]token.Position) {
291 all := make([]string, 0, len(m))
292 for path := range m {
293 all = append(all, path)
294 }
295 sort.Strings(all)
296 return all, m
297 }
298
299 var (
300 goBuildComment = []byte("//go:build")
301
302 errMultipleGoBuild = errors.New("multiple //go:build comments")
303 )
304
305 func isGoBuildComment(line []byte) bool {
306 if !bytes.HasPrefix(line, goBuildComment) {
307 return false
308 }
309 line = bytes.TrimSpace(line)
310 rest := line[len(goBuildComment):]
311 return len(rest) == 0 || len(bytes.TrimSpace(rest)) < len(rest)
312 }
313
314
315
316
317 var binaryOnlyComment = []byte("//go:binary-only-package")
318
319 func getConstraints(content []byte) (goBuild string, plusBuild []string, binaryOnly bool, err error) {
320
321
322
323 content, goBuildBytes, sawBinaryOnly, err := parseFileHeader(content)
324 if err != nil {
325 return "", nil, false, err
326 }
327
328
329
330 if goBuildBytes == nil {
331 p := content
332 for len(p) > 0 {
333 line := p
334 if i := bytes.IndexByte(line, '\n'); i >= 0 {
335 line, p = line[:i], p[i+1:]
336 } else {
337 p = p[len(p):]
338 }
339 line = bytes.TrimSpace(line)
340 if !bytes.HasPrefix(line, bSlashSlash) || !bytes.Contains(line, bPlusBuild) {
341 continue
342 }
343 text := string(line)
344 if !constraint.IsPlusBuild(text) {
345 continue
346 }
347 plusBuild = append(plusBuild, text)
348 }
349 }
350
351 return string(goBuildBytes), plusBuild, sawBinaryOnly, nil
352 }
353
354 func parseFileHeader(content []byte) (trimmed, goBuild []byte, sawBinaryOnly bool, err error) {
355 end := 0
356 p := content
357 ended := false
358 inSlashStar := false
359
360 Lines:
361 for len(p) > 0 {
362 line := p
363 if i := bytes.IndexByte(line, '\n'); i >= 0 {
364 line, p = line[:i], p[i+1:]
365 } else {
366 p = p[len(p):]
367 }
368 line = bytes.TrimSpace(line)
369 if len(line) == 0 && !ended {
370
371
372
373
374
375
376
377
378 end = len(content) - len(p)
379 continue Lines
380 }
381 if !bytes.HasPrefix(line, bSlashSlash) {
382 ended = true
383 }
384
385 if !inSlashStar && isGoBuildComment(line) {
386 if goBuild != nil {
387 return nil, nil, false, errMultipleGoBuild
388 }
389 goBuild = line
390 }
391 if !inSlashStar && bytes.Equal(line, binaryOnlyComment) {
392 sawBinaryOnly = true
393 }
394
395 Comments:
396 for len(line) > 0 {
397 if inSlashStar {
398 if i := bytes.Index(line, bStarSlash); i >= 0 {
399 inSlashStar = false
400 line = bytes.TrimSpace(line[i+len(bStarSlash):])
401 continue Comments
402 }
403 continue Lines
404 }
405 if bytes.HasPrefix(line, bSlashSlash) {
406 continue Lines
407 }
408 if bytes.HasPrefix(line, bSlashStar) {
409 inSlashStar = true
410 line = bytes.TrimSpace(line[len(bSlashStar):])
411 continue Comments
412 }
413
414 break Lines
415 }
416 }
417
418 return content[:end], goBuild, sawBinaryOnly, nil
419 }
420
421
422
423
424 func (ctxt *Context) saveCgo(filename string, di *build.Package, text string) error {
425 for line := range strings.SplitSeq(text, "\n") {
426 orig := line
427
428
429
430
431 line = strings.TrimSpace(line)
432 if len(line) < 5 || line[:4] != "#cgo" || (line[4] != ' ' && line[4] != '\t') {
433 continue
434 }
435
436
437 if fields := strings.Fields(line); len(fields) == 3 && (fields[1] == "nocallback" || fields[1] == "noescape") {
438 continue
439 }
440
441
442 line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":")
443 if !ok {
444 return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
445 }
446
447
448 f := strings.Fields(line)
449 if len(f) < 1 {
450 return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
451 }
452
453 cond, verb := f[:len(f)-1], f[len(f)-1]
454 if len(cond) > 0 {
455 ok := false
456 for _, c := range cond {
457 if ctxt.matchAuto(c, nil) {
458 ok = true
459 break
460 }
461 }
462 if !ok {
463 continue
464 }
465 }
466
467 args, err := splitQuoted(argstr)
468 if err != nil {
469 return fmt.Errorf("%s: invalid #cgo line: %s", filename, orig)
470 }
471 for i, arg := range args {
472 if arg, ok = expandSrcDir(arg, di.Dir); !ok {
473 return fmt.Errorf("%s: malformed #cgo argument: %s", filename, arg)
474 }
475 args[i] = arg
476 }
477
478 switch verb {
479 case "CFLAGS", "CPPFLAGS", "CXXFLAGS", "FFLAGS", "LDFLAGS":
480
481 ctxt.makePathsAbsolute(args, di.Dir)
482 }
483
484 switch verb {
485 case "CFLAGS":
486 di.CgoCFLAGS = append(di.CgoCFLAGS, args...)
487 case "CPPFLAGS":
488 di.CgoCPPFLAGS = append(di.CgoCPPFLAGS, args...)
489 case "CXXFLAGS":
490 di.CgoCXXFLAGS = append(di.CgoCXXFLAGS, args...)
491 case "FFLAGS":
492 di.CgoFFLAGS = append(di.CgoFFLAGS, args...)
493 case "LDFLAGS":
494 di.CgoLDFLAGS = append(di.CgoLDFLAGS, args...)
495 case "pkg-config":
496 di.CgoPkgConfig = append(di.CgoPkgConfig, args...)
497 default:
498 return fmt.Errorf("%s: invalid #cgo verb: %s", filename, orig)
499 }
500 }
501 return nil
502 }
503
504
505
506 func expandSrcDir(str string, srcdir string) (string, bool) {
507
508
509
510 srcdir = filepath.ToSlash(srcdir)
511
512 chunks := strings.Split(str, "${SRCDIR}")
513 if len(chunks) < 2 {
514 return str, safeCgoName(str)
515 }
516 ok := true
517 for _, chunk := range chunks {
518 ok = ok && (chunk == "" || safeCgoName(chunk))
519 }
520 ok = ok && (srcdir == "" || safeCgoName(srcdir))
521 res := strings.Join(chunks, srcdir)
522 return res, ok && res != ""
523 }
524
525
526
527
528
529
530
531
532
533
534
535
536 func (ctxt *Context) makePathsAbsolute(args []string, srcDir string) {
537 nextPath := false
538 for i, arg := range args {
539 if nextPath {
540 if !filepath.IsAbs(arg) {
541 args[i] = filepath.Join(srcDir, arg)
542 }
543 nextPath = false
544 } else if strings.HasPrefix(arg, "-I") || strings.HasPrefix(arg, "-L") {
545 if len(arg) == 2 {
546 nextPath = true
547 } else {
548 if !filepath.IsAbs(arg[2:]) {
549 args[i] = arg[:2] + filepath.Join(srcDir, arg[2:])
550 }
551 }
552 }
553 }
554 }
555
556
557
558
559
560
561
562
563 const safeString = "+-.,/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz:$@%! ~^"
564
565 func safeCgoName(s string) bool {
566 if s == "" {
567 return false
568 }
569 for i := 0; i < len(s); i++ {
570 if c := s[i]; c < utf8.RuneSelf && strings.IndexByte(safeString, c) < 0 {
571 return false
572 }
573 }
574 return true
575 }
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592 func splitQuoted(s string) (r []string, err error) {
593 var args []string
594 arg := make([]rune, len(s))
595 escaped := false
596 quoted := false
597 quote := '\x00'
598 i := 0
599 for _, rune := range s {
600 switch {
601 case escaped:
602 escaped = false
603 case rune == '\\':
604 escaped = true
605 continue
606 case quote != '\x00':
607 if rune == quote {
608 quote = '\x00'
609 continue
610 }
611 case rune == '"' || rune == '\'':
612 quoted = true
613 quote = rune
614 continue
615 case unicode.IsSpace(rune):
616 if quoted || i > 0 {
617 quoted = false
618 args = append(args, string(arg[:i]))
619 i = 0
620 }
621 continue
622 }
623 arg[i] = rune
624 i++
625 }
626 if quoted || i > 0 {
627 args = append(args, string(arg[:i]))
628 }
629 if quote != 0 {
630 err = errors.New("unclosed quote")
631 } else if escaped {
632 err = errors.New("unfinished escaping")
633 }
634 return args, err
635 }
636
637
638
639
640
641
642 func (ctxt *Context) matchAuto(text string, allTags map[string]bool) bool {
643 if strings.ContainsAny(text, "&|()") {
644 text = "//go:build " + text
645 } else {
646 text = "// +build " + text
647 }
648 x, err := constraint.Parse(text)
649 if err != nil {
650 return false
651 }
652 return ctxt.eval(x, allTags)
653 }
654
655 func (ctxt *Context) eval(x constraint.Expr, allTags map[string]bool) bool {
656 return x.Eval(func(tag string) bool { return ctxt.matchTag(tag, allTags) })
657 }
658
659
660
661
662
663
664
665
666
667
668
669
670
671 func (ctxt *Context) matchTag(name string, allTags map[string]bool) bool {
672 if allTags != nil {
673 allTags[name] = true
674 }
675
676
677 if ctxt.CgoEnabled && name == "cgo" {
678 return true
679 }
680 if name == ctxt.GOOS || name == ctxt.GOARCH || name == ctxt.Compiler {
681 return true
682 }
683 if ctxt.GOOS == "android" && name == "linux" {
684 return true
685 }
686 if ctxt.GOOS == "illumos" && name == "solaris" {
687 return true
688 }
689 if ctxt.GOOS == "ios" && name == "darwin" {
690 return true
691 }
692 if name == "unix" && syslist.UnixOS[ctxt.GOOS] {
693 return true
694 }
695 if name == "boringcrypto" {
696 name = "goexperiment.boringcrypto"
697 }
698
699
700 return slices.Contains(ctxt.BuildTags, name) || slices.Contains(ctxt.ToolTags, name) ||
701 slices.Contains(ctxt.ReleaseTags, name)
702 }
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719 func (ctxt *Context) goodOSArchFile(name string, allTags map[string]bool) bool {
720 name, _, _ = strings.Cut(name, ".")
721
722
723
724
725
726
727
728
729 i := strings.Index(name, "_")
730 if i < 0 {
731 return true
732 }
733 name = name[i:]
734
735 l := strings.Split(name, "_")
736 if n := len(l); n > 0 && l[n-1] == "test" {
737 l = l[:n-1]
738 }
739 n := len(l)
740 if n >= 2 && syslist.KnownOS[l[n-2]] && syslist.KnownArch[l[n-1]] {
741 if allTags != nil {
742
743 allTags[l[n-2]] = true
744 }
745 return ctxt.matchTag(l[n-1], allTags) && ctxt.matchTag(l[n-2], allTags)
746 }
747 if n >= 1 && (syslist.KnownOS[l[n-1]] || syslist.KnownArch[l[n-1]]) {
748 return ctxt.matchTag(l[n-1], allTags)
749 }
750 return true
751 }
752
View as plain text