Skip to content

Commit e103db8

Browse files
Spdx generation filter
1 parent b09a1a6 commit e103db8

File tree

5 files changed

+210
-0
lines changed

5 files changed

+210
-0
lines changed

fusesoc/capi2/core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,9 @@ def get_name(self):
637637
def get_description(self):
638638
return self._coredata.get_description()
639639

640+
def get_license(self):
641+
return self._coredata.get("license")
642+
640643
@property
641644
def mapping(self) -> Optional[Mapping[str, str]]:
642645
return MappingProxyType(self._coredata.get("mapping", {}))

fusesoc/edalizer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ def create_edam(self):
229229
str(core.name): {
230230
"core_file": os.path.relpath(core.core_file, self.work_root),
231231
"dependencies": core.direct_deps,
232+
"license": core.get_license(),
232233
}
233234
}
234235

fusesoc/filters/spdxgen.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env python
2+
# Copyright FuseSoC contributors
3+
# Licensed under the 2-Clause BSD License, see LICENSE for details.
4+
# SPDX-License-Identifier: BSD-2-Clause
5+
6+
import datetime
7+
import hashlib
8+
import json
9+
import os
10+
11+
try:
12+
import nanoid
13+
except:
14+
print(
15+
"Filter spdxgen needs the nanoid python package, please install it and try again."
16+
)
17+
18+
19+
class Spdxgen:
20+
no_assertion_id = "https://FuseSoC/license/noAssertion"
21+
22+
def _generate_preamble(self, ci_id, now):
23+
ei_id = "https://FuseSoC/externalid/" + nanoid.generate()
24+
doc_id = "https://FuseSoC/document/" + nanoid.generate()
25+
bom_id = "https://FuseSoC/document/" + nanoid.generate()
26+
27+
preamble = [
28+
# Creation info and children
29+
{
30+
# The id of the creating agent (this program) is
31+
# hardcoded. Should probably be a generated hash.
32+
"type": "CreationInfo",
33+
"created": now,
34+
"createdBy": "https://FuseSoC/program/3mLEPOMKT/Q3QmuONJ+QuQ",
35+
"spdx:Core/specVersion": "3.0.1",
36+
"@id": ci_id,
37+
},
38+
{
39+
# The id of the external identifier for the creating
40+
# agent is random for each run. This makes no
41+
# particular sense.
42+
"spdxId": "https://FuseSoC/program/3mLEPOMKT/Q3QmuONJ+QuQ",
43+
"type": "SoftwareAgent",
44+
"creationInfo": ci_id,
45+
"externalIdentifier": ei_id,
46+
"spdx:Core/name": "FuseSoC",
47+
},
48+
{
49+
"type": "ExternalIdentifier",
50+
"externalIdentifierType": "other",
51+
"spdx:Core/identifier": "https://github.com/olofk/fusesoc/",
52+
"@id": ei_id,
53+
},
54+
# "NoAssertion" license to reference anywhere one is needed.
55+
{
56+
"spdxId": self.no_assertion_id,
57+
"type": "simplelicensing_LicenseExpression",
58+
"creationInfo": ci_id,
59+
"spdx:SimpleLicensing/licenseExpression": "No assertion",
60+
},
61+
# Document and sbom
62+
{
63+
"spdxId": doc_id,
64+
"type": "SpdxDocument",
65+
"creationInfo": ci_id,
66+
"profileConformance": ["core", "lite"],
67+
"rootElement": bom_id,
68+
},
69+
{
70+
"spdxId": bom_id,
71+
"type": "software_Sbom",
72+
"creationInfo": ci_id,
73+
"rootElement": "", # Filled in later
74+
"element": [], # Filled in later
75+
"software_sbomType": "source",
76+
},
77+
]
78+
return preamble, preamble[-1]
79+
80+
def _generate_package(self, work_root, ci_id, vlnv, license_expr, flist):
81+
[vln, version] = vlnv.rsplit(":", 1)
82+
[vendor, library, name] = vln.split(":")
83+
84+
concl_license_id = "https://FuseSoC/license/" + nanoid.generate()
85+
concl_license_rel_id = "https://FuseSoC/relationship/" + nanoid.generate()
86+
decl_license_rel_id = "https://FuseSoC/relationship/" + nanoid.generate()
87+
purl = f"pkg:fusesoc/{vendor}/{library}/{name}@{version}"
88+
pkg_id = "https://FuseSoC/package/" + vlnv
89+
pkg = [
90+
{
91+
"spdxId": pkg_id,
92+
"type": "software_Package",
93+
"creationInfo": ci_id,
94+
"spdx:Core/name": vln,
95+
"software_packageUrl": purl,
96+
"spdx:Software/packageVersion": version,
97+
},
98+
# Declared license, hardcoded NoAssertion
99+
{
100+
"spdxId": decl_license_rel_id,
101+
"type": "Relationship",
102+
"creationInfo": ci_id,
103+
"from": pkg_id,
104+
"relationshipType": "hasDeclaredLicense",
105+
"to": self.no_assertion_id,
106+
},
107+
# Concluded license, from core file
108+
{
109+
"spdxId": concl_license_id,
110+
"type": "simplelicensing_LicenseExpression",
111+
"creationInfo": ci_id,
112+
"spdx:SimpleLicensing/licenseExpression": license_expr,
113+
},
114+
{
115+
"spdxId": concl_license_rel_id,
116+
"type": "Relationship",
117+
"creationInfo": ci_id,
118+
"from": pkg_id,
119+
"relationshipType": "hasConcludedLicense",
120+
"to": concl_license_id,
121+
},
122+
{
123+
"spdxId": "relationShip:" + vlnv + ":files",
124+
"type": "Relationship",
125+
"creationInfo": ci_id,
126+
"from": pkg_id,
127+
"relationshipType": "contains",
128+
"to": [],
129+
},
130+
]
131+
file_rel = pkg[-1]
132+
for fname in flist:
133+
file_id = "https://FuseSoC/file/" + nanoid.generate()
134+
hash_id = "https://FuseSoC/hash/" + nanoid.generate()
135+
with open(os.path.join(work_root, fname), "rb", buffering=0) as f:
136+
hash_val = hashlib.file_digest(f, "sha256").hexdigest()
137+
pkg += [
138+
{
139+
"spdxId": file_id,
140+
"type": "software_File",
141+
"creationInfo": ci_id,
142+
"spdx:Core/name": fname,
143+
"verifiedUsing": hash_id,
144+
"software_fileKind": "file",
145+
},
146+
{
147+
"type": "Hash",
148+
"algorithm": "sha256",
149+
"spdx:Core/hashValue": hash_val,
150+
"@id": hash_id,
151+
},
152+
]
153+
file_rel["to"].append(file_id)
154+
return pkg, pkg_id
155+
156+
def run(self, edam, work_root):
157+
with open(os.path.join(work_root, edam["name"] + ".spdx.json"), "w") as f:
158+
ci_id = "https://FuseSoC/creationinfo/" + nanoid.generate()
159+
dt = datetime.datetime.now(datetime.UTC)
160+
dt.replace(microsecond=0) # for isoformat to stop at seconds
161+
now = dt.isoformat()
162+
graph, sbom = self._generate_preamble(ci_id, now)
163+
164+
core_dict = {}
165+
for fn in edam["files"]:
166+
core = fn["core"]
167+
name = fn["name"]
168+
if core in core_dict:
169+
core_dict[core].append(name)
170+
else:
171+
core_dict[core] = [name]
172+
for vlnv, flist in core_dict.items():
173+
license_expr = "No assertion"
174+
le = edam["cores"][vlnv]["license"]
175+
if le:
176+
license_expr = le
177+
package, pid = self._generate_package(
178+
work_root, ci_id, vlnv, license_expr, flist
179+
)
180+
graph += package
181+
if len(sbom["rootElement"]) < 1:
182+
sbom["rootElement"] = pid
183+
else:
184+
sbom["element"].append(pid)
185+
context = "https://spdx.github.io/spdx-spec/v3.0.1/rdf/spdx-context.jsonld"
186+
f.write(json.dumps({"@context": context, "@graph": graph}))
187+
return edam
188+
189+
190+
def main():
191+
print("foo")
192+
193+
194+
if __name__ == "__main__":
195+
main()

