Skip to content

port_manager: generate SPDX SBOM v2.2 during build#288

Draft
adamgreloch wants to merge 2 commits into
masterfrom
adamgreloch/RTOS-1378
Draft

port_manager: generate SPDX SBOM v2.2 during build#288
adamgreloch wants to merge 2 commits into
masterfrom
adamgreloch/RTOS-1378

Conversation

@adamgreloch

@adamgreloch adamgreloch commented Jul 16, 2026

Copy link
Copy Markdown
Member

TASK: RTOS-1378

Description

Motivation and Context

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Chore (refactoring, style fixes, git/CI config, submodule management, no code logic changes)

How Has This Been Tested?

  • Already covered by automatic testing.
  • New test added: (add PR link here).
  • Tested by hand on: (list targets here).

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing linter checks and tests passed.
  • My changes generate no new compilation warnings for any of the targets.

Special treatment

  • This PR needs additional PRs to work (list the PRs, preferably in merge-order).
  • I will merge this PR by myself when appropriate.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces SPDX v2.2 Software Bill of Materials (SBOM) generation capabilities to the port manager, refactoring candidate classes to track metadata such as licenses, SHA256 checksums, sources, and CPEs, and adding a shared environment file (ports.env). Feedback on these changes highlights a missing Path import in sbom.py that would cause a runtime error, a backward-compatibility syntax error with nested double quotes in f-strings, and a fragile relative path when sourcing ports.env in Bash. Additionally, improvements were suggested to use timezone-aware UTC datetimes for SPDX compliance, specify file encoding explicitly, and enhance the robustness of the environment parser.

Comment thread port_manager/sbom.py Outdated
Comment thread port_manager/port_manager.py
Comment thread port_manager/port_internal.subr Outdated
Comment thread port_manager/sbom.py Outdated
Comment thread port_manager/build_layer.py Outdated
Comment thread port_manager/build_layer.py
@adamgreloch
adamgreloch force-pushed the adamgreloch/RTOS-1378 branch 2 times, most recently from 6ca5559 to 231f487 Compare July 16, 2026 09:55
Comment thread port_manager/port_internal.subr
@adamgreloch
adamgreloch force-pushed the adamgreloch/RTOS-1378 branch from 231f487 to 0585304 Compare July 16, 2026 10:03
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Unit Test Results

10 890 tests   10 220 ✅  53m 38s ⏱️
   680 suites     670 💤
     1 files         0 ❌

Results for commit c826481.

♻️ This comment has been updated with latest results.

@adamgreloch
adamgreloch force-pushed the adamgreloch/RTOS-1378 branch 2 times, most recently from c826481 to d95e269 Compare July 20, 2026 12:32
Comment thread build.sh
b_build_test
fi

SBOM_ARGS=()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [shellcheck] reported by reviewdog 🐶
SBOM_ARGS appears unused. Verify use (or export if used externally). SC2034

Comment thread build.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[shfmt] reported by reviewdog 🐶

phoenix-rtos-build/build.sh

Lines 110 to 135 in d95e269

case "$arg"
in
clean)
B_CLEAN="y";;
fs)
B_FS="y";;
core)
B_CORE="y";;
host)
B_HOST="y";;
test|tests)
B_TEST="y";;
ports)
B_PORTS="y";;
project)
B_PROJECT="y";;
image)
B_IMAGE="y";;
sbom)
B_SBOM="y";;
all)
B_FS="y"; B_CORE="y"; B_HOST="y"; B_PORTS="y"; B_PROJECT="y"; B_IMAGE="y";;
*)
echo "Unknown build option: \"$arg\"."
exit 1;;
esac;

@adamgreloch
adamgreloch force-pushed the adamgreloch/RTOS-1378 branch from d95e269 to 47057fa Compare July 20, 2026 12:34
@adamgreloch

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces Software Bill of Materials (SBOM) generation capabilities to the Phoenix-RTOS build system, adding a new aggregation script, integrating metadata collection into the build process, and updating the port manager to generate ports SBOMs. The review feedback highlights several critical improvements, such as resolving potential KeyError exceptions when accessing environment variables, fixing an incorrect logging call, adding safety checks for directory existence and enum parsing, specifying explicit file encoding, and replacing external hashing subprocesses with Python's native hashlib for better portability and performance.

Comment thread port_manager/sbom.py
)

write_document_to_file(doc, str(output_path))
logger.info("SBOM saved to", output_path.absolute())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Python's logging library (and typical custom loggers), logger.info does not concatenate multiple arguments like print does. Instead, additional arguments are used as string formatting parameters. Since there are no placeholders in the format string, the output path will be omitted from the log. Use an f-string instead.

Suggested change
logger.info("SBOM saved to", output_path.absolute())
logger.info(f"SBOM saved to {output_path.absolute()}")

