19
19
20
20
from packagedcode import models
21
21
from packagedcode import spec
22
- from packagedcode import utils
22
+ from packagedcode .utils import get_base_purl
23
+ from packagedcode .utils import build_description
23
24
24
25
"""
25
26
Handle cocoapods packages manifests for macOS and iOS
@@ -232,7 +233,7 @@ def parse(cls, location, package_only=False):
232
233
extracted_license_statement = podspec .get ('license' )
233
234
summary = podspec .get ('summary' )
234
235
description = podspec .get ('description' )
235
- description = utils . build_description (
236
+ description = build_description (
236
237
summary = summary ,
237
238
description = description ,
238
239
)
@@ -292,6 +293,96 @@ class PodfileLockHandler(BasePodHandler):
292
293
default_primary_language = 'Objective-C'
293
294
description = 'Cocoapods Podfile.lock'
294
295
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
295
386
296
387
@classmethod
297
388
def parse (cls , location , package_only = False ):
@@ -301,52 +392,145 @@ def parse(cls, location, package_only=False):
301
392
with open (location ) as pfl :
302
393
data = saneyaml .load (pfl )
303
394
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
+
305
400
dependencies = []
306
401
402
+ pods = data .get ('PODS' ) or []
307
403
for pod in pods :
404
+ # dependencies with mappings have direct dependencies
308
405
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 ,
323
410
)
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 )
324
417
418
+ # These packages have no direct dependencies
325
419
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 ,
339
422
)
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
+ }
340
431
341
432
package_data = dict (
342
433
datasource_id = cls .datasource_id ,
343
434
type = cls .default_package_type ,
344
435
primary_language = cls .default_primary_language ,
345
436
dependencies = dependencies ,
437
+ extra_data = extra_data ,
346
438
)
347
439
yield models .PackageData .from_data (package_data , package_only )
348
440
349
441
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
+
350
534
class PodspecJsonHandler (models .DatafileHandler ):
351
535
datasource_id = 'cocoapods_podspec_json'
352
536
path_patterns = ('*.podspec.json' ,)
@@ -566,3 +750,44 @@ def parse_dep_requirements(dep):
566
750
version = version ,
567
751
)
568
752
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