Skip to content

Commit

Permalink
Merge pull request #623 from ferrocene/pa-spdx-concluded-license
Browse files Browse the repository at this point in the history
Populate the `LicenseConcluded` field in the SPDX report
  • Loading branch information
linozen authored Apr 6, 2023
2 parents 5284f0a + 854b5f3 commit 0804dc4
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 27 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ The versions follow [semantic versioning](https://semver.org).
- Clang format (`.clang-format`) (#632)
- Added loglevel argument to pytest and skip one test if loglevel is too high
(#645).
- `--add-license-concluded`, `--creator-person`, and `--creator-organization`
added to `reuse spdx`. (#623)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion src/reuse/lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ def lint_summary(report: ProjectReport, out=sys.stdout) -> None:
def add_arguments(parser):
"""Add arguments to parser."""
parser.add_argument(
"-q", "--quiet", action="store_true", help="Prevents output"
"-q", "--quiet", action="store_true", help=_("prevents output")
)


Expand Down
70 changes: 59 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 <florian@familysnow.net>
# SPDX-FileCopyrightText: 2022 Pietro Albini <pietro.albini@ferrous-systems.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -27,17 +28,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 @@ -98,7 +103,11 @@ 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 @@ -118,9 +127,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 @@ -145,9 +155,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 @@ -177,6 +187,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 @@ -186,7 +197,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 @@ -306,6 +319,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


Expand Down Expand Up @@ -337,7 +351,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 @@ -378,6 +396,26 @@ def generate(
# Add license to report.
report.spdxfile.licenses_in_file.append(identifier)

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))

Expand All @@ -387,3 +425,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: str) -> str:
"""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 + " ()"
45 changes: 43 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 <pietro.albini@ferrous-systems.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand All @@ -22,10 +23,43 @@ 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
):
args.parser.error(
_(
"error: --creator-person=NAME or --creator-organization=NAME"
" required when --add-license-concluded is provided"
),
)

with contextlib.ExitStack() as stack:
if args.file:
out = stack.enter_context(args.file.open("w", encoding="utf-8"))
Expand All @@ -43,9 +77,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
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,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 @@ -124,6 +129,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,11 +174,13 @@ def git_repository(fake_repository: Path, git_exe: Optional[str]) -> Path:
os.chdir(fake_repository)
_repo_contents(fake_repository)

# TODO: To speed this up, maybe directly write to '.gitconfig' instead.
subprocess.run([git_exe, "init", str(fake_repository)], check=True)
subprocess.run([git_exe, "config", "user.name", "Example"], check=True)
subprocess.run(
[git_exe, "config", "user.email", "example@example.com"], check=True
)
subprocess.run([git_exe, "config", "commit.gpgSign", "false"], check=True)

subprocess.run([git_exe, "add", str(fake_repository)], check=True)
subprocess.run(
Expand Down
1 change: 1 addition & 0 deletions tests/resources/fake_repository/LICENSES/Apache-2.0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
License text.
1 change: 1 addition & 0 deletions tests/resources/fake_repository/src/multiple_licenses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
// This file is overridden by the fake_repository fixture.
59 changes: 58 additions & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: 2019 Stefan Bakker <s.bakker777@gmail.com>
# SPDX-FileCopyrightText: © 2020 Liferay, Inc. <https://liferay.com>
# SPDX-FileCopyrightText: 2022 Florian Snow <florian@familysnow.net>
# SPDX-FileCopyrightText: 2022 Pietro Albini <pietro.albini@ferrous-systems.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -277,10 +278,66 @@ def test_spdx(fake_repository, stringio):
"""Compile to an SPDX document."""
os.chdir(str(fake_repository))
result = main(["spdx"], out=stringio)
output = stringio.getvalue()

# Ensure no LicenseConcluded is included without the flag
assert "\nLicenseConcluded: NOASSERTION\n" in output
assert "\nLicenseConcluded: GPL-3.0-or-later\n" not in output
assert "\nCreator: Person: Anonymous ()\n" in output
assert "\nCreator: Organization: Anonymous ()\n" in output

# TODO: This test is rubbish.
assert result == 0
assert stringio.getvalue()
assert output


def test_spdx_creator_info(fake_repository, stringio):
"""Ensure the --creator-* flags are properly formatted"""
os.chdir(str(fake_repository))
result = main(
[
"spdx",
"--creator-person=Jane Doe (jane.doe@example.org)",
"--creator-organization=FSFE",
],
out=stringio,
)
output = stringio.getvalue()

assert result == 0
assert "\nCreator: Person: Jane Doe (jane.doe@example.org)\n" in output
assert "\nCreator: Organization: FSFE ()\n" in output


def test_spdx_add_license_concluded(fake_repository, stringio):
"""Compile to an SPDX document with the LicenseConcluded field."""
os.chdir(str(fake_repository))
result = main(
[
"spdx",
"--add-license-concluded",
"--creator-person=Jane Doe",
"--creator-organization=FSFE",
],
out=stringio,
)
output = stringio.getvalue()

# Ensure no LicenseConcluded is included without the flag
assert result == 0
assert "\nLicenseConcluded: NOASSERTION\n" not in output
assert "\nLicenseConcluded: GPL-3.0-or-later\n" in output
assert "\nCreator: Person: Jane Doe ()\n" in output
assert "\nCreator: Organization: FSFE ()\n" in output


def test_spdx_add_license_concluded_without_creator_info(
fake_repository, stringio
):
"""Adding LicenseConcluded should require creator information"""
os.chdir(str(fake_repository))
with pytest.raises(SystemExit):
main(["spdx", "--add-license-concluded"], out=stringio)


def test_spdx_no_multiprocessing(fake_repository, stringio, multiprocessing):
Expand Down
Loading

0 comments on commit 0804dc4

Please sign in to comment.