Skip to content

Commit a777e78

Browse files
AyanSinhaMahapatrapombredanne
authored andcommitted
Add podspec.json and podfile.lock parsers
Adds functions to build scancode packages from podspec.json metadata files from cocoapods specs and also from podfile.lock files. Adds tests and testfiles. Signed-off-by: Ayan Sinha Mahapatra <[email protected]>
1 parent 884f829 commit a777e78

File tree

6 files changed

+601
-14
lines changed

6 files changed

+601
-14
lines changed

src/packagedcode/cocoapods.py

Lines changed: 301 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,26 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
import hashlib
11+
import json
1012
import logging
1113
import re
1214

1315
import attr
16+
import saneyaml
1417

1518
from commoncode import filetype
1619
from packagedcode import models
20+
from packagedcode.licensing import get_license_matches
21+
from packagedcode.licensing import get_license_expression_from_matches
1722
from packagedcode.spec import Spec
1823

24+
from packageurl import PackageURL
25+
1926
"""
2027
Handle cocoapods packages manifests for macOS and iOS
21-
including .podspec, Podfile and Podfile.lock files.
28+
and from Xcode projects, including .podspec, Podfile and Podfile.lock files,
29+
and .podspec.json files from https://github.com/CocoaPods/Specs.
2230
See https://cocoapods.org
2331
"""
2432

@@ -36,23 +44,89 @@
3644

3745
@attr.s()
3846
class CocoapodsPackage(models.Package):
39-
metafiles = ('*.podspec',)
40-
extensions = ('.podspec',)
47+
metafiles = (
48+
'*.podspec',
49+
'*podfile.lock',
50+
'*.podspec.json',
51+
)
52+
extensions = (
53+
'.podspec',
54+
'.lock',
55+
)
4156
default_type = 'pods'
4257
default_primary_language = 'Objective-C'
4358
default_web_baseurl = 'https://cocoapods.org'
44-
default_download_baseurl = None
45-
default_api_baseurl = None
59+
github_specs_repo_baseurl = 'https://raw.githubusercontent.com/CocoaPods/Specs/blob/master/Specs'
60+
default_cdn_baseurl='https://cdn.cocoapods.org/Specs'
4661

4762
@classmethod
4863
def recognize(cls, location):
4964
yield parse(location)
5065

5166
def repository_homepage_url(self, baseurl=default_web_baseurl):
52-
return '{}/pods/{}'.format(baseurl, self.name)
67+
return f'{baseurl}/pods/{self.name}'
5368

5469
def repository_download_url(self):
55-
return '{}/archive/{}.zip'.format(self.homepage_url, self.version)
70+
if self.homepage_url:
71+
return f'{self.homepage_url}/archive/{self.version}.zip'
72+
elif self.reponame:
73+
return f'{self.reponame}/archive/refs/tags/{self.version}.zip'
74+
75+
def get_api_data_url(self):
76+
return self.specs_json_github_url
77+
78+
def get_code_view_url(self):
79+
if isinstance(self.reponame, str):
80+
return self.reponame+'/tree/'+self.version
81+
82+
def get_bug_tracking_url(self):
83+
if isinstance(self.reponame, str):
84+
return self.reponame+'/issues/'
85+
86+
def specs_json_cdn_url(self, baseurl=default_cdn_baseurl):
87+
return f'{baseurl}/{self.hashed_path}/{self.name}/{self.version}/{self.name}.podspec.json'
88+
89+
def specs_json_github_url(self, baseurl=github_specs_repo_baseurl):
90+
return f'{baseurl}/{self.hashed_path}/{self.name}/{self.version}/{self.name}.podspec.json'
91+
92+
@property
93+
def reponame(self):
94+
if isinstance(self.vcs_url, str):
95+
if self.vcs_url[-4:] == '.git':
96+
return self.vcs_url[:-4]
97+
98+
@property
99+
def hashed_path(self):
100+
"""
101+
Returns a string with a part of the file path derived from the md5 hash.
102+
103+
From https://github.com/CocoaPods/cdn.cocoapods.org:
104+
"There are a set of known prefixes for all Podspec paths, you take the name of the pod,
105+
create a SHA (using md5) of it and take the first three characters."
106+
"""
107+
podname = self.get_podname_proper(self.name)
108+
if self.name != podname:
109+
name_to_hash = podname
110+
else:
111+
name_to_hash = self.name
112+
113+
hash_init = self.get_first_3_mdf_hash_characters(name_to_hash)
114+
hashed_path = '/'.join(list(hash_init))
115+
return hashed_path
116+
117+
@staticmethod
118+
def get_first_3_mdf_hash_characters(podname):
119+
return hashlib.md5(podname.encode('utf-8')).hexdigest()[0:3]
120+
121+
@staticmethod
122+
def get_podname_proper(podname):
123+
"""
124+
Podnames in cocoapods sometimes are files inside a pods package (like 'OHHTTPStubs/Default')
125+
This returns proper podname in those cases.
126+
"""
127+
if '/' in podname:
128+
return podname.split('/')[0]
129+
return podname
56130

