DepperDan/package/package_info.py

157 lines
4.9 KiB
Python

# (c) 2025 by Stephan Menzel
# Licensed under the Apache License, Version 2.0.
# See attached file LICENSE for full details
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_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]