listpkgs.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // listpkgs prints the import paths that match the Go package patterns
  4. // given on the command line and conditionally filters them in various ways.
  5. package main
  6. import (
  7. "bufio"
  8. "flag"
  9. "fmt"
  10. "go/build/constraint"
  11. "log"
  12. "os"
  13. "slices"
  14. "strings"
  15. "sync"
  16. "golang.org/x/tools/go/packages"
  17. )
  18. var (
  19. ignore3p = flag.Bool("ignore-3p", false, "ignore third-party packages forked/vendored into Tailscale")
  20. goos = flag.String("goos", "", "GOOS to use for loading packages (default: current OS)")
  21. goarch = flag.String("goarch", "", "GOARCH to use for loading packages (default: current architecture)")
  22. withTagsAllStr = flag.String("with-tags-all", "", "if non-empty, a comma-separated list of builds tags to require (a package will only be listed if it contains all of these build tags)")
  23. withoutTagsAnyStr = flag.String("without-tags-any", "", "if non-empty, a comma-separated list of build constraints to exclude (a package will be omitted if it contains any of these build tags)")
  24. shard = flag.String("shard", "", "if non-empty, a string of the form 'N/M' to only print packages in shard N of M (e.g. '1/3', '2/3', '3/3/' for different thirds of the list)")
  25. )
  26. func main() {
  27. flag.Parse()
  28. patterns := flag.Args()
  29. if len(patterns) == 0 {
  30. flag.Usage()
  31. os.Exit(1)
  32. }
  33. cfg := &packages.Config{
  34. Mode: packages.LoadFiles,
  35. Env: os.Environ(),
  36. }
  37. if *goos != "" {
  38. cfg.Env = append(cfg.Env, "GOOS="+*goos)
  39. }
  40. if *goarch != "" {
  41. cfg.Env = append(cfg.Env, "GOARCH="+*goarch)
  42. }
  43. pkgs, err := packages.Load(cfg, patterns...)
  44. if err != nil {
  45. log.Fatalf("loading packages: %v", err)
  46. }
  47. var withoutAny []string
  48. if *withoutTagsAnyStr != "" {
  49. withoutAny = strings.Split(*withoutTagsAnyStr, ",")
  50. }
  51. var withAll []string
  52. if *withTagsAllStr != "" {
  53. withAll = strings.Split(*withTagsAllStr, ",")
  54. }
  55. seen := map[string]bool{}
  56. matches := 0
  57. Pkg:
  58. for _, pkg := range pkgs {
  59. if pkg.PkgPath == "" { // malformed (shouldn’t happen)
  60. continue
  61. }
  62. if seen[pkg.PkgPath] {
  63. continue // suppress duplicates when patterns overlap
  64. }
  65. seen[pkg.PkgPath] = true
  66. pkgPath := pkg.PkgPath
  67. if *ignore3p && isThirdParty(pkgPath) {
  68. continue
  69. }
  70. if withAll != nil {
  71. for _, t := range withAll {
  72. if !hasBuildTag(pkg, t) {
  73. continue Pkg
  74. }
  75. }
  76. }
  77. for _, t := range withoutAny {
  78. if hasBuildTag(pkg, t) {
  79. continue Pkg
  80. }
  81. }
  82. matches++
  83. if *shard != "" {
  84. var n, m int
  85. if _, err := fmt.Sscanf(*shard, "%d/%d", &n, &m); err != nil || n < 1 || m < 1 {
  86. log.Fatalf("invalid shard format %q; expected 'N/M'", *shard)
  87. }
  88. if m > 0 && (matches-1)%m != n-1 {
  89. continue // not in this shard
  90. }
  91. }
  92. fmt.Println(pkgPath)
  93. }
  94. // If any package had errors (e.g. missing deps) report them via packages.PrintErrors.
  95. // This mirrors `go list` behaviour when -e is *not* supplied.
  96. if packages.PrintErrors(pkgs) > 0 {
  97. os.Exit(1)
  98. }
  99. }
  100. func isThirdParty(pkg string) bool {
  101. return strings.HasPrefix(pkg, "tailscale.com/tempfork/")
  102. }
  103. // hasBuildTag reports whether any source file in pkg mentions `tag`
  104. // in a //go:build constraint.
  105. func hasBuildTag(pkg *packages.Package, tag string) bool {
  106. all := slices.Concat(pkg.CompiledGoFiles, pkg.OtherFiles, pkg.IgnoredFiles)
  107. suffix := "_" + tag + ".go"
  108. for _, name := range all {
  109. if strings.HasSuffix(name, suffix) {
  110. return true
  111. }
  112. ok, err := fileMentionsTag(name, tag)
  113. if err != nil {
  114. log.Printf("reading %s: %v", name, err)
  115. continue
  116. }
  117. if ok {
  118. return true
  119. }
  120. }
  121. return false
  122. }
  123. // tagSet is a set of build tags.
  124. // The values are always true. We avoid non-std set types
  125. // to make this faster to "go run" on empty caches.
  126. type tagSet map[string]bool
  127. var (
  128. mu sync.Mutex
  129. fileTags = map[string]tagSet{} // abs path -> set of build tags mentioned in file
  130. )
  131. func getFileTags(filename string) (tagSet, error) {
  132. mu.Lock()
  133. tags, ok := fileTags[filename]
  134. mu.Unlock()
  135. if ok {
  136. return tags, nil
  137. }
  138. f, err := os.Open(filename)
  139. if err != nil {
  140. return nil, err
  141. }
  142. defer f.Close()
  143. ts := make(tagSet)
  144. s := bufio.NewScanner(f)
  145. for s.Scan() {
  146. line := s.Text()
  147. if strings.TrimSpace(line) == "" {
  148. continue // still in leading blank lines
  149. }
  150. if !strings.HasPrefix(line, "//") {
  151. // hit real code – done with header comments
  152. // TODO(bradfitz): care about /* */ comments?
  153. break
  154. }
  155. if !strings.HasPrefix(line, "//go:build") {
  156. continue // some other comment
  157. }
  158. expr, err := constraint.Parse(line)
  159. if err != nil {
  160. return nil, fmt.Errorf("parsing %q: %w", line, err)
  161. }
  162. // Call Eval to populate ts with the tags mentioned in the expression.
  163. // We don't care about the result, just the side effect of populating ts.
  164. expr.Eval(func(tag string) bool {
  165. ts[tag] = true
  166. return true // arbitrary
  167. })
  168. }
  169. if err := s.Err(); err != nil {
  170. return nil, fmt.Errorf("reading %s: %w", filename, err)
  171. }
  172. mu.Lock()
  173. defer mu.Unlock()
  174. fileTags[filename] = ts
  175. return tags, nil
  176. }
  177. func fileMentionsTag(filename, tag string) (bool, error) {
  178. tags, err := getFileTags(filename)
  179. if err != nil {
  180. return false, err
  181. }
  182. return tags[tag], nil
  183. }