main.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. /*
  2. Copyright 2018 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // do a fast type check of kubernetes code, for all platforms.
  14. package main
  15. import (
  16. "flag"
  17. "fmt"
  18. "go/ast"
  19. "go/build"
  20. "go/parser"
  21. "go/token"
  22. "go/types"
  23. "io"
  24. "log"
  25. "os"
  26. "path/filepath"
  27. "sort"
  28. "strings"
  29. "sync"
  30. "sync/atomic"
  31. "time"
  32. "golang.org/x/crypto/ssh/terminal"
  33. srcimporter "k8s.io/kubernetes/third_party/go-srcimporter"
  34. )
  35. var (
  36. verbose = flag.Bool("verbose", false, "print more information")
  37. cross = flag.Bool("cross", true, "build for all platforms")
  38. platforms = flag.String("platform", "", "comma-separated list of platforms to typecheck")
  39. timings = flag.Bool("time", false, "output times taken for each phase")
  40. defuses = flag.Bool("defuse", false, "output defs/uses")
  41. serial = flag.Bool("serial", false, "don't type check platforms in parallel")
  42. skipTest = flag.Bool("skip-test", false, "don't type check test code")
  43. tags = flag.String("tags", "", "comma-separated list of build tags to apply in addition to go's defaults")
  44. ignoreDirs = flag.String("ignore-dirs", "", "comma-separated list of directories to ignore in addition to the default hardcoded list including staging, vendor, and hidden dirs")
  45. isTerminal = terminal.IsTerminal(int(os.Stdout.Fd()))
  46. logPrefix = ""
  47. // When processed in order, windows and darwin are early to make
  48. // interesting OS-based errors happen earlier.
  49. crossPlatforms = []string{
  50. "linux/amd64", "windows/386",
  51. "darwin/amd64", "linux/arm",
  52. "linux/386", "windows/amd64",
  53. "linux/arm64", "linux/ppc64le",
  54. "linux/s390x", "darwin/386",
  55. }
  56. darwinPlatString = "darwin/386,darwin/amd64"
  57. windowsPlatString = "windows/386,windows/amd64"
  58. // directories we always ignore
  59. standardIgnoreDirs = []string{
  60. // Staging code is symlinked from vendor/k8s.io, and uses import
  61. // paths as if it were inside of vendor/. It fails typechecking
  62. // inside of staging/, but works when typechecked as part of vendor/.
  63. "staging",
  64. // OS-specific vendor code tends to be imported by OS-specific
  65. // packages. We recursively typecheck imported vendored packages for
  66. // each OS, but don't typecheck everything for every OS.
  67. "vendor",
  68. "_output",
  69. // This is a weird one. /testdata/ is *mostly* ignored by Go,
  70. // and this translates to kubernetes/vendor not working.
  71. // edit/record.go doesn't compile without gopkg.in/yaml.v2
  72. // in $GOSRC/$GOROOT (both typecheck and the shell script).
  73. "pkg/kubectl/cmd/testdata/edit",
  74. }
  75. )
  76. type analyzer struct {
  77. fset *token.FileSet // positions are relative to fset
  78. conf types.Config
  79. ctx build.Context
  80. failed bool
  81. platform string
  82. donePaths map[string]interface{}
  83. errors []string
  84. }
  85. func newAnalyzer(platform string) *analyzer {
  86. ctx := build.Default
  87. platSplit := strings.Split(platform, "/")
  88. ctx.GOOS, ctx.GOARCH = platSplit[0], platSplit[1]
  89. ctx.CgoEnabled = true
  90. if *tags != "" {
  91. tagsSplit := strings.Split(*tags, ",")
  92. ctx.BuildTags = append(ctx.BuildTags, tagsSplit...)
  93. }
  94. // add selinux tag explicitly
  95. ctx.BuildTags = append(ctx.BuildTags, "selinux")
  96. a := &analyzer{
  97. platform: platform,
  98. fset: token.NewFileSet(),
  99. ctx: ctx,
  100. donePaths: make(map[string]interface{}),
  101. }
  102. a.conf = types.Config{
  103. FakeImportC: true,
  104. Error: a.handleError,
  105. Sizes: types.SizesFor("gc", a.ctx.GOARCH),
  106. }
  107. a.conf.Importer = srcimporter.New(
  108. &a.ctx, a.fset, make(map[string]*types.Package))
  109. if *verbose {
  110. fmt.Printf("context: %#v\n", ctx)
  111. }
  112. return a
  113. }
  114. func (a *analyzer) handleError(err error) {
  115. a.errors = append(a.errors, err.Error())
  116. if *serial {
  117. fmt.Fprintf(os.Stderr, "%sERROR(%s) %s\n", logPrefix, a.platform, err)
  118. }
  119. a.failed = true
  120. }
  121. func (a *analyzer) dumpAndResetErrors() []string {
  122. es := a.errors
  123. a.errors = nil
  124. return es
  125. }
  126. // collect extracts test metadata from a file.
  127. func (a *analyzer) collect(dir string) {
  128. if _, ok := a.donePaths[dir]; ok {
  129. return
  130. }
  131. a.donePaths[dir] = nil
  132. // Create the AST by parsing src.
  133. fs, err := parser.ParseDir(a.fset, dir, nil, parser.AllErrors)
  134. if err != nil {
  135. fmt.Println(logPrefix+"ERROR(syntax)", err)
  136. a.failed = true
  137. return
  138. }
  139. if len(fs) > 1 && *verbose {
  140. fmt.Println("multiple packages in dir:", dir)
  141. }
  142. for _, p := range fs {
  143. // returns first error, but a.handleError deals with it
  144. files := a.filterFiles(p.Files)
  145. if *verbose {
  146. fmt.Printf("path: %s package: %s files: ", dir, p.Name)
  147. for _, f := range files {
  148. fname := filepath.Base(a.fset.File(f.Pos()).Name())
  149. fmt.Printf("%s ", fname)
  150. }
  151. fmt.Printf("\n")
  152. }
  153. a.typeCheck(dir, files)
  154. }
  155. }
  156. // filterFiles restricts a list of files to only those that should be built by
  157. // the current platform. This includes both build suffixes (_windows.go) and build
  158. // tags ("// +build !linux" at the beginning).
  159. func (a *analyzer) filterFiles(fs map[string]*ast.File) []*ast.File {
  160. files := []*ast.File{}
  161. for _, f := range fs {
  162. fpath := a.fset.File(f.Pos()).Name()
  163. if *skipTest && strings.HasSuffix(fpath, "_test.go") {
  164. continue
  165. }
  166. dir, name := filepath.Split(fpath)
  167. matches, err := a.ctx.MatchFile(dir, name)
  168. if err != nil {
  169. fmt.Fprintf(os.Stderr, "%sERROR reading %s: %s\n", logPrefix, fpath, err)
  170. a.failed = true
  171. continue
  172. }
  173. if matches {
  174. files = append(files, f)
  175. }
  176. }
  177. return files
  178. }
  179. func (a *analyzer) typeCheck(dir string, files []*ast.File) error {
  180. info := types.Info{
  181. Defs: make(map[*ast.Ident]types.Object),
  182. Uses: make(map[*ast.Ident]types.Object),
  183. }
  184. // NOTE: this type check does a *recursive* import, but srcimporter
  185. // doesn't do a full type check (ignores function bodies)-- this has
  186. // some additional overhead.
  187. //
  188. // This means that we need to ensure that typeCheck runs on all
  189. // code we will be compiling.
  190. //
  191. // TODO(rmmh): Customize our forked srcimporter to do this better.
  192. pkg, err := a.conf.Check(dir, a.fset, files, &info)
  193. if err != nil {
  194. return err // type error
  195. }
  196. // A significant fraction of vendored code only compiles on Linux,
  197. // but it's only imported by code that has build-guards for Linux.
  198. // Track vendored code to type-check it in a second pass.
  199. for _, imp := range pkg.Imports() {
  200. if strings.HasPrefix(imp.Path(), "k8s.io/kubernetes/vendor/") {
  201. vendorPath := imp.Path()[len("k8s.io/kubernetes/"):]
  202. if *verbose {
  203. fmt.Println("recursively checking vendor path:", vendorPath)
  204. }
  205. a.collect(vendorPath)
  206. }
  207. }
  208. if *defuses {
  209. for id, obj := range info.Defs {
  210. fmt.Printf("%s: %q defines %v\n",
  211. a.fset.Position(id.Pos()), id.Name, obj)
  212. }
  213. for id, obj := range info.Uses {
  214. fmt.Printf("%s: %q uses %v\n",
  215. a.fset.Position(id.Pos()), id.Name, obj)
  216. }
  217. }
  218. return nil
  219. }
  220. type collector struct {
  221. dirs []string
  222. ignoreDirs []string
  223. }
  224. // handlePath walks the filesystem recursively, collecting directories,
  225. // ignoring some unneeded directories (hidden/vendored) that are handled
  226. // specially later.
  227. func (c *collector) handlePath(path string, info os.FileInfo, err error) error {
  228. if err != nil {
  229. return err
  230. }
  231. if info.IsDir() {
  232. // Ignore hidden directories (.git, .cache, etc)
  233. if len(path) > 1 && path[0] == '.' {
  234. return filepath.SkipDir
  235. }
  236. for _, dir := range c.ignoreDirs {
  237. if path == dir {
  238. return filepath.SkipDir
  239. }
  240. }
  241. c.dirs = append(c.dirs, path)
  242. }
  243. return nil
  244. }
  245. type analyzerResult struct {
  246. platform string
  247. dir string
  248. errors []string
  249. }
  250. func dedupeErrors(out io.Writer, results chan analyzerResult, nDirs, nPlatforms int) {
  251. pkgRes := make(map[string][]analyzerResult)
  252. for done := 0; done < nDirs; {
  253. res := <-results
  254. pkgRes[res.dir] = append(pkgRes[res.dir], res)
  255. if len(pkgRes[res.dir]) != nPlatforms {
  256. continue // expect more results for dir
  257. }
  258. done++
  259. // Collect list of platforms for each error
  260. errPlats := map[string][]string{}
  261. for _, res := range pkgRes[res.dir] {
  262. for _, err := range res.errors {
  263. errPlats[err] = append(errPlats[err], res.platform)
  264. }
  265. }
  266. // Print each error (in the same order!) once.
  267. for _, res := range pkgRes[res.dir] {
  268. for _, err := range res.errors {
  269. if errPlats[err] == nil {
  270. continue // already printed
  271. }
  272. sort.Strings(errPlats[err])
  273. plats := strings.Join(errPlats[err], ",")
  274. if len(errPlats[err]) == len(crossPlatforms) {
  275. plats = "all"
  276. } else if plats == darwinPlatString {
  277. plats = "darwin"
  278. } else if plats == windowsPlatString {
  279. plats = "windows"
  280. }
  281. fmt.Fprintf(out, "%sERROR(%s) %s\n", logPrefix, plats, err)
  282. delete(errPlats, err)
  283. }
  284. }
  285. delete(pkgRes, res.dir)
  286. }
  287. }
  288. func main() {
  289. flag.Parse()
  290. args := flag.Args()
  291. if *verbose {
  292. *serial = true // to avoid confusing interleaved logs
  293. }
  294. if len(args) == 0 {
  295. args = append(args, ".")
  296. }
  297. c := collector{
  298. ignoreDirs: append([]string(nil), standardIgnoreDirs...),
  299. }
  300. if *ignoreDirs != "" {
  301. c.ignoreDirs = append(c.ignoreDirs, strings.Split(*ignoreDirs, ",")...)
  302. }
  303. for _, arg := range args {
  304. err := filepath.Walk(arg, c.handlePath)
  305. if err != nil {
  306. log.Fatalf("Error walking: %v", err)
  307. }
  308. }
  309. sort.Strings(c.dirs)
  310. ps := crossPlatforms[:]
  311. if *platforms != "" {
  312. ps = strings.Split(*platforms, ",")
  313. } else if !*cross {
  314. ps = ps[:1]
  315. }
  316. fmt.Println("type-checking: ", strings.Join(ps, ", "))
  317. var wg sync.WaitGroup
  318. var processedDirs int64
  319. var currentWork int64 // (dir_index << 8) | platform_index
  320. statuses := make([]int, len(ps))
  321. var results chan analyzerResult
  322. if !*serial {
  323. results = make(chan analyzerResult)
  324. wg.Add(1)
  325. go func() {
  326. dedupeErrors(os.Stderr, results, len(c.dirs), len(ps))
  327. wg.Done()
  328. }()
  329. }
  330. for i, p := range ps {
  331. wg.Add(1)
  332. fn := func(i int, p string) {
  333. start := time.Now()
  334. a := newAnalyzer(p)
  335. for n, dir := range c.dirs {
  336. a.collect(dir)
  337. atomic.AddInt64(&processedDirs, 1)
  338. atomic.StoreInt64(&currentWork, int64(n<<8|i))
  339. if results != nil {
  340. results <- analyzerResult{p, dir, a.dumpAndResetErrors()}
  341. }
  342. }
  343. if a.failed {
  344. statuses[i] = 1
  345. }
  346. if *timings {
  347. fmt.Printf("%s took %.1fs\n", p, time.Since(start).Seconds())
  348. }
  349. wg.Done()
  350. }
  351. if *serial {
  352. fn(i, p)
  353. } else {
  354. go fn(i, p)
  355. }
  356. }
  357. if isTerminal {
  358. logPrefix = "\r" // clear status bar when printing
  359. // Display a status bar so devs can estimate completion times.
  360. wg.Add(1)
  361. go func() {
  362. total := len(ps) * len(c.dirs)
  363. for proc := 0; ; proc = int(atomic.LoadInt64(&processedDirs)) {
  364. work := atomic.LoadInt64(&currentWork)
  365. dir := c.dirs[work>>8]
  366. platform := ps[work&0xFF]
  367. if len(dir) > 80 {
  368. dir = dir[:80]
  369. }
  370. fmt.Printf("\r%d/%d \033[2m%-13s\033[0m %-80s", proc, total, platform, dir)
  371. if proc == total {
  372. fmt.Println()
  373. break
  374. }
  375. time.Sleep(50 * time.Millisecond)
  376. }
  377. wg.Done()
  378. }()
  379. }
  380. wg.Wait()
  381. for _, status := range statuses {
  382. if status != 0 {
  383. os.Exit(status)
  384. }
  385. }
  386. }