diff mbox series

[DRAFT,v3,1/2] spdx: add entries for meta-layers

Message ID 20261106115455.4141199-2-dwagenknecht@emlix.com
State New
Headers show
Series Add meta layer information to spdx | expand

Commit Message

Daniel Wagenknecht Nov. 6, 2026, 11:54 a.m. UTC
This adds support for determining the revision and downloadLocation of
meta-layers and adds them to the OE generated SPDX document.

This downloadLocation will result in a NOASSERTION entry if
* the meta-layer is not in a git repo
* the git repo is dirty
* the git remote does not have a valid URL
* the git remote does not contain the used revision as far as can be
  eluded from the local repo

Variables to configure the behavior:
* SPDX_LAYER_REMOTE (default: origin): select the name of the git remote
  to use for layers. Set to an empty string to always return NOASSERTION
* SPDX_LAYER_REMOTE_layername: override SPDX_LAYER_REMOTE for a specific
  layer
* SPDX_REQUIRE_LAYER_ASSERTION: set to true to fail a build if
  determining downloadLocation fails for any layer

Signed-off-by: Daniel Wagenknecht <dwagenknecht@emlix.com>
---
 meta/classes/create-spdx-3.0.bbclass |  1 +
 meta/classes/spdx-common.bbclass     |  3 ++
 meta/lib/oe/sbom30.py                | 12 +++++
 meta/lib/oe/spdx30_tasks.py          | 74 ++++++++++++++++++++++++++++
 4 files changed, 90 insertions(+)

Comments

Paul Barker Nov. 6, 2026, 12:20 p.m. UTC | #1
On Fri Nov 6, 2026 at 11:54 AM GMT, Daniel Wagenknecht wrote:
> This adds support for determining the revision and downloadLocation of
> meta-layers and adds them to the OE generated SPDX document.
>
> This downloadLocation will result in a NOASSERTION entry if
> * the meta-layer is not in a git repo
> * the git repo is dirty
> * the git remote does not have a valid URL
> * the git remote does not contain the used revision as far as can be
>   eluded from the local repo
>
> Variables to configure the behavior:
> * SPDX_LAYER_REMOTE (default: origin): select the name of the git remote
>   to use for layers. Set to an empty string to always return NOASSERTION
> * SPDX_LAYER_REMOTE_layername: override SPDX_LAYER_REMOTE for a specific
>   layer
> * SPDX_REQUIRE_LAYER_ASSERTION: set to true to fail a build if
>   determining downloadLocation fails for any layer
>
> Signed-off-by: Daniel Wagenknecht <dwagenknecht@emlix.com>

Hi Daniel,

I don't think downloadLocation should be populated by default, it may
refer to an internal network location for an organisations' private
repositories and leaking those URLs may not be desired.

This probably needs to be opt-in.

Best regards,
diff mbox series

Patch

diff --git a/meta/classes/create-spdx-3.0.bbclass b/meta/classes/create-spdx-3.0.bbclass
index 6d47aa13016..507486cfd19 100644
--- a/meta/classes/create-spdx-3.0.bbclass
+++ b/meta/classes/create-spdx-3.0.bbclass
@@ -288,6 +288,7 @@  python spdx30_build_started_handler () {
     import oe.spdx30_tasks
     d = e.data.createCopy()
     oe.spdx30_tasks.write_bitbake_spdx(d)
+    oe.spdx30_tasks.write_layers_spdx(d)
 }
 
 addhandler spdx30_build_started_handler
diff --git a/meta/classes/spdx-common.bbclass b/meta/classes/spdx-common.bbclass
index 40701730a69..b64537dfb48 100644
--- a/meta/classes/spdx-common.bbclass
+++ b/meta/classes/spdx-common.bbclass
@@ -89,6 +89,9 @@  SPDX_FILE_EXCLUDE_PATTERNS[doc] = "Space-separated list of Python regular \
     (no filtering). Example: \
     SPDX_FILE_EXCLUDE_PATTERNS = '\\.patch$ \\.diff$ /test/ \\.pyc$ \\.o$'"
 