Comment on lines +123 to +126
def run_reuse_on_directory(directory_path: Path) -> tuple[str, str | None]:
"""Runs reuse tool on given path. Returns a pair of strings: copyrights
field and licenses SPDX expression"""
project = Project(directory_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If directory_path does not exist or is not a directory, initializing Project(directory_path) or calling project.all_files() will raise an exception (e.g., FileNotFoundError). It is safer to check if the path is a directory before proceeding.

Suggested change
def run_reuse_on_directory(directory_path: Path) -> tuple[str, str | None]:
"""Runs reuse tool on given path. Returns a pair of strings: copyrights
field and licenses SPDX expression"""
project = Project(directory_path)
def run_reuse_on_directory(directory_path: Path) -> tuple[str, str | None]:
"""Runs reuse tool on given path. Returns a pair of strings: copyrights
field and licenses SPDX expression"""
if not directory_path.is_dir():
logger.warning(f"Directory {directory_path} does not exist or is not a directory.")
return "", None
project = Project(directory_path)

Comment on lines +60 to +80
def run_subprocess(filepath: Path, command: str) -> str:
result = subprocess.run(
[command, filepath.absolute()], capture_output=True, text=True, check=True
)
return result.stdout.split()[0]


def libpath_to_spdx_file(path: Path, license_spdx: str) -> File:
lib_name = path.name
spdx_id = libpath_to_spdx_id(str(path))

if not os.path.isfile(path):
raise FileNotFoundError(path)

return File(
name=lib_name,
spdx_id=spdx_id,
checksums=[
Checksum(ChecksumAlgorithm.SHA1, run_subprocess(path, "sha1sum")),
Checksum(ChecksumAlgorithm.SHA256, run_subprocess(path, "sha256sum")),
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Spawning external processes like sha1sum and sha256sum via subprocess.run is slow, resource-intensive, and not cross-platform (e.g., macOS does not have these commands by default). Since this is Python, we can easily compute SHA1 and SHA256 in pure Python using the standard hashlib library. This is much faster, more secure, and completely portable.

def compute_file_hash(filepath: Path, algo: str) -> str:
    import hashlib
    hasher = hashlib.new(algo)
    with filepath.open("rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            hasher.update(chunk)
    return hasher.hexdigest()


def libpath_to_spdx_file(path: Path, license_spdx: str) -> File:
    lib_name = path.name
    spdx_id = libpath_to_spdx_id(str(path))

    if not os.path.isfile(path):
        raise FileNotFoundError(path)

    return File(
        name=lib_name,
        spdx_id=spdx_id,
        checksums=[
            Checksum(ChecksumAlgorithm.SHA1, compute_file_hash(path, "sha1")),
            Checksum(ChecksumAlgorithm.SHA256, compute_file_hash(path, "sha256")),
        ],

Comment thread generate_sbom.py
sys.exit(1)
version = frag.get("version", phoenix_version)
makefile_path = Path(frag["makefile_path"])
generated_type = GeneratedType(frag.get("type", ""))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If frag does not contain a "type" key, or if the value is not one of the defined GeneratedType enum values (e.g., if it is empty or has an unexpected value), GeneratedType(frag.get("type", "")) will raise a ValueError and crash the script. It is safer to handle this gracefully or use a fallback.

Suggested change
generated_type = GeneratedType(frag.get("type", ""))
try:
generated_type = GeneratedType(frag.get("type", ""))
except ValueError:
generated_type = None


def generate_sbom(self):
from .sbom import generate_ports_sbom
output_path = Path(os.environ.get("PORTS_SBOM_PATH", Path(os.environ["PREFIX_BUILD"]) / "ports.spdx.json"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Python, the default argument of dict.get(key, default) is always evaluated, even if the key exists in the dictionary. Therefore, os.environ["PREFIX_BUILD"] will be accessed and can raise a KeyError if PREFIX_BUILD is missing, even when PORTS_SBOM_PATH is set. Use a conditional expression to avoid evaluating the default value unnecessarily.

Suggested change
output_path = Path(os.environ.get("PORTS_SBOM_PATH", Path(os.environ["PREFIX_BUILD"]) / "ports.spdx.json"))
output_path = Path(os.environ["PORTS_SBOM_PATH"]) if "PORTS_SBOM_PATH" in os.environ else Path(os.environ["PREFIX_BUILD"]) / "ports.spdx.json"

Comment on lines +43 to +45
def get_phoenix_ver() -> str:
# ignore any abbrevs that may possibly be emitted if version is taken with `git describe`
return os.environ["PHOENIX_VER"].split("-", 1)[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If PHOENIX_VER is not set in the environment (for example, if build-ports.sh or port_manager.py is run directly without going through build.sh), os.environ["PHOENIX_VER"] will raise a KeyError. It is safer to use os.environ.get() with a sensible default fallback.

Suggested change
def get_phoenix_ver() -> str:
# ignore any abbrevs that may possibly be emitted if version is taken with `git describe`
return os.environ["PHOENIX_VER"].split("-", 1)[0]
def get_phoenix_ver() -> str:
# ignore any abbrevs that may possibly be emitted if version is taken with `git describe`
phoenix_ver = os.environ.get("PHOENIX_VER", "v0.0.0")
return phoenix_ver.split("-", 1)[0]

Comment thread generate_sbom.py
fragments = []
for path in sorted(build_dir.rglob("*.sbom.json")):
try:
data = json.loads(path.read_text())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When reading files using Path.read_text(), it is highly recommended to specify the encoding explicitly (e.g., encoding="utf-8"). Otherwise, Python will fall back to the system's default encoding, which can cause UnicodeDecodeError on systems with different default locales (like Windows or some minimal Docker containers).

Suggested change
data = json.loads(path.read_text())
data = json.loads(path.read_text(encoding="utf-8"))

Assisted-by: Claude:Claude-sonnet-4-6
TASK: RTOS-1378
@adamgreloch
adamgreloch force-pushed the adamgreloch/RTOS-1378 branch from 47057fa to e80cbe2 Compare July 20, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant