Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: environment - gather declared license information according to PEP639 #755

Merged
merged 36 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 6 additions & 2 deletions cyclonedx_py/_internal/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,16 @@ def __call__(self, *, # type:ignore[override]
if path[0] in ('', getcwd()):
path.pop(0)

# TODO: make a CLI switch, disabled by default
gather_license_text = True

bom = make_bom()
self.__add_components(bom, rc, path=path)
self.__add_components(bom, rc, gather_license_text, path=path)
return bom

def __add_components(self, bom: 'Bom',
rc: Optional[Tuple['Component', Iterable['Requirement']]],
gather_license_text,
**kwargs: Any) -> None:
all_components: 'T_AllComponents' = {}
self._logger.debug('distribution context args: %r', kwargs)
Expand All @@ -163,7 +167,7 @@ def __add_components(self, bom: 'Bom',
name=dist_name,
version=dist_version,
description=dist_meta['Summary'] if 'Summary' in dist_meta else None,
licenses=licenses_fixup(metadata2licenses(dist_meta)),
licenses=licenses_fixup(metadata2licenses(dist, gather_license_text)),
external_references=metadata2extrefs(dist_meta),
# path of dist-package on disc? naaa... a package may have multiple files/folders on disc
)
Expand Down
42 changes: 35 additions & 7 deletions cyclonedx_py/_internal/utils/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

from base64 import b64encode
from mimetypes import guess_type
from os.path import join
from re import compile as re_compile
from typing import TYPE_CHECKING, Generator, List

from cyclonedx.exception.model import InvalidUriException
from cyclonedx.factory.license import LicenseFactory
from cyclonedx.model import AttachedText, ExternalReference, ExternalReferenceType, XsUri
from cyclonedx.model import AttachedText, Encoding, ExternalReference, ExternalReferenceType, XsUri
from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement

from .cdx import url_label_to_ert
from .pep621 import classifiers2licenses

if TYPE_CHECKING: # pragma: no cover
import sys
from importlib.metadata import Distribution

from cyclonedx.model.license import License

Expand All @@ -37,15 +40,16 @@
from email.message import Message as PackageMetadata


def metadata2licenses(metadata: 'PackageMetadata') -> Generator['License', None, None]:
def metadata2licenses(dist: 'Distribution', gather_text: bool) -> Generator['License', None, None]:
lfac = LicenseFactory()
lack = LicenseAcknowledgement.DECLARED
metadata = dist.metadata # see https://packaging.python.org/en/latest/specifications/core-metadata/
if 'Classifier' in metadata:
# see spec: https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
classifiers: List[str] = metadata.get_all('Classifier') # type:ignore[assignment]
yield from classifiers2licenses(classifiers, lfac, lack)
for mlicense in metadata.get_all('License', ()):
# see spec: https://packaging.python.org/en/latest/specifications/core-metadata/#license
for mlicense in set(metadata.get_all('License', ())):
# see spec: https://packaging.python.org/en/latest/specifications/core-metadata/#license
if len(mlicense) <= 0:
continue
license = lfac.make_from_string(mlicense,
Expand All @@ -57,8 +61,32 @@ def metadata2licenses(metadata: 'PackageMetadata') -> Generator['License', None,
text=AttachedText(content=mlicense))
else:
yield license
# TODO: iterate over "License-File" declarations and read them
# for mlfile in metadata.get_all('License-File'): ...
if (lexp := metadata.get('License-Expression')) is not None:
# see spec: https://peps.python.org/pep-0639/#add-license-expression-field
yield lfac.make_from_string(lexp,
license_acknowledgement=lack)
if gather_text:
for mlfile in set(metadata.get_all('License-File', ())):
# see spec: https://peps.python.org/pep-0639/#add-license-file-field
try:
# the PEP draft tells to put the file in the package root ... but some just dont ...
mlfile_c = dist.read_text(mlfile) \
or dist.read_text(join('licenses', mlfile)) \
or dist.read_text(join('license_files', mlfile))
if mlfile_c is None:
pass # todo add debug output
else:
mlfile_cb64, mlfile_c = b64encode(bytes(mlfile_c, 'utf-8')).decode('ascii'), None
yield DisjunctiveLicense(name=f"declared license file '{mlfile}'",
acknowledgement=lack,
text=AttachedText(
content=mlfile_cb64,
encoding=Encoding.BASE_64,
content_type=guess_type(mlfile)[0]
or AttachedText.DEFAULT_CONTENT_TYPE
))
except BaseException:
pass


def metadata2extrefs(metadata: 'PackageMetadata') -> Generator['ExternalReference', None, None]:
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading