| Message ID | 20260107181541.141957-1-stondo@gmail.com |
|---|---|
| State | Under Review |
| Headers | show |
| Series | [v4] spdx30_tasks: Add concluded license support with SPDX_CONCLUDED_LICENSE | expand |
On Wed, Jan 7, 2026 at 11:15 AM <stondo@gmail.com> wrote: > > From: Stefano Tondo <stefano.tondo.ext@siemens.com> > > Add hasConcludedLicense relationship to SBOM packages with support for > manual license conclusion override via SPDX_CONCLUDED_LICENSE variable. > > The concluded license represents the license determination after manual > or external license analysis. This should be set manually in recipes or > layers when: > > 1. Manual license review identifies differences from the declared LICENSE > 2. External license scanning tools detect additional license information > 3. Legal review concludes a different license applies > > The hasConcludedLicense relationship is ONLY added to the SBOM when > SPDX_CONCLUDED_LICENSE is explicitly set. When unset or empty, no > concluded license is included in the SBOM, correctly indicating that > no license analysis was performed (per SPDX semantics). > > When differences from the declared LICENSE are found, users should: > > 1. Preferably: Correct the LICENSE field in the recipe and contribute > the fix upstream to OpenEmbedded > 2. Alternatively: Set SPDX_CONCLUDED_LICENSE locally in your layer when > upstream contribution is not immediately possible or when the license > conclusion is environment-specific > > The implementation checks both package-specific overrides > (SPDX_CONCLUDED_LICENSE:${PN}) and the global variable, allowing > per-package license conclusions when needed. > > The concluded license expression is automatically de-duplicated by > add_license_expression() to avoid redundant license objects in the SBOM. > > The variable is initialized in spdx-common.bbclass with comprehensive > documentation explaining its purpose, usage guidelines, and examples. > > Example usage in recipe or layer: > SPDX_CONCLUDED_LICENSE = "MIT & Apache-2.0" > SPDX_CONCLUDED_LICENSE:${PN} = "MIT & Apache-2.0" > > Signed-off-by: Stefano Tondo <stefano.tondo.ext@siemens.com> LGTM, Thanks Reviewed-by: Joshua Watt <JPEWhacker@gmail.com> > --- > meta/classes/spdx-common.bbclass | 16 ++++++++++++++++ > meta/lib/oe/spdx30_tasks.py | 18 ++++++++++++++++-- > 2 files changed, 32 insertions(+), 2 deletions(-) > > diff --git a/meta/classes/spdx-common.bbclass b/meta/classes/spdx-common.bbclass > index ca0416d1c7..3110230c9e 100644 > --- a/meta/classes/spdx-common.bbclass > +++ b/meta/classes/spdx-common.bbclass > @@ -36,6 +36,22 @@ SPDX_LICENSES ??= "${COREBASE}/meta/files/spdx-licenses.json" > > SPDX_CUSTOM_ANNOTATION_VARS ??= "" > > +SPDX_CONCLUDED_LICENSE ??= "" > +SPDX_CONCLUDED_LICENSE[doc] = "The license concluded by manual or external \ > + license analysis. This should only be set when explicit license analysis \ > + (manual review or external scanning tools) has been performed and a license \ > + conclusion has been reached. When unset or empty, no concluded license is \ > + included in the SBOM, indicating that no license analysis was performed. \ > + When differences from the declared LICENSE are found, the preferred approach \ > + is to correct the LICENSE field in the recipe and contribute the fix upstream \ > + to OpenEmbedded. Use this variable locally only when upstream contribution is \ > + not immediately possible or when the license conclusion is environment-specific. \ > + Supports package-specific overrides via SPDX_CONCLUDED_LICENSE:${PN}. \ > + This allows tracking license analysis results in SBOM while maintaining recipe \ > + LICENSE field for build compatibility. \ > + Example: SPDX_CONCLUDED_LICENSE = 'MIT & Apache-2.0' or \ > + SPDX_CONCLUDED_LICENSE:${PN} = 'MIT & Apache-2.0'" > + > SPDX_MULTILIB_SSTATE_ARCHS ??= "${SSTATE_ARCHS}" > > python () { > diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py > index 286a08ed9b..a99b017c26 100644 > --- a/meta/lib/oe/spdx30_tasks.py > +++ b/meta/lib/oe/spdx30_tasks.py > @@ -636,7 +636,7 @@ def create_spdx(d): > set_var_field( > "HOMEPAGE", spdx_package, "software_homePage", package=package > ) > - > + > # Add summary with fallback to DESCRIPTION > summary = None > if package: > @@ -651,7 +651,7 @@ def create_spdx(d): > summary = f"Package {package or d.getVar('PN')}" > if summary: > spdx_package.summary = summary > - > + > set_var_field("DESCRIPTION", spdx_package, "description", package=package) > > if d.getVar("SPDX_PACKAGE_URL:%s" % package) or d.getVar("SPDX_PACKAGE_URL"): > @@ -713,6 +713,20 @@ def create_spdx(d): > [oe.sbom30.get_element_link_id(package_spdx_license)], > ) > > + # Add concluded license relationship if manually set > + # Only add when license analysis has been explicitly performed > + concluded_license_str = d.getVar("SPDX_CONCLUDED_LICENSE:%s" % package) or d.getVar("SPDX_CONCLUDED_LICENSE") > + if concluded_license_str: > + concluded_spdx_license = add_license_expression( > + d, build_objset, concluded_license_str, license_data > + ) > + > + pkg_objset.new_relationship( > + [spdx_package], > + oe.spdx30.RelationshipType.hasConcludedLicense, > + [oe.sbom30.get_element_link_id(concluded_spdx_license)], > + ) > + > # NOTE: CVE Elements live in the recipe collection > all_cves = set() > for status, cves in cve_by_status.items(): > -- > 2.52.0 >
diff --git a/meta/classes/spdx-common.bbclass b/meta/classes/spdx-common.bbclass index ca0416d1c7..3110230c9e 100644 --- a/meta/classes/spdx-common.bbclass +++ b/meta/classes/spdx-common.bbclass @@ -36,6 +36,22 @@ SPDX_LICENSES ??= "${COREBASE}/meta/files/spdx-licenses.json" SPDX_CUSTOM_ANNOTATION_VARS ??= "" +SPDX_CONCLUDED_LICENSE ??= "" +SPDX_CONCLUDED_LICENSE[doc] = "The license concluded by manual or external \ + license analysis. This should only be set when explicit license analysis \ + (manual review or external scanning tools) has been performed and a license \ + conclusion has been reached. When unset or empty, no concluded license is \ + included in the SBOM, indicating that no license analysis was performed. \ + When differences from the declared LICENSE are found, the preferred approach \ + is to correct the LICENSE field in the recipe and contribute the fix upstream \ + to OpenEmbedded. Use this variable locally only when upstream contribution is \ + not immediately possible or when the license conclusion is environment-specific. \ + Supports package-specific overrides via SPDX_CONCLUDED_LICENSE:${PN}. \ + This allows tracking license analysis results in SBOM while maintaining recipe \ + LICENSE field for build compatibility. \ + Example: SPDX_CONCLUDED_LICENSE = 'MIT & Apache-2.0' or \ + SPDX_CONCLUDED_LICENSE:${PN} = 'MIT & Apache-2.0'" + SPDX_MULTILIB_SSTATE_ARCHS ??= "${SSTATE_ARCHS}" python () { diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py index 286a08ed9b..a99b017c26 100644 --- a/meta/lib/oe/spdx30_tasks.py +++ b/meta/lib/oe/spdx30_tasks.py @@ -636,7 +636,7 @@ def create_spdx(d): set_var_field( "HOMEPAGE", spdx_package, "software_homePage", package=package ) - + # Add summary with fallback to DESCRIPTION summary = None if package: @@ -651,7 +651,7 @@ def create_spdx(d): summary = f"Package {package or d.getVar('PN')}" if summary: spdx_package.summary = summary - + set_var_field("DESCRIPTION", spdx_package, "description", package=package) if d.getVar("SPDX_PACKAGE_URL:%s" % package) or d.getVar("SPDX_PACKAGE_URL"): @@ -713,6 +713,20 @@ def create_spdx(d): [oe.sbom30.get_element_link_id(package_spdx_license)], ) + # Add concluded license relationship if manually set + # Only add when license analysis has been explicitly performed + concluded_license_str = d.getVar("SPDX_CONCLUDED_LICENSE:%s" % package) or d.getVar("SPDX_CONCLUDED_LICENSE") + if concluded_license_str: + concluded_spdx_license = add_license_expression( + d, build_objset, concluded_license_str, license_data + ) + + pkg_objset.new_relationship( + [spdx_package], + oe.spdx30.RelationshipType.hasConcludedLicense, + [oe.sbom30.get_element_link_id(concluded_spdx_license)], + ) + # NOTE: CVE Elements live in the recipe collection all_cves = set() for status, cves in cve_by_status.items():