1919
2020from packagedcode import models
2121from 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"""
2526Handle 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+
350534class 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: "[email protected] :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