Skip to content

Commit a5c0617

Browse files
authored
✨Generate Licensing report summaries (#47)
* generate summary * Build a summary added various templates * trying to make code climate happy
1 parent 612c6a1 commit a5c0617

File tree

10 files changed

+315
-25
lines changed

10 files changed

+315
-25
lines changed

mbed_tools_ci_scripts/report_third_party_ip.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def generate_spdx_reports(output_directory: Path) -> SpdxProject:
3636
logger.info("Generating SPDX report.")
3737
project = SpdxProject(CurrentProjectMetadataParser())
3838
project.generate_tag_value_files(output_directory)
39+
logger.info("Generating licensing summary.")
40+
project.generate_licensing_summary(output_directory)
3941
return project
4042

4143

mbed_tools_ci_scripts/spdx_report/spdx_document.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@
88
from spdx.document import Document, License
99
from spdx.review import Review
1010
from spdx.version import Version
11-
from typing import List
11+
from typing import List, Optional
1212

13-
from mbed_tools_ci_scripts.spdx_report.spdx_package import SpdxPackage, PackageInfo
1413
from mbed_tools_ci_scripts.spdx_report.spdx_dependency import DependencySpdxDocumentRef
14+
from mbed_tools_ci_scripts.spdx_report.spdx_helpers import determine_spdx_value, get_project_namespace
15+
from mbed_tools_ci_scripts.spdx_report.spdx_package import SpdxPackage, PackageInfo
1516
from mbed_tools_ci_scripts.utils.configuration import (
1617
configuration,
1718
ConfigurationVariable,
1819
)
1920
from mbed_tools_ci_scripts.utils.hash_helpers import generate_uuid_based_on_str
2021
from mbed_tools_ci_scripts.utils.package_helpers import PackageMetadata
21-
from mbed_tools_ci_scripts.spdx_report.spdx_helpers import determine_spdx_value, get_project_namespace
2222

2323
TOOL_NAME = "mbed-spdx-generator"
2424

@@ -45,6 +45,7 @@ def __init__(
4545
self._is_dependency: bool = is_dependency
4646
self._other_document_references: List[DependencySpdxDocumentRef] = other_document_refs
4747
self._document_namespace = document_namespace
48+
self._spdx_package: Optional[SpdxPackage] = None
4849

4950
@property
5051
def document_name(self) -> str:
@@ -188,15 +189,17 @@ def generate_spdx_package(self) -> SpdxPackage:
188189
Returns:
189190
corresponding SPDX package.
190191
"""
191-
return SpdxPackage(
192-
PackageInfo(
193-
metadata=self._package_metadata,
194-
root_dir=self._project_root,
195-
source_dir=self._project_source,
196-
uuid=self._project_uuid,
197-
),
198-
is_dependency=self._is_dependency,
199-
)
192+
if not self._spdx_package:
193+
self._spdx_package = SpdxPackage(
194+
PackageInfo(
195+
metadata=self._package_metadata,
196+
root_dir=self._project_root,
197+
source_dir=self._project_source,
198+
uuid=self._project_uuid,
199+
),
200+
is_dependency=self._is_dependency,
201+
)
202+
return self._spdx_package
200203

201204
def generate_spdx_document(self) -> Document:
202205
"""Generates the SPDX document.

mbed_tools_ci_scripts/spdx_report/spdx_package.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ def id(self) -> str:
8787
"""
8888
return self.name if self._is_dependency else self._package_info.uuid
8989

90+
@property
91+
def is_dependency(self) -> bool:
92+
"""States whether the package is a dependency or not."""
93+
return self._is_dependency
94+
9095
@property
9196
def name(self) -> str:
9297
"""Gets Package's name.

mbed_tools_ci_scripts/spdx_report/spdx_project.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from mbed_tools_ci_scripts.utils.hash_helpers import determine_sha1_hash_of_file
1515
from mbed_tools_ci_scripts.utils.package_helpers import ProjectMetadataParser
1616
from mbed_tools_ci_scripts.spdx_report.spdx_helpers import is_package_licence_checked
17+
from mbed_tools_ci_scripts.spdx_report.spdx_summary import SummaryGenerator
1718

1819

1920
class SpdxProject:
@@ -71,10 +72,20 @@ def generate_tag_value_file(dir: Path, spdx_doc: SpdxDocument, filename: str = "
7172
raise NotADirectoryError(str(dir))
7273

7374
path = dir.joinpath(filename)
74-
with open(path, mode="w", encoding="utf-8") as out:
75+
with open(str(path), mode="w", encoding="utf-8") as out:
7576
write_document(spdx_doc.generate_spdx_document(), out)
7677
return determine_sha1_hash_of_file(path)
7778

79+
def generate_licensing_summary(self, dir: Path) -> None:
80+
"""Generates licensing summary into the specified directory.
81+
82+
Args:
83+
dir: output directory
84+
"""
85+
SummaryGenerator(
86+
self.main_document.generate_spdx_package(), [d.generate_spdx_package() for d in self.dependency_documents]
87+
).generate_summary(dir)
88+
7889
def generate_tag_value_files(self, dir: Path) -> None:
7990
"""Generates SPDX tag-value files into the specified directory.
8091
@@ -102,17 +113,6 @@ def generate_tag_value_files(self, dir: Path) -> None:
102113
self.main_document.external_refs = externalRefs
103114
SpdxProject.generate_tag_value_file(dir, self.main_document, f"{self.main_document.name}.spdx")
104115

105-
@staticmethod
106-
def _check_package_licence(package_document: SpdxDocument) -> Tuple[bool, bool, str, str, str]:
107-
package = package_document.generate_spdx_package()
108-
return (
109-
package.is_main_licence_accepted,
110-
package.is_licence_accepted,
111-
package.name,
112-
package.main_licence,
113-
package.licence,
114-
)
115-
116116
def _report_issues(self, issues: Dict[str, str]) -> None:
117117
if issues:
118118
raise ValueError(
@@ -125,7 +125,7 @@ def _report_issues(self, issues: Dict[str, str]) -> None:
125125
)
126126

127127
def _check_one_licence_compliance(self, spdx_document: SpdxDocument, issues: Dict[str, str]) -> None:
128-
main_valid, actual_valid, name, main_licence, actual_licence = SpdxProject._check_package_licence(spdx_document)
128+
main_valid, actual_valid, name, main_licence, actual_licence = _check_package_licence(spdx_document)
129129
if not ((main_valid and actual_valid) or is_package_licence_checked(name)):
130130
issues[name] = actual_licence if main_valid else main_licence
131131

@@ -145,3 +145,14 @@ def check_licence_compliance(self) -> None:
145145
self._check_package_licence_compliance(issues)
146146
self._check_package_dependencies_licence_compliance(issues)
147147
self._report_issues(issues)
148+
149+
150+
def _check_package_licence(package_document: SpdxDocument) -> Tuple[bool, bool, str, str, str]:
151+
package = package_document.generate_spdx_package()
152+
return (
153+
package.is_main_licence_accepted,
154+
package.is_licence_accepted,
155+
package.name,
156+
package.main_licence,
157+
package.licence,
158+
)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""Summary generators."""
2+
import datetime
3+
import jinja2
4+
import logging
5+
import os
6+
from pathlib import Path
7+
from typing import List, Tuple, Optional, Dict, Any
8+
9+
from mbed_tools_ci_scripts.spdx_report.spdx_helpers import is_package_licence_checked
10+
from mbed_tools_ci_scripts.spdx_report.spdx_package import SpdxPackage
11+
12+
JINJA_TEMPLATE_SUMMARY_HTML = "third_party_IP_report.html.jinja2"
13+
JINJA_TEMPLATE_SUMMARY_CSV = "third_party_IP_report.csv.jinja2"
14+
JINJA_TEMPLATE_SUMMARY_TEXT = "third_party_IP_report.txt.jinja2"
15+
JINJA_TEMPLATES = [JINJA_TEMPLATE_SUMMARY_HTML, JINJA_TEMPLATE_SUMMARY_CSV, JINJA_TEMPLATE_SUMMARY_TEXT]
16+
logger = logging.getLogger(__name__)
17+
try:
18+
jinja2_env = jinja2.Environment(
19+
loader=jinja2.PackageLoader("mbed_tools_ci_scripts.spdx_report.spdx_summary", "templates"),
20+
autoescape=jinja2.select_autoescape(["html", "xml"]),
21+
)
22+
except ModuleNotFoundError as e:
23+
logger.error(e)
24+
25+
26+
def generate_file_based_on_template(
27+
output_dir: Path, template_name: str, template_args: dict, suffix: str = None
28+
) -> None:
29+
"""Write file based on template and arguments."""
30+
logger.info("Loading template '%s'.", template_name)
31+
template = jinja2_env.get_template(template_name)
32+
filename = Path(template_name.rsplit(".", 1)[0])
33+
if suffix:
34+
filename = Path(
35+
"{0}_{2}{1}".format(
36+
*(str(filename.name), str(filename.suffix), str(suffix.replace(".", "_").replace("-", "_")),)
37+
)
38+
)
39+
output_filename = output_dir.joinpath(filename)
40+
rendered = template.render(**template_args)
41+
logger.info("Writing to '%s'.", output_filename)
42+
output_filename.write_text(rendered, encoding="utf8")
43+
44+
45+
class SummaryGenerator:
46+
"""Licensing summary generator."""
47+
48+
def __init__(self, project_package: SpdxPackage, dependencies_documents: List[SpdxPackage]) -> None:
49+
"""Initialiser."""
50+
self.project = project_package
51+
self.all_packages = list(dependencies_documents)
52+
self.all_packages.append(self.project)
53+
self._template_arguments: Optional[dict] = None
54+
55+
def _generate_template_arguments(self) -> Dict[str, Any]:
56+
arguments: Dict[str, Any] = dict()
57+
58+
global_compliance, description_list = self._generate_packages_description()
59+
arguments["project"] = {
60+
"name": self.project.name,
61+
"compliance": global_compliance,
62+
"compliance_details": (
63+
f"Project [{self.project.name}]'s licence is compliant: {self.project.licence}.{os.linesep}"
64+
"All its dependencies are also compliant licence-wise."
65+
)
66+
if global_compliance
67+
else f"Project [{self.project.name}] or one, at least, of its dependencies has a non compliant licence",
68+
}
69+
arguments["packages"] = description_list
70+
arguments["render_time"] = datetime.datetime.now()
71+
return arguments
72+
73+
def _generate_packages_description(self) -> Tuple[bool, dict]:
74+
description_list = dict()
75+
global_compliance = True
76+
for p in self.all_packages:
77+
main_licence_valid = p.is_main_licence_accepted
78+
actual_licence_valid = p.is_licence_accepted
79+
package_checked = is_package_licence_checked(p.name)
80+
is_licence_compliant = main_licence_valid and actual_licence_valid
81+
is_compliant = is_licence_compliant or package_checked
82+
if not is_compliant:
83+
global_compliance = False
84+
description_list[p.name] = self._generate_description_for_one_package(
85+
is_compliant, is_licence_compliant, package_checked, p
86+
)
87+
88+
return global_compliance, description_list
89+
90+
def _generate_description_for_one_package(
91+
self, is_compliant: bool, is_licence_compliant: bool, package_checked: bool, p: SpdxPackage
92+
) -> dict:
93+
return {
94+
"name": p.name,
95+
"is_dependency": p.is_dependency,
96+
"url": p.url,
97+
"licence": p.licence,
98+
"is_compliant": is_compliant,
99+
"mark_as_problematic": not is_licence_compliant,
100+
"licence_compliance_details": "Licence is compliant."
101+
if is_licence_compliant
102+
else (
103+
"Package's licence has been checked"
104+
if package_checked
105+
else "Licence is not compliant according to project's configuration."
106+
),
107+
}
108+
109+
@property
110+
def template_arguments(self) -> dict:
111+
"""Gets template arguments."""
112+
if not self._template_arguments:
113+
self._template_arguments = self._generate_template_arguments()
114+
return self._template_arguments
115+
116+
def generate_summary(self, dir: Path) -> None:
117+
"""Generates a licensing summary into the specified directory.
118+
119+
Args:
120+
dir: output directory
121+
"""
122+
for t in JINJA_TEMPLATES:
123+
generate_file_based_on_template(dir, t, self.template_arguments)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Compliance, Name,Is a dependency?,URL,Licence,Details
2+
{% for name,package in packages|dictsort %}
3+
{%- if package.is_compliant == True -%}True{%- else -%}False{%- endif -%},{{ package.name }},{%- if package.is_dependency == True -%}True{%- else -%}False{%- endif -%},{{ package.url }},{{ package.licence }},{{ package.licence_compliance_details }}
4+
{% endfor %}

0 commit comments

Comments
 (0)