+SPDX_REQUIRE_LAYER_ASSERTION ??= "0"
+SPDX_LAYER_REMOTE ??= "origin"
+
 python () {
     from oe.cve_check import extend_cve_status
     extend_cve_status(d)
diff --git a/meta/lib/oe/sbom30.py b/meta/lib/oe/sbom30.py
index e279cfc82bf..bde3ff1f553 100644
--- a/meta/lib/oe/sbom30.py
+++ b/meta/lib/oe/sbom30.py
@@ -816,6 +816,16 @@  class ObjectSet(oe.spdx30.SHACLObjectSet):
 
         return bb_objset
 
+    def import_layers_objset(self):
+        deploy_dir_spdx = Path(self.d.getVar("DEPLOY_DIR_SPDX"))
+        layers_objset = load_jsonld(
+            self.d, deploy_dir_spdx / "layers.spdx.json", required=True
+        )
+        self.doc.import_.extend(layers_objset.doc.import_)
+        self.update(layers_objset.objects)
+
+        return layers_objset
+
     def import_bitbake_build(self):
         def find_bitbake_build(objset):
             return objset.find_filter(
@@ -857,6 +867,8 @@  class ObjectSet(oe.spdx30.SHACLObjectSet):
                 [build],
             )
 
+        layers = self.import_layers_objset()
+
         if self.d.getVar("SPDX_INCLUDE_BUILD_VARIABLES") == "1":
             for varname in sorted(self.d.keys()):
                 if varname.startswith("__"):
diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
index 646ec7d0743..31d964d8ce8 100644
--- a/meta/lib/oe/spdx30_tasks.py
+++ b/meta/lib/oe/spdx30_tasks.py
@@ -1232,6 +1232,80 @@  def create_package_spdx(d):
     oe.sbom30.write_recipe_jsonld_doc(d, common_objset, "common-package", deploydir)
 
 
+def git_remote_has_rev(path, remote, rev):
+    try:
+        branchlist, _ = bb.process.run(f"git branch --remotes --list '{remote}/*' --contains {rev}", cwd=path)
+    except bb.process.ExecutionError:
+        return False
+    return len(branchlist) > 0
+
+def get_layer_downloadLocation(d, l_path, l_name, l_branch, l_rev, l_ismodified):
+    from urllib.parse import urlparse, urlunparse
+
+    repo = oe.buildcfg.get_metadata_git_toplevel(l_path)
+
+    if not repo or not l_name or l_ismodified:
+        return "NOASSERTION"
+
+    remoteName = d.getVar(f"SPDX_LAYER_REMOTE_{l_name}") or d.getVar(f"SPDX_LAYER_REMOTE")
+    if not remoteName:
+        return "NOASSERTION"
+
+    # parse and reconstruct the url to drop potentially present username and password
+    remoteUrl = urlparse(oe.buildcfg.get_metadata_git_remote_url(repo, remoteName))
+    if not remoteUrl or remoteUrl.scheme not in ["https", "http", "git", "ssh"]:
+        return "NOASSERTION"
+    remoteUrl = urlunparse(remoteUrl)
+
+    revision = l_rev
+    if revision == "<unknown>" or not git_remote_has_rev(repo, remoteName, revision):
+        return "NOASSERTION"
+
+    relpath = os.path.relpath(l_path, repo)
+
+    return f"git+{remoteUrl}@{revision}{'#' + relpath if relpath else ''}"
+
+
+def write_layers_spdx(d):
+    d.setVar("PN", "layers")
+    d.setVar("BB_TASKHASH", "layers")
+    oe.spdx_common.load_spdx_license_data(d)
+    deploy_dir_spdx = Path(d.getVar("DEPLOY_DIR_SPDX"))
+
+    layer_objset = oe.sbom30.ObjectSet.new_objset(d, "layers")
+
+    layers = oe.buildcfg.get_layer_revisions(d)
+    for (l_path, l_name, l_branch, l_rev, l_ismodified) in layers:
+        downloadLocation = get_layer_downloadLocation(d, l_path, l_name, l_branch, l_rev, l_ismodified)
+
+
+        if oe.utils.vartrue("SPDX_REQUIRE_LAYER_ASSERTION", True, False, d) and downloadLocation == "NOASSERTION":
+            bb.fatal(f"Layer {l_name} does not have a valid downloadLocation")
+        layer = oe.spdx30.software_Package(
+            _id=layer_objset.new_spdxid("layer", l_name),
+            creationInfo=layer_objset.doc.creationInfo,
+            name=l_name,
+            software_packageVersion=l_rev,
+            software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.specification,
+            software_downloadLocation=downloadLocation,
+            software_sourceInfo=json.dumps(
+                {
+                    "branch": l_branch,
+                    "is-modified": (len(l_ismodified) > 0),
+                },
+                separators=(",", ":"),
+            ),
+        )
+
+        layer_objset.add_root(layer)
+
+    for obj in layer_objset.foreach_type(oe.spdx30.Element):
+        obj.extension.append(oe.sbom30.OEIdAliasExtension())
+
+    oe.sbom30.write_jsonld_doc(d, layer_objset, deploy_dir_spdx / "layers.spdx.json")
+
+
+
 def write_bitbake_spdx(d):
     # Set PN to "bitbake" so that SPDX IDs can be generated
     d.setVar("PN", "bitbake")