buildstream-sbom 1.1__tar.gz

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,14 @@
1
+ # SPDX-FileCopyrightText: 2025 Codethink Limited
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ # Python-generated files
6
+ __pycache__/
7
+ *.py[oc]
8
+ build/
9
+ dist/
10
+ wheels/
11
+ *.egg-info
12
+
13
+ # Virtual environments
14
+ .venv
@@ -0,0 +1,57 @@
1
+ # SPDX-FileCopyrightText: 2025 Codethink Limited
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ workflow:
6
+ rules:
7
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
8
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
9
+ - if: $CI_COMMIT_TAG
10
+
11
+ stages:
12
+ - build
13
+ - publish
14
+
15
+ .ruff:
16
+ image:
17
+ name: ghcr.io/astral-sh/ruff:0.12.9-alpine
18
+ stage: build
19
+ interruptible: true
20
+
21
+ Ruff Check:
22
+ extends: .ruff
23
+ script:
24
+ - ruff check --output-format=gitlab > code-quality-report.json
25
+ artifacts:
26
+ reports:
27
+ codequality: code-quality-report.json
28
+
29
+ Ruff Format:
30
+ extends: .ruff
31
+ script:
32
+ - ruff format --diff
33
+
34
+ REUSE Compliance Check:
35
+ stage: build
36
+ image:
37
+ name: fsfe/reuse:latest
38
+ entrypoint: [""]
39
+ script:
40
+ - reuse lint
41
+
42
+ # publish to PyPi for pipelines run on tags
43
+ pypi-release:
44
+ stage: publish
45
+ rules:
46
+ - if: $CI_COMMIT_TAG
47
+ id_tokens:
48
+ PYPI_ID_TOKEN:
49
+ aud: pypi
50
+ image: docker.io/library/python:3.14
51
+ before_script:
52
+ - |
53
+ python3 -mvenv venv
54
+ ./venv/bin/pip install twine build
55
+ script:
56
+ - ./venv/bin/python3 -m build .
57
+ - ./venv/bin/twine upload --skip-existing dist/*
@@ -0,0 +1,28 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Codethink Limited
3
+
4
+ SPDX-License-Identifier: Apache-2.0
5
+ -->
6
+
7
+ # Changelog
8
+
9
+ ## [1.1] - 2026-04-30
10
+
11
+ ### Added
12
+
13
+ - Make use of a source's (optional) provenance field
14
+ - Add license extraction support
15
+ - Add --spdx-comment option
16
+ - Add element name as externalRef
17
+
18
+ ### Changed
19
+
20
+ - Don't use the BST environment variable
21
+ - Check creators are in the spdx format
22
+ - Drop special casing of patch sources
23
+ - Set fallback package name to 'UNKNOWN' for sources
24
+ - docker images are currently the only known "unknown"
25
+
26
+ ## [1.0] - 2025-10-14
27
+
28
+ Initial release.
@@ -0,0 +1,73 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
10
+
11
+ "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
12
+
13
+ "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
14
+
15
+ "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
16
+
17
+ "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
18
+
19
+ "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
20
+
21
+ "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
22
+
23
+ "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
24
+
25
+ "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
26
+
27
+ "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
28
+
29
+ 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
30
+
31
+ 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
32
+
33
+ 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
34
+
35
+ (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
36
+
37
+ (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
38
+
39
+ (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
40
+
41
+ (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
42
+
43
+ You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
44
+
45
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
46
+
47
+ 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
48
+
49
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
50
+
51
+ 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
52
+
53
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
54
+
55
+ END OF TERMS AND CONDITIONS
56
+
57
+ APPENDIX: How to apply the Apache License to your work.
58
+
59
+ To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
60
+
61
+ Copyright [yyyy] [name of copyright owner]
62
+
63
+ Licensed under the Apache License, Version 2.0 (the "License");
64
+ you may not use this file except in compliance with the License.
65
+ You may obtain a copy of the License at
66
+
67
+ http://www.apache.org/licenses/LICENSE-2.0
68
+
69
+ Unless required by applicable law or agreed to in writing, software
70
+ distributed under the License is distributed on an "AS IS" BASIS,
71
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
72
+ See the License for the specific language governing permissions and
73
+ limitations under the License.
@@ -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,103 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Codethink Limited
3
+
4
+ SPDX-License-Identifier: Apache-2.0
5
+ -->
6
+
7
+ # BuildStream SBOM generator
8
+
9
+ This tool can be used to produce an SBoM (Software Bill of Materials)
10
+ describing a BuildStream element and its dependencies. It currently
11
+ generates [SPDX](https://spdx.dev/) version 2.3.
12
+
13
+ The manifest contains useful information, such as the package name, version,
14
+ source locations and dependencies.
15
+
16
+
17
+ ## Source provenance data
18
+
19
+ This tool relies on the [Source Provenance API](https://docs.buildstream.build/2.5/buildstream.source.html#generating-sourceinfo-for-provenance-information)
20
+ introduced in BuildStream 2.5. This API is implemented by buildstream-plugins
21
+ version 2.5.0 and buildstream-plugins-community 2.1.0. Please make sure your
22
+ project is using those (or more recent) versions. If your project uses custom
23
+ source plugins, please make sure that they also implement this API.
24
+
25
+ Version guessing is handled by individual source plugins, please check the
26
+ individual plugin documentation for details. Most plugins that implement it
27
+ do something similar to what [DownloadableFileSource](https://docs.buildstream.build/2.5/buildstream.downloadablefilesource.html#core-downloadable-source-builtins)
28
+ does, so it is a good starting point for understanding how this works.
29
+
30
+ The list of currently supported source provenance attributes that can be
31
+ specified and used for a Buildstream project is as follows:
32
+
33
+ | Attribute name | Corresponding SPDX attribute |
34
+ | -------------- | ---------------------------- |
35
+ | concluded-license | [licenseConcluded](https://spdx.github.io/spdx-spec/v2.3/package-information/#713-concluded-license-field) |
36
+ | copyright-text | [copyrightText](https://spdx.github.io/spdx-spec/v2.3/package-information/#717-copyright-text-field) |
37
+ | declared-license | [licenseDeclared](https://spdx.github.io/spdx-spec/v2.3/package-information/#715-declared-license-field) |
38
+ | description | [summary](https://spdx.github.io/spdx-spec/v2.3/package-information/#718-package-summary-description-field) |
39
+ | homepage | [homepage](https://spdx.github.io/spdx-spec/v2.3/package-information/#711-package-home-page-field) |
40
+ | name | [name](https://spdx.github.io/spdx-spec/v2.3/package-information/#71-package-name-field) |
41
+ | originator | [originator](https://spdx.github.io/spdx-spec/v2.3/package-information/#76-package-originator-field) |
42
+ | supplier | [supplier](https://spdx.github.io/spdx-spec/v2.3/package-information/#75-package-supplier-field) |
43
+
44
+ These can be used in projects by use of the `source-provenance-attributes`
45
+ field in the project.conf, this is described in the provenance section of
46
+ the [BuildStream documentation](https://docs.buildstream.build/master/buildstream.source.html#built-in-functionality).
47
+
48
+ ### source-provenance-attributes snippet
49
+
50
+ ```
51
+ # project.conf
52
+
53
+ source-provenance-attributes:
54
+ concluded-license: The license as determined by the evidence provided by the source project
55
+ copyright-text: Copyright text defined by the source project
56
+ declared-license: The license of the source project as decided by the authors
57
+ description: Description of the source project
58
+ homepage: The URL of the source project's homepage
59
+ name: The name of the source project
60
+ originator: The name of the person or organisation that created the source package originally
61
+ supplier: The name of the person or organisation that provided the source package
62
+ ```
63
+
64
+ Should any BuildStream plugins implement tracking for source provenance attributes, similar to
65
+ [source tracking](https://docs.buildstream.build/master/using_commands.html#bst-source-track),
66
+ it is recommended for all projects and plugins to use the attribute names exactly as seen above.
67
+ This ensures the source provenance attributes are always generated identically between different
68
+ plugins and makes sure they align with projects' definitions in the same way.
69
+
70
+ ## Usage
71
+
72
+ To install, clone this repository and install it using `pip` (or preferably a
73
+ tool like [`uv tool`](https://docs.astral.sh/uv/#tools) or
74
+ [`pipx`](https://pipx.pypa.io/stable/) which install it in a virtual
75
+ environment).
76
+
77
+ To use, run `buildstream-sbom` in a buildstream project passing in the name of
78
+ elements like you would pass to `bst`. There are two additional required
79
+ arguments `--spdx-name` and `--spdx-namespace`, to set the SPDX document name
80
+ and document namespace respectively. See the [SPDX specification](https://spdx.github.io/spdx-spec/v2.3/document-creation-information/)
81
+ for details.
82
+
83
+ `buildstream-sbom` also accepts some buildstream options, notably `-o/--option`
84
+ to set buildstream options, `-C/--directory` to set the directory containing
85
+ the buildstream project, and `--deps` to choose whether to include only runtime
86
+ dependencies or all dependencies.
87
+
88
+ On the topic of runtime dependencies, two BuildStream core plugins are treated
89
+ specially: `filter` and `compose`. All build dependencies of elements using
90
+ these plugins are considered runtime dependencies. You can set the `depends-on`
91
+ key in the `sbom` public domain data to a list of build dependencies to have
92
+ `buildstream-sbom` treat these build dependencies as runtime dependencies. This
93
+ is useful for a `script` or `manual` element that copies artifacts from a build
94
+ dependency into its own artifact.
95
+
96
+ You can also include licenses that have been extracted from the element's
97
+ artifacts using `--include-licenses`. This uses license information installed
98
+ 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
99
+ also be used in conjunction with `--artifact-checkout-directory` (`-A`), to
100
+ control where element artifacts are checked out to during processing; useful
101
+ for if you are checking out larger artifacts and want to specify a different
102
+ part of a filesystem with more available space (note that artifacts are removed
103
+ once processed anyway to minimise required storage space).
@@ -0,0 +1,22 @@
1
+ # SPDX-FileCopyrightText: 2025 Codethink Limited
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ [build-system]
6
+ requires = ["setuptools", "setuptools-scm"]
7
+ build-backend = "setuptools.build_meta"
8
+
9
+ [project]
10
+ name = "buildstream-sbom"
11
+ dynamic = ["version"]
12
+ description = "A tool to generate SPDX SBoMs from buildstream projects"
13
+ readme = "README.md"
14
+ dependencies = [
15
+ "pyyaml",
16
+ ]
17
+ requires-python = ">=3.9"
18
+
19
+ [project.scripts]
20
+ buildstream-sbom = "buildstream_sbom.frontend:main"
21
+
22
+ [tool.setuptools_scm]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,16 @@
1
+ .gitignore
2
+ .gitlab-ci.yml
3
+ CHANGELOG.md
4
+ README.md
5
+ pyproject.toml
6
+ LICENSES/Apache-2.0.txt
7
+ src/buildstream_sbom/frontend.py
8
+ src/buildstream_sbom/licensing.py
9
+ src/buildstream_sbom/spdx.py
10
+ src/buildstream_sbom/utils.py
11
+ src/buildstream_sbom.egg-info/PKG-INFO
12
+ src/buildstream_sbom.egg-info/SOURCES.txt
13
+ src/buildstream_sbom.egg-info/dependency_links.txt
14
+ src/buildstream_sbom.egg-info/entry_points.txt
15
+ src/buildstream_sbom.egg-info/requires.txt
16
+ src/buildstream_sbom.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ buildstream-sbom = buildstream_sbom.frontend:main
@@ -0,0 +1 @@
1
+ buildstream_sbom