Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ v33.0.0 (next next, roadmap)
of these in other summary plugins.
See https://github.com/nexB/scancode-toolkit/issues/1745

- We now support parsing the Swift manifest JSON dump and the ``Package.resolved`` file https://github.com/nexB/scancode-toolkit/issues/2657.
- Run the commands below on your local Swift project before running the scan.
- ::

swift package dump-package > Package.swift.json
- ::

swift package resolve

v32.1.0 (next, roadmap)
----------------------------

Expand Down
26 changes: 26 additions & 0 deletions docs/source/reference/available_package_parsers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ parsers in scancode-toolkit during documentation builds.
- https://r-pkgs.org/description.html
* - Debian control file - extracted layout
- ``*/control.tar.gz-extract/control``
``*/control.tar.xz-extract/control``
- ``deb``
- ``debian_control_extracted_deb``
- None
Expand Down Expand Up @@ -716,6 +717,19 @@ parsers in scancode-toolkit during documentation builds.
- ``rpm_installed_database_sqlite``
- None
- https://fedoraproject.org/wiki/Changes/Sqlite_Rpmdb
* - RPM mariner distroless package manifest
- ``*var/lib/rpmmanifest/container-manifest-2``
- ``rpm``
- ``rpm_mariner_manifest``
- None
- https://github.com/microsoft/marinara/
* - RPM mariner distroless package license files
- ``*usr/share/licenses/*/COPYING*``
``*usr/share/licenses/*/LICENSE*``
- ``rpm``
- ``rpm_package_licenses``
- None
- https://github.com/microsoft/marinara/
* - RPM specfile
- ``*.spec``
- ``rpm``
Expand All @@ -734,6 +748,18 @@ parsers in scancode-toolkit during documentation builds.
- ``squashfs_disk_image``
- None
- https://en.wikipedia.org/wiki/SquashFS
* - json dump of swift manifest
- ``*/Package.swift.json``
- ``swift``
- ``swift_package_manifest_json``
- swift
- https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html
* - resolved dependency for swift package
- ``*/Package.resolved``
- ``swift``
- ``swift_package_resolved``
- swift
- https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#package-dependency
* - Java Web Application Archive
- ``*.war``
- ``war``
Expand Down
4 changes: 4 additions & 0 deletions src/packagedcode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from packagedcode import readme
from packagedcode import rpm
from packagedcode import rubygems
from packagedcode import swift
from packagedcode import win_pe
from packagedcode import windows

Expand Down Expand Up @@ -196,6 +197,9 @@
rubygems.GemspecInExtractedGemHandler,
rubygems.GemspecHandler,

swift.SwiftManifestJsonHandler,
swift.SwiftPackageResolvedHandler,

windows.MicrosoftUpdateManifestHandler,

win_pe.WindowsExecutableHandler,
Expand Down
283 changes: 283 additions & 0 deletions src/packagedcode/swift.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
# Copyright (c) nexB Inc. and others. All rights reserved.
# ScanCode is a trademark of nexB Inc.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/nexB/scancode-toolkit for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#

import io
import json
import logging
import os
from urllib import parse

from packagedcode import models
from packageurl import PackageURL

"""
Handle the resolved file and JSON dump of the manifest for Swift packages.
https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html

Run the commands below before running the scan:
``swift package dump-package > Package.swift.json``
Comment thread
keshav-space marked this conversation as resolved.
Outdated
``swift package resolve``
Comment thread
keshav-space marked this conversation as resolved.
Outdated
"""


SCANCODE_DEBUG_PACKAGE = os.environ.get("SCANCODE_DEBUG_PACKAGE", False)

TRACE = SCANCODE_DEBUG_PACKAGE


def logger_debug(*args):
pass


logger = logging.getLogger(__name__)

if TRACE:
import sys

logging.basicConfig(stream=sys.stdout)
logger.setLevel(logging.DEBUG)

def logger_debug(*args):
return logger.debug(" ".join(isinstance(a, str) and a or repr(a) for a in args))


class SwiftManifestJsonHandler(models.DatafileHandler):
datasource_id = "swift_package_manifest_json"
path_patterns = ("*/Package.swift.json",)
default_package_type = "swift"
default_primary_language = "swift"
Comment thread
keshav-space marked this conversation as resolved.
Outdated
description = "json dump of swift manifest"
Comment thread
keshav-space marked this conversation as resolved.
Outdated
documentation_url = "https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html"

