walk.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /* Copyright 2018 The Bazel Authors. All rights reserved.
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. // Package walk provides customizable functionality for visiting each
  13. // subdirectory in a directory tree.
  14. package walk
  15. import (
  16. "io/ioutil"
  17. "log"
  18. "os"
  19. "path"
  20. "path/filepath"
  21. "strings"
  22. "github.com/bazelbuild/bazel-gazelle/config"
  23. "github.com/bazelbuild/bazel-gazelle/pathtools"
  24. "github.com/bazelbuild/bazel-gazelle/rule"
  25. )
  26. // Mode determines which directories Walk visits and which directories
  27. // should be updated.
  28. type Mode int
  29. const (
  30. // In VisitAllUpdateSubdirsMode, Walk visits every directory in the
  31. // repository. The directories given to Walk and their subdirectories are
  32. // updated.
  33. VisitAllUpdateSubdirsMode Mode = iota
  34. // In VisitAllUpdateDirsMode, Walk visits every directory in the repository.
  35. // Only the directories given to Walk are updated (not their subdirectories).
  36. VisitAllUpdateDirsMode
  37. // In UpdateDirsMode, Walk only visits and updates directories given to Walk.
  38. // Build files in parent directories are read in order to produce a complete
  39. // configuration, but the callback is not called for parent directories.
  40. UpdateDirsMode
  41. )
  42. // WalkFunc is a callback called by Walk in each visited directory.
  43. //
  44. // dir is the absolute file system path to the directory being visited.
  45. //
  46. // rel is the relative slash-separated path to the directory from the
  47. // repository root. Will be "" for the repository root directory itself.
  48. //
  49. // c is the configuration for the current directory. This may have been
  50. // modified by directives in the directory's build file.
  51. //
  52. // update is true when the build file may be updated.
  53. //
  54. // f is the existing build file in the directory. Will be nil if there
  55. // was no file.
  56. //
  57. // subdirs is a list of base names of subdirectories within dir, not
  58. // including excluded files.
  59. //
  60. // regularFiles is a list of base names of regular files within dir, not
  61. // including excluded files.
  62. //
  63. // genFiles is a list of names of generated files, found by reading
  64. // "out" and "outs" attributes of rules in f.
  65. type WalkFunc func(dir, rel string, c *config.Config, update bool, f *rule.File, subdirs, regularFiles, genFiles []string)
  66. // Walk traverses the directory tree rooted at c.RepoRoot. Walk visits
  67. // subdirectories in depth-first post-order.
  68. //
  69. // When Walk visits a directory, it lists the files and subdirectories within
  70. // that directory. If a build file is present, Walk reads the build file and
  71. // applies any directives to the configuration (a copy of the parent directory's
  72. // configuration is made, and the copy is modified). After visiting
  73. // subdirectories, the callback wf may be called, depending on the mode.
  74. //
  75. // c is the root configuration to start with. This includes changes made by
  76. // command line flags, but not by the root build file. This configuration
  77. // should not be modified.
  78. //
  79. // cexts is a list of configuration extensions. When visiting a directory,
  80. // before visiting subdirectories, Walk makes a copy of the parent configuration
  81. // and Configure for each extension on the copy. If Walk sees a directive
  82. // that is not listed in KnownDirectives of any extension, an error will
  83. // be logged.
  84. //
  85. // dirs is a list of absolute, canonical file system paths of directories
  86. // to visit.
  87. //
  88. // mode determines whether subdirectories of dirs should be visited recursively,
  89. // when the wf callback should be called, and when the "update" argument
  90. // to the wf callback should be set.
  91. //
  92. // wf is a function that may be called in each directory.
  93. func Walk(c *config.Config, cexts []config.Configurer, dirs []string, mode Mode, wf WalkFunc) {
  94. knownDirectives := make(map[string]bool)
  95. for _, cext := range cexts {
  96. for _, d := range cext.KnownDirectives() {
  97. knownDirectives[d] = true
  98. }
  99. }
  100. symlinks := symlinkResolver{visited: []string{c.RepoRoot}}
  101. updateRels := buildUpdateRelMap(c.RepoRoot, dirs)
  102. var visit func(*config.Config, string, string, bool)
  103. visit = func(c *config.Config, dir, rel string, updateParent bool) {
  104. haveError := false
  105. // TODO: OPT: ReadDir stats all the files, which is slow. We just care about
  106. // names and modes, so we should use something like
  107. // golang.org/x/tools/internal/fastwalk to speed this up.
  108. files, err := ioutil.ReadDir(dir)
  109. if err != nil {
  110. log.Print(err)
  111. return
  112. }
  113. f, err := loadBuildFile(c, rel, dir, files)
  114. if err != nil {
  115. log.Print(err)
  116. haveError = true
  117. }
  118. c = configure(cexts, knownDirectives, c, rel, f)
  119. wc := getWalkConfig(c)
  120. if wc.isExcluded(rel, ".") {
  121. return
  122. }
  123. var subdirs, regularFiles []string
  124. for _, fi := range files {
  125. base := fi.Name()
  126. switch {
  127. case base == "" || wc.isExcluded(rel, base):
  128. continue
  129. case fi.IsDir() || fi.Mode()&os.ModeSymlink != 0 && symlinks.follow(c, dir, rel, base):
  130. subdirs = append(subdirs, base)
  131. default:
  132. regularFiles = append(regularFiles, base)
  133. }
  134. }
  135. shouldUpdate := shouldUpdate(rel, mode, updateParent, updateRels)
  136. for _, sub := range subdirs {
  137. if subRel := path.Join(rel, sub); shouldVisit(subRel, mode, updateRels) {
  138. visit(c, filepath.Join(dir, sub), subRel, shouldUpdate)
  139. }
  140. }
  141. update := !haveError && !wc.ignore && shouldUpdate
  142. if shouldCall(rel, mode, updateRels) {
  143. genFiles := findGenFiles(wc, f)
  144. wf(dir, rel, c, update, f, subdirs, regularFiles, genFiles)
  145. }
  146. }
  147. visit(c, c.RepoRoot, "", false)
  148. }
  149. // buildUpdateRelMap builds a table of prefixes, used to determine which
  150. // directories to update and visit.
  151. //
  152. // root and dirs must be absolute, canonical file paths. Each entry in dirs
  153. // must be a subdirectory of root. The caller is responsible for checking this.
  154. //
  155. // buildUpdateRelMap returns a map from slash-separated paths relative to the
  156. // root directory ("" for the root itself) to a boolean indicating whether
  157. // the directory should be updated.
  158. func buildUpdateRelMap(root string, dirs []string) map[string]bool {
  159. relMap := make(map[string]bool)
  160. for _, dir := range dirs {
  161. rel, _ := filepath.Rel(root, dir)
  162. rel = filepath.ToSlash(rel)
  163. if rel == "." {
  164. rel = ""
  165. }
  166. i := 0
  167. for {
  168. next := strings.IndexByte(rel[i:], '/') + i
  169. if next-i < 0 {
  170. relMap[rel] = true
  171. break
  172. }
  173. prefix := rel[:next]
  174. relMap[prefix] = relMap[prefix] // set to false if not present
  175. i = next + 1
  176. }
  177. }
  178. return relMap
  179. }
  180. // shouldCall returns true if Walk should call the callback in the
  181. // directory rel.
  182. func shouldCall(rel string, mode Mode, updateRels map[string]bool) bool {
  183. return mode != UpdateDirsMode || updateRels[rel]
  184. }
  185. // shouldUpdate returns true if Walk should pass true to the callback's update
  186. // parameter in the directory rel. This indicates the build file should be
  187. // updated.
  188. func shouldUpdate(rel string, mode Mode, updateParent bool, updateRels map[string]bool) bool {
  189. return mode == VisitAllUpdateSubdirsMode && updateParent || updateRels[rel]
  190. }
  191. // shouldVisit returns true if Walk should visit the subdirectory rel.
  192. func shouldVisit(rel string, mode Mode, updateRels map[string]bool) bool {
  193. if mode != UpdateDirsMode {
  194. return true
  195. }
  196. _, ok := updateRels[rel]
  197. return ok
  198. }
  199. func loadBuildFile(c *config.Config, pkg, dir string, files []os.FileInfo) (*rule.File, error) {
  200. var err error
  201. readDir := dir
  202. readFiles := files
  203. if c.ReadBuildFilesDir != "" {
  204. readDir = filepath.Join(c.ReadBuildFilesDir, filepath.FromSlash(pkg))
  205. readFiles, err = ioutil.ReadDir(readDir)
  206. if err != nil {
  207. return nil, err
  208. }
  209. }
  210. path := rule.MatchBuildFileName(readDir, c.ValidBuildFileNames, readFiles)
  211. if path == "" {
  212. return nil, nil
  213. }
  214. return rule.LoadFile(path, pkg)
  215. }
  216. func configure(cexts []config.Configurer, knownDirectives map[string]bool, c *config.Config, rel string, f *rule.File) *config.Config {
  217. if rel != "" {
  218. c = c.Clone()
  219. }
  220. if f != nil {
  221. for _, d := range f.Directives {
  222. if !knownDirectives[d.Key] {
  223. log.Printf("%s: unknown directive: gazelle:%s", f.Path, d.Key)
  224. }
  225. }
  226. }
  227. for _, cext := range cexts {
  228. cext.Configure(c, rel, f)
  229. }
  230. return c
  231. }
  232. func findGenFiles(wc *walkConfig, f *rule.File) []string {
  233. if f == nil {
  234. return nil
  235. }
  236. var strs []string
  237. for _, r := range f.Rules {
  238. for _, key := range []string{"out", "outs"} {
  239. if s := r.AttrString(key); s != "" {
  240. strs = append(strs, s)
  241. } else if ss := r.AttrStrings(key); len(ss) > 0 {
  242. strs = append(strs, ss...)
  243. }
  244. }
  245. }
  246. var genFiles []string
  247. for _, s := range strs {
  248. if !wc.isExcluded(f.Pkg, s) {
  249. genFiles = append(genFiles, s)
  250. }
  251. }
  252. return genFiles
  253. }
  254. type symlinkResolver struct {
  255. visited []string
  256. }
  257. // Decide if symlink dir/base should be followed.
  258. func (r *symlinkResolver) follow(c *config.Config, dir, rel, base string) bool {
  259. if dir == c.RepoRoot && strings.HasPrefix(base, "bazel-") {
  260. // Links such as bazel-<workspace>, bazel-out, bazel-genfiles are created by
  261. // Bazel to point to internal build directories.
  262. return false
  263. }
  264. // See if the user has explicitly directed us to follow the link.
  265. wc := getWalkConfig(c)
  266. linkRel := path.Join(rel, base)
  267. for _, follow := range wc.follow {
  268. if linkRel == follow {
  269. return true
  270. }
  271. }
  272. // See if the symlink points to a tree that has been already visited.
  273. fullpath := filepath.Join(dir, base)
  274. dest, err := filepath.EvalSymlinks(fullpath)
  275. if err != nil {
  276. return false
  277. }
  278. if !filepath.IsAbs(dest) {
  279. dest, err = filepath.Abs(filepath.Join(dir, dest))
  280. if err != nil {
  281. return false
  282. }
  283. }
  284. for _, p := range r.visited {
  285. if pathtools.HasPrefix(dest, p) || pathtools.HasPrefix(p, dest) {
  286. return false
  287. }
  288. }
  289. r.visited = append(r.visited, dest)
  290. stat, err := os.Stat(fullpath)
  291. if err != nil {
  292. return false
  293. }
  294. return stat.IsDir()
  295. }