Skip to content

Commit 89f8851

Browse files
Merge pull request #3827 from nexB/update-cocoapods-dependencies
Update cocoapods podfile.lock parser
2 parents 9a6354d + 203dec1 commit 89f8851

11 files changed

+5132
-125
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ v33.0.0 (next next, roadmap)
3838
from nuget lockfile `packages.lock.json`.
3939
See https://github.com/nexB/scancode-toolkit/pull/3825
4040

41+
- Add support for parsing resolved packages and dependency relationships
42+
from cocoapods lockfile `Podfile.lock`.
43+
See https://github.com/nexB/scancode-toolkit/pull/3827
44+
4145
v32.2.0 - 2024-06-19
4246
----------------------
4347

src/packagedcode/cocoapods.py

Lines changed: 255 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919

2020
from packagedcode import models
2121
from packagedcode import spec
22-
from packagedcode import utils
22+
from packagedcode.utils import get_base_purl
23+
from packagedcode.utils import build_description
2324

2425
"""
2526
Handle cocoapods packages manifests for macOS and iOS
@@ -232,7 +233,7 @@ def parse(cls, location, package_only=False):
232233
extracted_license_statement = podspec.get('license')
233234
summary = podspec.get('summary')
234235
description = podspec.get('description')
235-
description = utils.build_description(
236+
description = build_description(
236237
summary=summary,
237238
description=description,
238239
)
@@ -292,6 +293,96 @@ class PodfileLockHandler(BasePodHandler):
292293
default_primary_language = 'Objective-C'
293294
description = 'Cocoapods Podfile.lock'
294295
documentation_url = 'https://guides.cocoapods.org/using/the-podfile.html'
296+
is_lockfile = True
297+
298+
@classmethod
299+
def get_pods_dependency_with_resolved_package(
300+
cls,
301+
dependency_data,
302+
main_pod,
303+
dependencies_for_resolved=[],
304+
):
305+
"""
306+
Get a DependentPackage object with its resolved package and
307+
dependencies from the `main_pod` string, with additional data
308+
populated from the `PodfileLockDataByPurl` mappings.
309+
"""
310+
purl, xreq = parse_dep_requirements(main_pod)
311+
base_purl = get_base_purl(purl.to_string())
312+
313+
resolved_package_mapping = dict(
314+
datasource_id=cls.datasource_id,
315+
type=cls.default_package_type,
316+
primary_language=cls.default_primary_language,
317+
namespace=purl.namespace,
318+
name=purl.name,
319+
version=purl.version,
320+
dependencies=dependencies_for_resolved,
321+
is_virtual=True,
322+
)
323+
resolved_package = models.PackageData.from_data(resolved_package_mapping)
324+
325+
checksum = dependency_data.checksum_by_base_purl.get(base_purl)
326+
if checksum:
327+
resolved_package.sha1 = checksum
328+
329+
is_direct = False
330+
if base_purl in dependency_data.direct_dependency_purls:
331+
is_direct = True
332+
333+
spec_repo = dependency_data.spec_by_base_purl.get(base_purl)
334+
if spec_repo:
335+
resolved_package.extra_data["spec_repo"] = spec_repo
336+
337+
external_source = dependency_data.external_sources_by_base_purl.get(base_purl)
338+
if external_source:
339+
resolved_package.extra_data["external_source"] = external_source
340+
341+
return models.DependentPackage(
342+
purl=purl.to_string(),
343+
# FIXME: why dev?
344+
scope='requires',
345+
extracted_requirement=xreq,
346+
is_runtime=False,
347+
is_optional=True,
348+
is_resolved=True,
349+
is_direct=is_direct,
350+
resolved_package=resolved_package,
351+
)
352+
353+
@classmethod
354+
def get_dependencies_for_resolved_package(cls, dependency_data, dep_pods):
355+
"""
356+
Get the list of dependencies with versions and version requirements
357+
for a cocoapods resolved package.
358+
"""
359+
dependencies_for_resolved = []
360+
for dep_pod in dep_pods:
361+
dep_purl, dep_xreq = parse_dep_requirements(dep_pod)
362+
base_dep_purl = get_base_purl(dep_purl.to_string())
363+
364+
dep_version = dependency_data.versions_by_base_purl.get(base_dep_purl)
365+
if dep_version:
366+
purl_mapping = dep_purl.to_dict()
367+
purl_mapping["version"] = dep_version
368+
dep_purl = PackageURL(**purl_mapping)
369+
370+
if not dep_xreq:
371+
dep_xreq = dep_version
372+
373+
dependency_for_resolved = models.DependentPackage(
374+
purl=dep_purl.to_string(),
375+
# FIXME: why dev?
376+
scope='requires',
377+
extracted_requirement=dep_xreq,
378+
is_runtime=False,
379+
is_optional=True,
380+
is_resolved=True,
381+
is_direct=True,
382+
).to_dict()
383+
dependencies_for_resolved.append(dependency_for_resolved)
384+
385+
return dependencies_for_resolved
295386

296387
@classmethod
297388
def parse(cls, location, package_only=False):
@@ -301,52 +392,145 @@ def parse(cls, location, package_only=False):
301392
with open(location) as pfl:
302393
data = saneyaml.load(pfl)
303394

304-
pods = data['PODS']
395+
dependency_data = PodfileLockDataByPurl.collect_dependencies_data_by_purl(
396+
data=data,
397+
package_type=cls.default_package_type,
398+
)
399+
305400
dependencies = []
306401

402+
pods = data.get('PODS') or []
307403
for pod in pods:
404+
# dependencies with mappings have direct dependencies
308405
if isinstance(pod, dict):
309-
for main_pod, _dep_pods in pod.items():
310-
311-
purl, xreq = parse_dep_requirements(main_pod)
312-
313-
dependencies.append(
314-
models.DependentPackage(
315-
purl=str(purl),
316-
# FIXME: why dev?
317-
scope='requires',
318-
extracted_requirement=xreq,
319-
is_runtime=False,
320-
is_optional=True,
321-
is_resolved=True,
322-
)
406+
for main_pod, dep_pods in pod.items():
407+
dependencies_for_resolved = cls.get_dependencies_for_resolved_package(
408+
dependency_data=dependency_data,
409+
dep_pods=dep_pods,
323410
)
411+
dependency = cls.get_pods_dependency_with_resolved_package(
412+
dependency_data=dependency_data,
413+
main_pod=main_pod,
414+
dependencies_for_resolved=dependencies_for_resolved,
415+
)
416+
dependencies.append(dependency)
324417

418+
# These packages have no direct dependencies
325419
elif isinstance(pod, str):
326-
327-
purl, xreq = parse_dep_requirements(pod)
328-
329-
dependencies.append(
330-
models.DependentPackage(
331-
purl=str(purl),
332-
# FIXME: why dev?
333-
scope='requires',
334-
extracted_requirement=xreq,
335-
is_runtime=False,
336-
is_optional=True,
337-
is_resolved=True,
338-
)
420+
dependency = cls.get_pods_dependency_with_resolved_package(
421+
dependency_data, pod,
339422
)
423+
dependencies.append(dependency)
424+
425+
podfile_checksum = data.get('PODFILE CHECKSUM')
426+
cocoapods_version = data.get('COCOAPODS')
427+
extra_data = {
428+
'cocoapods': cocoapods_version,
429+
'podfile_checksum': podfile_checksum,
430+
}
340431

341432
package_data = dict(
342433
datasource_id=cls.datasource_id,
343434
type=cls.default_package_type,
344435
primary_language=cls.default_primary_language,
345436
dependencies=dependencies,
437+
extra_data=extra_data,
346438
)
347439
yield models.PackageData.from_data(package_data, package_only)
348440

349441

442+
class PodfileLockDataByPurl:
443+
"""
444+
Podfile.lock locskfiles contains information about its cocoapods
445+
dependencies in multiple parallel lists by it's name.
446+
447+
These are:
448+
- PODS : Dependency graph with resolved package versions, dependency
449+
relationships and dependency requirements
450+
- DEPENDENCIES : list of direct dependencies
451+
- SPEC REPOS : location of spec repo having the package metadata podspec
452+
- SPEC CHECKSUMS : sha1 checksums of the package
453+
- CHECKOUT OPTIONS : the version control system info for the package with exact commit
454+
- EXTERNAL SOURCES : External source for a package, locally, or in a external vcs repo
455+
456+
Additionally the resolved package version for dependencies are also only
457+
present in the top-level, but not in the dependency relationships.
458+
459+
This class parses these information and stores them in mappings by purl.
460+
"""
461+
462+
versions_by_base_purl = {}
463+
direct_dependency_purls = []
464+
spec_by_base_purl = {}
465+
checksum_by_base_purl = {}
466+
external_sources_by_base_purl = {}
467+
468+
@classmethod
469+
def collect_dependencies_data_by_purl(cls, data, package_type):
470+
"""
471+
Parse and populate cocoapods dependency information by purl,
472+
from the `data` mapping.
473+
"""
474+
dep_data = cls()
475+
476+
# collect versions of all dependencies
477+
pods = data.get('PODS') or []
478+
for pod in pods:
479+
if isinstance(pod, dict):
480+
for main_pod, _dep_pods in pod.items():
481+
purl, xreq = parse_dep_requirements(main_pod)
482+
base_purl = get_base_purl(purl.to_string())
483+
dep_data.versions_by_base_purl[base_purl] = xreq
484+
485+
elif isinstance(pod, str):
486+
purl, xreq = parse_dep_requirements(pod)
487+
base_purl = get_base_purl(purl.to_string())
488+
dep_data.versions_by_base_purl[base_purl] = xreq
489+
490+
direct_dependencies = data.get('DEPENDENCIES') or []
491+
for direct_dep in direct_dependencies:
492+
purl, _xreq = parse_dep_requirements(direct_dep)
493+
base_purl = get_base_purl(purl.to_string())
494+
dep_data.direct_dependency_purls.append(base_purl)
495+
496+
spec_repos = data.get('SPEC REPOS') or {}
497+
for spec_repo, packages in spec_repos.items():
498+
for package in packages:
499+
purl, _xreq = parse_dep_requirements(package)
500+
base_purl = get_base_purl(purl.to_string())
501+
dep_data.spec_by_base_purl[base_purl] = spec_repo
502+
503+
checksums = data.get('SPEC CHECKSUMS') or {}
504+
for name, checksum in checksums.items():
505+
purl, _xreq = parse_dep_requirements(name)
506+
base_purl = get_base_purl(purl.to_string())
507+
dep_data.checksum_by_base_purl[base_purl] = checksum
508+
509+
checkout_options = data.get('CHECKOUT OPTIONS') or {}
510+
for name, source in checkout_options.items():
511+
processed_source = process_external_source(source)
512+
base_purl = PackageURL(
513+
type=package_type,
514+
name=name,
515+
).to_string()
516+
dep_data.external_sources_by_base_purl[base_purl] = processed_source
517+
518+
external_sources = data.get('EXTERNAL SOURCES') or {}
519+
for name, source in external_sources.items():
520+
base_purl = PackageURL(
521+
type=package_type,
522+
name=name,
523+
).to_string()
524+
525+
# `CHECKOUT OPTIONS` is more verbose than `EXTERNAL SOURCES`
526+
if base_purl in dep_data.external_sources_by_base_purl:
527+
continue
528+
processed_source = process_external_source(source)
529+
dep_data.external_sources_by_base_purl[base_purl] = processed_source
530+
531+
return dep_data
532+
533+
350534
class PodspecJsonHandler(models.DatafileHandler):
351535
datasource_id = 'cocoapods_podspec_json'
352536
path_patterns = ('*.podspec.json',)
@@ -566,3 +750,44 @@ def parse_dep_requirements(dep):
566750
version=version,
567751
)
568752
return purl, requirement
753+
754+
755+
def process_external_source(source_mapping):
756+
"""
757+
Process dependencies with external sources into
758+
a path or URL string.
759+
760+
Some examples:
761+
762+
boost:
763+
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
764+
Pulley:
765+
:branch: master
766+
:git: https://github.com/artsy/Pulley.git
767+
SnapKit:
768+
:branch: xcode102
769+
:git: "git@github.com:alanzeino/SnapKit.git"
770+
SwiftyJSON:
771+
:commit: af76cf3ef710b6ca5f8c05f3a31307d44a3c5828
772+
:git: https://github.com/SwiftyJSON/SwiftyJSON/
773+
tipsi-stripe:
774+
:path: "../node_modules/tipsi-stripe"
775+
"""
776+
777+
# this could be either `:path`, `:podspec` or `:git`
778+
if len(source_mapping.keys()) == 1:
779+
return str(list(source_mapping.values()).pop())
780+
781+
# this is a link to a git repository
782+
elif len(source_mapping.keys()) == 2 and ':git' in source_mapping:
783+
repo_url = source_mapping.get(':git').replace('.git', '').replace('git@', 'https://')
784+
repo_url = repo_url.rstrip('/')
785+
if ':commit' in source_mapping:
786+
commit = source_mapping.get(':commit')
787+
return f"{repo_url}/tree/{commit}"
788+
elif ':branch' in source_mapping:
789+
branch = source_mapping.get(':branch')
790+
return f"{repo_url}/tree/{branch}"
791+
792+
# In all other cases
793+
return str(source_mapping)

0 commit comments

Comments
 (0)