57131

58132
def is_podspec(location):
@@ -62,21 +136,64 @@ def is_podspec(location):
62136
return (filetype.is_file(location) and location.endswith('.podspec'))
63137

64138

139+
def is_podfile_lock(location):
140+
"""
141+
Checks if the file is actually a podfile.lock file
142+
"""
143+
return (filetype.is_file(location) and location.endswith(('podfile.lock', 'Podfile.lock')))
144+
145+
def is_podspec_json(location):
146+
"""
147+
Checks if the file is actually a podspec.json metadata file
148+
"""
149+
return (filetype.is_file(location) and location.endswith('.podspec.json'))
150+
151+
152+
def read_podspec_json(location):
153+
"""
154+
Reads from podspec.json file at location as JSON.
155+
"""
156+
with open(location, "r") as file:
157+
data = json.load(file)
158+
159+
return data
160+
161+
162+
def read_podfile_lock(location):
163+
"""
164+
Reads from podfile.lock file at location as YML.
165+
"""
166+
with open(location, 'r') as file:
167+
data = saneyaml.load(file)
168+
169+
return data
170+
171+
65172
def parse(location):
66173
"""
67-
Return a Package object from a .podspec file or None.
174+
Return a Package object from:
175+
1. `.podspec` files
176+
2. `.podspec.json` files
177+
3. `podfile.lock` files
178+
or returns None otherwise.
68179
"""
69-
if not is_podspec(location):
70-
return
180+
if is_podspec(location):
181+
podspec_object = Spec()
182+
podspec_data = podspec_object.parse_spec(location)
183+
return build_package(podspec_data)
184+
185+
if is_podspec_json(location):
186+
podspec_json_data = read_podspec_json(location)
187+
return build_xcode_package(podspec_json_data)
71188

72-
podspec_object = Spec()
73-
podspec_data = podspec_object.parse_spec(location)
74-
return build_package(podspec_data)
189+
if is_podfile_lock(location):
190+
podfile_lock_data = read_podfile_lock(location)
191+
return build_xcode_package_from_lockfile(podfile_lock_data)
75192

76193

77194
def build_package(podspec_data):
78195
"""
79-
Return a Package object from a package data mapping or None.
196+
Return a Package object from a podspec.json package data mapping.
80197
"""
81198
name = podspec_data.get('name')
82199
version = podspec_data.get('version')
@@ -170,3 +287,173 @@ def parse_person(person):
170287
email = parsed.group('email')
171288

