Skip to content

Commit

Permalink
Merge branch 'pa-spdx-concluded-license' of https://github.com/ferroc…
Browse files Browse the repository at this point in the history
…ene/reuse-tool into feature/comments
  • Loading branch information
alpianon committed Nov 17, 2022
2 parents 1cfb0b2 + 6353b3d commit d851e83
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 53 deletions.
72 changes: 61 additions & 11 deletions src/reuse/report.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. <https://fsfe.org>
# SPDX-FileCopyrightText: 2022 Florian Snow <[email protected]>
# SPDX-FileCopyrightText: 2022 Pietro Albini <[email protected]>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -28,17 +29,21 @@
class _MultiprocessingContainer:
"""Container that remembers some data in order to generate a FileReport."""

def __init__(self, project, do_checksum):
def __init__(self, project, do_checksum, add_license_concluded):
self.project = project
self.do_checksum = do_checksum
self.add_license_concluded = add_license_concluded

def __call__(self, file_):
# pylint: disable=broad-except
try:
return _MultiprocessingResult(
file_,
FileReport.generate(
self.project, file_, do_checksum=self.do_checksum
self.project,
file_,
do_checksum=self.do_checksum,
add_license_concluded=self.add_license_concluded,
),
None,
)
Expand Down Expand Up @@ -99,7 +104,12 @@ def to_dict(self):
"file_reports": [report.to_dict() for report in self.file_reports],
}

def bill_of_materials(self) -> str:
def bill_of_materials(
self,
*,
creator_person: Optional[str] = None,
creator_organization: Optional[str] = None,
) -> str:
"""Generate a bill of materials from the project.
See https://spdx.org/specifications.
Expand All @@ -120,8 +130,10 @@ def bill_of_materials(self) -> str:

# Author
# TODO: Fix Person and Organization
out.write("Creator: Person: Anonymous ()\n")
out.write("Creator: Organization: Anonymous ()\n")
out.write(f"Creator: Person: {format_creator(creator_person)}\n")
out.write(
f"Creator: Organization: {format_creator(creator_organization)}\n"
)
out.write(f"Creator: Tool: reuse-{__version__}\n")

now = datetime.datetime.utcnow()
Expand All @@ -146,9 +158,9 @@ def bill_of_materials(self) -> str:
out.write(f"FileName: {report.spdxfile.name}\n")
out.write(f"SPDXID: {report.spdxfile.spdx_id}\n")
out.write(f"FileChecksum: SHA1: {report.spdxfile.chk_sum}\n")
# IMPORTANT: Make no assertion about concluded license. This tool
# cannot, with full certainty, determine the license of a file.
out.write("LicenseConcluded: NOASSERTION\n")
out.write(
f"LicenseConcluded: {report.spdxfile.license_concluded}\n"
)

for lic in sorted(report.spdxfile.licenses_in_file):
out.write(f"LicenseInfoInFile: {lic}\n")
Expand Down Expand Up @@ -184,6 +196,7 @@ def generate(
project: Project,
do_checksum: bool = True,
multiprocessing: bool = cpu_count() > 1,
add_license_concluded: bool = False,
) -> "ProjectReport":
"""Generate a ProjectReport from a Project."""
project_report = cls(do_checksum=do_checksum)
Expand All @@ -193,7 +206,9 @@ def generate(
project.licenses_without_extension
)

container = _MultiprocessingContainer(project, do_checksum)
container = _MultiprocessingContainer(
project, do_checksum, add_license_concluded
)

if multiprocessing:
with mp.Pool() as pool:
Expand Down Expand Up @@ -313,6 +328,7 @@ def __init__(self, name, spdx_id=None, chk_sum=None):
self.spdx_id: str = spdx_id
self.chk_sum: str = chk_sum
self.licenses_in_file: List[str] = []
self.license_concluded: str = None
self.copyright: str = None
self.comment: str = None

Expand Down Expand Up @@ -345,7 +361,11 @@ def to_dict(self):

@classmethod
def generate(
cls, project: Project, path: PathLike, do_checksum: bool = True
cls,
project: Project,
path: PathLike,
do_checksum: bool = True,
add_license_concluded: bool = False,
) -> "FileReport":
"""Generate a FileReport from a path in a Project."""
path = Path(path)
Expand Down Expand Up @@ -386,7 +406,27 @@ def generate(
# Add license to report.
report.spdxfile.licenses_in_file.append(identifier)

# Copyright text and comment
if not add_license_concluded:
report.spdxfile.license_concluded = "NOASSERTION"
elif not spdx_info.spdx_expressions:
report.spdxfile.license_concluded = "NONE"
else:
# Merge all the license expressions together, wrapping them in
# parentheses to make sure an expression doesn't spill into another
# one. The extra parentheses will be removed by the roundtrip
# through parse() -> simplify() -> render().
report.spdxfile.license_concluded = (
_LICENSING.parse(
" AND ".join(
f"({expression})"
for expression in spdx_info.spdx_expressions
),
)
.simplify()
.render()
)

# Copyright text
report.spdxfile.copyright = "\n".join(sorted(spdx_info.copyright_lines))
report.spdxfile.comment = spdx_info.comment

Expand All @@ -396,3 +436,13 @@ def __hash__(self):
if self.spdxfile.chk_sum is not None:
return hash(self.spdxfile.name + self.spdxfile.chk_sum)
return super().__hash__(self)


def format_creator(creator):
"""Render the creator field based on the provided flag"""
if creator is None:
return "Anonymous ()"
if "(" in creator and creator.endswith(")"):
# The creator field already contains an email address
return creator
return creator + " ()"
49 changes: 47 additions & 2 deletions src/reuse/spdx.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. <https://fsfe.org>
# SPDX-FileCopyrightText: 2022 Pietro Albini <[email protected]>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand All @@ -22,10 +23,47 @@ def add_arguments(parser) -> None:
parser.add_argument(
"--output", "-o", dest="file", action="store", type=PathType("w")
)
parser.add_argument(
"--add-license-concluded",
action="store_true",
help=_(
"Populate the LicenseConcluded field. Note that REUSE cannot "
"guarantee the field is accurate."
),
)
parser.add_argument(
"--creator-person",
metavar="NAME",
help=_("Name of the person signing off on the SPDX report"),
)
parser.add_argument(
"--creator-organization",
metavar="NAME",
help=_("Name of the organization signing off on the SPDX report"),
)


def run(args, project: Project, out=sys.stdout) -> int:
"""Print the project's bill of materials."""
# The SPDX spec mandates that a creator must be specified when a license
# conclusion is made, so here we enforce that. More context:
#
# https://github.com/fsfe/reuse-tool/issues/586#issuecomment-1310425706
#
if (
args.add_license_concluded
and args.creator_person is None
and args.creator_organization is None
):
print(
_(
"error: --creator-person=NAME or --creator-organization=NAME"
" required when --add-license-concluded is provided"
),
file=sys.stderr,
)
return 1

