From c937daa16a10e65ecc340acbb136f6da2080a736 Mon Sep 17 00:00:00 2001 From: Kendall Harter Date: Thu, 2 Jul 2026 10:40:51 -0700 Subject: [PATCH 1/6] rootfs downloader for Ubuntu --- plugins/rootfsdownloader/README.md | 17 ++ plugins/rootfsdownloader/pyproject.toml | 34 ++++ .../surfactantplugin_rootfsdownloader.py | 177 ++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 plugins/rootfsdownloader/README.md create mode 100644 plugins/rootfsdownloader/pyproject.toml create mode 100644 plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py diff --git a/plugins/rootfsdownloader/README.md b/plugins/rootfsdownloader/README.md new file mode 100644 index 00000000..ce24adeb --- /dev/null +++ b/plugins/rootfsdownloader/README.md @@ -0,0 +1,17 @@ +# rootfs Downloader for Surfactant + +A plugin that downloads `rootfs` files for Surfactant based on ELF metadata. + +## Quickstart + +In the same virtual environment that Surfactant was installed in, install this plugin with `pip install .`. +If pipx was used to install Surfactant, install this plugin with +`pipx inject surfactant git+https://github.com/LLNL/Surfactant#subdirectory=plugins/rootfsdownloader`. + +For developers making changes to this plugin, install it with `pip install -e .`. + +## Uninstalling + +This plugin can be uninstalled with `pip uninstall surfactantplugin-rootfsdownloader`. +If pipx was used, it can be uninstalled with +`pipx uninject surfactant surfactantplugin-rootfsdownloader` diff --git a/plugins/rootfsdownloader/pyproject.toml b/plugins/rootfsdownloader/pyproject.toml new file mode 100644 index 00000000..e4d46069 --- /dev/null +++ b/plugins/rootfsdownloader/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["setuptools>=77.0.3", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "surfactantplugin-rootfsdownloader" +authors = [ + {name = "Kendall Harter", email = "harter8@llnl.gov"}, +] +description = "Automatic rootfs downloading based on ELF information." +readme = "README.md" +requires-python = ">=3.10" +keywords = ["surfactant"] +license = "MIT" +classifiers = [ + "Programming Language :: Python :: 3", + "Environment :: Console", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", +] +dependencies = [ + "surfactant", + "loguru==0.7.*", + "requests>=2.32.3", + "beautifulsoup4==4.*", +] +dynamic = ["version"] + +[project.entry-points."surfactant"] +"surfactantplugin_rootfsdownloader" = "surfactantplugin_rootfsdownloader" + +[tool.setuptools] +py-modules=["surfactantplugin_rootfsdownloader"] diff --git a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py b/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py new file mode 100644 index 00000000..665b0bee --- /dev/null +++ b/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py @@ -0,0 +1,177 @@ +# Copyright 2026 Lawrence Livermore National Security, LLC +# See the top-level LICENSE file for details. +# +# SPDX-License-Identifier: MIT + +# Currently using Ubuntu downloads at: https://partner-images.canonical.com/oci/ +# Another possible source (for other distros): https://images.linuxcontainers.org/ + +from loguru import logger +import dataclasses +import datetime +import requests +from bs4 import BeautifulSoup, Tag +from typing import Generator +import gzip +import tarfile +import io + +import surfactant.plugin +import surfactant.configmanager +from surfactant.sbomtypes import SBOM, Software, Relationship + +@dataclasses.dataclass +class DownloadedInfo: + distro: str + version: str + date: datetime.date + + def __hash__(self) -> int: + return hash((self.distro, self.version, self.date)) + + +class RootfsManager: + __config = surfactant.configmanager.ConfigManager() + __webpage_archive: dict[str, str] = {} + __downloaded_info: set[DownloadedInfo] = set() + # The version info extracted from a webpage + # Not a dict so the order on the webpage can be retained, which puts + # the newest version at the end + __version_archive: list[tuple[str, list[tuple[str, datetime.date]]]] = [] + # Mapping from ELF arch names to download arch names + __ELF_ARCH_TO_DOWNLOAD_ARCH: dict[str, str] = { + "EM_X86_64": "amd64", + "EM_AARCH64": "arm64", + "EM_ARM": "armhf", + "EM_PPC64": "ppc64el", + "EM_RISCV": "riscv64", + "EM_S390": "s390x", + } + __UBUNTU_BASE_DOWNLOAD_URL: str = "https://partner-images.canonical.com/oci/" + + def __init__(self): + # Create the data directory if needed + self.data_dir = self.__config.get_data_dir_path() / "rootfs_downloads" + if not self.data_dir.exists(): + self.data_dir.mkdir(parents=True) + else: + for child in self.data_dir.iterdir(): + if child.is_dir(): + if info := self.try_decode_filename(child.name): + self.__downloaded_info.add(info) + else: + logger.warning(f"Could not parse filename of downloaded rootfs: {child}") + + def try_decode_filename(self, name: str) -> DownloadedInfo | None: + split = name.split("@") + if len(split) != 3: + return None + datesplit = split[2].split("-") + if len(datesplit) != 3: + return None + return DownloadedInfo( + split[0], + split[1], + datetime.date(*(int(x) for x in datesplit)) + ) + + def get_web_subpage(self, subpage: str) -> str | None: + if subpage in self.__webpage_archive: + return self.__webpage_archive[subpage] + + r = requests.get(f"{self.__UBUNTU_BASE_DOWNLOAD_URL}{subpage}") + if r.status_code != 200: + return None + + self.__webpage_archive[subpage] = r.text + return r.text + + # Each table has the same format, so this can be used to iterate + # over meaningful entries in the table + def webpage_table_info(self, subpage: str) -> Generator[list[Tag], None, None]: + if root_page := self.get_web_subpage(subpage): + html = BeautifulSoup(root_page, features="html.parser") + raw_info: list[list[Tag]] = [] + # Extract each row of the table into a list + if body := html.find("body"): + if table := body.find("table"): + for tr in (tr for tr in table if isinstance(tr, Tag)): + raw_info.append([td for td in tr if isinstance(td, Tag)]) + # Information starts at the fourth row + yield from raw_info[3:] + + # Iterates over the directories on a webpage + def dirs_on_webpage(self, subpage: str) -> Generator[tuple[str, datetime.date], None, None]: + for row in self.webpage_table_info(subpage): + # Directories have 5 elements and the first column is an image with a [DIR] alt img + if len(row) == 5: + if img := row[0].find("img"): + if "alt" in img.attrs and img.attrs["alt"] == "[DIR]": + if a := row[1].find("a"): + raw_date = row[2].text.split(' ')[0] + yield (a.text, datetime.date(*(int(x) for x in raw_date.split('-')))) + + # Iterates over the .tar.gz files on a webpage + # This is needed since, for some reason, the number of "ubuntu" strings is inconsistent + # Just returns the name of the files + def tar_gz_on_webpage(self, subpage: str) -> Generator[str, None, None]: + # This is very similar to the dirs_on_webpage; they could maybe be combined? + for row in self.webpage_table_info(subpage): + if len(row) == 5: + if img := row[0].find("img"): + if "src" in img.attrs and img.attrs["src"] == "/icons/compressed.gif": + if a := row[1].find("a"): + yield a.text + + # Potential future work: + # - More considerations than just architecture + # - More distros than just Ubuntu + # - Download older versions, etc. + def download_if_needed(self, architecture: str): + # Download the version information if needed + if len(self.__version_archive) == 0: + # This takes a long time so print some status messages on what's happening + logger.info("Downloading Ubuntu version info") + for (ver_name, _) in self.dirs_on_webpage(""): + logger.info(f"Parsing Ubuntu version {ver_name}...") + self.__version_archive.append(( + ver_name, + [x for x in self.dirs_on_webpage(ver_name) if x[0] != "current/"] + )) + + # The latest version is always at the end so use that + version_name, version_downloads = self.__version_archive[-1] + # Also use the latest version download + download_dir, modified_date = min(version_downloads, key=lambda x: x[1]) + # Download it if needed + arch = self.__ELF_ARCH_TO_DOWNLOAD_ARCH[architecture] + dir_name = f"ubuntu-{version_name[:-1]}@{arch}@{modified_date.strftime("%Y-%m-%d")}" + if dir_ver := self.try_decode_filename(dir_name): + if dir_ver not in self.__downloaded_info: + for f in self.tar_gz_on_webpage(f"{version_name}{download_dir}"): + if f"{arch}-root" in f: + r = requests.get( + f"{self.__UBUNTU_BASE_DOWNLOAD_URL}{version_name}{download_dir}{f}", + stream=True + ) + + if r.status_code != 200: + continue + + logger.info(f"Downloading {f}") + + (self.data_dir / dir_name).mkdir(exist_ok=True) + with gzip.GzipFile(fileobj=io.BytesIO(r.raw.read())) as gfile: + with tarfile.TarFile(fileobj=gfile) as tfile: + tfile.extractall(self.data_dir / dir_name) + self.__downloaded_info.add(dir_ver) + + +rootfs_manager = RootfsManager() + +@surfactant.plugin.hookimpl +def establish_relationships( + sbom: SBOM, software: Software, metadata: object +) -> list[Relationship] | None: + if type(metadata) is dict and "elfArchitecture" in metadata: + rootfs_manager.download_if_needed(metadata["elfArchitecture"]) From f23ff5da2a9e276fe5eedfc6018166f43c9eb4b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:42:21 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../surfactantplugin_rootfsdownloader.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py b/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py index 665b0bee..e8a2f790 100644 --- a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py +++ b/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py @@ -6,19 +6,21 @@ # Currently using Ubuntu downloads at: https://partner-images.canonical.com/oci/ # Another possible source (for other distros): https://images.linuxcontainers.org/ -from loguru import logger import dataclasses import datetime -import requests -from bs4 import BeautifulSoup, Tag -from typing import Generator import gzip -import tarfile import io +import tarfile +from collections.abc import Generator + +import requests +from bs4 import BeautifulSoup, Tag +from loguru import logger -import surfactant.plugin import surfactant.configmanager -from surfactant.sbomtypes import SBOM, Software, Relationship +import surfactant.plugin +from surfactant.sbomtypes import SBOM, Relationship, Software + @dataclasses.dataclass class DownloadedInfo: @@ -69,11 +71,7 @@ def try_decode_filename(self, name: str) -> DownloadedInfo | None: datesplit = split[2].split("-") if len(datesplit) != 3: return None - return DownloadedInfo( - split[0], - split[1], - datetime.date(*(int(x) for x in datesplit)) - ) + return DownloadedInfo(split[0], split[1], datetime.date(*(int(x) for x in datesplit))) def get_web_subpage(self, subpage: str) -> str | None: if subpage in self.__webpage_archive: @@ -108,8 +106,8 @@ def dirs_on_webpage(self, subpage: str) -> Generator[tuple[str, datetime.date], if img := row[0].find("img"): if "alt" in img.attrs and img.attrs["alt"] == "[DIR]": if a := row[1].find("a"): - raw_date = row[2].text.split(' ')[0] - yield (a.text, datetime.date(*(int(x) for x in raw_date.split('-')))) + raw_date = row[2].text.split(" ")[0] + yield (a.text, datetime.date(*(int(x) for x in raw_date.split("-")))) # Iterates over the .tar.gz files on a webpage # This is needed since, for some reason, the number of "ubuntu" strings is inconsistent @@ -132,12 +130,11 @@ def download_if_needed(self, architecture: str): if len(self.__version_archive) == 0: # This takes a long time so print some status messages on what's happening logger.info("Downloading Ubuntu version info") - for (ver_name, _) in self.dirs_on_webpage(""): + for ver_name, _ in self.dirs_on_webpage(""): logger.info(f"Parsing Ubuntu version {ver_name}...") - self.__version_archive.append(( - ver_name, - [x for x in self.dirs_on_webpage(ver_name) if x[0] != "current/"] - )) + self.__version_archive.append( + (ver_name, [x for x in self.dirs_on_webpage(ver_name) if x[0] != "current/"]) + ) # The latest version is always at the end so use that version_name, version_downloads = self.__version_archive[-1] @@ -145,14 +142,14 @@ def download_if_needed(self, architecture: str): download_dir, modified_date = min(version_downloads, key=lambda x: x[1]) # Download it if needed arch = self.__ELF_ARCH_TO_DOWNLOAD_ARCH[architecture] - dir_name = f"ubuntu-{version_name[:-1]}@{arch}@{modified_date.strftime("%Y-%m-%d")}" + dir_name = f"ubuntu-{version_name[:-1]}@{arch}@{modified_date.strftime('%Y-%m-%d')}" if dir_ver := self.try_decode_filename(dir_name): if dir_ver not in self.__downloaded_info: for f in self.tar_gz_on_webpage(f"{version_name}{download_dir}"): if f"{arch}-root" in f: r = requests.get( f"{self.__UBUNTU_BASE_DOWNLOAD_URL}{version_name}{download_dir}{f}", - stream=True + stream=True, ) if r.status_code != 200: @@ -169,6 +166,7 @@ def download_if_needed(self, architecture: str): rootfs_manager = RootfsManager() + @surfactant.plugin.hookimpl def establish_relationships( sbom: SBOM, software: Software, metadata: object From bfbe8b7ee90ce6c77d0ebde3e885fa46e080daab Mon Sep 17 00:00:00 2001 From: Kendall Harter Date: Thu, 2 Jul 2026 10:47:04 -0700 Subject: [PATCH 3/6] Address ruff/pylint things --- .../surfactantplugin_rootfsdownloader.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py b/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py index e8a2f790..913f6103 100644 --- a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py +++ b/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py @@ -3,6 +3,10 @@ # # SPDX-License-Identifier: MIT +# This has a bunch of false positives (because := statements can't be combined) +# so disable it +#ruff: noqa SIM102 + # Currently using Ubuntu downloads at: https://partner-images.canonical.com/oci/ # Another possible source (for other distros): https://images.linuxcontainers.org/ @@ -158,10 +162,9 @@ def download_if_needed(self, architecture: str): logger.info(f"Downloading {f}") (self.data_dir / dir_name).mkdir(exist_ok=True) - with gzip.GzipFile(fileobj=io.BytesIO(r.raw.read())) as gfile: - with tarfile.TarFile(fileobj=gfile) as tfile: - tfile.extractall(self.data_dir / dir_name) - self.__downloaded_info.add(dir_ver) + with gzip.GzipFile(fileobj=io.BytesIO(r.raw.read())) as gfile, tarfile.TarFile(fileobj=gfile) as tfile: + tfile.extractall(self.data_dir / dir_name) + self.__downloaded_info.add(dir_ver) rootfs_manager = RootfsManager() @@ -171,5 +174,5 @@ def download_if_needed(self, architecture: str): def establish_relationships( sbom: SBOM, software: Software, metadata: object ) -> list[Relationship] | None: - if type(metadata) is dict and "elfArchitecture" in metadata: + if isinstance(metadata, dict) and "elfArchitecture" in metadata: rootfs_manager.download_if_needed(metadata["elfArchitecture"]) From 7470af035888e1088bc4eb0102821d77d686abf3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:47:27 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../rootfsdownloader/surfactantplugin_rootfsdownloader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py b/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py index 913f6103..67a5c605 100644 --- a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py +++ b/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py @@ -5,7 +5,7 @@ # This has a bunch of false positives (because := statements can't be combined) # so disable it -#ruff: noqa SIM102 +# ruff: noqa SIM102 # Currently using Ubuntu downloads at: https://partner-images.canonical.com/oci/ # Another possible source (for other distros): https://images.linuxcontainers.org/ @@ -162,7 +162,10 @@ def download_if_needed(self, architecture: str): logger.info(f"Downloading {f}") (self.data_dir / dir_name).mkdir(exist_ok=True) - with gzip.GzipFile(fileobj=io.BytesIO(r.raw.read())) as gfile, tarfile.TarFile(fileobj=gfile) as tfile: + with ( + gzip.GzipFile(fileobj=io.BytesIO(r.raw.read())) as gfile, + tarfile.TarFile(fileobj=gfile) as tfile, + ): tfile.extractall(self.data_dir / dir_name) self.__downloaded_info.add(dir_ver) From 5d5397d7becf5e919cd11daee6cc098b9a2970c7 Mon Sep 17 00:00:00 2001 From: Kendall Harter Date: Mon, 6 Jul 2026 11:36:25 -0700 Subject: [PATCH 5/6] Update name + multi-download error + missing exception handling --- .../README.md | 10 ++++---- .../pyproject.toml | 6 ++--- .../surfactantplugin_rootfsmanager.py} | 23 +++++++++++-------- 3 files changed, 22 insertions(+), 17 deletions(-) rename plugins/{rootfsdownloader => rootfsmanager}/README.md (66%) rename plugins/{rootfsdownloader => rootfsmanager}/pyproject.toml (81%) rename plugins/{rootfsdownloader/surfactantplugin_rootfsdownloader.py => rootfsmanager/surfactantplugin_rootfsmanager.py} (89%) diff --git a/plugins/rootfsdownloader/README.md b/plugins/rootfsmanager/README.md similarity index 66% rename from plugins/rootfsdownloader/README.md rename to plugins/rootfsmanager/README.md index ce24adeb..25660f81 100644 --- a/plugins/rootfsdownloader/README.md +++ b/plugins/rootfsmanager/README.md @@ -1,17 +1,17 @@ -# rootfs Downloader for Surfactant +# rootfs Manager for Surfactant -A plugin that downloads `rootfs` files for Surfactant based on ELF metadata. +A plugin that manages `rootfs` files for Surfactant based on ELF metadata. ## Quickstart In the same virtual environment that Surfactant was installed in, install this plugin with `pip install .`. If pipx was used to install Surfactant, install this plugin with -`pipx inject surfactant git+https://github.com/LLNL/Surfactant#subdirectory=plugins/rootfsdownloader`. +`pipx inject surfactant git+https://github.com/LLNL/Surfactant#subdirectory=plugins/rootfsmanager`. For developers making changes to this plugin, install it with `pip install -e .`. ## Uninstalling -This plugin can be uninstalled with `pip uninstall surfactantplugin-rootfsdownloader`. +This plugin can be uninstalled with `pip uninstall surfactantplugin-rootfsmanager`. If pipx was used, it can be uninstalled with -`pipx uninject surfactant surfactantplugin-rootfsdownloader` +`pipx uninject surfactant surfactantplugin-rootfsmanager` diff --git a/plugins/rootfsdownloader/pyproject.toml b/plugins/rootfsmanager/pyproject.toml similarity index 81% rename from plugins/rootfsdownloader/pyproject.toml rename to plugins/rootfsmanager/pyproject.toml index e4d46069..d154bf2e 100644 --- a/plugins/rootfsdownloader/pyproject.toml +++ b/plugins/rootfsmanager/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=77.0.3", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] -name = "surfactantplugin-rootfsdownloader" +name = "surfactantplugin-rootfsmanager" authors = [ {name = "Kendall Harter", email = "harter8@llnl.gov"}, ] @@ -28,7 +28,7 @@ dependencies = [ dynamic = ["version"] [project.entry-points."surfactant"] -"surfactantplugin_rootfsdownloader" = "surfactantplugin_rootfsdownloader" +"surfactantplugin_rootfsmanager" = "surfactantplugin_rootfsmanager" [tool.setuptools] -py-modules=["surfactantplugin_rootfsdownloader"] +py-modules=["surfactantplugin_rootfsmanager"] diff --git a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py b/plugins/rootfsmanager/surfactantplugin_rootfsmanager.py similarity index 89% rename from plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py rename to plugins/rootfsmanager/surfactantplugin_rootfsmanager.py index 67a5c605..54cb8bfb 100644 --- a/plugins/rootfsdownloader/surfactantplugin_rootfsdownloader.py +++ b/plugins/rootfsmanager/surfactantplugin_rootfsmanager.py @@ -9,6 +9,7 @@ # Currently using Ubuntu downloads at: https://partner-images.canonical.com/oci/ # Another possible source (for other distros): https://images.linuxcontainers.org/ +# See also: https://github.com/llnl/Surfactant/pull/646#issuecomment-4870762467 import dataclasses import datetime @@ -135,7 +136,7 @@ def download_if_needed(self, architecture: str): # This takes a long time so print some status messages on what's happening logger.info("Downloading Ubuntu version info") for ver_name, _ in self.dirs_on_webpage(""): - logger.info(f"Parsing Ubuntu version {ver_name}...") + logger.info(f"Parsing Ubuntu version {ver_name}") self.__version_archive.append( (ver_name, [x for x in self.dirs_on_webpage(ver_name) if x[0] != "current/"]) ) @@ -143,7 +144,7 @@ def download_if_needed(self, architecture: str): # The latest version is always at the end so use that version_name, version_downloads = self.__version_archive[-1] # Also use the latest version download - download_dir, modified_date = min(version_downloads, key=lambda x: x[1]) + download_dir, modified_date = max(version_downloads, key=lambda x: x[1]) # Download it if needed arch = self.__ELF_ARCH_TO_DOWNLOAD_ARCH[architecture] dir_name = f"ubuntu-{version_name[:-1]}@{arch}@{modified_date.strftime('%Y-%m-%d')}" @@ -161,13 +162,17 @@ def download_if_needed(self, architecture: str): logger.info(f"Downloading {f}") - (self.data_dir / dir_name).mkdir(exist_ok=True) - with ( - gzip.GzipFile(fileobj=io.BytesIO(r.raw.read())) as gfile, - tarfile.TarFile(fileobj=gfile) as tfile, - ): - tfile.extractall(self.data_dir / dir_name) - self.__downloaded_info.add(dir_ver) + try: + (self.data_dir / dir_name).mkdir(exist_ok=True) + with ( + gzip.GzipFile(fileobj=io.BytesIO(r.raw.read())) as gfile, + tarfile.TarFile(fileobj=gfile) as tfile, + ): + tfile.extractall(self.data_dir / dir_name) + self.__downloaded_info.add(dir_ver) + break + except (gzip.BadGzipFile, tarfile.TarError) as e: + logger.warning(f"Error extracting {f} - {e}") rootfs_manager = RootfsManager() From fc42c4bb90bcc77839a365f18541a20d1bfa500b Mon Sep 17 00:00:00 2001 From: Kendall Harter <66640839+KendallHarterAtWork@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:58:43 -0700 Subject: [PATCH 6/6] Update plugins/rootfsmanager/pyproject.toml Remove `surfactantplugin-` prefix Co-authored-by: Ryan Mast <3969255+nightlark@users.noreply.github.com> --- plugins/rootfsmanager/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/rootfsmanager/pyproject.toml b/plugins/rootfsmanager/pyproject.toml index d154bf2e..0abf3d60 100644 --- a/plugins/rootfsmanager/pyproject.toml +++ b/plugins/rootfsmanager/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=77.0.3", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] -name = "surfactantplugin-rootfsmanager" +name = "rootfsmanager" authors = [ {name = "Kendall Harter", email = "harter8@llnl.gov"}, ]