172289
return name, email
290+
291+
292+
def get_sha1_file(location):
293+
"""
294+
Get sha1 hash for a file at location.
295+
"""
296+
with open(location, "rb") as f:
297+
return hashlib.sha1(f.read()).hexdigest()
298+
299+
300+
def build_xcode_package(podspec_json_data):
301+
"""
302+
Return a Package object from a podspec.json package data mapping.
303+
"""
304+
name = podspec_json_data.get('name')
305+
version = podspec_json_data.get('version')
306+
summary = podspec_json_data.get('summary', '')
307+
description = podspec_json_data.get('description', '')
308+
homepage_url = podspec_json_data.get('homepage')
309+
310+
license = podspec_json_data.get('license')
311+
if isinstance(license, dict):
312+
declared_license = ' '.join(list(license.values()))
313+
else:
314+
declared_license = license
315+
316+
source = podspec_json_data.get('source')
317+
vcs_url = None
318+
download_url = None
319+
320+
if isinstance(source, dict):
321+
git_url = source.get('git', '')
322+
http_url = source.get('http', '')
323+
if git_url:
324+
vcs_url = git_url
325+
elif http_url:
326+
download_url = http_url
327+
328+
if not vcs_url:
329+
vcs_url = source
330+
331+
authors = podspec_json_data.get('authors') or {}
332+
333+
license_matches = get_license_matches(query_string=declared_license)
334+
if not license_matches:
335+
license_expression = 'unknown'
336+
else:
337+
license_expression = get_license_expression_from_matches(license_matches)
338+
339+
if summary and not description.startswith(summary):
340+
desc = [summary]
341+
if description:
342+
desc += [description]
343+
description = '. '.join(desc)
344+
345+
parties = []
346+
if authors:
347+
if isinstance(authors, dict):
348+
for key, value in authors.items():
349+
party = models.Party(
350+
type=models.party_org,
351+
name=key,
352+
url=value+'.com',
353+
role='owner'
354+
)
355+
parties.append(party)
356+
else:
357+
party = models.Party(
358+
type=models.party_org,
359+
name=authors,
360+
role='owner'
361+
)
362+
parties.append(party)
363+
364+
extra_data = {}
365+
extra_data['source'] = podspec_json_data['source']
366+
dependencies = podspec_json_data.get('dependencies', '')
367+
if dependencies:
368+
extra_data['dependencies'] = dependencies
369+
extra_data['podspec.json'] = podspec_json_data
370+
371+
package = CocoapodsPackage(
372+
name=name,
373+
version=version,
374+
vcs_url=vcs_url,
375+
description=description,
376+
declared_license=declared_license,
377+
license_expression=license_expression,
378+
homepage_url=homepage_url,
379+
download_url=download_url,
380+
parties=parties,
381+
)
382+
383+
package.api_data_url = package.get_api_data_url()
384+
385+
return package
386+
387+
388+
def get_data_from_pods(dep_pod_version):
389+
390+
if '(' in dep_pod_version:
391+
podname, _, version = dep_pod_version.strip(')').partition(' (')
392+
else:
393+
version = None
394+
podname = dep_pod_version
395+
396+
if '/' in podname:
397+
namespace, _, podname = podname.partition('/')
398+
else:
399+
namespace = None
400+
401+
return podname, namespace, version
402+
403+
404+
def build_xcode_package_from_lockfile(podfile_lock_data):
405+
"""
406+
Return a Package object from a data mapping obtained from a podfile.lock
407+
"""
408+
pods = podfile_lock_data['PODS']
409+
pod_deps = []
410+
411+
for pod in pods:
412+
413+
if isinstance(pod, dict):
414+
for main_pod, _dep_pods in pod.items():
415+
416+
podname, namespace, version = get_data_from_pods(main_pod)
417+
418+
purl = PackageURL(
419+
type='pods',
420+
namespace=namespace,
421+
name=podname,
422+
version=version,
423+
).to_string()
424+
425+
pod_deps.append(
426+
models.DependentPackage(
427+
purl=purl,
428+
scope='requires-dev',
429+
requirement=version,
430+
is_runtime=False,
431+
is_optional=True,
432+
is_resolved=True,
433+
)
434+
)
435+
436+
elif isinstance(pod, str):
437+
podname, namespace, version = get_data_from_pods(pod)
438+
purl = PackageURL(
439+
type='pods',
440+
namespace=namespace,
441+
name=podname,
442+
version=version,
443+
).to_string()
444+
445+
pod_deps.append(
446+
models.DependentPackage(
447+
purl=purl,
448+
scope='requires-dev',
449+
requirement=version,
450+
is_runtime=False,
451+
is_optional=True,
452+
is_resolved=True,
453+
)
454+
)
455+
456+
yield CocoapodsPackage(
457+
dependencies=pod_deps,
458+
declared_license=None,
459+
)

0 commit comments

Comments
 (0)