resolve-deps.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. package main
  2. import (
  3. "encoding/json"
  4. "flag"
  5. "fmt"
  6. "os"
  7. "sort"
  8. "strings"
  9. "go/build"
  10. "path/filepath"
  11. "path"
  12. )
  13. func prettyPrintJSON(j interface{}) {
  14. e := json.NewEncoder(os.Stdout)
  15. e.SetIndent("", " ")
  16. e.Encode(j)
  17. }
  18. func main() {
  19. flag.Usage = func() {
  20. fmt.Println(` Scans the imports from all Go packages (and subpackages) rooted in current dir,
  21. and prints the dependency graph in a JSON format that can be imported via npmjs.com/graphlib.
  22. `)
  23. flag.PrintDefaults()
  24. fmt.Println("")
  25. }
  26. var ignoredPkgs = flag.String("ignoredPkgs", "", "Comma separated list of packages (canonically named) to ignore when scanning subfolders")
  27. var outputDOT = flag.Bool("dot", false, "Output as Graphviz DOT format")
  28. var outputList = flag.Bool("list", false, "Output a flat JSON array of all reachable deps")
  29. flag.Parse()
  30. ignoredPkgsList := strings.Split(*ignoredPkgs, ",")
  31. var rc ResolveContext
  32. err := rc.ResolvePath(".", ignoredPkgsList)
  33. if err != nil {
  34. panic(err)
  35. }
  36. graph := rc.GetGraph()
  37. if *outputDOT {
  38. fmt.Println(graph.ToDOT())
  39. } else if *outputList {
  40. prettyPrintJSON(graph.SortedNodeNames())
  41. } else {
  42. prettyPrintJSON(graph)
  43. }
  44. unresolved := rc.GetUnresolvedPackages()
  45. if len(unresolved) != 0 {
  46. fmt.Println("\nUnresolved packages:")
  47. sort.Strings(unresolved)
  48. for _, pkg := range unresolved {
  49. fmt.Println(" - ", pkg)
  50. }
  51. os.Exit(1)
  52. }
  53. }
  54. /*
  55. This code is based on https://github.com/KyleBanks/depth
  56. MIT License
  57. Copyright (c) 2017 Kyle Banks
  58. Permission is hereby granted, free of charge, to any person obtaining a copy
  59. of this software and associated documentation files (the "Software"), to deal
  60. in the Software without restriction, including without limitation the rights
  61. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  62. copies of the Software, and to permit persons to whom the Software is
  63. furnished to do so, subject to the following conditions:
  64. The above copyright notice and this permission notice shall be included in all
  65. copies or substantial portions of the Software.
  66. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  67. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  68. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  69. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  70. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  71. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  72. SOFTWARE.
  73. */
  74. /*
  75. package resolver
  76. import (
  77. "fmt"
  78. "go/build"
  79. "path/filepath"
  80. "strings"
  81. "./dirwalk"
  82. "./graph"
  83. )
  84. */
  85. // ResolveContext represents all the pkg trees rooted at all the subfolders with Go code.
  86. type ResolveContext struct {
  87. roots []*Pkg
  88. unresolvedPkgs map[string]struct{}
  89. pkgCache map[string]*Pkg
  90. importCache map[string]struct{}
  91. ignoredPkgs []string
  92. }
  93. // ResolvePath recursively finds all direct & transitive dependencies for all the packages (and sub-packages),
  94. // rooted at given path
  95. func (rc *ResolveContext) ResolvePath(rootPath string, ignoredPkgs []string) error {
  96. rc.init()
  97. rc.ignoredPkgs = ignoredPkgs
  98. abs, err := filepath.Abs(rootPath)
  99. if err != nil {
  100. return fmt.Errorf("filepath.Abs(%s) failed with: %s", rootPath, err.Error())
  101. }
  102. rootPath = abs
  103. virtualRootPkg, err := rc.resolveVirtualRoot(rootPath)
  104. if err != nil {
  105. return err
  106. }
  107. rc.roots = append(rc.roots, virtualRootPkg)
  108. return WalkGoFolders(rootPath, func(path string) error {
  109. rootPkg := rc.resolveFolder(path)
  110. if rootPkg.isResolved {
  111. rc.roots = append(rc.roots, rootPkg)
  112. }
  113. return nil
  114. })
  115. }
  116. // GetUnresolvedPackages returns a list of all the pkgs that failed to resolve
  117. func (rc ResolveContext) GetUnresolvedPackages() []string {
  118. unresolved := []string{}
  119. for pkg := range rc.unresolvedPkgs {
  120. unresolved = append(unresolved, pkg)
  121. }
  122. return unresolved
  123. }
  124. // GetGraph returns the graph of resolved packages
  125. func (rc *ResolveContext) GetGraph() Graph {
  126. nodesMap := map[string]Node{}
  127. edgesMap := map[string]Edge{}
  128. var recurse func(pkg *Pkg)
  129. recurse = func(pkg *Pkg) {
  130. _, exists := nodesMap[pkg.Name]
  131. if exists {
  132. return
  133. }
  134. node := Node{
  135. Name: pkg.Name,
  136. Value: *pkg,
  137. }
  138. nodesMap[pkg.Name] = node
  139. for _, child := range pkg.deps {
  140. edge := Edge{
  141. From: pkg.Name,
  142. To: child.Name,
  143. }
  144. edgesMap[pkg.Name+":"+child.Name] = edge
  145. recurse(&child)
  146. }
  147. }
  148. for _, r := range rc.roots {
  149. recurse(r)
  150. }
  151. var nodes []Node
  152. for _, v := range nodesMap {
  153. nodes = append(nodes, v)
  154. }
  155. var edges []Edge
  156. for _, v := range edgesMap {
  157. edges = append(edges, v)
  158. }
  159. return Graph{
  160. Nodes: nodes,
  161. Edges: edges,
  162. Options: Options{
  163. Directed: true,
  164. },
  165. }
  166. }
  167. func (rc *ResolveContext) init() {
  168. rc.roots = []*Pkg{}
  169. rc.importCache = map[string]struct{}{}
  170. rc.unresolvedPkgs = map[string]struct{}{}
  171. rc.pkgCache = map[string]*Pkg{}
  172. }
  173. func (rc *ResolveContext) resolveVirtualRoot(rootPath string) (*Pkg, error) {
  174. rootImport, err := build.Default.Import(".", rootPath, build.FindOnly)
  175. if err != nil {
  176. return nil, err
  177. }
  178. if rootImport.ImportPath == "" || rootImport.ImportPath == "." {
  179. return nil, fmt.Errorf("Can't resolve root package at %s.\nIs $GOPATH defined correctly?", rootPath)
  180. }
  181. virtualRootPkg := &Pkg{
  182. Name: ".",
  183. FullImportPath: rootImport.ImportPath,
  184. Dir: rootImport.Dir,
  185. }
  186. return virtualRootPkg, nil
  187. }
  188. func (rc *ResolveContext) resolveFolder(path string) *Pkg {
  189. rootPkg := &Pkg{
  190. Name: ".",
  191. resolveContext: rc,
  192. parentDir: path,
  193. }
  194. rootPkg.Resolve()
  195. rootPkg.Name = rootPkg.FullImportPath
  196. return rootPkg
  197. }
  198. // hasSeenImport returns true if the import name provided has already been seen within the tree.
  199. // This function only returns false for a name once.
  200. func (rc *ResolveContext) hasSeenImport(name string) bool {
  201. if _, ok := rc.importCache[name]; ok {
  202. return true
  203. }
  204. rc.importCache[name] = struct{}{}
  205. return false
  206. }
  207. func (rc *ResolveContext) markUnresolvedPkg(name string) {
  208. rc.unresolvedPkgs[name] = struct{}{}
  209. }
  210. func (rc *ResolveContext) cacheResolvedPackage(pkg *Pkg) {
  211. rc.pkgCache[pkg.Name] = pkg
  212. }
  213. func (rc *ResolveContext) getCachedPkg(name string) *Pkg {
  214. pkg, ok := rc.pkgCache[name]
  215. if !ok {
  216. return nil
  217. }
  218. return pkg
  219. }
  220. func (rc ResolveContext) shouldIgnorePkg(name string) bool {
  221. for _, ignored := range rc.ignoredPkgs {
  222. if name == ignored {
  223. return true
  224. }
  225. if strings.HasSuffix(ignored, "*") {
  226. // note that ignoring "url/to/pkg*" will also ignore "url/to/pkg-other",
  227. // this is quite confusing, but is dep's behaviour
  228. if strings.HasPrefix(name, strings.TrimSuffix(ignored, "*")) {
  229. return true
  230. }
  231. }
  232. }
  233. return false
  234. }
  235. /*
  236. package dirwalk
  237. import (
  238. "os"
  239. "path/filepath"
  240. "strings"
  241. )
  242. */
  243. // WalkGoFolders will call cb for every folder with Go code under the given root path,
  244. // unless it's:
  245. // - one of "vendor", "Godeps", "node_modules", "testdata", "internal"
  246. // - starts with "." or "_"
  247. // - is a test package, i.e. ends with _test
  248. func WalkGoFolders(root string, cb WalkFunc) error {
  249. err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
  250. // if it's not a folder (or a symlink to folder), do nothing
  251. if info.Mode()&os.ModeSymlink > 0 {
  252. if info, err = os.Stat(path); err != nil {
  253. if os.IsNotExist(err) {
  254. // ignore broken symlinks
  255. return nil
  256. }
  257. return err
  258. }
  259. }
  260. if !info.IsDir() {
  261. return nil
  262. }
  263. folderName := info.Name()
  264. switch folderName {
  265. case "vendor", "Godeps", "node_modules", "testdata", "internal":
  266. return filepath.SkipDir
  267. }
  268. if strings.HasSuffix(folderName, "_test") ||
  269. (folderName != "." && strings.HasPrefix(folderName, ".")) ||
  270. strings.HasPrefix(folderName, "_") {
  271. return filepath.SkipDir
  272. }
  273. gofiles, err := filepath.Glob(filepath.Join(path, "*.go"))
  274. if err != nil {
  275. return nil
  276. }
  277. if len(gofiles) > 0 {
  278. return cb(path)
  279. }
  280. return nil
  281. })
  282. return err
  283. }
  284. // WalkFunc defines the prototype for WalkGoFolders's callback.
  285. // the error passed as the return value of the undrelying filepath.Walk
  286. type WalkFunc func(path string) error
  287. /*
  288. package graph
  289. import (
  290. "fmt"
  291. "sort"
  292. )
  293. */
  294. // Node is Grpah's node
  295. type Node struct {
  296. Name string `json:"v"`
  297. Value interface{} `json:"value"`
  298. }
  299. // Edge is Graph's edge
  300. type Edge struct {
  301. From string `json:"v"`
  302. To string `json:"w"`
  303. }
  304. // Options is Graph's options
  305. type Options struct {
  306. Directed bool `json:"directed"`
  307. Multigraph bool `json:"multigraph"`
  308. Compound bool `json:"compound"`
  309. }
  310. // Graph is graph that when marshaled to JSON can be imported via Graphlib JS pkg from NPM
  311. type Graph struct {
  312. Nodes []Node `json:"nodes"`
  313. Edges []Edge `json:"edges"`
  314. Options Options `json:"options"`
  315. }
  316. // ToDOT return graph as GraphViz .dot format string
  317. func (g Graph) ToDOT() string {
  318. dot := "digraph {\n"
  319. id := 0
  320. nodeIDs := map[string]int{}
  321. for _, n := range g.Nodes {
  322. nodeIDs[n.Name] = id
  323. dot += fmt.Sprintf("\t%d [label=\"%s\"]\n", id, n.Name)
  324. id++
  325. }
  326. dot += "\n"
  327. for _, e := range g.Edges {
  328. dot += fmt.Sprintf("\t%d -> %d;\n", nodeIDs[e.From], nodeIDs[e.To])
  329. }
  330. dot += "}\n"
  331. return dot
  332. }
  333. // SortedNodeNames returns a sorted list of all the node names
  334. func (g Graph) SortedNodeNames() []string {
  335. names := []string{}
  336. for _, n := range g.Nodes {
  337. names = append(names, n.Name)
  338. }
  339. sort.Strings(names)
  340. return names
  341. }
  342. /*
  343. package resolver
  344. import (
  345. "go/build"
  346. "path"
  347. "sort"
  348. "strings"
  349. )
  350. */
  351. // Pkg represents a Go source package, and its dependencies.
  352. type Pkg struct {
  353. Name string
  354. FullImportPath string
  355. Dir string
  356. raw *build.Package
  357. isBuiltin bool
  358. isResolved bool
  359. parentDir string
  360. deps []Pkg
  361. parent *Pkg
  362. resolveContext *ResolveContext
  363. }
  364. // Resolve recursively finds all dependencies for the Pkg and the packages it depends on.
  365. func (p *Pkg) Resolve() {
  366. // isResolved is always true, regardless of if we skip the import,
  367. // it is only false if there is an error while importing.
  368. p.isResolved = true
  369. name := p.cleanName()
  370. if name == "" {
  371. return
  372. }
  373. // Stop resolving imports if we've reached a loop.
  374. var importMode build.ImportMode
  375. if p.resolveContext.hasSeenImport(name) && p.isAncestor(name) {
  376. importMode = build.FindOnly
  377. }
  378. pkg, err := build.Default.Import(name, p.parentDir, importMode)
  379. if err != nil {
  380. // TODO: Check the error type?
  381. p.isResolved = false
  382. // this is package we dediced to scan, and probably shouldn't have.
  383. // probably can remove this when we have handling of build tags
  384. if name != "." {
  385. p.resolveContext.markUnresolvedPkg(name)
  386. }
  387. return
  388. }
  389. if name == "." && p.resolveContext.shouldIgnorePkg(pkg.ImportPath) {
  390. p.isResolved = false
  391. return
  392. }
  393. p.raw = pkg
  394. p.Dir = pkg.Dir
  395. // Clear some too verbose fields
  396. p.raw.ImportPos = nil
  397. p.raw.TestImportPos = nil
  398. // Update the name with the fully qualified import path.
  399. p.FullImportPath = pkg.ImportPath
  400. // If this is an builtin package, we don't resolve deeper
  401. if pkg.Goroot {
  402. p.isBuiltin = true
  403. return
  404. }
  405. imports := pkg.Imports
  406. p.setDeps(imports, pkg.Dir)
  407. }
  408. // setDeps takes a slice of import paths and the source directory they are relative to,
  409. // and creates the deps of the Pkg. Each dependency is also further resolved prior to being added
  410. // to the Pkg.
  411. func (p *Pkg) setDeps(imports []string, parentDir string) {
  412. unique := make(map[string]struct{})
  413. for _, imp := range imports {
  414. // Mostly for testing files where cyclic imports are allowed.
  415. if imp == p.Name {
  416. continue
  417. }
  418. // Skip duplicates.
  419. if _, ok := unique[imp]; ok {
  420. continue
  421. }
  422. unique[imp] = struct{}{}
  423. if p.resolveContext.shouldIgnorePkg(imp) {
  424. continue
  425. }
  426. p.addDep(imp, parentDir)
  427. }
  428. sort.Sort(sortablePkgsList(p.deps))
  429. }
  430. // addDep creates a Pkg and it's dependencies from an imported package name.
  431. func (p *Pkg) addDep(name string, parentDir string) {
  432. var dep Pkg
  433. cached := p.resolveContext.getCachedPkg(name)
  434. if cached != nil {
  435. dep = *cached
  436. dep.parentDir = parentDir
  437. dep.parent = p
  438. } else {
  439. dep = Pkg{
  440. Name: name,
  441. resolveContext: p.resolveContext,
  442. //TODO: maybe better pass parentDir as a param to Resolve() instead
  443. parentDir: parentDir,
  444. parent: p,
  445. }
  446. dep.Resolve()
  447. p.resolveContext.cacheResolvedPackage(&dep)
  448. }
  449. if dep.isBuiltin || dep.Name == "C" {
  450. return
  451. }
  452. if isInternalImport(dep.Name) {
  453. p.deps = append(p.deps, dep.deps...)
  454. } else {
  455. p.deps = append(p.deps, dep)
  456. }
  457. }
  458. // isAncestor goes recursively up the chain of Pkgs to determine if the name provided is ever a
  459. // parent of the current Pkg.
  460. func (p *Pkg) isAncestor(name string) bool {
  461. if p.parent == nil {
  462. return false
  463. }
  464. if p.parent.Name == name {
  465. return true
  466. }
  467. return p.parent.isAncestor(name)
  468. }
  469. // cleanName returns a cleaned version of the Pkg name used for resolving dependencies.
  470. //
  471. // If an empty string is returned, dependencies should not be resolved.
  472. func (p *Pkg) cleanName() string {
  473. name := p.Name
  474. // C 'package' cannot be resolved.
  475. if name == "C" {
  476. return ""
  477. }
  478. // Internal golang_org/* packages must be prefixed with vendor/
  479. //
  480. // Thanks to @davecheney for this:
  481. // https://github.com/davecheney/graphpkg/blob/master/main.go#L46
  482. if strings.HasPrefix(name, "golang_org") {
  483. name = path.Join("vendor", name)
  484. }
  485. return name
  486. }
  487. func isInternalImport(importPath string) bool {
  488. return strings.Contains(importPath, "/internal/")
  489. }
  490. // sortablePkgsList ensures a slice of Pkgs are sorted such that the builtin stdlib
  491. // packages are always above external packages (ie. github.com/whatever).
  492. type sortablePkgsList []Pkg
  493. func (b sortablePkgsList) Len() int {
  494. return len(b)
  495. }
  496. func (b sortablePkgsList) Swap(i, j int) {
  497. b[i], b[j] = b[j], b[i]
  498. }
  499. func (b sortablePkgsList) Less(i, j int) bool {
  500. if b[i].isBuiltin && !b[j].isBuiltin {
  501. return true
  502. } else if !b[i].isBuiltin && b[j].isBuiltin {
  503. return false
  504. }
  505. return b[i].Name < b[j].Name
  506. }