diff mbox series

[v4] spdx30_tasks: Add concluded license support with SPDX_CONCLUDED_LICENSE

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

Commit Message

Stefano Tondo Jan. 7, 2026, 6:15 p.m. UTC
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>
---
 meta/classes/spdx-common.bbclass | 16 ++++++++++++++++
 meta/lib/oe/spdx30_tasks.py      | 18 ++++++++++++++++--
 2 files changed, 32 insertions(+), 2 deletions(-)

Comments

Joshua Watt Jan. 7, 2026, 7:40 p.m. UTC | #1
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 mbox series

Patch

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():