with contextlib.ExitStack() as stack:
if args.file:
out = stack.enter_context(args.file.open("w", encoding="utf-8"))
Expand All @@ -43,9 +81,16 @@ def run(args, project: Project, out=sys.stdout) -> int:
)

report = ProjectReport.generate(
project, multiprocessing=not args.no_multiprocessing
project,
multiprocessing=not args.no_multiprocessing,
add_license_concluded=args.add_license_concluded,
)

out.write(report.bill_of_materials())
out.write(
report.bill_of_materials(
creator_person=args.creator_person,
creator_organization=args.creator_organization,
)
)

return 0
61 changes: 34 additions & 27 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2017 Free Software Foundation Europe e.V. <https://fsfe.org>
# SPDX-FileCopyrightText: 2022 Florian Snow <[email protected]>
# SPDX-FileCopyrightText: 2022 Carmen Bianca Bakker <[email protected]>
# SPDX-FileCopyrightText: 2022 Pietro Albini <[email protected]>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -84,6 +85,11 @@ def multiprocessing(request, monkeypatch) -> bool:
yield request.param


@pytest.fixture(params=[True, False])
def add_license_concluded(request) -> bool:
yield request


@pytest.fixture()
def empty_directory(tmpdir_factory) -> Path:
"""Create a temporary empty directory."""
Expand Down Expand Up @@ -117,6 +123,13 @@ def fake_repository(tmpdir_factory) -> Path:
"SPDX-License-Identifier: LicenseRef-custom",
encoding="utf-8",
)
(directory / "src/multiple_licenses.rs").write_text(
"SPDX-FileCopyrightText: 2022 Jane Doe\n"
"SPDX-License-Identifier: GPL-3.0-or-later\n"
"SPDX-License-Identifier: Apache-2.0 OR CC0-1.0"
" WITH Autoconf-exception-3.0\n",
encoding="utf-8",
)

os.chdir(directory)
return directory
Expand Down Expand Up @@ -162,15 +175,7 @@ def git_repository(fake_repository: Path, git_exe: Optional[str]) -> Path:
)

subprocess.run([git_exe, "add", str(fake_repository)], check=True)
subprocess.run(
[
git_exe,
"commit",
"-m",
"initial",
],
check=True,
)
git_commit(git_exe, "initial")

return fake_repository

Expand Down Expand Up @@ -227,15 +232,7 @@ def submodule_repository(
)

subprocess.run([git_exe, "add", str(submodule)], check=True)
subprocess.run(
[
git_exe,
"commit",
"-m",
"initial",
],
check=True,
)
git_commit(git_exe, "initial")

os.chdir(git_repository)

Expand All @@ -256,15 +253,7 @@ def submodule_repository(
],
check=True,
)
subprocess.run(
[
git_exe,
"commit",
"-m",
"add submodule",
],
check=True,
)
git_commit(git_exe, "add submodule")

(git_repository / ".gitmodules.license").write_text(header)

Expand Down Expand Up @@ -367,4 +356,22 @@ def mock_date_today(monkeypatch):
monkeypatch.setattr(datetime, "date", date)


def git_commit(git_exe, message):
subprocess.run(
[
git_exe,
# Git can be globally configured to digitally sign all commits,
# which causes problems when running the test suite, as the user
# would be prompted to authorize the signing multiple times per
# test execution. The option below configures git to skip signing.
"-c",
"commit.gpgSign=false",
"commit",
"-m",
message,
],
check=True,
)


# REUSE-IgnoreEnd
Loading

0 comments on commit d851e83

Please sign in to comment.