github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/buildtools/stack/stack.go (about)

     1  package stack
     2  
     3  import (
     4  	"strconv"
     5  	"strings"
     6  
     7  	"github.com/fossas/fossa-cli/exec"
     8  	"github.com/fossas/fossa-cli/graph"
     9  	"github.com/fossas/fossa-cli/module"
    10  	"github.com/fossas/fossa-cli/pkg"
    11  )
    12  
    13  // ----- Dep graph retrieval
    14  
    15  func GetDeps(m module.Module) (graph.Deps, error) {
    16  	stackAllDeps, err := GetStackDependencies(m.Dir, 0)
    17  	if err != nil {
    18  		return graph.Deps{}, err
    19  	}
    20  
    21  	ghcPkgDeps, err := GetGhcPkgDepMap()
    22  	if err != nil {
    23  		return graph.Deps{}, err
    24  	}
    25  
    26  	stackImmediateDeps, err := GetStackDependencies(m.Dir, 1)
    27  	if err != nil {
    28  		return graph.Deps{}, err
    29  	}
    30  
    31  	return GetDepsPure(stackAllDeps, stackImmediateDeps, ghcPkgDeps), nil
    32  }
    33  
    34  func GetDepsPure(stackAllDeps []Dep, stackImmediateDeps []Dep, depMap GhcPkgDeps) graph.Deps {
    35  	// Our direct dependencies
    36  	var depGraph = make(map[pkg.ID]pkg.Package)
    37  
    38  	// Build out the full graph
    39  	for _, stackDep := range stackAllDeps {
    40  		pkgID := DepToPkgId(stackDep)
    41  
    42  		var imports []pkg.Import
    43  
    44  		for _, ghcPkgDep := range depMap[DepToCanonical(stackDep)] {
    45  			imports = append(imports, pkg.Import{
    46  				Resolved: DepToPkgId(DepFromCanonical(ghcPkgDep)),
    47  			})
    48  		}
    49  
    50  		depGraph[pkgID] = pkg.Package{
    51  			ID:      pkgID,
    52  			Imports: imports,
    53  		}
    54  	}
    55  
    56  	var directImports []pkg.Import
    57  
    58  	// Build our direct dependencies
    59  	for _, stackDep := range stackImmediateDeps {
    60  		pkgID := DepToPkgId(stackDep)
    61  
    62  		directImports = append(directImports, pkg.Import{
    63  			Resolved: pkgID,
    64  		})
    65  	}
    66  
    67  	return graph.Deps{
    68  		Direct:     directImports,
    69  		Transitive: depGraph,
    70  	}
    71  }
    72  
    73  // ----- Types
    74  
    75  // A stack dependency
    76  type Dep struct {
    77  	Name    string
    78  	Version string
    79  }
    80  
    81  func DepToPkgId(dep Dep) pkg.ID {
    82  	return pkg.ID{
    83  		Type:     pkg.Haskell,
    84  		Name:     dep.Name,
    85  		Revision: dep.Version,
    86  	}
    87  }
    88  
    89  // Canonical string representation of a ghc-pkg package of the form:
    90  // package-name-0.0.1.0
    91  type Canonical struct {
    92  	Identifier string
    93  }
    94  
    95  func DepToCanonical(dep Dep) Canonical {
    96  	return Canonical{Identifier: dep.Name + "-" + dep.Version}
    97  }
    98  
    99  func DepFromCanonical(canonical Canonical) Dep {
   100  	ix := strings.LastIndex(canonical.Identifier, "-")
   101  	return Dep{
   102  		Name:    canonical.Identifier[:ix],
   103  		Version: canonical.Identifier[ix+1:],
   104  	}
   105  }
   106  
   107  // A mapping of ghc-pkg packages to their dependencies
   108  type GhcPkgDeps = map[Canonical][]Canonical
   109  
   110  // ----- Command output parsing
   111  
   112  func ParseStackDependencies(output string) []Dep {
   113  	// Stack ls dependencies outputs deps in the form:
   114  	// packageone 0.0.1.0
   115  	// packagetwo 0.0.1.0
   116  	// ...
   117  
   118  	var deps []Dep
   119  
   120  	for _, line := range strings.Split(output, "\n") {
   121  		var dep = strings.Split(line, " ")
   122  
   123  		if len(dep) < 2 {
   124  			continue
   125  		}
   126  
   127  		var name = dep[0]
   128  		var version = dep[1]
   129  
   130  		deps = append(deps, Dep{
   131  			Name:    name,
   132  			Version: version,
   133  		})
   134  	}
   135  
   136  	return deps
   137  }
   138  
   139  func ParseGhcPkgDepMap(output string) GhcPkgDeps {
   140  	// ghc-pkg dot outputs deps in the form:
   141  	// digraph {
   142  	// "packageone-0.0.1.0" -> "packagetwo-0.0.1.0"
   143  	// "packageone-0.0.1.0" -> "packagethree-0.0.1.0"
   144  	// }
   145  	deps := make(GhcPkgDeps)
   146  
   147  	lines := strings.Split(output, "\n")
   148  
   149  	for _, line := range lines {
   150  		line = strings.ReplaceAll(line, "\"", "")
   151  
   152  		split := strings.Split(line, " -> ")
   153  		if len(split) < 2 {
   154  			continue // The first and last lines are "digraph {" and "}", so they won't have a dep
   155  		}
   156  
   157  		from := Canonical{Identifier: split[0]}
   158  		to := Canonical{Identifier: split[1]}
   159  
   160  		cur := deps[from]
   161  		cur = append(cur, to)
   162  		deps[from] = cur
   163  	}
   164  
   165  	return deps
   166  }
   167  
   168  // ----- Command invocation
   169  
   170  func GetStackDependencies(dir string, depth int) ([]Dep, error) {
   171  	args := []string{"ls", "dependencies"}
   172  
   173  	if depth != 0 {
   174  		args = append(args, "--depth", strconv.Itoa(depth))
   175  	}
   176  
   177  	stdout, _, err := exec.Run(exec.Cmd{
   178  		Name: "stack",
   179  		Argv: args,
   180  		Dir:  dir,
   181  	})
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	return ParseStackDependencies(stdout), nil
   187  }
   188  
   189  func GetGhcPkgDepMap() (GhcPkgDeps, error) {
   190  	stdout, _, err := exec.Run(exec.Cmd{
   191  		Name: "stack",
   192  		Argv: []string{"exec", "--", "ghc-pkg", "dot"},
   193  	})
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	return ParseGhcPkgDepMap(stdout), nil
   199  }