buildstream-sbom 1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,190 @@
1
+ # SPDX-FileCopyrightText: 2025 Codethink Limited
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import re
9
+ import subprocess
10
+ import sys
11
+
12
+ import yaml
13
+
14
+ from pathlib import Path
15
+
16
+ from .spdx import generate_spdx
17
+ from .utils import get_hosttool, bst_common_args
18
+
19
+
20
+ def configure():
21
+ parser = argparse.ArgumentParser(
22
+ description="Generate an SPDX formatted SBoM manifest from a BuildStream project"
23
+ )
24
+
25
+ parser.add_argument(
26
+ "--spdx-name",
27
+ metavar="NAME",
28
+ help="SPDX document name",
29
+ required=True,
30
+ )
31
+ parser.add_argument(
32
+ "--spdx-namespace",
33
+ metavar="NAMESPACE",
34
+ help="SPDX document namespace",
35
+ required=True,
36
+ )
37
+ parser.add_argument(
38
+ "--spdx-comment",
39
+ metavar="COMMENT",
40
+ help="SPDX document comment",
41
+ )
42
+ parser.add_argument(
43
+ "--spdx-creator",
44
+ metavar="CREATOR",
45
+ action="append",
46
+ dest="spdx_creators",
47
+ help="SPDX creator, in the format mandated by SPDX. "
48
+ "Can be passed multiple times for multiple creators.",
49
+ )
50
+
51
+ parser.add_argument(
52
+ "-C",
53
+ "--directory",
54
+ metavar="DIRECTORY",
55
+ help="BuildStream project directory",
56
+ )
57
+
58
+ parser.add_argument(
59
+ "-o",
60
+ "--option",
61
+ metavar="OPTION VALUE",
62
+ nargs=2,
63
+ action="append",
64
+ dest="options",
65
+ default=[],
66
+ help="BuildStream project option",
67
+ )
68
+
69
+ parser.add_argument(
70
+ "--output",
71
+ metavar="FILE",
72
+ type=argparse.FileType("w"),
73
+ help="Output filename",
74
+ default="-",
75
+ )
76
+
77
+ parser.add_argument("--deps", choices=["all", "run"], default="run")
78
+
79
+ parser.add_argument(
80
+ "--with-licenses",
81
+ action=argparse.BooleanOptionalAction,
82
+ default=False,
83
+ help="Attempt to extract licenses from build artifacts and add them to the SBOM",
84
+ )
85
+
86
+ # A lot of artifacts can be checked out so it is a good idea to let users choose where these
87
+ # go in case of storage space limitations
88
+ parser.add_argument(
89
+ "--artifact-checkout-directory",
90
+ "-A",
91
+ default=Path(os.getcwd()),
92
+ help="Directory to use when checking out artifacts for processing; "
93
+ "the artifacts will be removed once they have been processed "
94
+ "(default: %(default)s)",
95
+ type=Path,
96
+ )
97
+
98
+ parser.add_argument(
99
+ "elements",
100
+ metavar="ELEMENT",
101
+ nargs="+",
102
+ help="Elements to generate SBoM for",
103
+ )
104
+
105
+ return parser.parse_args()
106
+
107
+
108
+ def extract_element_infos(config):
109
+ element_infos = []
110
+
111
+ try:
112
+ result = run_bst_show(config)
113
+ except subprocess.CalledProcessError:
114
+ exit(1)
115
+
116
+ documents = yaml.load_all(result.stdout, yaml.BaseLoader)
117
+ try:
118
+ while True:
119
+ name = next(documents)
120
+ kind = next(documents)
121
+ source_infos = next(documents)
122
+ build_deps = next(documents)
123
+ runtime_deps = next(documents)
124
+ config = next(documents)
125
+ public = next(documents)
126
+ vars = next(documents)
127
+
128
+ element_infos.append(
129
+ {
130
+ "name": name,
131
+ "kind": None if kind == "%{kind}" else kind,
132
+ "source-infos": source_infos,
133
+ "build-deps": build_deps,
134
+ "runtime-deps": runtime_deps,
135
+ "config": config,
136
+ "public": public,
137
+ "vars": vars,
138
+ }
139
+ )
140
+ except StopIteration:
141
+ pass
142
+
143
+ return element_infos
144
+
145
+
146
+ def check_creators(config):
147
+ spdx_creators = ["Person", "Organization", "Tool"]
148
+ valid_creator = True
149
+ for c in config.spdx_creators:
150
+ valid_creator &= any(re.split(":", c)[0] in sc for sc in spdx_creators)
151
+ if not valid_creator:
152
+ sys.exit(
153
+ "ERROR: SPDX creator should follow the SPDX format.\n"
154
+ "See: https://spdx.github.io/spdx-spec/v2.3/document-creation-information/#68-creator-field"
155
+ )
156
+
157
+
158
+ def run_bst_show(config):
159
+ # Python documentation recommends using a full path for the executable
160
+ # https://docs.python.org/3/library/subprocess.html#subprocess.Popen
161
+ bst_cmdline = [get_hosttool("bst")]
162
+
163
+ bst_cmdline += bst_common_args(config)
164
+
165
+ bst_cmdline += [
166
+ "show",
167
+ "--format",
168
+ "---\n%{name}\n---\n'%{kind}'\n---\n%{source-info}\n---\n%{build-deps}\n---\n%{runtime-deps}\n---\n%{config}\n---\n%{public}---\n%{vars}",
169
+ ]
170
+ bst_cmdline += config.elements
171
+
172
+ return subprocess.run(bst_cmdline, stdout=subprocess.PIPE, text=True, check=True)
173
+
174
+
175
+ def main():
176
+ config = configure()
177
+
178
+ if config.spdx_creators:
179
+ check_creators(config)
180
+
181
+ element_infos = extract_element_infos(config)
182
+
183
+ spdx = generate_spdx(element_infos, config)
184
+
185
+ with config.output:
186
+ json.dump(spdx, config.output, indent=2)
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
@@ -0,0 +1,151 @@
1
+ # SPDX-FileCopyrightText: 2026 Codethink Limited
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import subprocess
6
+ import sys
7
+ import json
8
+ import tempfile
9
+
10
+ from argparse import Namespace
11
+ from pathlib import Path
12
+
13
+ from .utils import get_hosttool, bst_common_args
14
+
15
+
16
+ def get_license_artifacts(
17
+ element: str,
18
+ artifact_checkout_path: Path,
19
+ licenses_path: Path,
20
+ config: Namespace,
21
+ ) -> Path | None:
22
+ """Use Buildstream artifact checkout to get the installed licenses of an element and outputs the licenses, specified by
23
+ `license_artifact_path`, to the chosen output directory.
24
+
25
+ If the artifact checkout failed then the it will cause the program to exit.
26
+
27
+ Returns a Path to the licenses or None if none were found in the artifacts
28
+ """
29
+
30
+ # bst artifact checkout
31
+ bst_cmdline = [get_hosttool("bst")]
32
+
33
+ bst_cmdline += bst_common_args(config)
34
+
35
+ bst_cmdline += [
36
+ "artifact",
37
+ "checkout",
38
+ element,
39
+ "--directory", # Note: This is the artifact checkout directory arg and not the same as the bst project directory
40
+ artifact_checkout_path,
41
+ "--deps",
42
+ "none",
43
+ "--no-integrate",
44
+ ]
45
+
46
+ r = subprocess.run(bst_cmdline)
47
+
48
+ if r.returncode != 0:
49
+ sys.exit("ERROR: Failed to checkout artifacts")
50
+
51
+ if licenses_path.exists():
52
+ return licenses_path
53
+
54
+ return None
55
+
56
+
57
+ def extract_licenses(
58
+ input_path, element_name, artifact_checkout_path
59
+ ) -> list[str] | None:
60
+ """Attempt to extract licenses from `input_path` using Askalono.
61
+
62
+ Unrecognised licenses automatically trigger warning outputs.
63
+
64
+ If the Akalono command fails for any reason it will cause the program to exit.
65
+
66
+ `element_name` and `artifact_checkout_path` are only used for providing error
67
+ context reporting and have no impact on the license extraction.
68
+ """
69
+ askalono_bin = get_hosttool("askalono")
70
+
71
+ r = subprocess.run(
72
+ [askalono_bin, "--format", "json", "crawl", str(input_path)],
73
+ stdout=subprocess.PIPE,
74
+ stderr=subprocess.PIPE,
75
+ text=True,
76
+ )
77
+
78
+ if r.returncode != 0:
79
+ sys.exit(f"ERROR: Askalono command failed.\n{r.stderr}")
80
+
81
+ license_json = r.stdout
82
+
83
+ # Askalono ouptuts multiple licenses as separate objects but not as an array,
84
+ # need to combine them in to an array
85
+ license_json_split = license_json.splitlines()
86
+
87
+ # use set to avoid duplicates
88
+ licenses = set()
89
+ failures = {}
90
+ for license_object_json in license_json_split:
91
+ license_object = json.loads(license_object_json)
92
+ license_result = license_object.get("result", None)
93
+ if license_result is None:
94
+ offending_file = license_object.get("path", "Missing File Name")
95
+ if offending_file is not None:
96
+ offending_file = offending_file.removeprefix(
97
+ str(artifact_checkout_path)
98
+ )
99
+
100
+ files_with_error = failures.setdefault(
101
+ license_object.get("error", "Missing Error"), []
102
+ )
103
+ files_with_error.append(offending_file)
104
+
105
+ continue
106
+
107
+ licenses.add(license_result["license"]["name"])
108
+
109
+ for error, files in failures.items():
110
+ # python doesn't allow for '\' in f-string parts so need to use format
111
+ print(
112
+ "{0}: {1} found in\n\t{2}\n".format(
113
+ element_name, error, "\n\t".join(files)
114
+ ),
115
+ file=sys.stderr,
116
+ )
117
+
118
+ if len(licenses) == 0:
119
+ return None
120
+
121
+ return list(licenses)
122
+
123
+
124
+ def retrieve_licenses(
125
+ element_name: str, artifact_license_path: Path, config: Namespace
126
+ ) -> list[str] | None:
127
+ """Retrieve licenses for an element by checking out its artifacts to `artifact_checkout_root` and scanning
128
+ `licenses_artifact_path` for any identifiable licenses.
129
+
130
+ Returns None if none could be found otherwise returns licenses as in a list
131
+ """
132
+
133
+ # path to use for artifact checkout
134
+ with tempfile.TemporaryDirectory(
135
+ dir=config.artifact_checkout_directory
136
+ ) as artifact_checkout_path:
137
+ # path to the licenses inside the artifact
138
+ licenses_path = Path(artifact_checkout_path, artifact_license_path)
139
+
140
+ license_data_path = get_license_artifacts(
141
+ element_name, artifact_checkout_path, licenses_path, config
142
+ )
143
+
144
+ if license_data_path is None:
145
+ return None
146
+
147
+ license_data = extract_licenses(
148
+ license_data_path, element_name, artifact_checkout_path
149
+ )
150
+
151
+ return license_data
@@ -0,0 +1,337 @@
1
+ # SPDX-FileCopyrightText: 2025 Codethink Limited
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from buildstream_sbom.utils import bst_common_args
6
+ import os
7
+ import sys
8
+ import re
9
+ import subprocess
10
+
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+
14
+ from .licensing import retrieve_licenses
15
+
16
+ from .utils import get_hosttool
17
+
18
+ # buildstream-sbom supported spdx attributes
19
+ # this could be generic for any key in provenance but being
20
+ # explicit about these attributes guarantees valid SPDX
21
+ SOURCE_PROVENANCE_ATTRIBUTES = {
22
+ "concluded-license": "licenseConcluded",
23
+ "copyright-text": "copyrightText",
24
+ "declared-license": "licenseDeclared",
25
+ "description": "summary",
26
+ "homepage": "homepage",
27
+ "name": "name",
28
+ "originator": "originator",
29
+ "supplier": "supplier",
30
+ }
31
+
32
+
33
+ def generate_spdx(element_infos, config):
34
+ element_infos = {
35
+ element_info["name"]: element_info for element_info in element_infos
36
+ }
37
+ packages = []
38
+ relationships = []
39
+
40
+ elements_done = set()
41
+ elements_to_do = set()
42
+
43
+ for element in config.elements:
44
+ relationships.append(
45
+ {
46
+ "spdxElementId": "SPDXRef-DOCUMENT",
47
+ "relationshipType": "DESCRIBES",
48
+ "relatedSpdxElement": element_spdxid(element),
49
+ }
50
+ )
51
+ elements_to_do.add(element)
52
+
53
+ while elements_to_do:
54
+ name = elements_to_do.pop()
55
+ if name in elements_done:
56
+ continue
57
+ elements_done.add(name)
58
+
59
+ elem_info = element_infos[name]
60
+ source_infos = elem_info["source-infos"]
61
+
62
+ element_package, source_packages = generate_spdx_for_element(
63
+ name,
64
+ source_infos,
65
+ elem_info,
66
+ config,
67
+ )
68
+
69
+ packages.append(element_package)
70
+ packages.extend(source_packages)
71
+
72
+ for source_package in source_packages:
73
+ relationships.append(
74
+ {
75
+ "spdxElementId": element_spdxid(name),
76
+ "relationshipType": "GENERATED_FROM",
77
+ "relatedSpdxElement": source_package["SPDXID"],
78
+ }
79
+ )
80
+
81
+ # Order here doesn't matter, as we're sorting the relationships at the end
82
+ all_deps = set(elem_info["runtime-deps"])
83
+
84
+ extra_deps = get_extra_dependencies(elem_info, config.deps == "all", config)
85
+ all_deps.update(extra_deps)
86
+
87
+ for dep in all_deps:
88
+ relationships.append(
89
+ {
90
+ "spdxElementId": element_package["SPDXID"],
91
+ "relationshipType": "DEPENDS_ON",
92
+ "relatedSpdxElement": element_spdxid(dep),
93
+ }
94
+ )
95
+ elements_to_do.add(dep)
96
+
97
+ def relationship_sort_key(relationship):
98
+ return (
99
+ relationship["spdxElementId"],
100
+ relationship["relatedSpdxElement"],
101
+ relationship["relationshipType"],
102
+ )
103
+
104
+ spdx = {
105
+ "spdxVersion": "SPDX-2.3",
106
+ "dataLicense": "CC0-1.0",
107
+ "SPDXID": "SPDXRef-DOCUMENT",
108
+ }
109
+
110
+ creation_info = spdx["creationInfo"] = {}
111
+
112
+ spdx["name"] = config.spdx_name
113
+
114
+ spdx["documentNamespace"] = config.spdx_namespace
115
+
116
+ if config.spdx_comment:
117
+ spdx["comment"] = config.spdx_comment
118
+
119
+ if config.spdx_creators:
120
+ creation_info["creators"] = config.spdx_creators
121
+
122
+ created_datetime = datetime.now(timezone.utc)
123
+ # This is a specific variant of isoformat, that python's datetime
124
+ # module doesn't support
125
+ # https://spdx.github.io/spdx-spec/v2.3/document-creation-information/#691-description
126
+ creation_info["created"] = created_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
127
+
128
+ spdx["packages"] = sorted(packages, key=lambda pkg: pkg["SPDXID"])
129
+ spdx["relationships"] = sorted(relationships, key=relationship_sort_key)
130
+
131
+ return spdx
132
+
133
+
134
+ def resolve_element_name(name, config):
135
+ # Python documentation recommends using a full path for the executable
136
+ # https://docs.python.org/3/library/subprocess.html#subprocess.Popen
137
+ bst_cmdline = [get_hosttool("bst")]
138
+
139
+ bst_cmdline += bst_common_args(config)
140
+
141
+ bst_cmdline += ["show", "--deps", "none", "--format", "%{name}", name]
142
+
143
+ return subprocess.run(
144
+ bst_cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
145
+ )
146
+
147
+
148
+ def get_extra_dependencies(elem_info, all_deps, config):
149
+ """
150
+ Return extra dependencies (beyond the element runtime dependencies) that need to be
151
+ included in the SPDX. If `all_deps` is true, then all build dependencies are included.
152
+ Otherwise, it uses heuristics are based on the element kind, or hints in the element's
153
+ public data.
154
+ """
155
+
156
+ if all_deps:
157
+ return elem_info["build-deps"]
158
+
159
+ # See if we have explicit configuration in the public data
160
+ public_data = elem_info["public"].get("sbom", {})
161
+ if public_data:
162
+ extra_deps = []
163
+ junction, _, _ = elem_info["name"].rpartition(":")
164
+ for dep_name in public_data.get("depends-on", []):
165
+ # need to account for the junction where the element resides
166
+ if junction:
167
+ result = resolve_element_name(f"{junction}:{dep_name}", config)
168
+ if result.returncode == 0:
169
+ resolved_dep_name = result.stdout.strip()
170
+ else:
171
+ sys.exit(
172
+ f"Could not find dependency {dep_name}, referenced "
173
+ f"from public data of element {elem_info['name']}:\n"
174
+ f"{result.stderr.strip()}"
175
+ )
176
+ else:
177
+ resolved_dep_name = dep_name
178
+
179
+ if resolved_dep_name not in elem_info["build-deps"]:
180
+ deps_str = "\n".join(elem_info["build-deps"])
181
+ sys.exit(
182
+ f"Could not find dependency {dep_name}, referenced "
183
+ f"from public data of element {elem_info['name']} "
184
+ f"(resolved to {resolved_dep_name}). "
185
+ f"Dependency must be one of:\n{deps_str}"
186
+ )
187
+
188
+ extra_deps.append(resolved_dep_name)
189
+ return extra_deps
190
+
191
+ if include_build_deps(elem_info["kind"], elem_info["config"].keys()):
192
+ return elem_info["build-deps"]
193
+
194
+ return []
195
+
196
+
197
+ def include_build_deps(kind, config_keys):
198
+ """
199
+ Returns whether build dependencies should be always included even when generating
200
+ an SBoM with runtime dependencies. This currently means the compose and filter
201
+ elements.
202
+ """
203
+ if kind is None:
204
+ # Try to guess the element kind depending on its configuration
205
+ if config_keys == set(
206
+ ["include", "exclude", "include-orphans", "pass-integration"]
207
+ ):
208
+ kind = "filter"
209
+ if config_keys == set(["include", "exclude", "include-orphans", "integrate"]):
210
+ kind = "compose"
211
+
212
+ return kind in ("filter", "compose")
213
+
214
+
215
+ def element_licenses(name: str, elem_info, config):
216
+ license_dir: str = elem_info["vars"].get("licensedir", None)
217
+
218
+ if license_dir is not None:
219
+ license_dir = Path(license_dir.lstrip(os.sep))
220
+
221
+ licenses = retrieve_licenses(name, license_dir, config)
222
+ if licenses is None:
223
+ print(f"{name}: Skipping: No licenses found", file=sys.stderr)
224
+ return None
225
+
226
+ return " AND ".join(licenses)
227
+ else:
228
+ print(
229
+ f"Could not find license directory for {name}, skipping licenses.",
230
+ file=sys.stderr,
231
+ )
232
+
233
+ return None
234
+
235
+
236
+ def generate_spdx_for_element(
237
+ name,
238
+ source_infos,
239
+ elem_info,
240
+ config,
241
+ ):
242
+ """
243
+ Generate SPDX package information from the passed-in module, which is an
244
+ entry from a new style manifest (using the source provenance API)
245
+ """
246
+ package_name = element_package_name(name)
247
+ spdx_name = element_spdxid(name)
248
+
249
+ element_package = {
250
+ "name": package_name,
251
+ "SPDXID": spdx_name,
252
+ "filesAnalyzed": False,
253
+ "downloadLocation": "NOASSERTION",
254
+ }
255
+
256
+ element_ext_refs = []
257
+ bst_element_name = {
258
+ "referenceCategory": "OTHER",
259
+ "referenceType": "bst-element",
260
+ "referenceLocator": f"{name}",
261
+ }
262
+ element_ext_refs.append(bst_element_name)
263
+
264
+ element_package["externalRefs"] = element_ext_refs
265
+
266
+ if config.with_licenses:
267
+ licenses = element_licenses(name, elem_info, config)
268
+ if licenses is not None:
269
+ element_package["licenseDeclared"] = licenses
270
+
271
+ source_packages = []
272
+
273
+ for idx, source in enumerate(source_infos):
274
+ package = {
275
+ "SPDXID": f"{spdx_name}-{idx}",
276
+ "filesAnalyzed": False,
277
+ }
278
+ source_ext_refs = []
279
+
280
+ if "provenance" in source:
281
+ provenance = source["provenance"]
282
+ for attribute, spdx_field in SOURCE_PROVENANCE_ATTRIBUTES.items():
283
+ if attribute in provenance:
284
+ package[spdx_field] = provenance[attribute]
285
+
286
+ package.setdefault("name", source_package_name(source))
287
+
288
+ package["downloadLocation"] = source["url"]
289
+ if "version-guess" in source:
290
+ package["versionInfo"] = source["version-guess"]
291
+ else:
292
+ package["versionInfo"] = source["version"]
293
+
294
+ package["sourceInfo"] = source["kind"]
295
+
296
+ source_ext_refs.append(bst_element_name)
297
+ package["externalRefs"] = source_ext_refs
298
+
299
+ source_packages.append(package)
300
+
301
+ return element_package, source_packages
302
+
303
+
304
+ def element_spdxid(name):
305
+ # SPDXID can only contain letters, numbers, `.`, and `-`
306
+ not_allowed = "[^a-zA-Z0-9.-]"
307
+ spdx_name = re.sub(not_allowed, "-", name)
308
+ return f"SPDXRef-{spdx_name}"
309
+
310
+
311
+ def element_package_name(name):
312
+ element_name = name.split(":")[-1]
313
+ return os.path.splitext(os.path.basename(element_name))[0]
314
+
315
+
316
+ def source_package_name(source_info):
317
+ basename = os.path.basename(source_info["url"])
318
+
319
+ if "extra-data" in source_info and "crate-name" in source_info["extra-data"]:
320
+ return source_info["extra-data"]["crate-name"]
321
+
322
+ if source_info["medium"] == "git":
323
+ if basename.endswith(".git"):
324
+ return basename[: -len(".git")]
325
+ return basename
326
+ elif source_info["medium"] in ("remote-file", "archive"):
327
+ stem = re.split(r"\.(tar|tgz|tbz|zip|crate)", basename)[0]
328
+ package = re.split("[-_][0-9]", stem)[0]
329
+ return package
330
+ elif source_info["medium"] == "local":
331
+ return basename
332
+ else:
333
+ print(
334
+ f"Can't extract a package name from source info {source_info}",
335
+ file=sys.stderr,
336
+ )
337
+ return "UNKNOWN"
@@ -0,0 +1,30 @@
1
+ # SPDX-FileCopyrightText: 2026 Codethink Limited
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from argparse import Namespace
6
+ import shutil
7
+ import sys
8
+
9
+
10
+ def get_hosttool(name: str):
11
+ """Returns the path to the executable of the specified command.
12
+
13
+ Errors if the command could not be found.
14
+ """
15
+ command = shutil.which(name)
16
+ if command is None:
17
+ sys.exit(f"`{name}` is not installed or could not be found.")
18
+
19
+ return command
20
+
21
+
22
+ def bst_common_args(config: Namespace) -> list[str]:
23
+ args = []
24
+ if config.directory:
25
+ args += ["--directory", config.directory]
26
+
27
+ for option, value in config.options:
28
+ args += ["--option", option, value]
29
+
30
+ return args
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: buildstream-sbom
3
+ Version: 1.1
4
+ Summary: A tool to generate SPDX SBoMs from buildstream projects
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: pyyaml
8
+
9
+ <!--
10
+ SPDX-FileCopyrightText: 2025 Codethink Limited
11
+
12
+ SPDX-License-Identifier: Apache-2.0
13
+ -->
14
+
15
+ # BuildStream SBOM generator
16
+
17
+ This tool can be used to produce an SBoM (Software Bill of Materials)
18
+ describing a BuildStream element and its dependencies. It currently
19
+ generates [SPDX](https://spdx.dev/) version 2.3.
20
+
21
+ The manifest contains useful information, such as the package name, version,
22
+ source locations and dependencies.
23
+
24
+
25
+ ## Source provenance data
26
+
27
+ This tool relies on the [Source Provenance API](https://docs.buildstream.build/2.5/buildstream.source.html#generating-sourceinfo-for-provenance-information)
28
+ introduced in BuildStream 2.5. This API is implemented by buildstream-plugins
29
+ version 2.5.0 and buildstream-plugins-community 2.1.0. Please make sure your
30
+ project is using those (or more recent) versions. If your project uses custom
31
+ source plugins, please make sure that they also implement this API.
32
+
33
+ Version guessing is handled by individual source plugins, please check the
34
+ individual plugin documentation for details. Most plugins that implement it
35
+ do something similar to what [DownloadableFileSource](https://docs.buildstream.build/2.5/buildstream.downloadablefilesource.html#core-downloadable-source-builtins)
36
+ does, so it is a good starting point for understanding how this works.
37
+
38
+ The list of currently supported source provenance attributes that can be
39
+ specified and used for a Buildstream project is as follows:
40
+
41
+ | Attribute name | Corresponding SPDX attribute |
42
+ | -------------- | ---------------------------- |
43
+ | concluded-license | [licenseConcluded](https://spdx.github.io/spdx-spec/v2.3/package-information/#713-concluded-license-field) |
44
+ | copyright-text | [copyrightText](https://spdx.github.io/spdx-spec/v2.3/package-information/#717-copyright-text-field) |
45
+ | declared-license | [licenseDeclared](https://spdx.github.io/spdx-spec/v2.3/package-information/#715-declared-license-field) |
46
+ | description | [summary](https://spdx.github.io/spdx-spec/v2.3/package-information/#718-package-summary-description-field) |
47
+ | homepage | [homepage](https://spdx.github.io/spdx-spec/v2.3/package-information/#711-package-home-page-field) |
48
+ | name | [name](https://spdx.github.io/spdx-spec/v2.3/package-information/#71-package-name-field) |
49
+ | originator | [originator](https://spdx.github.io/spdx-spec/v2.3/package-information/#76-package-originator-field) |
50
+ | supplier | [supplier](https://spdx.github.io/spdx-spec/v2.3/package-information/#75-package-supplier-field) |
51
+
52
+ These can be used in projects by use of the `source-provenance-attributes`
53
+ field in the project.conf, this is described in the provenance section of
54
+ the [BuildStream documentation](https://docs.buildstream.build/master/buildstream.source.html#built-in-functionality).
55
+
56
+ ### source-provenance-attributes snippet
57
+
58
+ ```
59
+ # project.conf
60
+
61
+ source-provenance-attributes:
62
+ concluded-license: The license as determined by the evidence provided by the source project
63
+ copyright-text: Copyright text defined by the source project
64
+ declared-license: The license of the source project as decided by the authors
65
+ description: Description of the source project
66
+ homepage: The URL of the source project's homepage
67
+ name: The name of the source project
68
+ originator: The name of the person or organisation that created the source package originally
69
+ supplier: The name of the person or organisation that provided the source package
70
+ ```
71
+
72
+ Should any BuildStream plugins implement tracking for source provenance attributes, similar to
73
+ [source tracking](https://docs.buildstream.build/master/using_commands.html#bst-source-track),
74
+ it is recommended for all projects and plugins to use the attribute names exactly as seen above.
75
+ This ensures the source provenance attributes are always generated identically between different
76
+ plugins and makes sure they align with projects' definitions in the same way.
77
+
78
+ ## Usage
79
+
80
+ To install, clone this repository and install it using `pip` (or preferably a
81
+ tool like [`uv tool`](https://docs.astral.sh/uv/#tools) or
82
+ [`pipx`](https://pipx.pypa.io/stable/) which install it in a virtual
83
+ environment).
84
+
85
+ To use, run `buildstream-sbom` in a buildstream project passing in the name of
86
+ elements like you would pass to `bst`. There are two additional required
87
+ arguments `--spdx-name` and `--spdx-namespace`, to set the SPDX document name
88
+ and document namespace respectively. See the [SPDX specification](https://spdx.github.io/spdx-spec/v2.3/document-creation-information/)
89
+ for details.
90
+
91
+ `buildstream-sbom` also accepts some buildstream options, notably `-o/--option`
92
+ to set buildstream options, `-C/--directory` to set the directory containing
93
+ the buildstream project, and `--deps` to choose whether to include only runtime
94
+ dependencies or all dependencies.
95
+
96
+ On the topic of runtime dependencies, two BuildStream core plugins are treated
97
+ specially: `filter` and `compose`. All build dependencies of elements using
98
+ these plugins are considered runtime dependencies. You can set the `depends-on`
99
+ key in the `sbom` public domain data to a list of build dependencies to have
100
+ `buildstream-sbom` treat these build dependencies as runtime dependencies. This
101
+ is useful for a `script` or `manual` element that copies artifacts from a build
102
+ dependency into its own artifact.
103
+
104
+ You can also include licenses that have been extracted from the element's
105
+ artifacts using `--include-licenses`. This uses license information installed
106
+ with tooling such as [Freedesktop-SDK's install-extra script](https://gitlab.com/freedesktop-sdk/freedesktop-sdk/-/blob/master/include/install-extra.yml?ref_type=heads). This option can
107
+ also be used in conjunction with `--artifact-checkout-directory` (`-A`), to
108
+ control where element artifacts are checked out to during processing; useful
109
+ for if you are checking out larger artifacts and want to specify a different
110
+ part of a filesystem with more available space (note that artifacts are removed
111
+ once processed anyway to minimise required storage space).
@@ -0,0 +1,9 @@
1
+ buildstream_sbom/frontend.py,sha256=6R_aPdfTg-2FxCSp-ZecDDz3CFtIfdGR-WVQ_ovqVo4,5014
2
+ buildstream_sbom/licensing.py,sha256=ql_BKyJGNqujnjtM64tjuBxXNhdouhNLD_bwio1xpaQ,4533
3
+ buildstream_sbom/spdx.py,sha256=Ir-_GIhtSR6aMVP7rSw6BmBCx89h9BxoHJ4mKBvXHAs,10645
4
+ buildstream_sbom/utils.py,sha256=aTILJ6VWqisHAFRYohyrzjr-pU4NJuYCjuwd-xqtM-k,694
5
+ buildstream_sbom-1.1.dist-info/METADATA,sha256=k-gsPxqJyWPuDA0QM_r2AdC0PGfyHaM1X8CXeOOZSjQ,6178
6
+ buildstream_sbom-1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ buildstream_sbom-1.1.dist-info/entry_points.txt,sha256=EkX2hGZJaWEHr38PebgrbxzFfVZodXWb9JAKcabtv1M,68
8
+ buildstream_sbom-1.1.dist-info/top_level.txt,sha256=ohCnaVZk9kD6XX4uugH099Lld1PvWwi0mXTmw_v0LU0,17
9
+ buildstream_sbom-1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ buildstream-sbom = buildstream_sbom.frontend:main
@@ -0,0 +1 @@
1
+ buildstream_sbom