|
| 1 | +# SPDX-License-Identifier: Apache-2.0 |
| 2 | + |
| 3 | +import tomlkit |
| 4 | +from packagedcode import models |
| 5 | +from packageurl import PackageURL |
| 6 | + |
| 7 | +class BuildpackHandler(models.DatafileHandler): |
| 8 | + """ |
| 9 | + Handle buildpack.toml manifests. |
| 10 | + See https://buildpacks.io/ for details on buildpack format. |
| 11 | + """ |
| 12 | + datasource_id = "buildpack_toml" |
| 13 | + path_patterns = ("*buildpack.toml",) |
| 14 | + default_package_type = "buildpack" |
| 15 | + description = "Cloud Native Buildpack manifest" |
| 16 | + documentation_url = "https://buildpacks.io/" |
| 17 | + |
| 18 | + @classmethod |
| 19 | + def parse(cls, location, package_only=False): |
| 20 | + """ |
| 21 | + Parse the buildpack.toml file at `location` and yield PackageData. |
| 22 | + """ |
| 23 | + with open(location, "r", encoding="utf-8") as f: |
| 24 | + data = tomlkit.parse(f.read()) |
| 25 | + |
| 26 | + # Extract required fields |
| 27 | + api_version = data.get("api") |
| 28 | + buildpack = data.get("buildpack", {}) |
| 29 | + if not buildpack: |
| 30 | + return # Skip files missing required fields |
| 31 | + |
| 32 | + buildpack_id = buildpack.get("id") |
| 33 | + buildpack_version = buildpack.get("version", "unknown") |
| 34 | + buildpack_name = buildpack.get("name") |
| 35 | + |
| 36 | + if not (api_version and buildpack_id and buildpack_version and buildpack_name): |
| 37 | + return # Skip incomplete data |
| 38 | + |
| 39 | + # Optional fields |
| 40 | + description = buildpack.get("description") |
| 41 | + homepage_url = buildpack.get("homepage") |
| 42 | + licenses = buildpack.get("licenses", []) |
| 43 | + keywords = buildpack.get("keywords", []) |
| 44 | + sbom_formats = buildpack.get("sbom-formats", []) |
| 45 | + |
| 46 | + # Parse licenses |
| 47 | + license_expressions = [] |
| 48 | + for license_entry in licenses: |
| 49 | + license_type = license_entry.get("type") |
| 50 | + license_uri = license_entry.get("uri") |
| 51 | + if license_type: |
| 52 | + license_expressions.append(license_type) |
| 53 | + |
| 54 | + # Parse dependencies from "metadata.dependencies" |
| 55 | + dependencies = [] |
| 56 | + metadata = data.get("metadata", {}) |
| 57 | + metadata_dependencies = metadata.get("dependencies", []) |
| 58 | + for dep in metadata_dependencies: |
| 59 | + dep_purl = dep.get("purl") |
| 60 | + dep_name = dep.get("name") |
| 61 | + dep_version = dep.get("version") |
| 62 | + if dep_purl: |
| 63 | + dependencies.append( |
| 64 | + models.DependentPackage( |
| 65 | + purl=dep_purl, |
| 66 | + scope="runtime", |
| 67 | + is_runtime=True, |
| 68 | + is_optional=False, |
| 69 | + ) |
| 70 | + ) |
| 71 | + elif dep_name and dep_version: |
| 72 | + dependencies.append( |
| 73 | + models.DependentPackage( |
| 74 | + purl=PackageURL(type="generic", name=dep_name, version=dep_version).to_string(), |
| 75 | + scope="runtime", |
| 76 | + is_runtime=True, |
| 77 | + is_optional=False, |
| 78 | + ) |
| 79 | + ) |
| 80 | + |
| 81 | + # Parse "order" section for additional dependencies |
| 82 | + orders = data.get("order", []) |
| 83 | + for order in orders: |
| 84 | + for group in order.get("group", []): |
| 85 | + group_id = group.get("id") |
| 86 | + group_version = group.get("version") |
| 87 | + if group_id and group_version: |
| 88 | + dependencies.append( |
| 89 | + models.DependentPackage( |
| 90 | + purl=PackageURL(type="buildpack", name=group_id, version=group_version).to_string(), |
| 91 | + scope="runtime", |
| 92 | + is_runtime=True, |
| 93 | + is_optional=group.get("optional", False), |
| 94 | + ) |
| 95 | + ) |
| 96 | + |
| 97 | + package_data = dict( |
| 98 | + datasource_id=cls.datasource_id, |
| 99 | + type=cls.default_package_type, |
| 100 | + name=buildpack_name, |
| 101 | + version=buildpack_version, |
| 102 | + description=description, |
| 103 | + homepage_url=homepage_url, |
| 104 | + keywords=keywords, |
| 105 | + sbom_formats=sbom_formats, |
| 106 | + declared_license_expression=" AND ".join(license_expressions) if license_expressions else None, |
| 107 | + dependencies=dependencies, |
| 108 | + extra_data={"id": buildpack_id}, # Store the id in extra_data |
| 109 | + ) |
| 110 | + |
| 111 | + yield models.PackageData.from_data(package_data, package_only) |
0 commit comments