Skip to content

Commit 1e27f85

Browse files
anuccio1elldritch
authored andcommitted
feat(builders): #44 add Cocoapods integration with path data (#130)
* added Cocoapods integration with path data * fix pod regex * add ios as case. version lock cocoapods gem
1 parent 75e6747 commit 1e27f85

File tree

7 files changed

+804
-1
lines changed

7 files changed

+804
-1
lines changed

Dockerfile

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ RUN wget https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip -O
4848
sudo chmod -R 775 /opt/android-sdk
4949
ENV PATH=$PATH:/opt/android-sdk/tools/bin ANDROID_HOME=/opt/android-sdk
5050

51+
# Install Cocoapods
52+
RUN sudo gem install cocoapods -v 0.39.0
53+
5154
# Install Go compiler
5255
RUN wget https://dl.google.com/go/go1.9.4.linux-amd64.tar.gz -O /tmp/go.tar.gz && \
5356
sudo tar -xf /tmp/go.tar.gz -C /usr/local

builders/builder.go

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ func New(moduleType module.Type) module.Builder {
99
switch moduleType {
1010
case module.Bower:
1111
return &BowerBuilder{}
12+
case module.Cocoapods:
13+
return &CocoapodsBuilder{}
1214
case module.Composer:
1315
return &ComposerBuilder{}
1416
case module.Golang:

builders/cocoapods.go

+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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+
}

module/module.go

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ func New(moduleType Type, conf Config) (Module, error) {
4848
case Bower:
4949
manifestName = "bower.json"
5050
break
51+
case Cocoapods:
52+
manifestName = "Podfile"
53+
break
5154
case Composer:
5255
manifestName = "composer.json"
5356
break

module/types.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const (
5555

5656
// Bower is the module type for bower.io
5757
Bower = Type("bower")
58+
// Cocoapods is the module type for cocoapods
59+
Cocoapods = Type("cocoapods")
5860
// Composer is the module type for getcomposer.org
5961
Composer = Type("composer")
6062
// Maven is the module type for maven.apache.org
@@ -83,7 +85,7 @@ const (
8385
)
8486

8587
// Types holds the list of all available module types for analysis
86-
var Types = []Type{Bower, Composer, Maven, SBT, Gradle, NuGet, Pip, Ruby, Nodejs, Golang, VendoredArchives}
88+
var Types = []Type{Bower, Cocoapods, Composer, Maven, SBT, Gradle, NuGet, Pip, Ruby, Nodejs, Golang, VendoredArchives}
8789

8890
// Parse returns a module Type given a string
8991
func Parse(key string) (Type, error) {
@@ -98,6 +100,14 @@ func Parse(key string) (Type, error) {
98100
case "bower":
99101
return Bower, nil
100102

103+
// Cocoapods aliases
104+
case "ios":
105+
fallthrough
106+
case "pod":
107+
fallthrough
108+
case "cocoapods":
109+
return Cocoapods, nil
110+
101111
// Compower aliases
102112
case "composer":
103113
return Composer, nil

test/fixtures/cocoapods/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pods

0 commit comments

Comments
 (0)