diff mbox series

[2/2] rootfs,spdx: handle removed packages

Message ID 20260605120456.130264-2-peter.marko@siemens.com
State Under Review
Headers show
Series [1/2] rootfs: move tasks using image_list_installed_packages to postuninstall | expand

Commit Message

Marko, Peter June 5, 2026, 12:04 p.m. UTC
From: Peter Marko <peter.marko@siemens.com>

SPDX should not list packages which were removed from rootfs as installed.

The list of installed packages does not contain them directly, but as
dependencies of other installed packages.

Siwtch them to "other" to keep them in SPDX as part of the build and
installation process.

Signed-off-by: Peter Marko <peter.marko@siemens.com>
---
 meta/classes-recipe/create-spdx-image-3.0.bbclass |  7 +++++++
 meta/lib/oe/rootfs.py                             |  6 ++++++
 meta/lib/oe/sbom30.py                             |  8 +++++++-
 meta/lib/oe/spdx30_tasks.py                       | 14 +++++++++++++-
 4 files changed, 33 insertions(+), 2 deletions(-)

Comments

Marko, Peter June 5, 2026, 12:38 p.m. UTC | #1
Sorry, I forgot to copy Joshua...
Peter

> -----Original Message-----
> From: Marko, Peter (FT D EU SK BFS1) <Peter.Marko@siemens.com>
> Sent: Friday, June 5, 2026 2:05 PM
> To: openembedded-core@lists.openembedded.org
> Cc: Marko, Peter (FT D EU SK BFS1) <Peter.Marko@siemens.com>
> Subject: [PATCH 2/2] rootfs,spdx: handle removed packages
> 
> From: Peter Marko <peter.marko@siemens.com>
> 
> SPDX should not list packages which were removed from rootfs as installed.
> 
> The list of installed packages does not contain them directly, but as
> dependencies of other installed packages.
> 
> Siwtch them to "other" to keep them in SPDX as part of the build and
> installation process.
> 
> Signed-off-by: Peter Marko <peter.marko@siemens.com>
> ---
>  meta/classes-recipe/create-spdx-image-3.0.bbclass |  7 +++++++
>  meta/lib/oe/rootfs.py                             |  6 ++++++
>  meta/lib/oe/sbom30.py                             |  8 +++++++-
>  meta/lib/oe/spdx30_tasks.py                       | 14 +++++++++++++-
>  4 files changed, 33 insertions(+), 2 deletions(-)
> 
> diff --git a/meta/classes-recipe/create-spdx-image-3.0.bbclass b/meta/classes-
> recipe/create-spdx-image-3.0.bbclass
> index 15a91e90e2..dfbd2961b3 100644
> --- a/meta/classes-recipe/create-spdx-image-3.0.bbclass
> +++ b/meta/classes-recipe/create-spdx-image-3.0.bbclass
> @@ -6,6 +6,7 @@
>  # SPDX image tasks
> 
>  SPDX_ROOTFS_PACKAGES = "${SPDXDIR}/rootfs-packages.json"
> +SPDX_ROOTFS_REMOVED_PACKAGES = "${SPDXDIR}/rootfs-removed-
> packages.json"
>  SPDXIMAGEDEPLOYDIR = "${SPDXDIR}/image-deploy"
>  SPDXROOTFSDEPLOY = "${SPDXDIR}/rootfs-deploy"
> 
> @@ -15,14 +16,20 @@ python spdx_collect_rootfs_packages() {
>      from oe.rootfs import image_list_installed_packages
> 
>      root_packages_file = Path(d.getVar("SPDX_ROOTFS_PACKAGES"))
> +    root_removed_packages_file =
> Path(d.getVar("SPDX_ROOTFS_REMOVED_PACKAGES"))
> 
>      packages = image_list_installed_packages(d)
>      if not packages:
>          packages = {}
> 
> +    removed_packages = (d.getVar("ROOTFS_REMOVED_PACKAGES") or
> "").split()
> +
>      root_packages_file.parent.mkdir(parents=True, exist_ok=True)
>      with root_packages_file.open("w") as f:
>          json.dump(packages, f)
> +
> +    with root_removed_packages_file.open("w") as f:
> +        json.dump(removed_packages, f)
>  }
>  ROOTFS_POSTUNINSTALL_COMMAND =+ "spdx_collect_rootfs_packages"
> 
> diff --git a/meta/lib/oe/rootfs.py b/meta/lib/oe/rootfs.py
> index 5eee48f587..b8830596ed 100644
> --- a/meta/lib/oe/rootfs.py
> +++ b/meta/lib/oe/rootfs.py
> @@ -261,10 +261,13 @@ class Rootfs(object, metaclass=ABCMeta):
> 
> 
>      def _uninstall_unneeded(self):
> +        removed_pkgs = set()
> +
>          # Remove the run-postinsts package if no delayed postinsts are found
>          delayed_postinsts = self._get_delayed_postinsts()
>          if delayed_postinsts is None:
>              if os.path.exists(self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/init.d/run-
> postinsts")) or
> os.path.exists(self.d.expand("${IMAGE_ROOTFS}${systemd_system_unitdir}/run-
> postinsts.service")):
> +                removed_pkgs.add("run-postinsts")
>                  self.pm.remove(["run-postinsts"])
> 
>          image_rorfs = bb.utils.contains("IMAGE_FEATURES", "read-only-rootfs",
> @@ -285,6 +288,7 @@ class Rootfs(object, metaclass=ABCMeta):
>              # to be uninstalled or to be managed correctly otherwise.
>              provider = self.d.getVar("VIRTUAL-RUNTIME_update-alternatives")
>              pkgs_to_remove = sorted([pkg for pkg in pkgs_installed if pkg in
> unneeded_pkgs], key=lambda x: x == provider)
> +            removed_pkgs.update(pkgs_to_remove)
> 
>              # update-alternatives provider is removed in its own remove()
>              # call because all package managers do not guarantee the packages
> @@ -296,6 +300,8 @@ class Rootfs(object, metaclass=ABCMeta):
>              if len(pkgs_to_remove) > 0:
>                  self.pm.remove([pkgs_to_remove[-1]], False)
> 
> +        self.d.setVar("ROOTFS_REMOVED_PACKAGES", "
> ".join(sorted(removed_pkgs)))
> +
>          if delayed_postinsts:
>              self._save_postinsts()
>              if image_rorfs:
> diff --git a/meta/lib/oe/sbom30.py b/meta/lib/oe/sbom30.py
> index b379ff947c..4fa32266fa 100644
> --- a/meta/lib/oe/sbom30.py
> +++ b/meta/lib/oe/sbom30.py
> @@ -1122,7 +1122,7 @@ def find_by_spdxid(d, spdxid, *, required=False):
>      return find_jsonld(d, *jsonld_hash_path(hash_id(spdxid)), required=required)
> 
> 
> -def create_sbom(d, name, root_elements, add_objectsets=[]):
> +def create_sbom(d, name, root_elements, add_objectsets=[],
> removed_packages=[]):
>      objset = ObjectSet.new_objset(d, name)
> 
>      sbom = objset.add(
> @@ -1142,6 +1142,12 @@ def create_sbom(d, name, root_elements,
> add_objectsets=[]):
>              + "\n  ".join(sorted(list(missing_spdxids)))
>          )
> 
> +    if removed_packages:
> +        for pkg in objset.foreach_type(oe.spdx30.software_Package):
> +            if pkg.name in removed_packages and pkg.software_primaryPurpose ==
> oe.spdx30.software_SoftwarePurpose.install:
> +                pkg.software_primaryPurpose =
> oe.spdx30.software_SoftwarePurpose.other
> +                bb.note("Reclassified removed package %s SPDX entry from install to
> other" % pkg.name)
> +
>      # Filter out internal extensions from final SBoMs
>      objset.remove_internal_extensions()
> 
> diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
> index 7cc46d579b..18c68f47de 100644
> --- a/meta/lib/oe/spdx30_tasks.py
> +++ b/meta/lib/oe/spdx30_tasks.py
> @@ -1532,6 +1532,7 @@ def create_image_sbom_spdx(d):
>      image_link_name = d.getVar("IMAGE_LINK_NAME")
>      imgdeploydir = Path(d.getVar("SPDXIMAGEDEPLOYDIR"))
>      machine = d.getVar("MACHINE")
> +    root_removed_packages_file =
> Path(d.getVar("SPDX_ROOTFS_REMOVED_PACKAGES"))
> 
>      spdx_path = imgdeploydir / (image_name + ".spdx.json")
> 
> @@ -1553,7 +1554,18 @@ def create_image_sbom_spdx(d):
>      for o in image_objset.foreach_root(oe.spdx30.software_File):
>          root_elements.append(oe.sbom30.get_element_link_id(o))
> 
> -    objset, sbom = oe.sbom30.create_sbom(d, image_name, root_elements)
> +    try:
> +        with root_removed_packages_file.open("r") as f:
> +            removed_packages = json.load(f)
> +    except FileNotFoundError:
> +        removed_packages = []
> +
> +    objset, sbom = oe.sbom30.create_sbom(
> +        d,
> +        image_name,
> +        root_elements,
> +        removed_packages=removed_packages,
> +    )
> 
>      # Set supplier on root elements if SPDX_IMAGE_SUPPLIER is defined
>      supplier = objset.new_agent("SPDX_IMAGE_SUPPLIER", add=False)
diff mbox series

Patch

diff --git a/meta/classes-recipe/create-spdx-image-3.0.bbclass b/meta/classes-recipe/create-spdx-image-3.0.bbclass
index 15a91e90e2..dfbd2961b3 100644
--- a/meta/classes-recipe/create-spdx-image-3.0.bbclass
+++ b/meta/classes-recipe/create-spdx-image-3.0.bbclass
@@ -6,6 +6,7 @@ 
 # SPDX image tasks
 
 SPDX_ROOTFS_PACKAGES = "${SPDXDIR}/rootfs-packages.json"
+SPDX_ROOTFS_REMOVED_PACKAGES = "${SPDXDIR}/rootfs-removed-packages.json"
 SPDXIMAGEDEPLOYDIR = "${SPDXDIR}/image-deploy"
 SPDXROOTFSDEPLOY = "${SPDXDIR}/rootfs-deploy"
 
@@ -15,14 +16,20 @@  python spdx_collect_rootfs_packages() {
     from oe.rootfs import image_list_installed_packages
 
     root_packages_file = Path(d.getVar("SPDX_ROOTFS_PACKAGES"))
+    root_removed_packages_file = Path(d.getVar("SPDX_ROOTFS_REMOVED_PACKAGES"))
 
     packages = image_list_installed_packages(d)
     if not packages:
         packages = {}
 
+    removed_packages = (d.getVar("ROOTFS_REMOVED_PACKAGES") or "").split()
+
     root_packages_file.parent.mkdir(parents=True, exist_ok=True)
     with root_packages_file.open("w") as f:
         json.dump(packages, f)
+
+    with root_removed_packages_file.open("w") as f:
+        json.dump(removed_packages, f)
 }
 ROOTFS_POSTUNINSTALL_COMMAND =+ "spdx_collect_rootfs_packages"
 
diff --git a/meta/lib/oe/rootfs.py b/meta/lib/oe/rootfs.py
index 5eee48f587..b8830596ed 100644
--- a/meta/lib/oe/rootfs.py
+++ b/meta/lib/oe/rootfs.py
@@ -261,10 +261,13 @@  class Rootfs(object, metaclass=ABCMeta):
 
 
     def _uninstall_unneeded(self):
+        removed_pkgs = set()
+
         # Remove the run-postinsts package if no delayed postinsts are found
         delayed_postinsts = self._get_delayed_postinsts()
         if delayed_postinsts is None:
             if os.path.exists(self.d.expand("${IMAGE_ROOTFS}${sysconfdir}/init.d/run-postinsts")) or os.path.exists(self.d.expand("${IMAGE_ROOTFS}${systemd_system_unitdir}/run-postinsts.service")):
+                removed_pkgs.add("run-postinsts")
                 self.pm.remove(["run-postinsts"])
 
         image_rorfs = bb.utils.contains("IMAGE_FEATURES", "read-only-rootfs",
@@ -285,6 +288,7 @@  class Rootfs(object, metaclass=ABCMeta):
             # to be uninstalled or to be managed correctly otherwise.
             provider = self.d.getVar("VIRTUAL-RUNTIME_update-alternatives")
             pkgs_to_remove = sorted([pkg for pkg in pkgs_installed if pkg in unneeded_pkgs], key=lambda x: x == provider)
+            removed_pkgs.update(pkgs_to_remove)
 
             # update-alternatives provider is removed in its own remove()
             # call because all package managers do not guarantee the packages
@@ -296,6 +300,8 @@  class Rootfs(object, metaclass=ABCMeta):
             if len(pkgs_to_remove) > 0:
                 self.pm.remove([pkgs_to_remove[-1]], False)
 
+        self.d.setVar("ROOTFS_REMOVED_PACKAGES", " ".join(sorted(removed_pkgs)))
+
         if delayed_postinsts:
             self._save_postinsts()
             if image_rorfs:
diff --git a/meta/lib/oe/sbom30.py b/meta/lib/oe/sbom30.py
index b379ff947c..4fa32266fa 100644
--- a/meta/lib/oe/sbom30.py
+++ b/meta/lib/oe/sbom30.py
@@ -1122,7 +1122,7 @@  def find_by_spdxid(d, spdxid, *, required=False):
     return find_jsonld(d, *jsonld_hash_path(hash_id(spdxid)), required=required)
 
 
-def create_sbom(d, name, root_elements, add_objectsets=[]):
+def create_sbom(d, name, root_elements, add_objectsets=[], removed_packages=[]):
     objset = ObjectSet.new_objset(d, name)
 
     sbom = objset.add(
@@ -1142,6 +1142,12 @@  def create_sbom(d, name, root_elements, add_objectsets=[]):
             + "\n  ".join(sorted(list(missing_spdxids)))
         )
 
+    if removed_packages:
+        for pkg in objset.foreach_type(oe.spdx30.software_Package):
+            if pkg.name in removed_packages and pkg.software_primaryPurpose == oe.spdx30.software_SoftwarePurpose.install:
+                pkg.software_primaryPurpose = oe.spdx30.software_SoftwarePurpose.other
+                bb.note("Reclassified removed package %s SPDX entry from install to other" % pkg.name)
+
     # Filter out internal extensions from final SBoMs
     objset.remove_internal_extensions()
 
diff --git a/meta/lib/oe/spdx30_tasks.py b/meta/lib/oe/spdx30_tasks.py
index 7cc46d579b..18c68f47de 100644
--- a/meta/lib/oe/spdx30_tasks.py
+++ b/meta/lib/oe/spdx30_tasks.py
@@ -1532,6 +1532,7 @@  def create_image_sbom_spdx(d):
     image_link_name = d.getVar("IMAGE_LINK_NAME")
     imgdeploydir = Path(d.getVar("SPDXIMAGEDEPLOYDIR"))
     machine = d.getVar("MACHINE")
+    root_removed_packages_file = Path(d.getVar("SPDX_ROOTFS_REMOVED_PACKAGES"))
 
     spdx_path = imgdeploydir / (image_name + ".spdx.json")
 
@@ -1553,7 +1554,18 @@  def create_image_sbom_spdx(d):
     for o in image_objset.foreach_root(oe.spdx30.software_File):
         root_elements.append(oe.sbom30.get_element_link_id(o))
 
-    objset, sbom = oe.sbom30.create_sbom(d, image_name, root_elements)
+    try:
+        with root_removed_packages_file.open("r") as f:
+            removed_packages = json.load(f)
+    except FileNotFoundError:
+        removed_packages = []
+
+    objset, sbom = oe.sbom30.create_sbom(
+        d,
+        image_name,
+        root_elements,
+        removed_packages=removed_packages,
+    )
 
     # Set supplier on root elements if SPDX_IMAGE_SUPPLIER is defined
     supplier = objset.new_agent("SPDX_IMAGE_SUPPLIER", add=False)