# (c) 2025 by Stephan Menzel # Licensed under the Apache License, Version 2.0. # See attached file LICENSE for full details from typing import Self import json from pathlib import Path from common.errors import DependencyInfoError import common.settings from common.tags import sanitize_tag # will be populated by build_package_tree() packages = {} class PackageInfo: """Parsed from packages.json, wraps info about each dependency""" version = "" repo = "" tag = "" dependencies = {} # sbom relevant info name = "" description = None license_id = None license_name = None license_url = "" def __init__(self, package_name: str): pckjson = {} with open("packages.json", "r") as jsonfile: pckjson = json.load(jsonfile) if package_name not in pckjson: raise DependencyInfoError(f"Cannot find version info for {package_name} in packages.json") pckdict = pckjson[package_name] self.repo = pckdict["repo"] self.tag = pckdict["tag"] self.version = pckdict["version"] self.dependencies = {} if "depends" in pckdict: for dependency in pckdict["depends"]: if dependency not in packages: packages[dependency] = PackageInfo(dependency) self.dependencies[dependency] = packages[dependency] # Save some data to later generate the SBOM with self.name = package_name self.description = pckdict["description"] if "description" in pckdict else None if "license_id" in pckdict: self.license_id = pckdict["license_id"] else: self.license_name = pckdict["license_name"] self.license_url = pckdict["license_url"] def dependency(self, package_name: str) -> Self: """Give the path underneath which the package, according to its version, is installed""" if package_name not in self.dependencies: raise DependencyInfoError(f"Cannot find dependency {package_name} in {self.name}.") return self.dependencies[package_name] def get_dependencies(self) -> list[str]: """Hand out all the dependencies in order of suitable build Those without any dependencies will be first, then the ones depending on the above """ deps_0 = set[str]() deps_1 = set[str]() # I should be recursive here but right now I'm too lazy for this for name, info in self.dependencies.items(): if len(info.dependencies) == 0: deps_0.add(name) else: deps_1.add(name) return list(deps_0) + list(deps_1) def dependency_path(self, package_name: str) -> Path: """Give the path underneath which the package, according to its version, is installed""" if package_name not in self.dependencies: raise DependencyInfoError(f"Cannot find dependency {package_name} in {self.name}.") return self.dependencies[package_name].install_location() def bom_ref(self) -> str: """Return the CycloneDX bom-ref for this package""" return f"{self.name}=={self.version}" def purl(self) -> str: """Return the CycloneDX purl for this package""" return f"pkg:cmake/{self.name}@{self.version}" def add_to_sbom(self, sbom: dict) -> None: """Add all information we have about this package to a CycloneDX SBOM""" if "components" not in sbom: sbom["components"] = [] license = { "acknowledgement": "concluded", "url": self.license_url } if self.license_id: license["id"] = self.license_id else: license["name"] = self.license_name sbom["components"].append({ "name": self.name, "purl": self.purl(), "type": "library", "version": self.version, "bom-ref": self.bom_ref(), "description": self.description, "externalReferences": [ { "comment": "from packaging metadata Project-URL: Source Code", "type": "other", "url": self.repo, }, ], "licenses": [ { "license": license }, ] }) if "dependencies" not in sbom: sbom["dependencies"] = [] dependencies_entry = {"ref": self.purl()} for name, dep_info in self.dependencies.items(): if "dependsOn" not in dependencies_entry: dependencies_entry["dependsOn"] = [] dependencies_entry["dependsOn"].append(dep_info.purl()) sbom["dependencies"].append(dependencies_entry) def install_location(self) -> Path: """Get the directory where this package out to be installed in """ return common.settings.install_prefix / Path(f"{self.name}-{self.version}") def src_dir(self) -> Path: """Get us the directory where the source is going to be checked out in (a subdir under "raw" normally) """ return common.settings.build_dir / Path(f"{self.name}-{sanitize_tag(self.tag)}") def build_package_tree() -> None: """Assemble the global dependency tree of packages known to that system """ global packages pckjson = {} with open("packages.json", "r") as jsonfile: pckjson = json.load(jsonfile) for package_name in pckjson.keys(): if package_name not in packages: packages[package_name] = PackageInfo(package_name) def get_package_info(name: str) -> PackageInfo: """Get the PackageInfo struct of that name or :throws DependencyInfoError """ global packages if name not in packages: raise DependencyInfoError(f"No build info for {name}") return packages[name]