xref: /openbmc/openbmc/poky/meta/lib/oe/license.py (revision 90fd73cb)
1#
2# SPDX-License-Identifier: GPL-2.0-only
3#
4"""Code for parsing OpenEmbedded license strings"""
5
6import ast
7import re
8from fnmatch import fnmatchcase as fnmatch
9
10def license_ok(license, dont_want_licenses):
11    """ Return False if License exist in dont_want_licenses else True """
12    for dwl in dont_want_licenses:
13        if fnmatch(license, dwl):
14            return False
15    return True
16
17class LicenseError(Exception):
18    pass
19
20class LicenseSyntaxError(LicenseError):
21    def __init__(self, licensestr, exc):
22        self.licensestr = licensestr
23        self.exc = exc
24        LicenseError.__init__(self)
25
26    def __str__(self):
27        return "error in '%s': %s" % (self.licensestr, self.exc)
28
29class InvalidLicense(LicenseError):
30    def __init__(self, license):
31        self.license = license
32        LicenseError.__init__(self)
33
34    def __str__(self):
35        return "invalid characters in license '%s'" % self.license
36
37license_operator_chars = '&|() '
38license_operator = re.compile(r'([' + license_operator_chars + '])')
39license_pattern = re.compile(r'[a-zA-Z0-9.+_\-]+$')
40
41class LicenseVisitor(ast.NodeVisitor):
42    """Get elements based on OpenEmbedded license strings"""
43    def get_elements(self, licensestr):
44        new_elements = []
45        elements = list([x for x in license_operator.split(licensestr) if x.strip()])
46        for pos, element in enumerate(elements):
47            if license_pattern.match(element):
48                if pos > 0 and license_pattern.match(elements[pos-1]):
49                    new_elements.append('&')
50                element = '"' + element + '"'
51            elif not license_operator.match(element):
52                raise InvalidLicense(element)
53            new_elements.append(element)
54
55        return new_elements
56
57    """Syntax tree visitor which can accept elements previously generated with
58    OpenEmbedded license string"""
59    def visit_elements(self, elements):
60        self.visit(ast.parse(' '.join(elements)))
61
62    """Syntax tree visitor which can accept OpenEmbedded license strings"""
63    def visit_string(self, licensestr):
64        self.visit_elements(self.get_elements(licensestr))
65
66class FlattenVisitor(LicenseVisitor):
67    """Flatten a license tree (parsed from a string) by selecting one of each
68    set of OR options, in the way the user specifies"""
69    def __init__(self, choose_licenses):
70        self.choose_licenses = choose_licenses
71        self.licenses = []
72        LicenseVisitor.__init__(self)
73
74    def visit_Str(self, node):
75        self.licenses.append(node.s)
76
77    def visit_BinOp(self, node):
78        if isinstance(node.op, ast.BitOr):
79            left = FlattenVisitor(self.choose_licenses)
80            left.visit(node.left)
81
82            right = FlattenVisitor(self.choose_licenses)
83            right.visit(node.right)
84
85            selected = self.choose_licenses(left.licenses, right.licenses)
86            self.licenses.extend(selected)
87        else:
88            self.generic_visit(node)
89
90def flattened_licenses(licensestr, choose_licenses):
91    """Given a license string and choose_licenses function, return a flat list of licenses"""
92    flatten = FlattenVisitor(choose_licenses)
93    try:
94        flatten.visit_string(licensestr)
95    except SyntaxError as exc:
96        raise LicenseSyntaxError(licensestr, exc)
97    return flatten.licenses
98
99def is_included(licensestr, whitelist=None, blacklist=None):
100    """Given a license string and whitelist and blacklist, determine if the
101    license string matches the whitelist and does not match the blacklist.
102
103    Returns a tuple holding the boolean state and a list of the applicable
104    licenses that were excluded if state is False, or the licenses that were
105    included if the state is True.
106    """
107
108    def include_license(license):
109        return any(fnmatch(license, pattern) for pattern in whitelist)
110
111    def exclude_license(license):
112        return any(fnmatch(license, pattern) for pattern in blacklist)
113
114    def choose_licenses(alpha, beta):
115        """Select the option in an OR which is the 'best' (has the most
116        included licenses and no excluded licenses)."""
117        # The factor 1000 below is arbitrary, just expected to be much larger
118        # that the number of licenses actually specified. That way the weight
119        # will be negative if the list of licenses contains an excluded license,
120        # but still gives a higher weight to the list with the most included
121        # licenses.
122        alpha_weight = (len(list(filter(include_license, alpha))) -
123                        1000 * (len(list(filter(exclude_license, alpha))) > 0))
124        beta_weight = (len(list(filter(include_license, beta))) -
125                       1000 * (len(list(filter(exclude_license, beta))) > 0))
126        if alpha_weight >= beta_weight:
127            return alpha
128        else:
129            return beta
130
131    if not whitelist:
132        whitelist = ['*']
133
134    if not blacklist:
135        blacklist = []
136
137    licenses = flattened_licenses(licensestr, choose_licenses)
138    excluded = [lic for lic in licenses if exclude_license(lic)]
139    included = [lic for lic in licenses if include_license(lic)]
140    if excluded:
141        return False, excluded
142    else:
143        return True, included
144
145class ManifestVisitor(LicenseVisitor):
146    """Walk license tree (parsed from a string) removing the incompatible
147    licenses specified"""
148    def __init__(self, dont_want_licenses, canonical_license, d):
149        self._dont_want_licenses = dont_want_licenses
150        self._canonical_license = canonical_license
151        self._d = d
152        self._operators = []
153
154        self.licenses = []
155        self.licensestr = ''
156
157        LicenseVisitor.__init__(self)
158
159    def visit(self, node):
160        if isinstance(node, ast.Str):
161            lic = node.s
162
163            if license_ok(self._canonical_license(self._d, lic),
164                    self._dont_want_licenses) == True:
165                if self._operators:
166                    ops = []
167                    for op in self._operators:
168                        if op == '[':
169                            ops.append(op)
170                        elif op == ']':
171                            ops.append(op)
172                        else:
173                            if not ops:
174                                ops.append(op)
175                            elif ops[-1] in ['[', ']']:
176                                ops.append(op)
177                            else:
178                                ops[-1] = op
179
180                    for op in ops:
181                        if op == '[' or op == ']':
182                            self.licensestr += op
183                        elif self.licenses:
184                            self.licensestr += ' ' + op + ' '
185
186                    self._operators = []
187
188                self.licensestr += lic
189                self.licenses.append(lic)
190        elif isinstance(node, ast.BitAnd):
191            self._operators.append("&")
192        elif isinstance(node, ast.BitOr):
193            self._operators.append("|")
194        elif isinstance(node, ast.List):
195            self._operators.append("[")
196        elif isinstance(node, ast.Load):
197            self.licensestr += "]"
198
199        self.generic_visit(node)
200
201def manifest_licenses(licensestr, dont_want_licenses, canonical_license, d):
202    """Given a license string and dont_want_licenses list,
203       return license string filtered and a list of licenses"""
204    manifest = ManifestVisitor(dont_want_licenses, canonical_license, d)
205
206    try:
207        elements = manifest.get_elements(licensestr)
208
209        # Replace '()' to '[]' for handle in ast as List and Load types.
210        elements = ['[' if e == '(' else e for e in elements]
211        elements = [']' if e == ')' else e for e in elements]
212
213        manifest.visit_elements(elements)
214    except SyntaxError as exc:
215        raise LicenseSyntaxError(licensestr, exc)
216
217    # Replace '[]' to '()' for output correct license.
218    manifest.licensestr = manifest.licensestr.replace('[', '(').replace(']', ')')
219
220    return (manifest.licensestr, manifest.licenses)
221
222class ListVisitor(LicenseVisitor):
223    """Record all different licenses found in the license string"""
224    def __init__(self):
225        self.licenses = set()
226
227    def visit_Str(self, node):
228        self.licenses.add(node.s)
229
230def list_licenses(licensestr):
231    """Simply get a list of all licenses mentioned in a license string.
232       Binary operators are not applied or taken into account in any way"""
233    visitor = ListVisitor()
234    try:
235        visitor.visit_string(licensestr)
236    except SyntaxError as exc:
237        raise LicenseSyntaxError(licensestr, exc)
238    return visitor.licenses
239