diff --git a/cyclonedx_py/_internal/cli.py b/cyclonedx_py/_internal/cli.py
index c7b4a983..62856889 100644
--- a/cyclonedx_py/_internal/cli.py
+++ b/cyclonedx_py/_internal/cli.py
@@ -163,6 +163,7 @@ def __init__(self, *,
self._output_reproducible = output_reproducible
self._should_validate = should_validate
self._bbc = _bbc(**self._clean_kwargs(kwargs),
+ spec_version=spec_version,
logger=self._logger.getChild(_bbc.__name__))
def _shorten_purls(self, bom: 'Bom') -> bool:
diff --git a/cyclonedx_py/_internal/environment.py b/cyclonedx_py/_internal/environment.py
index 65e61644..1703e58a 100644
--- a/cyclonedx_py/_internal/environment.py
+++ b/cyclonedx_py/_internal/environment.py
@@ -22,6 +22,7 @@
from json import JSONDecodeError, loads as json_loads
from os import getcwd, name as os_name
from os.path import exists, isdir, join
+from platform import python_version
from subprocess import run # nosec
from sys import executable, path as sys_path
from textwrap import dedent
@@ -30,6 +31,7 @@
from cyclonedx.factory.license import LicenseFactory
from cyclonedx.model import Property
from cyclonedx.model.component import Component, ComponentType
+from cyclonedx.schema import SchemaVersion
from packageurl import PackageURL
from packaging.requirements import Requirement
@@ -51,6 +53,12 @@
T_AllComponents = dict[str, tuple['Component', Iterable[Requirement]]]
+# The Python runtime is added as an external component, which requires CycloneDX >= 1.7
+# and a `cyclonedx-python-lib` that supports `Component.is_external` (>= 11.12).
+_SV_PYTHON_RUNTIME: Optional[SchemaVersion] = getattr(SchemaVersion, 'V1_7', None)
+_LIB_SUPPORTS_IS_EXTERNAL = hasattr(Component, 'is_external')
+
+
class EnvironmentBB(BomBuilder):
@staticmethod
@@ -145,9 +153,14 @@ def make_argument_parser(**kwargs: Any) -> 'ArgumentParser':
def __init__(self, *,
logger: 'Logger',
gather_license_texts: bool,
+ spec_version: Optional[SchemaVersion] = None,
**__: Any) -> None:
self._logger = logger
self._gather_license_texts = gather_license_texts
+ self._add_python_runtime = _LIB_SUPPORTS_IS_EXTERNAL \
+ and spec_version is not None \
+ and _SV_PYTHON_RUNTIME is not None \
+ and spec_version >= _SV_PYTHON_RUNTIME
def __call__(self, *, # type:ignore[override]
import_site: bool,
@@ -169,21 +182,48 @@ def __call__(self, *, # type:ignore[override]
rc = (root_c, root_d)
path: list[str]
+ py_version: Optional[str] = None
if python:
path = self.__path4python(python, import_site, honor_py_env_vars)
+ if self._add_python_runtime:
+ py_version = self.__version4python(python, import_site, honor_py_env_vars)
elif not import_site or not honor_py_env_vars:
# the already-running interpreter imported site and honored PYTHON* vars at startup;
# probe a fresh one with the requested flags
path = self.__path4python(executable, import_site, honor_py_env_vars)
+ if self._add_python_runtime:
+ py_version = self.__version4python(executable, import_site, honor_py_env_vars)
else:
path = sys_path.copy()
+ if self._add_python_runtime:
+ py_version = python_version()
if path[0] in ('', getcwd()):
path.pop(0)
bom = make_bom()
self.__add_components(bom, rc, path=path)
+ if py_version is not None:
+ self.__add_python_runtime_component(bom, py_version)
return bom
+ def __add_python_runtime_component(self, bom: 'Bom', py_version: str) -> None:
+ # the runtime is expected to be provided by the deployment environment, it is not bundled.
+ # the implementation (CPython, PyPy, etc.) is intentionally not part of the SBOM.
+ runtime_c = Component(
+ type=ComponentType.PLATFORM,
+ bom_ref='python-runtime',
+ name='python',
+ version=py_version,
+ is_external=True,
+ )
+ self._logger.info('add component for Python runtime %r', py_version)
+ self._logger.debug('add component: %r', runtime_c)
+ bom.components.add(runtime_c)
+ if bom.metadata.component is None:
+ bom.register_dependency(runtime_c)
+ else:
+ bom.register_dependency(bom.metadata.component, (runtime_c,))
+
def __add_components(self, bom: 'Bom',
rc: Optional[tuple['Component', Iterable['Requirement']]],
**kwargs: Any) -> None:
@@ -306,6 +346,25 @@ def __py_interpreter(value: str) -> str:
raise ValueError(f'Failed to find python in directory: {value}')
return value
+ def __version4python(self, python: str, import_site: bool, honor_py_env_vars: bool) -> str:
+ cmd = [self.__py_interpreter(python),
+ '-c', 'import platform,sys;sys.stdout.write(platform.python_version())']
+ if not import_site:
+ cmd.insert(1, '-S')
+ if not honor_py_env_vars:
+ cmd.insert(1, '-E')
+
+ self._logger.debug('fetch `version` from python interpreter cmd: %r', cmd)
+ res = run(cmd, capture_output=True, encoding='utf8', shell=False) # nosec
+ version = res.stdout.strip()
+ if res.returncode != 0 or len(version) == 0:
+ raise RuntimeError('Fail fetching `version` from Python interpreter.\n'
+ f'returncode: {res.returncode}\n'
+ f'stdout: {res.stdout}\n'
+ f'stderr: {res.stderr}\n')
+ self._logger.debug('got `version` from Python interpreter: %r', version)
+ return version
+
def __path4python(self, python: str, import_site: bool, honor_py_env_vars: bool) -> list[str]:
cmd = [self.__py_interpreter(python),
'-c', 'import json,sys;json.dump(sys.path,sys.stdout)']
diff --git a/docs/usage.rst b/docs/usage.rst
index a4c3c6bc..61c45e99 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -50,6 +50,12 @@ For Python (virtual) environment
By analyzing the actually installed packages, this will produce the most accurate and complete CycloneDX BOM.
The generated CycloneDX SBOM will include metadata, licenses, dependency graph, and more.
+When CycloneDX 1.7 or later is requested (``--spec-version 1.7``),
+the Python runtime of the analyzed environment is added as a component
+of type ``platform`` with ``isExternal=true``, named ``python`` and versioned like ``3.13.5``.
+If a root component is known (``--pyproject``), it depends on this runtime component.
+The specific implementation (CPython, PyPy, etc.) is intentionally not part of the SBOM.
+
The full documentation can be issued by running with ``environment --help``:
.. code-block:: shell-session
diff --git a/tests/__init__.py b/tests/__init__.py
index 75a96819..49145a29 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -149,6 +149,12 @@ def make_xml_comparable(bom: str) -> str:
' \\1\n'
' ',
bom)
+ bom = re_sub( # replace the version of the Python runtime the tests happen to run with
+ '(\n'
+ ' *python\n'
+ ' *).*?()',
+ '\\1pythonVersion-testing\\2',
+ bom)
if sys.version_info < (3, 13):
# py3.13 finally fixed a bug in the XML writer: https://github.com/python/cpython/issues/81555
# no longer escape double-quotes(") in text/non-attribute.
@@ -214,6 +220,14 @@ def make_json_comparable(bom: str) -> str:
' "name": "cyclonedx-python-lib",\n'
' "vendor": "CycloneDX",\n',
bom)
+ bom = re_sub( # replace the version of the Python runtime the tests happen to run with
+ '("bom-ref": "python-runtime",\n'
+ ' *"isExternal": true,\n'
+ ' *"name": "python",\n'
+ ' *"type": "platform",\n'
+ ' *"version": )".*?"',
+ '\\1"pythonVersion-testing"',
+ bom)
return bom
diff --git a/tests/_data/snapshots/environment/plain_editable-self_1.7.json.bin b/tests/_data/snapshots/environment/plain_editable-self_1.7.json.bin
index 0eb85847..6111299d 100644
--- a/tests/_data/snapshots/environment/plain_editable-self_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_editable-self_1.7.json.bin
@@ -22,11 +22,22 @@
"purl": "pkg:pypi/six@1.16.0",
"type": "library",
"version": "1.16.0"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
+ "python-runtime",
"six==1.16.0"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_editable-self_1.7.xml.bin b/tests/_data/snapshots/environment/plain_editable-self_1.7.xml.bin
index c0dc85a8..adc9b786 100644
--- a/tests/_data/snapshots/environment/plain_editable-self_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_editable-self_1.7.xml.bin
@@ -77,9 +77,15 @@
+
+ python
+ pythonVersion-testing
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_local_1.7.json.bin b/tests/_data/snapshots/environment/plain_local_1.7.json.bin
index 66eb2410..6fc09f2b 100644
--- a/tests/_data/snapshots/environment/plain_local_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_local_1.7.json.bin
@@ -89,6 +89,13 @@
"name": "package-c",
"type": "library",
"version": "23.42"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
@@ -102,6 +109,12 @@
"ref": "package-c==23.42"
},
{
+ "ref": "python-runtime"
+ },
+ {
+ "dependsOn": [
+ "python-runtime"
+ ],
"ref": "root-component"
}
],
diff --git a/tests/_data/snapshots/environment/plain_local_1.7.xml.bin b/tests/_data/snapshots/environment/plain_local_1.7.xml.bin
index 08313df5..104ca558 100644
--- a/tests/_data/snapshots/environment/plain_local_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_local_1.7.xml.bin
@@ -120,11 +120,18 @@
+
+ python
+ pythonVersion-testing
+
-
+
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_no-deps_1.7.json.bin b/tests/_data/snapshots/environment/plain_no-deps_1.7.json.bin
index d25b9660..ed00fa50 100644
--- a/tests/_data/snapshots/environment/plain_no-deps_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_no-deps_1.7.json.bin
@@ -1,6 +1,21 @@
{
+ "components": [
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
+ }
+ ],
"dependencies": [
{
+ "ref": "python-runtime"
+ },
+ {
+ "dependsOn": [
+ "python-runtime"
+ ],
"ref": "root-component"
}
],
diff --git a/tests/_data/snapshots/environment/plain_no-deps_1.7.xml.bin b/tests/_data/snapshots/environment/plain_no-deps_1.7.xml.bin
index 0de97df8..9821bde0 100644
--- a/tests/_data/snapshots/environment/plain_no-deps_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_no-deps_1.7.xml.bin
@@ -98,7 +98,16 @@
true
+
+
+ python
+ pythonVersion-testing
+
+
-
+
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_normalize-packagename_1.7.json.bin b/tests/_data/snapshots/environment/plain_normalize-packagename_1.7.json.bin
index 6f676206..184fe257 100644
--- a/tests/_data/snapshots/environment/plain_normalize-packagename_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_normalize-packagename_1.7.json.bin
@@ -66,11 +66,22 @@
"purl": "pkg:pypi/ruamel.yaml.jinja2@0.2.7",
"type": "library",
"version": "0.2.7"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
+ "python-runtime",
"ruamel.yaml==0.18.5"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_normalize-packagename_1.7.xml.bin b/tests/_data/snapshots/environment/plain_normalize-packagename_1.7.xml.bin
index d7eae268..3412f3a1 100644
--- a/tests/_data/snapshots/environment/plain_normalize-packagename_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_normalize-packagename_1.7.xml.bin
@@ -109,9 +109,15 @@
+
+ python
+ pythonVersion-testing
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_private-packages_1.7.json.bin b/tests/_data/snapshots/environment/plain_private-packages_1.7.json.bin
index 428fa0f0..6dfbdc80 100644
--- a/tests/_data/snapshots/environment/plain_private-packages_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_private-packages_1.7.json.bin
@@ -56,11 +56,22 @@
"purl": "pkg:pypi/toml@0.10.2",
"type": "library",
"version": "0.10.2"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
+ "python-runtime",
"six==1.16.0"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_private-packages_1.7.xml.bin b/tests/_data/snapshots/environment/plain_private-packages_1.7.xml.bin
index 7b4ea79e..b0f5c525 100644
--- a/tests/_data/snapshots/environment/plain_private-packages_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_private-packages_1.7.xml.bin
@@ -101,9 +101,15 @@
+
+ python
+ pythonVersion-testing
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_via-pdm_1.7.json.bin b/tests/_data/snapshots/environment/plain_via-pdm_1.7.json.bin
index c86d7def..fdebeee0 100644
--- a/tests/_data/snapshots/environment/plain_via-pdm_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_via-pdm_1.7.json.bin
@@ -22,11 +22,22 @@
"purl": "pkg:pypi/toml@0.10.2",
"type": "library",
"version": "0.10.2"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
+ "python-runtime",
"toml==0.10.2"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_via-pdm_1.7.xml.bin b/tests/_data/snapshots/environment/plain_via-pdm_1.7.xml.bin
index de73d49f..b1e4b93c 100644
--- a/tests/_data/snapshots/environment/plain_via-pdm_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_via-pdm_1.7.xml.bin
@@ -90,9 +90,15 @@
+
+ python
+ pythonVersion-testing
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_via-pipenv_1.7.json.bin b/tests/_data/snapshots/environment/plain_via-pipenv_1.7.json.bin
index f503cefe..8036f087 100644
--- a/tests/_data/snapshots/environment/plain_via-pipenv_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_via-pipenv_1.7.json.bin
@@ -22,11 +22,22 @@
"purl": "pkg:pypi/toml@0.10.2",
"type": "library",
"version": "0.10.2"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
+ "python-runtime",
"toml==0.10.2"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_via-pipenv_1.7.xml.bin b/tests/_data/snapshots/environment/plain_via-pipenv_1.7.xml.bin
index 57ded70e..09208876 100644
--- a/tests/_data/snapshots/environment/plain_via-pipenv_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_via-pipenv_1.7.xml.bin
@@ -116,9 +116,15 @@
+
+ python
+ pythonVersion-testing
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_via-poetry_1.7.json.bin b/tests/_data/snapshots/environment/plain_via-poetry_1.7.json.bin
index 0843d199..9c762565 100644
--- a/tests/_data/snapshots/environment/plain_via-poetry_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_via-poetry_1.7.json.bin
@@ -22,11 +22,22 @@
"purl": "pkg:pypi/toml@0.10.2",
"type": "library",
"version": "0.10.2"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
+ "python-runtime",
"toml==0.10.2"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_via-poetry_1.7.xml.bin b/tests/_data/snapshots/environment/plain_via-poetry_1.7.xml.bin
index 961cc028..eeac7efc 100644
--- a/tests/_data/snapshots/environment/plain_via-poetry_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_via-poetry_1.7.xml.bin
@@ -116,9 +116,15 @@
+
+ python
+ pythonVersion-testing
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_via-uv_1.7.json.bin b/tests/_data/snapshots/environment/plain_via-uv_1.7.json.bin
index e177f89e..a4d1bdab 100644
--- a/tests/_data/snapshots/environment/plain_via-uv_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_via-uv_1.7.json.bin
@@ -22,11 +22,22 @@
"purl": "pkg:pypi/toml@0.10.2",
"type": "library",
"version": "0.10.2"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
+ "python-runtime",
"toml==0.10.2"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_via-uv_1.7.xml.bin b/tests/_data/snapshots/environment/plain_via-uv_1.7.xml.bin
index 38721fdf..88e9e9d5 100644
--- a/tests/_data/snapshots/environment/plain_via-uv_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_via-uv_1.7.xml.bin
@@ -116,9 +116,15 @@
+
+ python
+ pythonVersion-testing
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_with-extras_1.7.json.bin b/tests/_data/snapshots/environment/plain_with-extras_1.7.json.bin
index 3969afbf..e099f2f5 100644
--- a/tests/_data/snapshots/environment/plain_with-extras_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_with-extras_1.7.json.bin
@@ -1048,6 +1048,13 @@
"purl": "pkg:pypi/zipp@3.20.2",
"type": "library",
"version": "3.20.2"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
@@ -1156,6 +1163,9 @@
],
"ref": "python-dateutil==2.9.0.post0"
},
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
"attrs==25.4.0",
@@ -1181,7 +1191,8 @@
},
{
"dependsOn": [
- "cyclonedx-python-lib==11.2.0"
+ "cyclonedx-python-lib==11.2.0",
+ "python-runtime"
],
"ref": "root-component"
},
diff --git a/tests/_data/snapshots/environment/plain_with-extras_1.7.xml.bin b/tests/_data/snapshots/environment/plain_with-extras_1.7.xml.bin
index a5c80e48..1163c194 100644
--- a/tests/_data/snapshots/environment/plain_with-extras_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_with-extras_1.7.xml.bin
@@ -845,6 +845,10 @@
+
+ python
+ pythonVersion-testing
+
@@ -905,6 +909,7 @@
+
@@ -919,6 +924,7 @@
+
diff --git a/tests/_data/snapshots/environment/plain_with-license-file_1.7.json.bin b/tests/_data/snapshots/environment/plain_with-license-file_1.7.json.bin
index 3d2a0ce4..348afd14 100644
--- a/tests/_data/snapshots/environment/plain_with-license-file_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_with-license-file_1.7.json.bin
@@ -1,6 +1,21 @@
{
+ "components": [
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
+ }
+ ],
"dependencies": [
{
+ "ref": "python-runtime"
+ },
+ {
+ "dependsOn": [
+ "python-runtime"
+ ],
"ref": "root-component"
}
],
diff --git a/tests/_data/snapshots/environment/plain_with-license-file_1.7.xml.bin b/tests/_data/snapshots/environment/plain_with-license-file_1.7.xml.bin
index 3a615388..59ca1474 100644
--- a/tests/_data/snapshots/environment/plain_with-license-file_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_with-license-file_1.7.xml.bin
@@ -59,7 +59,16 @@
true
+
+
+ python
+ pythonVersion-testing
+
+
-
+
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_with-license-pep639_1.7.json.bin b/tests/_data/snapshots/environment/plain_with-license-pep639_1.7.json.bin
index 1df6ade9..2b4bbbb9 100644
--- a/tests/_data/snapshots/environment/plain_with-license-pep639_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_with-license-pep639_1.7.json.bin
@@ -219,6 +219,13 @@
"name": "regression-issue868",
"type": "library",
"version": "0.1"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
@@ -243,6 +250,9 @@
],
"ref": "license-expression==30.3.0"
},
+ {
+ "ref": "python-runtime"
+ },
{
"ref": "regression-issue868==0.1"
},
@@ -253,6 +263,7 @@
"cryptography==43.0.1",
"jsonpointer==2.4",
"license-expression==30.3.0",
+ "python-runtime",
"regression-issue868==0.1"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_with-license-pep639_1.7.xml.bin b/tests/_data/snapshots/environment/plain_with-license-pep639_1.7.xml.bin
index ae2a3430..cfb7ce9e 100644
--- a/tests/_data/snapshots/environment/plain_with-license-pep639_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_with-license-pep639_1.7.xml.bin
@@ -227,6 +227,10 @@
+
+ python
+ pythonVersion-testing
+
@@ -237,6 +241,7 @@
+
@@ -244,6 +249,7 @@
+
diff --git a/tests/_data/snapshots/environment/plain_with-license-text_1.7.json.bin b/tests/_data/snapshots/environment/plain_with-license-text_1.7.json.bin
index b9d4c733..c3f07495 100644
--- a/tests/_data/snapshots/environment/plain_with-license-text_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_with-license-text_1.7.json.bin
@@ -95,6 +95,13 @@
"name": "package-c",
"type": "library",
"version": "23.42"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
@@ -108,6 +115,12 @@
"ref": "package-c==23.42"
},
{
+ "ref": "python-runtime"
+ },
+ {
+ "dependsOn": [
+ "python-runtime"
+ ],
"ref": "root-component"
}
],
diff --git a/tests/_data/snapshots/environment/plain_with-license-text_1.7.xml.bin b/tests/_data/snapshots/environment/plain_with-license-text_1.7.xml.bin
index 6749dae7..66754f17 100644
--- a/tests/_data/snapshots/environment/plain_with-license-text_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_with-license-text_1.7.xml.bin
@@ -123,11 +123,18 @@
+
+ python
+ pythonVersion-testing
+
-
+
+
+
+
diff --git a/tests/_data/snapshots/environment/plain_with-urls_1.7.json.bin b/tests/_data/snapshots/environment/plain_with-urls_1.7.json.bin
index 97262af9..fb9f2b99 100644
--- a/tests/_data/snapshots/environment/plain_with-urls_1.7.json.bin
+++ b/tests/_data/snapshots/environment/plain_with-urls_1.7.json.bin
@@ -170,14 +170,25 @@
"purl": "pkg:pypi/urllib3@2.2.0?download_url=https://github.com/urllib3/urllib3/archive/refs/tags/2.2.0.zip",
"type": "library",
"version": "2.2.0"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
{
"ref": "packaging==23.2"
},
+ {
+ "ref": "python-runtime"
+ },
{
"dependsOn": [
+ "python-runtime",
"six==1.16.0"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/plain_with-urls_1.7.xml.bin b/tests/_data/snapshots/environment/plain_with-urls_1.7.xml.bin
index a3b6835b..868a07d2 100644
--- a/tests/_data/snapshots/environment/plain_with-urls_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/plain_with-urls_1.7.xml.bin
@@ -180,10 +180,16 @@
+
+ python
+ pythonVersion-testing
+
+
+
diff --git a/tests/_data/snapshots/environment/texts_with-license-file_1.7.json.bin b/tests/_data/snapshots/environment/texts_with-license-file_1.7.json.bin
index 549746cc..028bde47 100644
--- a/tests/_data/snapshots/environment/texts_with-license-file_1.7.json.bin
+++ b/tests/_data/snapshots/environment/texts_with-license-file_1.7.json.bin
@@ -1,6 +1,21 @@
{
+ "components": [
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
+ }
+ ],
"dependencies": [
{
+ "ref": "python-runtime"
+ },
+ {
+ "dependsOn": [
+ "python-runtime"
+ ],
"ref": "root-component"
}
],
diff --git a/tests/_data/snapshots/environment/texts_with-license-file_1.7.xml.bin b/tests/_data/snapshots/environment/texts_with-license-file_1.7.xml.bin
index 32db1b2a..042e9245 100644
--- a/tests/_data/snapshots/environment/texts_with-license-file_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/texts_with-license-file_1.7.xml.bin
@@ -65,7 +65,16 @@
true
+
+
+ python
+ pythonVersion-testing
+
+
-
+
+
+
+
diff --git a/tests/_data/snapshots/environment/texts_with-license-pep639_1.7.json.bin b/tests/_data/snapshots/environment/texts_with-license-pep639_1.7.json.bin
index dcddcfa3..1a2bb478 100644
--- a/tests/_data/snapshots/environment/texts_with-license-pep639_1.7.json.bin
+++ b/tests/_data/snapshots/environment/texts_with-license-pep639_1.7.json.bin
@@ -360,6 +360,13 @@
"name": "regression-issue868",
"type": "library",
"version": "0.1"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
@@ -384,6 +391,9 @@
],
"ref": "license-expression==30.3.0"
},
+ {
+ "ref": "python-runtime"
+ },
{
"ref": "regression-issue868==0.1"
},
@@ -394,6 +404,7 @@
"cryptography==43.0.1",
"jsonpointer==2.4",
"license-expression==30.3.0",
+ "python-runtime",
"regression-issue868==0.1"
],
"ref": "root-component"
diff --git a/tests/_data/snapshots/environment/texts_with-license-pep639_1.7.xml.bin b/tests/_data/snapshots/environment/texts_with-license-pep639_1.7.xml.bin
index 79f208f5..b9f141e8 100644
--- a/tests/_data/snapshots/environment/texts_with-license-pep639_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/texts_with-license-pep639_1.7.xml.bin
@@ -315,6 +315,10 @@
+
+ python
+ pythonVersion-testing
+
@@ -325,6 +329,7 @@
+
@@ -332,6 +337,7 @@
+
diff --git a/tests/_data/snapshots/environment/texts_with-license-text_1.7.json.bin b/tests/_data/snapshots/environment/texts_with-license-text_1.7.json.bin
index 8e0fe426..ba0beb51 100644
--- a/tests/_data/snapshots/environment/texts_with-license-text_1.7.json.bin
+++ b/tests/_data/snapshots/environment/texts_with-license-text_1.7.json.bin
@@ -107,6 +107,13 @@
"name": "package-c",
"type": "library",
"version": "23.42"
+ },
+ {
+ "bom-ref": "python-runtime",
+ "isExternal": true,
+ "name": "python",
+ "type": "platform",
+ "version": "pythonVersion-testing"
}
],
"dependencies": [
@@ -120,6 +127,12 @@
"ref": "package-c==23.42"
},
{
+ "ref": "python-runtime"
+ },
+ {
+ "dependsOn": [
+ "python-runtime"
+ ],
"ref": "root-component"
}
],
diff --git a/tests/_data/snapshots/environment/texts_with-license-text_1.7.xml.bin b/tests/_data/snapshots/environment/texts_with-license-text_1.7.xml.bin
index 935c27ac..84da9d8e 100644
--- a/tests/_data/snapshots/environment/texts_with-license-text_1.7.xml.bin
+++ b/tests/_data/snapshots/environment/texts_with-license-text_1.7.xml.bin
@@ -136,11 +136,18 @@ It is expected to be available in a SBOM.
+
+ python
+ pythonVersion-testing
+
-
+
+
+
+
diff --git a/tests/integration/test_cli_environment.py b/tests/integration/test_cli_environment.py
index edbf76d6..83f43a8a 100644
--- a/tests/integration/test_cli_environment.py
+++ b/tests/integration/test_cli_environment.py
@@ -21,6 +21,7 @@
from json import loads as json_loads
from os import environ, mkdir, name as os_name
from os.path import basename, dirname, join
+from platform import python_version
from subprocess import run # nosec:B404
from sys import executable, path as sys_path, stderr
from tempfile import TemporaryDirectory
@@ -306,6 +307,82 @@ def test_texts_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputF
self.assertEqual(0, res, err)
self.assertEqualSnapshot(out, 'texts', projectdir, sv, of)
+ def test_python_runtime_as_external_platform(self) -> None:
+ projectdir = join(INFILES_DIRECTORY, 'environment', 'no-deps')
+ res, out, err = run_cli(
+ 'environment',
+ '-vvv',
+ '--sv', '1.7',
+ '--of', OutputFormat.JSON.name,
+ '--output-reproducible',
+ '-o=-',
+ '--pyproject', join(projectdir, 'pyproject.toml'),
+ join(projectdir, '.venv'))
+ self.assertEqual(0, res, err)
+ venv_python = join(projectdir, '.venv', 'Scripts', 'python.exe') \
+ if os_name == 'nt' \
+ else join(projectdir, '.venv', 'bin', 'python')
+ expected_version = run(
+ (venv_python, '-c', 'import platform,sys;sys.stdout.write(platform.python_version())'),
+ capture_output=True, encoding='utf8', check=True, shell=False # nosec:B603
+ ).stdout
+ bom = json_loads(out)
+ runtime = [c for c in bom['components'] if c.get('bom-ref') == 'python-runtime']
+ self.assertEqual([{
+ 'bom-ref': 'python-runtime',
+ 'isExternal': True,
+ 'name': 'python',
+ 'type': 'platform',
+ 'version': expected_version,
+ }], runtime)
+ root_deps = next(d for d in bom['dependencies'] if d['ref'] == 'root-component')
+ self.assertIn('python-runtime', root_deps['dependsOn'])
+
+ def test_python_runtime_of_current_interpreter(self) -> None:
+ for flags in ((), ('-S',), ('-E',)):
+ with self.subTest(flags=flags):
+ res, out, err = run_cli(
+ 'environment', '--sv', '1.7', '--of', 'JSON', '-o=-',
+ *flags)
+ self.assertEqual(0, res, err)
+ bom = json_loads(out)
+ runtime = next(c for c in bom['components'] if c.get('bom-ref') == 'python-runtime')
+ self.assertEqual(python_version(), runtime['version'])
+ self.assertIn('python-runtime', {d['ref'] for d in bom['dependencies']})
+
+ @named_data(*(
+ (f'{sv.name}-{of.name}', sv, of)
+ for of, sv in SUPPORTED_OF_SV
+ if sv.to_version() != '1.7'
+ ))
+ def test_python_runtime_omitted_before_1_7(self, sv: SchemaVersion, of: OutputFormat) -> None:
+ projectdir = join(INFILES_DIRECTORY, 'environment', 'no-deps')
+ res, out, err = run_cli(
+ 'environment',
+ '-vvv',
+ '--sv', sv.to_version(),
+ '--of', of.name,
+ '--output-reproducible',
+ '-o=-',
+ '--pyproject', join(projectdir, 'pyproject.toml'),
+ join(projectdir, '.venv'))
+ self.assertEqual(0, res, err)
+ self.assertNotIn('python-runtime', out)
+
+ def test_python_runtime_omitted_without_lib_support(self) -> None:
+ projectdir = join(INFILES_DIRECTORY, 'environment', 'no-deps')
+ with patch('cyclonedx_py._internal.environment._LIB_SUPPORTS_IS_EXTERNAL', False):
+ res, out, err = run_cli(
+ 'environment',
+ '-vvv',
+ '--sv', '1.7',
+ '--of', OutputFormat.JSON.name,
+ '--output-reproducible',
+ '-o=-',
+ join(projectdir, '.venv'))
+ self.assertEqual(0, res, err)
+ self.assertNotIn('python-runtime', out)
+
def assertEqualSnapshot( # noqa:N802
self, actual: str,
purpose: str,