Skip to content

Commit d9776db

Browse files
committed
Add support for Swift package manager
Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent 4f49985 commit d9776db

File tree

2 files changed

+286
-0
lines changed

2 files changed

+286
-0
lines changed

src/packagedcode/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from packagedcode import readme
4040
from packagedcode import rpm
4141
from packagedcode import rubygems
42+
from packagedcode import swift
4243
from packagedcode import win_pe
4344
from packagedcode import windows
4445

@@ -196,6 +197,9 @@
196197
rubygems.GemspecInExtractedGemHandler,
197198
rubygems.GemspecHandler,
198199

200+
swift.SwiftManifestJsonHandler,
201+
swift.SwiftPackageResolvedHandler,
202+
199203
windows.MicrosoftUpdateManifestHandler,
200204

201205
win_pe.WindowsExecutableHandler,

src/packagedcode/swift.py

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# Copyright (c) nexB Inc. and others. All rights reserved.
2+
# ScanCode is a trademark of nexB Inc.
3+
# SPDX-License-Identifier: Apache-2.0
4+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
5+
# See https://github.com/nexB/scancode-toolkit for support or download.
6+
# See https://aboutcode.org for more information about nexB OSS projects.
7+
#
8+
9+
import io
10+
import json
11+
import logging
12+
import os
13+
from urllib import parse
14+
15+
from packagedcode import models
16+
from packageurl import PackageURL
17+
18+
"""
19+
Handle the resolved file and JSON dump of the manifest for Swift packages.
20+
https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html
21+
22+
Run the command below before running the scan:
23+
``swift package dump-package > Package.swift.json``
24+
"""
25+
26+
27+
SCANCODE_DEBUG_PACKAGE = os.environ.get("SCANCODE_DEBUG_PACKAGE", False)
28+
29+
TRACE = SCANCODE_DEBUG_PACKAGE
30+
31+
32+
def logger_debug(*args):
33+
pass
34+
35+
36+
logger = logging.getLogger(__name__)
37+
38+
if TRACE:
39+
import sys
40+
41+
logging.basicConfig(stream=sys.stdout)
42+
logger.setLevel(logging.DEBUG)
43+
44+
def logger_debug(*args):
45+
return logger.debug(" ".join(isinstance(a, str) and a or repr(a) for a in args))
46+
47+
48+
class SwiftManifestJsonHandler(models.DatafileHandler):
49+
datasource_id = "swift_package_manifest_json"
50+
path_patterns = ("*/Package.swift.json",)
51+
default_package_type = "swift"
52+
default_primary_language = "swift"
53+
description = "json dump of swift manifest"
54+
documentation_url = "https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html"
55+
56+
@classmethod
57+
def _parse(cls, swift_manifest, package_only=False):
58+
59+
if TRACE:
60+
logger_debug(
61+
f"SwiftManifestJsonHandler: manifest: package: {swift_manifest}"
62+
)
63+
64+
dependencies = get_dependencies(swift_manifest.get("dependencies"))
65+
66+
package_data = dict(
67+
datasource_id=cls.datasource_id,
68+
type=cls.default_package_type,
69+
primary_language=cls.default_primary_language,
70+
namespace=None,
71+
name=swift_manifest.get("name"),
72+
dependencies=dependencies,
73+
)
74+
75+
return models.PackageData.from_data(package_data, package_only)
76+
77+
@classmethod
78+
def parse(cls, location, package_only=False):
79+
with io.open(location, encoding="utf-8") as loc:
80+
swift_manifest = json.load(loc)
81+
82+
yield cls._parse(swift_manifest, package_only)
83+
84+
@classmethod
85+
def assemble(
86+
cls,
87+
package_data,
88+
resource,
89+
codebase,
90+
package_adder=models.add_to_package,
91+
):
92+
"""
93+
Use the dependencies from `Package.resolved` to create the
94+
top-level package with resolved dependencies.
95+
"""
96+
siblings = resource.siblings(codebase)
97+
swift_resolved_package_resource = [
98+
r for r in siblings if r.name == "Package.resolved"
99+
]
100+
dependencies_from_manifest = package_data.dependencies
101+
102+
processed_dependencies = []
103+
if swift_resolved_package_resource:
104+
swift_resolved_package_resource = swift_resolved_package_resource[0]
105+
swift_resolved_package_data = swift_resolved_package_resource.package_data
106+
107+
for package in swift_resolved_package_data:
108+
version = package.get("version")
109+
name = package.get("name")
110+
111+
purl = PackageURL(
112+
type=cls.default_package_type, name=name, version=version
113+
)
114+
processed_dependencies.append(
115+
models.DependentPackage(
116+
purl=purl.to_string(),
117+
scope="install",
118+
is_runtime=True,
119+
is_optional=False,
120+
is_resolved=True,
121+
extracted_requirement=version,
122+
)
123+
)
124+
125+
for dependency in dependencies_from_manifest[:]:
126+
dependency_purl = PackageURL.from_string(dependency.purl)
127+
128+
if dependency_purl.name == name:
129+
dependencies_from_manifest.remove(dependency)
130+
131+
processed_dependencies.extend(dependencies_from_manifest)
132+
133+
datafile_path = resource.path
134+
if package_data.purl:
135+
package = models.Package.from_package_data(
136+
package_data=package_data,
137+
datafile_path=datafile_path,
138+
)
139+
140+
if swift_resolved_package_resource:
141+
package.datafile_paths.append(swift_resolved_package_resource.path)
142+
package.datasource_ids.append(SwiftPackageResolvedHandler.datasource_id)
143+
144+
package.populate_license_fields()
145+
yield package
146+
147+
parent = resource.parent(codebase)
148+
cls.assign_package_to_resources(
149+
package=package,
150+
resource=parent,
151+
codebase=codebase,
152+
package_adder=package_adder,
153+
)
154+
155+
if processed_dependencies:
156+
yield from models.Dependency.from_dependent_packages(
157+
dependent_packages=processed_dependencies,
158+
datafile_path=datafile_path,
159+
datasource_id=package_data.datasource_id,
160+
package_uid=package.package_uid,
161+
)
162+
yield resource
163+
164+
165+
class SwiftPackageResolvedHandler(models.DatafileHandler):
166+
datasource_id = "swift_package_resolved"
167+
path_patterns = ("*/Package.resolved",)
168+
default_package_type = "swift"
169+
default_primary_language = "swift"
170+
description = "resolved dependency for swift package"
171+
documentation_url = (
172+
"https://docs.swift.org/package-manager/PackageDescription/"
173+
"PackageDescription.html#package-dependency"
174+
)
175+
176+
@classmethod
177+
def parse(cls, location, package_only=False):
178+
with io.open(location, encoding="utf-8") as loc:
179+
package_resolved = json.load(loc)
180+
181+
pinned = package_resolved.get("pins", [])
182+
183+
for dependency in pinned:
184+
name = dependency.get("identity")
185+
kind = dependency.get("kind")
186+
location = dependency.get("location")
187+
state = dependency.get("state", {})
188+
version = None
189+
190+
if location and kind == "remoteSourceControl":
191+
name = get_canonical_name(location)
192+
193+
version = state.get("version")
194+
195+
if not version:
196+
version = state.get("revision")
197+
198+
package_data = dict(
199+
datasource_id=cls.datasource_id,
200+
type=cls.default_package_type,
201+
primary_language=cls.default_primary_language,
202+
namespace=None,
203+
name=name,
204+
version=version,
205+
)
206+
yield models.PackageData.from_data(package_data, package_only)
207+
208+
@classmethod
209+
def assemble(
210+
cls, package_data, resource, codebase, package_adder=models.add_to_package
211+
):
212+
siblings = resource.siblings(codebase)
213+
swift_manifest_resource = [
214+
r for r in siblings if r.name == "Package.swift.json"
215+
]
216+
217+
# Skip the assembly if the ``Package.swift.json`` manifest is present.
218+
# SwiftManifestJsonHandler's assembly will take care of the resolved
219+
# dependencies from Package.resolved file.
220+
if swift_manifest_resource:
221+
return []
222+
223+
yield from super(SwiftPackageResolvedHandler, cls).assemble(
224+
package_data=package_data,
225+
resource=resource,
226+
codebase=codebase,
227+
package_adder=package_adder,
228+
)
229+
230+
231+
def get_dependencies(dependencies):
232+
dependent_packages = []
233+
for dependency in dependencies or []:
234+
source = dependency.get("sourceControl")
235+
if not source:
236+
continue
237+
238+
source = source[0]
239+
name = source.get("identity")
240+
version = None
241+
is_resolved = False
242+
243+
location = source.get("location")
244+
if remote := location.get("remote"):
245+
name = get_canonical_name(remote[0].get("urlString"))
246+
247+
requirement = source.get("requirement")
248+
if exact := requirement.get("exact"):
249+
version = exact[0]
250+
is_resolved = True
251+
elif range := requirement.get("range"):
252+
bound = range[0]
253+
lower_bound = bound.get("lowerBound")
254+
upper_bound = bound.get("upperBound")
255+
version = f"vers:swift/>={lower_bound}|<{upper_bound}"
256+
257+
purl = PackageURL(
258+
type="swift",
259+
name=name,
260+
version=version if is_resolved else None,
261+
)
262+
263+
dependent_packages.append(
264+
models.DependentPackage(
265+
purl=purl.to_string(),
266+
scope="install",
267+
is_runtime=True,
268+
is_optional=False,
269+
is_resolved=is_resolved,
270+
extracted_requirement=version,
271+
)
272+
)
273+
return dependent_packages
274+
275+
276+
def get_canonical_name(url):
277+
parsed_url = parse.urlparse(url)
278+
hostname = parsed_url.hostname
279+
path = parsed_url.path.removesuffix(".git")
280+
281+
return parse.quote(hostname + path, safe="")
282+

0 commit comments

Comments
 (0)