tests/capi2_cores/misc/generate/generate.core

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CAPI=2:
55

66
name : ::generate:0
77

8+
license: MIT
9+
810
filesets:
911
default:
1012
depend:

tests/test_edalizer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def test_tool_or_flow():
8888
"::flow:0": {
8989
"core_file": "tests/capi2_cores/misc/flow.core",
9090
"dependencies": [],
91+
"license": None,
9192
}
9293
},
9394
"dependencies": {"::flow:0": []},
@@ -196,6 +197,7 @@ def test_generators():
196197
"::generators:0": {
197198
"core_file": "generators.core",
198199
"dependencies": [],
200+
"license": None,
199201
},
200202
"::generate:0": {
201203
"core_file": "generate.core",
@@ -207,30 +209,37 @@ def test_generators():
207209
"::generate-testgenerate_with_cache:0",
208210
"::generate-testgenerate_with_file_cache:0",
209211
],
212+
"license": "MIT",
210213
},
211214
"::generators:0": {
212215
"core_file": "generators.core",
213216
"dependencies": [],
217+
"license": None,
214218
},
215219
"::generate-testgenerate_without_params:0": {
216220
"core_file": "generated.core",
217221
"dependencies": [],
222+
"license": None,
218223
},
219224
"::generate-testgenerate_with_params:0": {
220225
"core_file": "generated.core",
221226
"dependencies": [],
227+
"license": None,
222228
},
223229
"::generate-testgenerate_with_override:0": {
224230
"core_file": "generated.core",
225231
"dependencies": [],
232+
"license": None,
226233
},
227234
"::generate-testgenerate_with_cache:0": {
228235
"core_file": "generated.core",
229236
"dependencies": [],
237+
"license": None,
230238
},
231239
"::generate-testgenerate_with_file_cache:0": {
232240
"core_file": "generated.core",
233241
"dependencies": [],
242+
"license": None,
234243
},
235244
},
236245
"toplevel": "na",

0 commit comments

Comments
 (0)