@classmethod
def _parse(cls, swift_manifest, package_only=False):

if TRACE:
logger_debug(
f"SwiftManifestJsonHandler: manifest: package: {swift_manifest}"
)

dependencies = get_dependencies(swift_manifest.get("dependencies"))

package_data = dict(
datasource_id=cls.datasource_id,
type=cls.default_package_type,
primary_language=cls.default_primary_language,
namespace=None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add namespace

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pombredanne We don't have a namespace for the top-level package in the Package.swift manifest.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, how does this look like when installed?
Is there any trace of where it came from?
https://github.com/mapbox/mapbox-maps-ios/blob/main/Package.swift has some possible base URL. BUT when I see https://github.com/map-ir/mapir-ios-maps-sdk/blob/cfca260d03ab8f0d1889795ec012adae1d3bfbd8/Package.swift#L20 or https://github.com/mapbox/mapbox-navigation-ios/blob/b9c235e665ea2d3b7cd096e9c6128e97c1d4f00c/Package.swift#L27 it is clear that the actual name is one thing and the "package name" is another thing and seem to be resolved only when interpreting a Package.swift for dependencies.

So IMHO we cannot get that actual correct PURL name short of matching or have this used as a dependency. Just create a follow up issue for this so we can address it later.

@keshav-space keshav-space May 30, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, how does this look like when installed?
Is there any trace of where it came from?

Yes, we can trace the source of an already installed package from the .build directory.

Tracking this issue here #3793

The PURL for a Swift package depends on where the package is hosted, and given that Package.swift does not contain the repository URL for the top-level package, it is not possible to infer the correct PURL for the top-level package solely from the Package.swift manifest.
We can use https://swiftpackageindex.com/ to get the correct PURL, but this may not work for all packages since Swift Package Index is not authoritative in nature, i.e. a Swift package may exist, and the author may choose not to add their package to Swift Package Index. https://swiftpackageindex.com/add-a-package

name=swift_manifest.get("name"),
dependencies=dependencies,
)

return models.PackageData.from_data(package_data, package_only)

@classmethod
def parse(cls, location, package_only=False):
with io.open(location, encoding="utf-8") as loc:
swift_manifest = json.load(loc)

yield cls._parse(swift_manifest, package_only)

@classmethod
def assemble(
cls,
package_data,
resource,
codebase,
package_adder=models.add_to_package,
):
"""
Use the dependencies from `Package.resolved` to create the
top-level package with resolved dependencies.
"""
siblings = resource.siblings(codebase)
swift_resolved_package_resource = [
r for r in siblings if r.name == "Package.resolved"
]
dependencies_from_manifest = package_data.dependencies

processed_dependencies = []
if swift_resolved_package_resource:
swift_resolved_package_resource = swift_resolved_package_resource[0]
swift_resolved_package_data = swift_resolved_package_resource.package_data

for package in swift_resolved_package_data:
version = package.get("version")
name = package.get("name")

purl = PackageURL(
type=cls.default_package_type, name=name, version=version
)
processed_dependencies.append(
models.DependentPackage(
purl=purl.to_string(),
scope="install",
Comment thread
keshav-space marked this conversation as resolved.
Outdated
is_runtime=True,
is_optional=False,
is_resolved=True,
extracted_requirement=version,
)
)

for dependency in dependencies_from_manifest[:]:
dependency_purl = PackageURL.from_string(dependency.purl)

if dependency_purl.name == name:
dependencies_from_manifest.remove(dependency)

processed_dependencies.extend(dependencies_from_manifest)

datafile_path = resource.path
if package_data.purl:
package = models.Package.from_package_data(
package_data=package_data,
datafile_path=datafile_path,
)

if swift_resolved_package_resource:
package.datafile_paths.append(swift_resolved_package_resource.path)
package.datasource_ids.append(SwiftPackageResolvedHandler.datasource_id)

package.populate_license_fields()
yield package

parent = resource.parent(codebase)
cls.assign_package_to_resources(
package=package,
resource=parent,
codebase=codebase,
package_adder=package_adder,
)

