|
| 1 | +# |
| 2 | +# Copyright (c) nexB Inc. and others. All rights reserved. |
| 3 | +# ScanCode is a trademark of nexB Inc. |
| 4 | +# SPDX-License-Identifier: Apache-2.0 |
| 5 | +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. |
| 6 | +# See https://github.com/nexB/scancode-toolkit for support or download. |
| 7 | +# See https://aboutcode.org for more information about nexB OSS projects. |
| 8 | +# |
| 9 | + |
| 10 | +import logging |
| 11 | +import re |
| 12 | + |
| 13 | +from oelint_parser.cls_stash import Stash |
| 14 | +from packageurl import PackageURL |
| 15 | + |
| 16 | +from packagedcode import models |
| 17 | + |
| 18 | +TRACE = True |
| 19 | + |
| 20 | +logger = logging.getLogger(__name__) |
| 21 | + |
| 22 | +if TRACE: |
| 23 | + import sys |
| 24 | + logging.basicConfig(stream=sys.stdout) |
| 25 | + logger.setLevel(logging.DEBUG) |
| 26 | + |
| 27 | + |
| 28 | +class BitbakeBbManifestHandler(models.DatafileHandler): |
| 29 | + datasource_id = 'bitbake_bb_recipe' |
| 30 | + # note that there are .bbappend, .bbclass and bitbake.conf files. |
| 31 | + path_patterns = ('*.bb',) |
| 32 | + default_package_type = 'bitbake' |
| 33 | + description = 'BitBake bb recipe manifest' |
| 34 | + documentation_url = 'https://docs.yoctoproject.org/bitbake/bitbake-user-manual/bitbake-user-manual-metadata.html' |
| 35 | + |
| 36 | + @classmethod |
| 37 | + def parse(cls, location): |
| 38 | + |
| 39 | + oestash = Stash(quiet=True) |
| 40 | + |
| 41 | + # add any bitbake like-file |
| 42 | + # TODO: may be we should handle the bbclass and bbappend here? |
| 43 | + oestash.AddFile(location) |
| 44 | + |
| 45 | + # Resolves proper cross file dependencies |
| 46 | + oestash.Finalize() |
| 47 | + |
| 48 | + # collect all variables of interest. |
| 49 | + # TODO: we should not get list values. Instead plain strings |
| 50 | + data = { |
| 51 | + k: ' '.join(v) if isinstance(v, (list, tuple)) else v |
| 52 | + for k, v in oestash.ExpandVar(filename=location).items() |
| 53 | + if v |
| 54 | + } |
| 55 | + name = data.get('PN') |
| 56 | + version = data.get('PV') |
| 57 | + description = data.get('DESCRIPTION') |
| 58 | + homepage_url = data.get('HOMEPAGE') |
| 59 | + download_url = data.get('PREMIRRORS') |
| 60 | + extracted_license_statement = data.get('LICENSE') |
| 61 | + |
| 62 | + # The item.VarName for SRC_URI[*] from the parser are SRC_URI |
| 63 | + # Therefore, I cannot differentiate md5, sha1, or src file location reference |
| 64 | + # See: https://github.com/priv-kweihmann/oelint-parser/issues/3 |
| 65 | + sha1 = data.get('SRC_URI[sha1sum]') |
| 66 | + md5 = data.get('SRC_URI[md5sum]') |
| 67 | + sha256 = data.get('SRC_URI[sha256sum]') |
| 68 | + sha512 = data.get('SRC_URI[sha512sum]') |
| 69 | + |
| 70 | + dependencies = [] |
| 71 | + |
| 72 | + # Build deps: this is a list of plain BB recipes names |
| 73 | + # https://docs.yoctoproject.org/bitbake/bitbake-user-manual/bitbake-user-manual-ref-variables.html#term-DEPENDS |
| 74 | + build_deps = data.get('DEPENDS', '').split() |
| 75 | + for build_dep in build_deps: |
| 76 | + dep_purl = PackageURL( |
| 77 | + type=cls.default_package_type, |
| 78 | + name=build_dep, |
| 79 | + ).to_string() |
| 80 | + |
| 81 | + dependency = models.DependentPackage( |
| 82 | + purl=dep_purl, |
| 83 | + extracted_requirement=build_dep, |
| 84 | + scope='build', |
| 85 | + is_runtime=False, |
| 86 | + is_optional=True, |
| 87 | + is_pinned=False, |
| 88 | + is_direct=True, |
| 89 | + ) |
| 90 | + dependencies.append(dependency) |
| 91 | + |
| 92 | + # Runtime deps:this is a list of Package names with an optional (=> 12) version constraint |
| 93 | + # https://docs.yoctoproject.org/bitbake/bitbake-user-manual/bitbake-user-manual-ref-variables.html#term-RDEPENDS |
| 94 | + # FIXME: There are some fields such as "RDEPENDS_${PN}" so these may not be correct in all cases |
| 95 | + for key, value in data.items(): |
| 96 | + if not key.startswith('RDEPENDS'): |
| 97 | + continue |
| 98 | + if not value: |
| 99 | + continue |
| 100 | + for name, constraint in get_bitbake_deps(dependencies=value): |
| 101 | + if TRACE: |
| 102 | + logger.debug(f'RDEPENDS: name={name}, constraint={constraint}') |
| 103 | + dep_purl = PackageURL( |
| 104 | + type=cls.default_package_type, |
| 105 | + name=name, |
| 106 | + ).to_string() |
| 107 | + |
| 108 | + extracted_requirement = name |
| 109 | + if constraint: |
| 110 | + extracted_requirement += f' ({constraint})' |
| 111 | + |
| 112 | + dependency = models.DependentPackage( |
| 113 | + purl=dep_purl, |
| 114 | + extracted_requirement=extracted_requirement, |
| 115 | + scope='install', |
| 116 | + is_runtime=True, |
| 117 | + is_optional=False, |
| 118 | + is_pinned=False, |
| 119 | + is_direct=True, |
| 120 | + ) |
| 121 | + |
| 122 | + dependencies.append(dependency) |
| 123 | + |
| 124 | + yield models.PackageData( |
| 125 | + datasource_id=cls.datasource_id, |
| 126 | + type=cls.default_package_type, |
| 127 | + name=name, |
| 128 | + version=version, |
| 129 | + description=description, |
| 130 | + homepage_url=homepage_url, |
| 131 | + download_url=download_url, |
| 132 | + sha1=sha1, |
| 133 | + md5=md5, |
| 134 | + sha256=sha256, |
| 135 | + sha512=sha512, |
| 136 | + extracted_license_statement=extracted_license_statement, |
| 137 | + dependencies=dependencies, |
| 138 | + ) |
| 139 | + |
| 140 | + @classmethod |
| 141 | + def assign_package_to_resources(cls, package, resource, codebase, package_adder): |
| 142 | + return models.DatafileHandler.assign_package_to_parent_tree(package, resource, codebase, package_adder) |
| 143 | + |
| 144 | + |
| 145 | +def get_bitbake_deps(dependencies): |
| 146 | + """ |
| 147 | + Return a list of tuple of (name, version constraint) given a BitBake |
| 148 | + dependencies string. "version constraint" can be None. |
| 149 | +
|
| 150 | + See https://docs.yoctoproject.org/ref-manual/variables.html?#term-RDEPENDS |
| 151 | + For example: |
| 152 | + >>> expected = [('ABC', None), ('abcd', '=>12312'), ('defg', None)] |
| 153 | + >>> result = get_bitbake_deps(" ABC abcd (= > 12312) defg ") |
| 154 | + >>> assert result == expected, result |
| 155 | + >>> expected = [('grub', '==12.23'), ('parted', None), ('e2fsprogs-mke2fs', None)] |
| 156 | + >>> result = get_bitbake_deps("grub (== 12.23) parted e2fsprogs-mke2fs") |
| 157 | + >>> assert result == expected, result |
| 158 | + """ |
| 159 | + return [split_name_constraint(nc) for nc in split_deps(dependencies)] |
| 160 | + |
| 161 | + |
| 162 | +def split_name_constraint(dependency): |
| 163 | + """ |
| 164 | + Return a tuple (name, version constraint) strings given a name (version |
| 165 | + constraint) BitBake dependency string. |
| 166 | + See https://docs.yoctoproject.org/ref-manual/variables.html?#term-RDEPENDS |
| 167 | + For example: |
| 168 | + >>> assert split_name_constraint(" abcd ( = 12312 ) ") == ("abcd", "=12312",) |
| 169 | + >>> assert split_name_constraint("abcd ") == ("abcd", None) |
| 170 | + """ |
| 171 | + no_spaces = dependency.replace(' ', '') |
| 172 | + if '(' in no_spaces: |
| 173 | + name, _, constraint = no_spaces.partition('(') |
| 174 | + constraint = constraint.rstrip(')') |
| 175 | + return name, constraint |
| 176 | + return no_spaces, None |
| 177 | + |
| 178 | + |
| 179 | +def split_deps(dependencies): |
| 180 | + """ |
| 181 | + Return a list of name (version constraint) strings given a BitBake |
| 182 | + dependencies string. |
| 183 | +
|
| 184 | + See https://docs.yoctoproject.org/ref-manual/variables.html?#term-RDEPENDS |
| 185 | + For example: |
| 186 | + >>> expected = ['ABC', 'abcd (= > 12312)', 'defg', 'foo', 'bar'] |
| 187 | + >>> result = split_deps(" ABC abcd (= > 12312) defg foo bar ") |
| 188 | + >>> assert result == expected, result |
| 189 | + """ |
| 190 | + normalized_spaces = ' '.join(dependencies.split()) |
| 191 | + name = r'\w[\w\d_-]+' |
| 192 | + version_constraint = r'\([<>= ]+[^<>= ]+\s?\)' |
| 193 | + splitter = re.compile(fr'({name}\s?(?:{version_constraint})?)').findall |
| 194 | + return [s.strip() for s in splitter(normalized_spaces)] |
0 commit comments