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

     1  // Package cocoapods implements Cocoapods analysis.
     2  //
     3  // A `BuildTarget` for Cocoapods is the path to the directory with the Podfile.
     4  package cocoapods
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/mitchellh/mapstructure"
    13  
    14  	"github.com/apex/log"
    15  	"github.com/fossas/fossa-cli/buildtools/cocoapods"
    16  	"github.com/fossas/fossa-cli/exec"
    17  	"github.com/fossas/fossa-cli/files"
    18  	"github.com/fossas/fossa-cli/graph"
    19  	"github.com/fossas/fossa-cli/module"
    20  	"github.com/fossas/fossa-cli/pkg"
    21  )
    22  
    23  type Analyzer struct {
    24  	PodCmd     string
    25  	PodVersion string
    26  
    27  	Pod cocoapods.Cocoapods
    28  
    29  	Module  module.Module
    30  	Options Options
    31  }
    32  
    33  type Options struct{}
    34  
    35  func New(m module.Module) (*Analyzer, error) {
    36  	// Set Cocoapods context variables
    37  	podCmd, podVersion, err := exec.Which("--version", os.Getenv("COCOAPODS_BINARY"), "pod")
    38  	if err != nil {
    39  		log.Debugf("could not find Cocoapods binary (try setting $COCOAPODS_BINARY): %s", err.Error())
    40  	}
    41  
    42  	// Parse and validate options.
    43  	var options Options
    44  	err = mapstructure.Decode(m.Options, &options)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	log.WithField("options", options).Debug("parsed analyzer options")
    49  
    50  	// Edge case: `Dir` is not set when passing module configurations from the command line.
    51  	// TODO: we really need to refactor the Module struct.
    52  	if m.Dir == "" {
    53  		m.Dir = m.BuildTarget
    54  	}
    55  
    56  	analyzer := Analyzer{
    57  		PodCmd:     podCmd,
    58  		PodVersion: podVersion,
    59  
    60  		Pod: cocoapods.Cocoapods{
    61  			Bin: podCmd,
    62  		},
    63  
    64  		Module:  m,
    65  		Options: options,
    66  	}
    67  
    68  	log.WithField("analyzer", analyzer).Debug("constructed analyzer")
    69  	return &analyzer, nil
    70  }
    71  
    72  // Discover constructs modules in all directories with a `Podfile`.
    73  func Discover(dir string, options map[string]interface{}) ([]module.Module, error) {
    74  	var modules []module.Module
    75  	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
    76  		if err != nil {
    77  			log.WithError(err).WithField("path", path).Debug("error while walking for discovery")
    78  		}
    79  
    80  		if !info.IsDir() && info.Name() == "Podfile" {
    81  			moduleName := filepath.Base(path)
    82  
    83  			log.WithFields(log.Fields{
    84  				"path": path,
    85  				"name": moduleName,
    86  			}).Debug("constructing Cocoapods module")
    87  			relPath, _ := filepath.Rel(dir, path)
    88  			modules = append(modules, module.Module{
    89  				Name:        moduleName,
    90  				Type:        pkg.Cocoapods,
    91  				BuildTarget: filepath.Dir(relPath),
    92  				Dir:         filepath.Dir(relPath),
    93  			})
    94  		}
    95  
    96  		return nil
    97  	})
    98  
    99  	if err != nil {
   100  		return nil, fmt.Errorf("Could not find Cocoapods package manifests: %s", err.Error())
   101  	}
   102  	return modules, nil
   103  }
   104  
   105  // IsBuilt checks whether `Podfile.lock` exists
   106  func (a *Analyzer) IsBuilt() (bool, error) {
   107  	log.Debugf("Checking Cocoapods build: %#v", a.Module)
   108  
   109  	isBuilt, err := files.Exists(a.Module.Dir, "Podfile.lock")
   110  	if err != nil {
   111  		return false, err
   112  	}
   113  
   114  	log.Debugf("Done checking Cocoapods build: %#v", isBuilt)
   115  	return isBuilt, nil
   116  }
   117  
   118  func (a *Analyzer) Clean() error {
   119  	log.Warn("Clean is not implemented for Cocoapods")
   120  	return nil
   121  }
   122  
   123  func (a *Analyzer) Build() error {
   124  	return a.Pod.Install(a.Module.Dir)
   125  }
   126  
   127  func (a *Analyzer) Analyze() (graph.Deps, error) {
   128  	lockfile, err := cocoapods.FromLockfile(a.Module.Dir, "Podfile.lock")
   129  	if err != nil {
   130  		return graph.Deps{}, err
   131  	}
   132  
   133  	// Construct a map of spec repositories.
   134  	specRepos := make(map[string]string)
   135  	for repo, pods := range lockfile.SpecRepos {
   136  		for _, pod := range pods {
   137  			specRepos[pod] = repo
   138  		}
   139  	}
   140  
   141  	// Construct a map of all dependencies.
   142  	nameToID := make(map[string]pkg.ID)
   143  	for _, pod := range lockfile.Pods {
   144  		// Get pod name: strip subspecs.
   145  		name := PodName(pod.Name)
   146  		id := pkg.ID{
   147  			Type:     pkg.Cocoapods,
   148  			Name:     name,
   149  			Revision: pod.Version,
   150  		}
   151  
   152  		// Check if this pod is from an external source.
   153  		if source, ok := lockfile.ExternalSources[pod.Name]; ok {
   154  			if source.Git != "" {
   155  				// Check if this pod is from a git repository.
   156  				git, ok := lockfile.CheckoutOptions[pod.Name]
   157  				if !ok {
   158  					log.Warnf("Could not identify commit of git repository pod: %s", pod.Name)
   159  				} else {
   160  					// TODO: we should probably set this on Location instead, but this is
   161  					// a quick hack because otherwise we'd have to special-case pod
   162  					// handling to check Location.
   163  					id.Type = pkg.Git
   164  					id.Name = git.Git
   165  					id.Revision = git.Commit
   166  				}
   167  			} else if source.Path != "" {
   168  				// Check if this pod is vendored: we don't support this, but we can try
   169  				// to look the name up as a Cocoapod.
   170  			} else {
   171  				log.Warnf("Could not identify externally sourced pod: %s", pod.Name)
   172  			}
   173  		}
   174  
   175  		nameToID[name] = id
   176  	}
   177  
   178  	// Construct dependency graph edges.
   179  	deps := make(map[pkg.ID]pkg.Package)
   180  	for _, pod := range lockfile.Pods {
   181  		id := nameToID[PodName(pod.Name)]
   182  
   183  		var imports []pkg.Import
   184  		for _, dep := range pod.Dependencies {
   185  			imports = append(imports, pkg.Import{
   186  				Target:   dep.String(),
   187  				Resolved: nameToID[PodName(dep.Name)],
   188  			})
   189  		}
   190  
   191  		deps[id] = pkg.Package{
   192  			ID:      id,
   193  			Imports: imports,
   194  		}
   195  	}
   196  
   197  	// Construct direct dependencies list.
   198  	var imports []pkg.Import
   199  	for _, dep := range lockfile.Dependencies {
   200  		imports = append(imports, pkg.Import{
   201  			Target:   dep.String(),
   202  			Resolved: nameToID[PodName(dep.Name)],
   203  		})
   204  	}
   205  
   206  	return graph.Deps{
   207  		Direct:     imports,
   208  		Transitive: deps,
   209  	}, nil
   210  }
   211  
   212  // PodName strips subspecs. See https://guides.cocoapods.org/syntax/podspec.html#subspec
   213  // for details.
   214  func PodName(spec string) string {
   215  	return strings.Split(spec, "/")[0]
   216  }