if processed_dependencies:
yield from models.Dependency.from_dependent_packages(
dependent_packages=processed_dependencies,
datafile_path=datafile_path,
datasource_id=package_data.datasource_id,
package_uid=package.package_uid,
)
yield resource


class SwiftPackageResolvedHandler(models.DatafileHandler):
datasource_id = "swift_package_resolved"
path_patterns = ("*/Package.resolved",)
Comment thread
keshav-space marked this conversation as resolved.
Outdated
default_package_type = "swift"
default_primary_language = "swift"
description = "resolved dependency for swift package"
Comment thread
keshav-space marked this conversation as resolved.
Outdated
documentation_url = (
"https://docs.swift.org/package-manager/PackageDescription/"
"PackageDescription.html#package-dependency"
)

@classmethod
def parse(cls, location, package_only=False):
with io.open(location, encoding="utf-8") as loc:
package_resolved = json.load(loc)

pinned = package_resolved.get("pins", [])

for dependency in pinned:
name = dependency.get("identity")
kind = dependency.get("kind")
location = dependency.get("location")
state = dependency.get("state", {})
version = None

if location and kind == "remoteSourceControl":
name = get_canonical_name(location)

version = state.get("version")

if not version:
version = state.get("revision")

package_data = dict(
datasource_id=cls.datasource_id,
type=cls.default_package_type,
primary_language=cls.default_primary_language,
namespace=None,
Comment thread
keshav-space marked this conversation as resolved.
Outdated
name=name,
version=version,
)
yield models.PackageData.from_data(package_data, package_only)

@classmethod
def assemble(
cls, package_data, resource, codebase, package_adder=models.add_to_package
):
siblings = resource.siblings(codebase)
swift_manifest_resource = [
r for r in siblings if r.name == "Package.swift.json"
]

# Skip the assembly if the ``Package.swift.json`` manifest is present.
# SwiftManifestJsonHandler's assembly will take care of the resolved
# dependencies from Package.resolved file.
if swift_manifest_resource:
return []

yield from super(SwiftPackageResolvedHandler, cls).assemble(
package_data=package_data,
resource=resource,
codebase=codebase,
package_adder=package_adder,
)


def get_dependencies(dependencies):
dependent_packages = []
for dependency in dependencies or []:
source = dependency.get("sourceControl")
if not source:
continue

source = source[0]
name = source.get("identity")
version = None
is_resolved = False

location = source.get("location")
if remote := location.get("remote"):
name = get_canonical_name(remote[0].get("urlString"))

requirement = source.get("requirement")
if exact := requirement.get("exact"):
version = exact[0]
is_resolved = True
elif range := requirement.get("range"):
bound = range[0]
lower_bound = bound.get("lowerBound")
upper_bound = bound.get("upperBound")
version = f"vers:swift/>={lower_bound}|<{upper_bound}"

purl = PackageURL(
type="swift",
name=name,
Comment thread
keshav-space marked this conversation as resolved.
version=version if is_resolved else None,
)

dependent_packages.append(
models.DependentPackage(
purl=purl.to_string(),
scope="install",
Comment thread
keshav-space marked this conversation as resolved.
Outdated
is_runtime=True,
is_optional=False,
is_resolved=is_resolved,
extracted_requirement=version,
)
)
return dependent_packages


def get_canonical_name(url):
parsed_url = parse.urlparse(url)
hostname = parsed_url.hostname
path = parsed_url.path.removesuffix(".git")

return parse.quote(hostname + path, safe="")

14 changes: 14 additions & 0 deletions tests/packagedcode/data/plugin/help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,20 @@ Package type: squashfs
description: Squashfs disk image
path_patterns:
--------------------------------------------
Package type: swift
datasource_id: swift_package_manifest_json
documentation URL: https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html
primary language: swift
description: json dump of swift manifest
path_patterns: '*/Package.swift.json'
--------------------------------------------
Package type: swift
datasource_id: swift_package_resolved
documentation URL: https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#package-dependency
primary language: swift
description: resolved dependency for swift package
path_patterns: '*/Package.resolved'
--------------------------------------------
Package type: war
datasource_id: java_war_archive
documentation URL: https://en.wikipedia.org/wiki/WAR_(file_format)
Expand Down
Loading