|
| 1 | +package builders |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "io/ioutil" |
| 7 | + "os" |
| 8 | + "path/filepath" |
| 9 | + "regexp" |
| 10 | + "strings" |
| 11 | + |
| 12 | + "github.com/bmatcuk/doublestar" |
| 13 | + logging "github.com/op/go-logging" |
| 14 | + yaml "gopkg.in/yaml.v2" |
| 15 | + |
| 16 | + "github.com/fossas/fossa-cli/module" |
| 17 | +) |
| 18 | + |
| 19 | +var cocoapodsLogger = logging.MustGetLogger("cocoapods") |
| 20 | + |
| 21 | +const podNameRegex = `[\w/\.\-\\+]+` |
| 22 | + |
| 23 | +// CocoapodsBuilder implements Builder for Cocoapods (podfile & podfile.lock) builds |
| 24 | +type CocoapodsBuilder struct { |
| 25 | + CocoapodsCmd string |
| 26 | + CocoapodsVersion string |
| 27 | +} |
| 28 | + |
| 29 | +// CocoapodsModule implements Dependency for Cocoapods builds |
| 30 | +type CocoapodsModule struct { |
| 31 | + Name string `json:"name"` |
| 32 | + Version string `json:"version"` |
| 33 | +} |
| 34 | + |
| 35 | +// Fetcher always returns pod for CocoapodsModule |
| 36 | +func (m CocoapodsModule) Fetcher() string { |
| 37 | + return "pod" |
| 38 | +} |
| 39 | + |
| 40 | +// Package returns the package spec for CocoapodsModule |
| 41 | +func (m CocoapodsModule) Package() string { |
| 42 | + return m.Name |
| 43 | +} |
| 44 | + |
| 45 | +// Revision returns the version spec for CocoapodsModule |
| 46 | +func (m CocoapodsModule) Revision() string { |
| 47 | + return m.Version |
| 48 | +} |
| 49 | + |
| 50 | +// PodFileLock models Podfile.lock yaml file |
| 51 | +/* |
| 52 | + Note: we set Pods to `[]interface{}` because Golang doesn't support ADT's. |
| 53 | + The PODS section of Podfile.lock is actually [](map[string][]string | string) |
| 54 | +*/ |
| 55 | +type PodFileLock struct { |
| 56 | + Pods []interface{} `yaml:"PODS,omitempty"` // transitive deps + path |
| 57 | + Dependencies []string `yaml:"DEPENDENCIES,omitempty"` // top level deps |
| 58 | + CheckoutOptions map[string]map[string]string `yaml:"CHECKOUT OPTIONS,omitempty"` |
| 59 | +} |
| 60 | + |
| 61 | +// Read and parse `podfile.lock` |
| 62 | +func (p *PodFileLock) initLockFile(filePath string) error { |
| 63 | + bytes, err := ioutil.ReadFile(filePath) |
| 64 | + if err != nil { |
| 65 | + return fmt.Errorf("could not read Podfile.lock : %s", err.Error()) |
| 66 | + } |
| 67 | + |
| 68 | + err = yaml.Unmarshal(bytes, &p) |
| 69 | + if err != nil { |
| 70 | + return fmt.Errorf("could not parse Podfile.lock : %s", err.Error()) |
| 71 | + } |
| 72 | + |
| 73 | + return nil |
| 74 | +} |
| 75 | + |
| 76 | +func extractModule(fullDepStr string) CocoapodsModule { |
| 77 | + outputMatchRe := regexp.MustCompile(`(` + podNameRegex + `)\s+\(([\w/\.\-\\+=\s]+)\)`) |
| 78 | + match := outputMatchRe.FindStringSubmatch(fullDepStr) |
| 79 | + |
| 80 | + return CocoapodsModule{ |
| 81 | + Name: strings.Split(match[1], "/")[0], |
| 82 | + Version: match[2], |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +// used to grab the pod name from the line |
| 87 | +func extractPodName(fullDepStr string) string { |
| 88 | + outputMatchRe := regexp.MustCompile(podNameRegex) |
| 89 | + match := outputMatchRe.FindStringSubmatch(fullDepStr) |
| 90 | + |
| 91 | + return strings.Split(match[0], "/")[0] |
| 92 | +} |
| 93 | + |
| 94 | +// Initialize collects metadata on Cocoapods |
| 95 | +func (builder *CocoapodsBuilder) Initialize() error { |
| 96 | + cocoapodsLogger.Debug("Initializing Cocoapods builder...") |
| 97 | + |
| 98 | + // Set Ruby context variables |
| 99 | + cocoapodsCmd, cocoapodsVersion, err := which("--version", os.Getenv("COCOAPODS_BINARY"), "pod") |
| 100 | + if err != nil { |
| 101 | + cocoapodsLogger.Warningf("Could not find Pod binary (try setting $COCOAPODS_BINARY): %s", err.Error()) |
| 102 | + } |
| 103 | + builder.CocoapodsCmd = cocoapodsCmd |
| 104 | + builder.CocoapodsVersion = strings.TrimRight(cocoapodsVersion, "\n") |
| 105 | + |
| 106 | + cocoapodsLogger.Debugf("Initialized Cocoapods builder: %#v", builder) |
| 107 | + return nil |
| 108 | +} |
| 109 | + |
| 110 | +// Build runs `pod install` |
| 111 | +func (builder *CocoapodsBuilder) Build(m module.Module, force bool) error { |
| 112 | + cocoapodsLogger.Debugf("Running Cocoapods build: %#v %#v", m, force) |
| 113 | + |
| 114 | + _, _, err := runLogged(cocoapodsLogger, m.Dir, builder.CocoapodsCmd, "install") |
| 115 | + if err != nil { |
| 116 | + return fmt.Errorf("could not run Cocoapods build: %s", err.Error()) |
| 117 | + } |
| 118 | + |
| 119 | + cocoapodsLogger.Debug("Done running Cocoapods build.") |
| 120 | + return nil |
| 121 | +} |
| 122 | + |
| 123 | +// Analyze parses the `podfile.lock` YAML file and analyzes |
| 124 | +func (builder *CocoapodsBuilder) Analyze(m module.Module, allowUnresolved bool) ([]module.Dependency, error) { |
| 125 | + cocoapodsLogger.Debugf("Running Cocoapods analysis: %#v %#v", m, allowUnresolved) |
| 126 | + var podLockfile PodFileLock |
| 127 | + |
| 128 | + currentLockfile := filepath.Join(m.Dir, "Podfile.lock") |
| 129 | + err := podLockfile.initLockFile(currentLockfile) |
| 130 | + if err != nil { |
| 131 | + return nil, fmt.Errorf("could not read and initialize Podfile.lock at %s: %s", currentLockfile, err.Error()) |
| 132 | + } |
| 133 | + |
| 134 | + topLevelDeps := make(map[string]bool) // This is a "Set" |
| 135 | + gitDepMap := make(map[string]module.Locator) // This is for git deps included in podfile |
| 136 | + allDepsMap := make(map[string]module.Locator) // This is the final map of pod Name to Locator |
| 137 | + |
| 138 | + // We get all top level deps for accurate Path data (PODS lists a flat dep list) |
| 139 | + for _, dep := range podLockfile.Dependencies { |
| 140 | + depName := extractPodName(dep) |
| 141 | + topLevelDeps[depName] = true |
| 142 | + } |
| 143 | + |
| 144 | + // We check if any of the deps included are actually git dependencies |
| 145 | + for depName, checkoutOption := range podLockfile.CheckoutOptions { |
| 146 | + if checkoutOption[":git"] != "" && checkoutOption[":commit"] != "" { |
| 147 | + depName = strings.Split(depName, "/")[0] |
| 148 | + gitDepMap[depName] = module.Locator{ |
| 149 | + Fetcher: "git", |
| 150 | + Project: checkoutOption[":git"], |
| 151 | + Revision: checkoutOption[":commit"], |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + importMap := make(map[string][]string) // maps parent deps to all transitive deps |
| 157 | + var imports []Imported |
| 158 | + root := module.Locator{ |
| 159 | + Fetcher: "root", |
| 160 | + Project: "root", |
| 161 | + Revision: "", |
| 162 | + } |
| 163 | + |
| 164 | + // Pods in the yaml file can be either a string or a {string: []string} |
| 165 | + // It contains the Path and version data needed |
| 166 | + for _, directDep := range podLockfile.Pods { |
| 167 | + var currentLocator module.Locator |
| 168 | + var currentTransitiveDeps []interface{} |
| 169 | + var parentDep CocoapodsModule |
| 170 | + |
| 171 | + // here we attempt to cast to `map[interface{}]interface{}` then `string` |
| 172 | + if mapDep, isMap := directDep.(map[interface{}]interface{}); isMap { |
| 173 | + for dep, uncastedTransitiveDeps := range mapDep { |
| 174 | + transitiveDeps, transOk := uncastedTransitiveDeps.([]interface{}) |
| 175 | + depStr, depOk := dep.(string) |
| 176 | + if !depOk || !transOk { |
| 177 | + return nil, fmt.Errorf("malformed Podfile.lock file") |
| 178 | + } |
| 179 | + currentTransitiveDeps = transitiveDeps |
| 180 | + parentDep = extractModule(depStr) |
| 181 | + } |
| 182 | + } else if stringDep, isString := directDep.(string); isString { |
| 183 | + parentDep = extractModule(stringDep) |
| 184 | + } else { |
| 185 | + return nil, fmt.Errorf("malformed Podfile.lock file") |
| 186 | + } |
| 187 | + |
| 188 | + // substitute with git dep if brought in through github |
| 189 | + if val, ok := gitDepMap[parentDep.Name]; ok { |
| 190 | + currentLocator = val |
| 191 | + } else { |
| 192 | + currentLocator = module.Locator{ |
| 193 | + Fetcher: "pod", |
| 194 | + Project: parentDep.Name, |
| 195 | + Revision: parentDep.Version, |
| 196 | + } |
| 197 | + } |
| 198 | + // We set the locator of the dep here (if not already set) |
| 199 | + if _, depSet := allDepsMap[parentDep.Name]; !depSet { |
| 200 | + allDepsMap[parentDep.Name] = currentLocator |
| 201 | + } |
| 202 | + |
| 203 | + // add root as direct parent to import path if a top level dep (deduping occurs later on) |
| 204 | + if _, ok := topLevelDeps[parentDep.Name]; ok { |
| 205 | + imports = append(imports, Imported{ |
| 206 | + Locator: currentLocator, |
| 207 | + From: append(module.ImportPath{}, root), |
| 208 | + }) |
| 209 | + } |
| 210 | + |
| 211 | + // group transitive deps by parents |
| 212 | + if currentTransitiveDeps != nil { |
| 213 | + for _, dep := range currentTransitiveDeps { |
| 214 | + depStr, ok := dep.(string) |
| 215 | + if !ok { |
| 216 | + continue |
| 217 | + } |
| 218 | + currentTransitiveDepName := extractPodName(depStr) |
| 219 | + if parentDep.Name != currentTransitiveDepName { // Adjust may have Adjust/Core listed as a dep for example, so we ignore that |
| 220 | + importMap[parentDep.Name] = append(importMap[parentDep.Name], currentTransitiveDepName) |
| 221 | + } |
| 222 | + } |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + // Now that we have the locator for all deps, lets add the correct imports |
| 227 | + for parentDep, transitiveDeps := range importMap { |
| 228 | + duplicateDepMap := make(map[string]bool) |
| 229 | + for _, dep := range transitiveDeps { |
| 230 | + if duplicateDepMap[dep] != true { |
| 231 | + duplicateDepMap[dep] = true |
| 232 | + imports = append(imports, Imported{ |
| 233 | + Locator: allDepsMap[dep], |
| 234 | + From: module.ImportPath{root, allDepsMap[parentDep]}, |
| 235 | + }) |
| 236 | + } |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + deps := computeImportPaths(imports) |
| 241 | + |
| 242 | + cocoapodsLogger.Debugf("Done running Pod analysis: %#v", deps) |
| 243 | + return deps, nil |
| 244 | +} |
| 245 | + |
| 246 | +// IsBuilt checks whether `Podfile.lock` exists |
| 247 | +func (builder *CocoapodsBuilder) IsBuilt(m module.Module, allowUnresolved bool) (bool, error) { |
| 248 | + cocoapodsLogger.Debugf("Checking Cocoapods build: %#v %#v", m, allowUnresolved) |
| 249 | + |
| 250 | + isBuilt, err := hasFile(m.Dir, "Podfile.lock") |
| 251 | + if err != nil { |
| 252 | + return false, fmt.Errorf("could not find Podfile.lock file: %s", err.Error()) |
| 253 | + } |
| 254 | + |
| 255 | + cocoapodsLogger.Debugf("Done checking Cocoapods build: %#v", isBuilt) |
| 256 | + return isBuilt, nil |
| 257 | +} |
| 258 | + |
| 259 | +// IsModule is not implemented |
| 260 | +func (builder *CocoapodsBuilder) IsModule(target string) (bool, error) { |
| 261 | + return false, errors.New("IsModule is not implemented for CocoapodsBuilder") |
| 262 | +} |
| 263 | + |
| 264 | +// DiscoverModules returns ModuleConfigs that match Podfile(.lock) in the directory |
| 265 | +func (builder *CocoapodsBuilder) DiscoverModules(dir string) ([]module.Config, error) { |
| 266 | + cococapodsFilePaths, err := doublestar.Glob(filepath.Join(dir, "**", "Podfile")) |
| 267 | + if err != nil { |
| 268 | + return nil, err |
| 269 | + } |
| 270 | + var moduleConfigs []module.Config |
| 271 | + for _, path := range cococapodsFilePaths { |
| 272 | + podName := filepath.Base(filepath.Dir(path)) |
| 273 | + |
| 274 | + cocoapodsLogger.Debugf("Found Cocoapods package: %s (%s)", path, podName) |
| 275 | + path, _ = filepath.Rel(dir, path) |
| 276 | + moduleConfigs = append(moduleConfigs, module.Config{ |
| 277 | + Name: podName, |
| 278 | + Path: path, |
| 279 | + Type: string(module.Cocoapods), |
| 280 | + }) |
| 281 | + } |
| 282 | + return moduleConfigs, nil |
| 283 | +} |
0 commit comments