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 }