Skip to content

Commit 60b0e0f

Browse files
authored
Add parser for swift-show-dependencies.deplock (#3829)
* Add parser for swift-show-dependencies.deplock * Add package assembly for swift-show-dependencies.deplock Reference: Reference: aboutcode-org/scancode.io#1278 Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent 89f8851 commit 60b0e0f

File tree

10 files changed

+1735
-31
lines changed

10 files changed

+1735
-31
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ v33.0.0 (next next, roadmap)
4242
from cocoapods lockfile `Podfile.lock`.
4343
See https://github.com/nexB/scancode-toolkit/pull/3827
4444

45+
- Add support for parsing packages and dependency relationships
46+
from swift `swift-show-dependencies.deplock` generated by DepLock.
47+
See https://github.com/nexB/scancode-toolkit/pull/3829
48+
4549
v32.2.0 - 2024-06-19
4650
----------------------
4751

docs/source/reference/available_package_parsers.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -766,8 +766,9 @@ parsers in scancode-toolkit during documentation builds.
766766
- ``squashfs_disk_image``
767767
- None
768768
- https://en.wikipedia.org/wiki/SquashFS
769-
* - JSON dump of Package.swift created with ``swift package dump-package &gt; Package.swift.json``
769+
* - JSON dump of Package.swift created by DepLock or with ``swift package dump-package &gt; Package.swift.json``
770770
- ``*/Package.swift.json``
771+
``*/Package.swift.deplock``
771772
- ``swift``
772773
- ``swift_package_manifest_json``
773774
- Swift
@@ -779,6 +780,12 @@ parsers in scancode-toolkit during documentation builds.
779780
- ``swift_package_resolved``
780781
- swift
781782
- https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#package-dependency
783+
* - Swift dependency graph created by DepLock
784+
- ``*/swift-show-dependencies.deplock``
785+
- ``swift``
786+
- ``swift_package_show_dependencies``
787+
- Swift
788+
- https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154
782789
* - Java Web Application Archive
783790
- ``*.war``
784791
- ``war``

src/packagedcode/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@
203203

204204
swift.SwiftManifestJsonHandler,
205205
swift.SwiftPackageResolvedHandler,
206+
swift.SwiftShowDependenciesDepLockHandler,
206207

207208
windows.MicrosoftUpdateManifestHandler,
208209

src/packagedcode/swift.py

Lines changed: 228 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,76 @@ def logger_debug(*args):
4747
return logger.debug(" ".join(isinstance(a, str) and a or repr(a) for a in args))
4848

4949

50+
class SwiftShowDependenciesDepLockHandler(models.DatafileHandler):
51+
datasource_id = "swift_package_show_dependencies"
52+
path_patterns = ("*/swift-show-dependencies.deplock",)
53+
default_package_type = "swift"
54+
default_primary_language = "Swift"
55+
description = "Swift dependency graph created by DepLock"
56+
documentation_url = "https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154"
57+
58+
@classmethod
59+
def _parse(cls, swift_dependency_relation, package_only=False):
60+
61+
if TRACE:
62+
logger_debug(
63+
f"SwiftShowDependenciesDepLockHandler: deplock: package: {swift_dependency_relation}"
64+
)
65+
66+
dependencies = get_flatten_dependencies(
67+
dependency_tree=swift_dependency_relation.get("dependencies")
68+
)
69+
70+
package_data = dict(
71+
datasource_id=cls.datasource_id,
72+
type=cls.default_package_type,
73+
primary_language=cls.default_primary_language,
74+
# ``namespace`` is derived from repo URL and same is not available in dependency graph
75+
# See related issue: https://github.com/nexB/scancode-toolkit/issues/3793
76+
name=swift_dependency_relation.get("name"),
77+
dependencies=dependencies,
78+
)
79+
80+
return models.PackageData.from_data(package_data, package_only)
81+
82+
@classmethod
83+
def parse(cls, location, package_only=False):
84+
with io.open(location, encoding="utf-8") as loc:
85+
swift_dependency_relation = json.load(loc)
86+
87+
yield cls._parse(swift_dependency_relation, package_only)
88+
89+
@classmethod
90+
def assemble(
91+
cls, package_data, resource, codebase, package_adder=models.add_to_package
92+
):
93+
siblings = resource.siblings(codebase)
94+
swift_manifest_resource = [
95+
r
96+
for r in siblings
97+
if r.name in ("Package.swift.json", "Package.swift.deplock")
98+
]
99+
100+
# Skip the assembly if the Swift manifest is present.
101+
# SwiftManifestJsonHandler's assembly will take care of the
102+
# dependencies from swift-show-dependencies.deplock file.
103+
if swift_manifest_resource:
104+
return []
105+
106+
yield from super(SwiftShowDependenciesDepLockHandler, cls).assemble(
107+
package_data=package_data,
108+
resource=resource,
109+
codebase=codebase,
110+
package_adder=package_adder,
111+
)
112+
113+
50114
class SwiftManifestJsonHandler(models.DatafileHandler):
51115
datasource_id = "swift_package_manifest_json"
52-
path_patterns = ("*/Package.swift.json",)
116+
path_patterns = ("*/Package.swift.json", "*/Package.swift.deplock")
53117
default_package_type = "swift"
54118
default_primary_language = "Swift"
55-
description = "JSON dump of Package.swift created with ``swift package dump-package > Package.swift.json``"
119+
description = "JSON dump of Package.swift created by DepLock or with ``swift package dump-package > Package.swift.json``"
56120
documentation_url = "https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html"
57121

58122
@classmethod
@@ -98,41 +162,64 @@ def assemble(
98162
top-level package with resolved dependencies.
99163
"""
100164
siblings = resource.siblings(codebase)
165+
processed_dependencies = []
101166
swift_resolved_package_resource = [
102167
r for r in siblings if r.name == "Package.resolved"
103168
]
104-
dependencies_from_manifest = package_data.dependencies
105169

106-
processed_dependencies = []
107-
if swift_resolved_package_resource:
108-
swift_resolved_package_resource = swift_resolved_package_resource[0]
109-
swift_resolved_package_data = swift_resolved_package_resource.package_data
170+
swift_show_dependencies_resources = [
171+
r for r in siblings if r.name == "swift-show-dependencies.deplock"
172+
]
110173

111-
for package in swift_resolved_package_data:
112-
version = package.get("version")
113-
name = package.get("name")
174+
if swift_show_dependencies_resources:
175+
swift_show_dependencies_resource = swift_show_dependencies_resources[0]
176+
swift_show_dependencies_package_data = (
177+
swift_show_dependencies_resource.package_data
178+
)
179+
180+
# Dependencies from `swift-show-dependencies.deplock` supersede dependencies from other datafiles.
181+
processed_dependencies = swift_show_dependencies_package_data[0][
182+
"dependencies"
183+
]
184+
processed_dependencies = [
185+
models.DependentPackage.from_dict(i) for i in processed_dependencies
186+
]
187+
188+
# Use dependencies from `Package.resolved` when `swift-show-dependencies.deplock` is not present.
189+
else:
190+
dependencies_from_manifest = package_data.dependencies
114191

115-
purl = PackageURL(
116-
type=cls.default_package_type, name=name, version=version
192+
if swift_resolved_package_resource:
193+
swift_resolved_package_resource = swift_resolved_package_resource[0]
194+
swift_resolved_package_data = (
195+
swift_resolved_package_resource.package_data
117196
)
118-
processed_dependencies.append(
119-
models.DependentPackage(
120-
purl=purl.to_string(),
121-
scope="dependencies",
122-
is_runtime=True,
123-
is_optional=False,
124-
is_resolved=True,
125-
extracted_requirement=version,
197+
198+
for package in swift_resolved_package_data:
199+
version = package.get("version")
200+
name = package.get("name")
201+
202+
purl = PackageURL(
203+
type=cls.default_package_type, name=name, version=version
204+
)
205+
processed_dependencies.append(
206+
models.DependentPackage(
207+
purl=purl.to_string(),
208+
scope="dependencies",
209+
is_runtime=True,
210+
is_optional=False,
211+
is_resolved=True,
212+
extracted_requirement=version,
213+
)
126214
)
127-
)
128215

129-
for dependency in dependencies_from_manifest[:]:
130-
dependency_purl = PackageURL.from_string(dependency.purl)
216+
for dependency in dependencies_from_manifest[:]:
217+
dependency_purl = PackageURL.from_string(dependency.purl)
131218

132-
if dependency_purl.name == name:
133-
dependencies_from_manifest.remove(dependency)
219+
if dependency_purl.name == name:
220+
dependencies_from_manifest.remove(dependency)
134221

135-
processed_dependencies.extend(dependencies_from_manifest)
222+
processed_dependencies.extend(dependencies_from_manifest)
136223

137224
datafile_path = resource.path
138225
if package_data.purl:
@@ -141,7 +228,12 @@ def assemble(
141228
datafile_path=datafile_path,
142229
)
143230

144-
if swift_resolved_package_resource:
231+
if swift_show_dependencies_resources:
232+
package.datafile_paths.append(swift_show_dependencies_resource.path)
233+
package.datasource_ids.append(
234+
SwiftShowDependenciesDepLockHandler.datasource_id
235+
)
236+
elif swift_resolved_package_resource:
145237
package.datafile_paths.append(swift_resolved_package_resource.path)
146238
package.datasource_ids.append(SwiftPackageResolvedHandler.datasource_id)
147239

@@ -196,7 +288,9 @@ def assemble(
196288
):
197289
siblings = resource.siblings(codebase)
198290
swift_manifest_resource = [
199-
r for r in siblings if r.name == "Package.swift.json"
291+
r
292+
for r in siblings
293+
if r.name in ("Package.swift.json", "Package.swift.deplock")
200294
]
201295

202296
# Skip the assembly if the ``Package.swift.json`` manifest is present.
@@ -328,3 +422,109 @@ def get_namespace_and_name(url):
328422
canonical_name = hostname + path
329423

330424
return canonical_name.rsplit("/", 1)
425+
426+
427+
def get_flatten_dependencies(dependency_tree):
428+
"""
429+
Get the list of dependencies from the dependency graph where each
430+
element is a DependentPackage containing its 1st order dependencies.
431+
"""
432+
dependencies = []
433+
transitive_dependencies = []
434+
435+
# process direct dependency
436+
for dependency in dependency_tree:
437+
transitives = dependency.get("dependencies", [])
438+
transitive_dependencies.append(transitives)
439+
parent_child_dep = get_dependent_package_from_subtree(
440+
dependency=dependency,
441+
is_top_level_dependency=True,
442+
)
443+
dependencies.append(parent_child_dep)
444+
445+
# process all transitive dependencies
446+
while transitive_dependencies:
447+
transitive_dependency_tree = transitive_dependencies.pop(0)
448+
if not transitive_dependency_tree:
449+
continue
450+
451+
for transitive in transitive_dependency_tree:
452+
dependencies_of_transitive_dependency = transitive.get("dependencies", [])
453+
# add nested dependencies in transitive_dependencies queue for processing
454+
transitive_dependencies.append(dependencies_of_transitive_dependency)
455+
456+
parent_child_dep = get_dependent_package_from_subtree(
457+
dependency=transitive,
458+
is_top_level_dependency=False,
459+
)
460+
461+
dependencies.append(parent_child_dep)
462+
463+
return dependencies
464+
465+
466+
def get_dependent_package_from_subtree(dependency, is_top_level_dependency):
467+
"""
468+
Get the DependentPackage for a ``dependency`` subtree along with its 1st
469+
order dependencies. Set `is_direct` to True if the subtree is a direct
470+
dependency for the top-level package.
471+
"""
472+
dependencies_of_parent = []
473+
repository_url = dependency.get("url")
474+
version = dependency.get("version")
475+
transitives = dependency.get("dependencies", [])
476+
namespace, name = get_namespace_and_name(repository_url)
477+
purl = PackageURL(
478+
type="swift",
479+
namespace=namespace,
480+
name=name,
481+
version=version,
482+
)
483+
484+
for transitive in transitives:
485+
transitive_repository_url = transitive.get("url")
486+
transitive_version = transitive.get("version")
487+
transitive_namespace, transitive_name = get_namespace_and_name(
488+
transitive_repository_url
489+
)
490+
transitive_purl = PackageURL(
491+
type="swift",
492+
namespace=transitive_namespace,
493+
name=transitive_name,
494+
version=transitive_version,
495+
)
496+
497+
child_dependency = models.DependentPackage(
498+
purl=transitive_purl.to_string(),
499+
scope="dependencies",
500+
extracted_requirement=transitive_version,
501+
is_runtime=False,
502+
is_optional=False,
503+
is_resolved=True,
504+
is_direct=True,
505+
).to_dict()
506+
507+
dependencies_of_parent.append(child_dependency)
508+
509+
parent_package_data_mapping = dict(
510+
datasource_id=SwiftShowDependenciesDepLockHandler.datasource_id,
511+
type=SwiftShowDependenciesDepLockHandler.default_package_type,
512+
primary_language=SwiftShowDependenciesDepLockHandler.default_primary_language,
513+
namespace=namespace,
514+
name=name,
515+
version=version,
516+
dependencies=dependencies_of_parent,
517+
is_virtual=True,
518+
)
519+
parent_dependency = models.PackageData.from_data(parent_package_data_mapping)
520+
521+
return models.DependentPackage(
522+
purl=purl.to_string(),
523+
scope="dependencies",
524+
extracted_requirement=version,
525+
is_runtime=False,
526+
is_optional=False,
527+
is_resolved=True,
528+
is_direct=is_top_level_dependency,
529+
resolved_package=parent_dependency,
530+
)

tests/packagedcode/data/plugin/help.txt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -836,8 +836,8 @@ Package type: swift
836836
datasource_id: swift_package_manifest_json
837837
documentation URL: https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html
838838
primary language: Swift
839-
description: JSON dump of Package.swift created with ``swift package dump-package > Package.swift.json``
840-
path_patterns: '*/Package.swift.json'
839+
description: JSON dump of Package.swift created by DepLock or with ``swift package dump-package > Package.swift.json``
840+
path_patterns: '*/Package.swift.json', '*/Package.swift.deplock'
841841
--------------------------------------------
842842
Package type: swift
843843
datasource_id: swift_package_resolved
@@ -846,6 +846,13 @@ Package type: swift
846846
description: Resolved full dependency lockfile for Package.swift created with ``swift package resolve``
847847
path_patterns: '*/Package.resolved', '*/.package.resolved'
848848
--------------------------------------------
849+
Package type: swift
850+
datasource_id: swift_package_show_dependencies
851+
documentation URL: https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154
852+
primary language: Swift
853+
description: Swift dependency graph created by DepLock
854+
path_patterns: '*/swift-show-dependencies.deplock'
855+
--------------------------------------------
849856
Package type: war
850857
datasource_id: java_war_archive
851858
documentation URL: https://en.wikipedia.org/wiki/WAR_(file_format)

0 commit comments

Comments
 (0)