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

     1  package cabal
     2  
     3  import (
     4  	"path/filepath"
     5  
     6  	"github.com/mitchellh/mapstructure"
     7  
     8  	"github.com/fossas/fossa-cli/errors"
     9  	"github.com/fossas/fossa-cli/exec"
    10  	"github.com/fossas/fossa-cli/files"
    11  	"github.com/fossas/fossa-cli/graph"
    12  	"github.com/fossas/fossa-cli/module"
    13  	"github.com/fossas/fossa-cli/pkg"
    14  )
    15  
    16  const planRelPath = "dist-newstyle/cache/plan.json"
    17  
    18  // ----- Dep graph retrieval
    19  
    20  func GetDeps(m module.Module) (graph.Deps, error) {
    21  	plan, err := getSolverPlan(m.Dir)
    22  	if err != nil {
    23  		return graph.Deps{}, err
    24  	}
    25  
    26  	return GetDepsPure(plan), nil
    27  }
    28  
    29  func GetDepsPure(plan Plan) graph.Deps {
    30  	// Index a list of install plans by their IDs
    31  	// this is used when building out the dependency graph
    32  	var installPlans = make(map[string]InstallPlan)
    33  	for _, p := range plan.InstallPlans {
    34  		installPlans[p.Id] = p
    35  	}
    36  
    37  	var dependencyGraph = make(map[pkg.ID]pkg.Package)
    38  	var directDependencies []pkg.Import
    39  
    40  	// Build the entire dependency graph, keeping track of our direct
    41  	// dependencies
    42  	for _, p := range plan.InstallPlans {
    43  		var builtPackage = installPlanToPackage(installPlans, p)
    44  
    45  		if isDirectDependency(p) {
    46  			directDependencies = append(directDependencies, builtPackage.Imports...)
    47  		}
    48  
    49  		dependencyGraph[builtPackage.ID] = builtPackage
    50  	}
    51  
    52  	return graph.Deps{
    53  		Direct:     directDependencies,
    54  		Transitive: dependencyGraph,
    55  	}
    56  }
    57  
    58  
    59  // ----- Types
    60  
    61  type Plan struct {
    62  	InstallPlans []InstallPlan `mapstructure:"install-plan"`
    63  }
    64  
    65  type InstallPlan struct {
    66  	// There are two types of install plans: `pre-existing` and `configured`.
    67  	// `pre-existing` corresponds to system-level libraries, and `configured`
    68  	// is for globally-installed and project-local dependencies. To
    69  	// differentiate, the `style` field is used.
    70  	Type    string `mapstructure:"type"`
    71  	Id      string `mapstructure:"id"`
    72  	Name    string `mapstructure:"pkg-name"`
    73  	Version string `mapstructure:"pkg-version"`
    74  
    75  	Components map[string]Component `mapstructure:"components"` // Not always present
    76  	Depends    []string             `mapstructure:"depends"`    // Dependencies can be defined here or in Components.*.Depends
    77  	// This field only exists for packages with Type `configured`. It can
    78  	// contain one of two values: `global` for globally-installed dependencies,
    79  	// or `local` for project-local dependencies
    80  	Style string `mapstructure:"style"` // Only exists for packages with type `configured`
    81  }
    82  
    83  func installPlanToID(plan InstallPlan) pkg.ID {
    84  	return pkg.ID{
    85  		Type:     pkg.Haskell,
    86  		Name:     plan.Name,
    87  		Revision: plan.Version,
    88  	}
    89  }
    90  
    91  func isDirectDependency(plan InstallPlan) bool {
    92  	// See documentation on InstallPlan
    93  	return plan.Type == "configured" && plan.Style == "local"
    94  }
    95  
    96  type Component struct {
    97  	Depends []string `mapstructure:"depends"`
    98  }
    99  
   100  // ----- Command invocation
   101  
   102  func getSolverPlan(dir string) (Plan, error) {
   103  	cabalPlanPath := filepath.Join(dir, planRelPath)
   104  
   105  	// If plan.json doesn't exist, generate it
   106  	if exists, _ := files.Exists(cabalPlanPath); !exists {
   107  		_, _, err := exec.Run(exec.Cmd{
   108  			Name: "cabal",
   109  			Argv: []string{"v2-build", "--dry-run"},
   110  			Dir:  dir,
   111  		})
   112  		if err != nil {
   113  			return Plan{}, err
   114  		}
   115  	}
   116  
   117  	if exists, _ := files.Exists(cabalPlanPath); !exists {
   118  		// TODO: fallback to another strategy?
   119  		return Plan{}, errors.New("couldn't find or generate cabal solver plan")
   120  	}
   121  
   122  	// Parse cabal v2-build's build plan
   123  	var rawPlan map[string]interface{}
   124  	var plan Plan
   125  
   126  	if err := files.ReadJSON(&rawPlan, cabalPlanPath); err != nil {
   127  		return Plan{}, err
   128  	}
   129  	if err := mapstructure.Decode(rawPlan, &plan); err != nil {
   130  		return Plan{}, err
   131  	}
   132  
   133  	return plan, nil
   134  }
   135  
   136  func installPlanToPackage(installPlans map[string]InstallPlan, plan InstallPlan) pkg.Package {
   137  	var imports []pkg.Import
   138  
   139  	for _, depId := range plan.Depends {
   140  		dep := installPlans[depId]
   141  
   142  		imports = append(imports, pkg.Import{
   143  			Resolved: installPlanToID(dep),
   144  		})
   145  	}
   146  
   147  	for _, component := range plan.Components {
   148  		for _, depId := range component.Depends {
   149  			dep := installPlans[depId]
   150  
   151  			imports = append(imports, pkg.Import{
   152  				Resolved: installPlanToID(dep),
   153  			})
   154  		}
   155  	}
   156  
   157  	return pkg.Package{
   158  		ID:      installPlanToID(plan),
   159  		Imports: imports,
   160  	}
   161  }