From: Joshua Watt <jpewhacker@gmail.com>
To: openembedded-core@lists.openembedded.org
Cc: Joshua Watt <JPEWhacker@gmail.com>
Subject: [OE-core][PATCH 2/3] lib/oe/sbom.py: Fix partial licenses masking complete ones
Date: Mon, 26 Oct 2026 16:06:34 -0600 [thread overview]
Message-ID: <20261026220751.2480577-3-JPEWhacker@gmail.com> (raw)
In-Reply-To: <20261026220751.2480577-1-JPEWhacker@gmail.com>
When scanning files for license expressions, an "incomplete" (or
partial) license object can be created, in that if the Licenses
Expression contains a LicenseRef- license, the text for the license will
is not linked. This usually happens when parsing recipe License
expressions, since the license files are not always present until
do_fetch/do_unpack.
Since license expressions are matched based on the expression, it is
possible for the partial license to be used instead of the complete one.
It is also possible for the partial license to be preferred when
de-duplicating license expressions while generating the final SBoM.
Fix this by identifying when a partial license is provided (that is, a
license with no text map and a LicenseRef-), and tagging them with an
internal extension that causes it to be less preferred than an complete
license.
Signed-off-by: Joshua Watt <JPEWhacker@gmail.com>
---
meta/lib/oe/sbom30.py | 106 +++++++++++++++++++++++++++++++++---------
1 file changed, 85 insertions(+), 21 deletions(-)
diff --git a/meta/lib/oe/sbom30.py b/meta/lib/oe/sbom30.py
index b33860e4aa..713eec92d4 100644
--- a/meta/lib/oe/sbom30.py
+++ b/meta/lib/oe/sbom30.py
@@ -97,6 +97,32 @@ class OELicenseScannedExtension(oe.spdx30.extension_Extension):
INTERNAL = True
+class OEIncompleteExtension(oe.spdx30.extension_Extension):
+ """
+ The presence of this extension means that the Element is incomplete, and
+ should not be preferred over a completely defined Element
+ """
+
+ TYPE = OE_SPDX_BASE + "incomplete"
+ CLOSED = True
+ INTERNAL = True
+
+ @classmethod
+ def is_incomplete(cls, obj):
+ for e in obj.extension:
+ if isinstance(e, cls):
+ return True
+ return False
+
+ @classmethod
+ def is_complete(cls, obj):
+ return not cls.is_incomplete(obj)
+
+ @classmethod
+ def set(cls, obj):
+ obj.extension.append(cls())
+
+
class OEDocumentExtension(oe.spdx30.extension_Extension):
"""
This extension is added to a SpdxDocument to indicate various useful bits
@@ -194,21 +220,35 @@ def to_list(l):
class Dedup(object):
def __init__(self, objset):
- self.unique = set()
- self.dedup = {}
+ self.unique = []
+ self.dup_idx = {}
self.objset = objset
+ self.has_duplicates = False
- def find_duplicates(self, cmp, typ, **kwargs):
+ def find_duplicates(self, cmp, typ, choose=None, **kwargs):
for o in self.objset.foreach_filter(typ, **kwargs):
- for u in self.unique:
+ for idx, u in enumerate(self.unique):
if cmp(u, o):
- self.dedup[o] = u
+ self.has_duplicates = True
+ self.dup_idx[o] = idx
+ if choose:
+ self.unique[idx] = choose(u, o)
break
else:
- self.unique.add(o)
+ self.dup_idx[o] = len(self.unique)
+ self.unique.append(o)
def get(self, o):
- return self.dedup.get(o, o)
+ idx = self.dup_idx.get(o, -1)
+ if idx < 0:
+ return o
+ return self.unique[idx]
+
+ def duplicates(self):
+ for o, idx in self.dup_idx.items():
+ d = self.unique[idx]
+ if d is not o:
+ yield o, d
class ObjectSet(oe.spdx30.SHACLObjectSet):
@@ -582,23 +622,34 @@ class ObjectSet(oe.spdx30.SHACLObjectSet):
if license_list_version.count(".") < 2:
license_list_version += ".0"
+ license_text = sorted(license_text_map.items(), key=lambda t: t[0])
+
+ incomplete = not license_text and "licenseref-" in license_expression.lower()
+
spdxid = [
- "license",
+ "license" if not incomplete else "partial-license",
license_list_version,
re.sub(r"[^a-zA-Z0-9_-]", "_", license_expression),
]
- license_text = sorted(license_text_map.items(), key=lambda t: t[0])
-
if not license_text:
- lic = self.find_filter(
+ incomplete_lic = None
+ for lic in self.foreach_filter(
oe.spdx30.simplelicensing_LicenseExpression,
simplelicensing_licenseExpression=license_expression,
simplelicensing_licenseListVersion=license_list_version,
- )
- if lic is not None:
- return lic
+ ):
+ if OEIncompleteExtension.is_complete(lic):
+ return lic
+
+ if not incomplete_lic:
+ incomplete_lic = lic
+
+ if incomplete and incomplete_lic:
+ return incomplete_lic
+
else:
+ # Note: This branch will never be entered for incomplete licenses
spdxid.append(spdxid_hash(*(v for _, v in license_text)))
lic = self.find_by_id(self.new_spdxid(*spdxid))
if lic is not None:
@@ -618,6 +669,9 @@ class ObjectSet(oe.spdx30.SHACLObjectSet):
oe.spdx30.DictionaryEntry(key=key, value=value._id)
)
+ if incomplete:
+ OEIncompleteExtension.set(lic)
+
return lic
def scan_declared_licenses(self, spdx_file, filepath, license_data):
@@ -953,16 +1007,16 @@ class ObjectSet(oe.spdx30.SHACLObjectSet):
return True
- if d.dedup:
+ if d.has_duplicates:
for o in self.objects:
o.walk(visit)
- for k, v in d.dedup.items():
+ for dup, replacement in d.duplicates():
bb.debug(
1,
- f"Removing duplicate {k.__class__.__name__} {k._id or id(k)} -> {v._id or id(v)}",
+ f"Removing duplicate {dup.__class__.__name__} {dup._id or id(dup)} -> {replacement._id or id(replacement)}",
)
- self.objects.discard(k)
+ self.objects.discard(dup)
self.create_index()
@@ -1148,9 +1202,6 @@ def create_sbom(d, name, root_elements, add_objectsets=[]):
+ "\n ".join(sorted(list(missing_spdxids)))
)
- # Filter out internal extensions from final SBoMs
- objset.remove_internal_extensions()
-
# SBoM should be the only root element of the document
objset.doc.rootElement = [sbom]
@@ -1161,6 +1212,15 @@ def create_sbom(d, name, root_elements, add_objectsets=[]):
== b.simplelicensing_licenseListVersion
)
+ def choose_license_expressions(current, new):
+ if OEIncompleteExtension.is_complete(current):
+ return current
+
+ if OEIncompleteExtension.is_incomplete(new):
+ return current
+
+ return new
+
def cmp_creation_info(a, b):
data_a = {k: a[k] for k in a}
data_b = {k: b[k] for k in b}
@@ -1173,9 +1233,13 @@ def create_sbom(d, name, root_elements, add_objectsets=[]):
dedup.find_duplicates(
cmp_license_expression,
oe.spdx30.simplelicensing_LicenseExpression,
+ choose=choose_license_expressions,
)
# Deduplicate creation info
dedup.find_duplicates(cmp_creation_info, oe.spdx30.CreationInfo)
+ # Filter out internal extensions from final SBoMs
+ objset.remove_internal_extensions()
+
return objset, sbom
--
2.56.0
next prev parent reply other threads:[~2026-10-26 22:07 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-26 22:06 [OE-core][PATCH 0/3] Fix incomplete license expressions being preferred over complete ones Joshua Watt
2026-10-26 22:06 ` [OE-core][PATCH 1/3] lib/oe/license.py: Fix documentation of parse_legacy_license() Joshua Watt
2026-10-26 22:06 ` Joshua Watt [this message]
2026-10-26 22:06 ` [OE-core][PATCH 3/3] lib/oe/sbom.py: Split deduplication of CreationInfo Joshua Watt
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261026220751.2480577-3-JPEWhacker@gmail.com \
--to=jpewhacker@gmail.com \
--cc=openembedded-core@lists.openembedded.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.