123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617 |
- package main
- import (
- "encoding/json"
- "flag"
- "fmt"
- "os"
- "sort"
- "strings"
- "go/build"
- "path/filepath"
- "path"
- )
- func prettyPrintJSON(j interface{}) {
- e := json.NewEncoder(os.Stdout)
- e.SetIndent("", " ")
- e.Encode(j)
- }
- func main() {
- flag.Usage = func() {
- fmt.Println(` Scans the imports from all Go packages (and subpackages) rooted in current dir,
- and prints the dependency graph in a JSON format that can be imported via npmjs.com/graphlib.
- `)
- flag.PrintDefaults()
- fmt.Println("")
- }
- var ignoredPkgs = flag.String("ignoredPkgs", "", "Comma separated list of packages (canonically named) to ignore when scanning subfolders")
- var outputDOT = flag.Bool("dot", false, "Output as Graphviz DOT format")
- var outputList = flag.Bool("list", false, "Output a flat JSON array of all reachable deps")
- flag.Parse()
- ignoredPkgsList := strings.Split(*ignoredPkgs, ",")
- var rc ResolveContext
- err := rc.ResolvePath(".", ignoredPkgsList)
- if err != nil {
- panic(err)
- }
- graph := rc.GetGraph()
- if *outputDOT {
- fmt.Println(graph.ToDOT())
- } else if *outputList {
- prettyPrintJSON(graph.SortedNodeNames())
- } else {
- prettyPrintJSON(graph)
- }
- unresolved := rc.GetUnresolvedPackages()
- if len(unresolved) != 0 {
- fmt.Println("\nUnresolved packages:")
- sort.Strings(unresolved)
- for _, pkg := range unresolved {
- fmt.Println(" - ", pkg)
- }
- os.Exit(1)
- }
- }
- /*
- This code is based on https://github.com/KyleBanks/depth
- MIT License
- Copyright (c) 2017 Kyle Banks
- Permission is hereby granted, free of charge, to any person obtaining a copy
- of this software and associated documentation files (the "Software"), to deal
- in the Software without restriction, including without limitation the rights
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- copies of the Software, and to permit persons to whom the Software is
- furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- SOFTWARE.
- */
- /*
- package resolver
- import (
- "fmt"
- "go/build"
- "path/filepath"
- "strings"
- "./dirwalk"
- "./graph"
- )
- */
- // ResolveContext represents all the pkg trees rooted at all the subfolders with Go code.
- type ResolveContext struct {
- roots []*Pkg
- unresolvedPkgs map[string]struct{}
- pkgCache map[string]*Pkg
- importCache map[string]struct{}
- ignoredPkgs []string
- }
- // ResolvePath recursively finds all direct & transitive dependencies for all the packages (and sub-packages),
- // rooted at given path
- func (rc *ResolveContext) ResolvePath(rootPath string, ignoredPkgs []string) error {
- rc.init()
- rc.ignoredPkgs = ignoredPkgs
- abs, err := filepath.Abs(rootPath)
- if err != nil {
- return fmt.Errorf("filepath.Abs(%s) failed with: %s", rootPath, err.Error())
- }
- rootPath = abs
- virtualRootPkg, err := rc.resolveVirtualRoot(rootPath)
- if err != nil {
- return err
- }
- rc.roots = append(rc.roots, virtualRootPkg)
- return WalkGoFolders(rootPath, func(path string) error {
- rootPkg := rc.resolveFolder(path)
- if rootPkg.isResolved {
- rc.roots = append(rc.roots, rootPkg)
- }
- return nil
- })
- }
- // GetUnresolvedPackages returns a list of all the pkgs that failed to resolve
- func (rc ResolveContext) GetUnresolvedPackages() []string {
- unresolved := []string{}
- for pkg := range rc.unresolvedPkgs {
- unresolved = append(unresolved, pkg)
- }
- return unresolved
- }
- // GetGraph returns the graph of resolved packages
- func (rc *ResolveContext) GetGraph() Graph {
- nodesMap := map[string]Node{}
- edgesMap := map[string]Edge{}
- var recurse func(pkg *Pkg)
- recurse = func(pkg *Pkg) {
- _, exists := nodesMap[pkg.Name]
- if exists {
- return
- }
- node := Node{
- Name: pkg.Name,
- Value: *pkg,
- }
- nodesMap[pkg.Name] = node
- for _, child := range pkg.deps {
- edge := Edge{
- From: pkg.Name,
- To: child.Name,
- }
- edgesMap[pkg.Name+":"+child.Name] = edge
- recurse(&child)
- }
- }
- for _, r := range rc.roots {
- recurse(r)
- }
- var nodes []Node
- for _, v := range nodesMap {
- nodes = append(nodes, v)
- }
- var edges []Edge
- for _, v := range edgesMap {
- edges = append(edges, v)
- }
- return Graph{
- Nodes: nodes,
- Edges: edges,
- Options: Options{
- Directed: true,
- },
- }
- }
- func (rc *ResolveContext) init() {
- rc.roots = []*Pkg{}
- rc.importCache = map[string]struct{}{}
- rc.unresolvedPkgs = map[string]struct{}{}
- rc.pkgCache = map[string]*Pkg{}
- }
- func (rc *ResolveContext) resolveVirtualRoot(rootPath string) (*Pkg, error) {
- rootImport, err := build.Default.Import(".", rootPath, build.FindOnly)
- if err != nil {
- return nil, err
- }
- if rootImport.ImportPath == "" || rootImport.ImportPath == "." {
- return nil, fmt.Errorf("Can't resolve root package at %s.\nIs $GOPATH defined correctly?", rootPath)
- }
- virtualRootPkg := &Pkg{
- Name: ".",
- FullImportPath: rootImport.ImportPath,
- Dir: rootImport.Dir,
- }
- return virtualRootPkg, nil
- }
- func (rc *ResolveContext) resolveFolder(path string) *Pkg {
- rootPkg := &Pkg{
- Name: ".",
- resolveContext: rc,
- parentDir: path,
- }
- rootPkg.Resolve()
- rootPkg.Name = rootPkg.FullImportPath
- return rootPkg
- }
- // hasSeenImport returns true if the import name provided has already been seen within the tree.
- // This function only returns false for a name once.
- func (rc *ResolveContext) hasSeenImport(name string) bool {
- if _, ok := rc.importCache[name]; ok {
- return true
- }
- rc.importCache[name] = struct{}{}
- return false
- }
- func (rc *ResolveContext) markUnresolvedPkg(name string) {
- rc.unresolvedPkgs[name] = struct{}{}
- }
- func (rc *ResolveContext) cacheResolvedPackage(pkg *Pkg) {
- rc.pkgCache[pkg.Name] = pkg
- }
- func (rc *ResolveContext) getCachedPkg(name string) *Pkg {
- pkg, ok := rc.pkgCache[name]
- if !ok {
- return nil
- }
- return pkg
- }
- func (rc ResolveContext) shouldIgnorePkg(name string) bool {
- for _, ignored := range rc.ignoredPkgs {
- if name == ignored {
- return true
- }
- if strings.HasSuffix(ignored, "*") {
- // note that ignoring "url/to/pkg*" will also ignore "url/to/pkg-other",
- // this is quite confusing, but is dep's behaviour
- if strings.HasPrefix(name, strings.TrimSuffix(ignored, "*")) {
- return true
- }
- }
- }
- return false
- }
- /*
- package dirwalk
- import (
- "os"
- "path/filepath"
- "strings"
- )
- */
- // WalkGoFolders will call cb for every folder with Go code under the given root path,
- // unless it's:
- // - one of "vendor", "Godeps", "node_modules", "testdata", "internal"
- // - starts with "." or "_"
- // - is a test package, i.e. ends with _test
- func WalkGoFolders(root string, cb WalkFunc) error {
- err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
- // if it's not a folder (or a symlink to folder), do nothing
- if info.Mode()&os.ModeSymlink > 0 {
- if info, err = os.Stat(path); err != nil {
- if os.IsNotExist(err) {
- // ignore broken symlinks
- return nil
- }
- return err
- }
- }
- if !info.IsDir() {
- return nil
- }
- folderName := info.Name()
- switch folderName {
- case "vendor", "Godeps", "node_modules", "testdata", "internal":
- return filepath.SkipDir
- }
- if strings.HasSuffix(folderName, "_test") ||
- (folderName != "." && strings.HasPrefix(folderName, ".")) ||
- strings.HasPrefix(folderName, "_") {
- return filepath.SkipDir
- }
- gofiles, err := filepath.Glob(filepath.Join(path, "*.go"))
- if err != nil {
- return nil
- }
- if len(gofiles) > 0 {
- return cb(path)
- }
- return nil
- })
- return err
- }
- // WalkFunc defines the prototype for WalkGoFolders's callback.
- // the error passed as the return value of the undrelying filepath.Walk
- type WalkFunc func(path string) error
- /*
- package graph
- import (
- "fmt"
- "sort"
- )
- */
- // Node is Grpah's node
- type Node struct {
- Name string `json:"v"`
- Value interface{} `json:"value"`
- }
- // Edge is Graph's edge
- type Edge struct {
- From string `json:"v"`
- To string `json:"w"`
- }
- // Options is Graph's options
- type Options struct {
- Directed bool `json:"directed"`
- Multigraph bool `json:"multigraph"`
- Compound bool `json:"compound"`
- }
- // Graph is graph that when marshaled to JSON can be imported via Graphlib JS pkg from NPM
- type Graph struct {
- Nodes []Node `json:"nodes"`
- Edges []Edge `json:"edges"`
- Options Options `json:"options"`
- }
- // ToDOT return graph as GraphViz .dot format string
- func (g Graph) ToDOT() string {
- dot := "digraph {\n"
- id := 0
- nodeIDs := map[string]int{}
- for _, n := range g.Nodes {
- nodeIDs[n.Name] = id
- dot += fmt.Sprintf("\t%d [label=\"%s\"]\n", id, n.Name)
- id++
- }
- dot += "\n"
- for _, e := range g.Edges {
- dot += fmt.Sprintf("\t%d -> %d;\n", nodeIDs[e.From], nodeIDs[e.To])
- }
- dot += "}\n"
- return dot
- }
- // SortedNodeNames returns a sorted list of all the node names
- func (g Graph) SortedNodeNames() []string {
- names := []string{}
- for _, n := range g.Nodes {
- names = append(names, n.Name)
- }
- sort.Strings(names)
- return names
- }
- /*
- package resolver
- import (
- "go/build"
- "path"
- "sort"
- "strings"
- )
- */
- // Pkg represents a Go source package, and its dependencies.
- type Pkg struct {
- Name string
- FullImportPath string
- Dir string
- raw *build.Package
- isBuiltin bool
- isResolved bool
- parentDir string
- deps []Pkg
- parent *Pkg
- resolveContext *ResolveContext
- }
- // Resolve recursively finds all dependencies for the Pkg and the packages it depends on.
- func (p *Pkg) Resolve() {
- // isResolved is always true, regardless of if we skip the import,
- // it is only false if there is an error while importing.
- p.isResolved = true
- name := p.cleanName()
- if name == "" {
- return
- }
- // Stop resolving imports if we've reached a loop.
- var importMode build.ImportMode
- if p.resolveContext.hasSeenImport(name) && p.isAncestor(name) {
- importMode = build.FindOnly
- }
- pkg, err := build.Default.Import(name, p.parentDir, importMode)
- if err != nil {
- // TODO: Check the error type?
- p.isResolved = false
- // this is package we dediced to scan, and probably shouldn't have.
- // probably can remove this when we have handling of build tags
- if name != "." {
- p.resolveContext.markUnresolvedPkg(name)
- }
- return
- }
- if name == "." && p.resolveContext.shouldIgnorePkg(pkg.ImportPath) {
- p.isResolved = false
- return
- }
- p.raw = pkg
- p.Dir = pkg.Dir
- // Clear some too verbose fields
- p.raw.ImportPos = nil
- p.raw.TestImportPos = nil
- // Update the name with the fully qualified import path.
- p.FullImportPath = pkg.ImportPath
- // If this is an builtin package, we don't resolve deeper
- if pkg.Goroot {
- p.isBuiltin = true
- return
- }
- imports := pkg.Imports
- p.setDeps(imports, pkg.Dir)
- }
- // setDeps takes a slice of import paths and the source directory they are relative to,
- // and creates the deps of the Pkg. Each dependency is also further resolved prior to being added
- // to the Pkg.
- func (p *Pkg) setDeps(imports []string, parentDir string) {
- unique := make(map[string]struct{})
- for _, imp := range imports {
- // Mostly for testing files where cyclic imports are allowed.
- if imp == p.Name {
- continue
- }
- // Skip duplicates.
- if _, ok := unique[imp]; ok {
- continue
- }
- unique[imp] = struct{}{}
- if p.resolveContext.shouldIgnorePkg(imp) {
- continue
- }
- p.addDep(imp, parentDir)
- }
- sort.Sort(sortablePkgsList(p.deps))
- }
- // addDep creates a Pkg and it's dependencies from an imported package name.
- func (p *Pkg) addDep(name string, parentDir string) {
- var dep Pkg
- cached := p.resolveContext.getCachedPkg(name)
- if cached != nil {
- dep = *cached
- dep.parentDir = parentDir
- dep.parent = p
- } else {
- dep = Pkg{
- Name: name,
- resolveContext: p.resolveContext,
- //TODO: maybe better pass parentDir as a param to Resolve() instead
- parentDir: parentDir,
- parent: p,
- }
- dep.Resolve()
- p.resolveContext.cacheResolvedPackage(&dep)
- }
- if dep.isBuiltin || dep.Name == "C" {
- return
- }
- if isInternalImport(dep.Name) {
- p.deps = append(p.deps, dep.deps...)
- } else {
- p.deps = append(p.deps, dep)
- }
- }
- // isAncestor goes recursively up the chain of Pkgs to determine if the name provided is ever a
- // parent of the current Pkg.
- func (p *Pkg) isAncestor(name string) bool {
- if p.parent == nil {
- return false
- }
- if p.parent.Name == name {
- return true
- }
- return p.parent.isAncestor(name)
- }
- // cleanName returns a cleaned version of the Pkg name used for resolving dependencies.
- //
- // If an empty string is returned, dependencies should not be resolved.
- func (p *Pkg) cleanName() string {
- name := p.Name
- // C 'package' cannot be resolved.
- if name == "C" {
- return ""
- }
- // Internal golang_org/* packages must be prefixed with vendor/
- //
- // Thanks to @davecheney for this:
- // https://github.com/davecheney/graphpkg/blob/master/main.go#L46
- if strings.HasPrefix(name, "golang_org") {
- name = path.Join("vendor", name)
- }
- return name
- }
- func isInternalImport(importPath string) bool {
- return strings.Contains(importPath, "/internal/")
- }
- // sortablePkgsList ensures a slice of Pkgs are sorted such that the builtin stdlib
- // packages are always above external packages (ie. github.com/whatever).
- type sortablePkgsList []Pkg
- func (b sortablePkgsList) Len() int {
- return len(b)
- }
- func (b sortablePkgsList) Swap(i, j int) {
- b[i], b[j] = b[j], b[i]
- }
- func (b sortablePkgsList) Less(i, j int) bool {
- if b[i].isBuiltin && !b[j].isBuiltin {
- return true
- } else if !b[i].isBuiltin && b[j].isBuiltin {
- return false
- }
- return b[i].Name < b[j].Name
- }
|