xref: /openbmc/openbmc/poky/meta/classes-global/license.bbclass (revision c9537f57ab488bf5d90132917b0184e2527970a5)
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7# Populates LICENSE_DIRECTORY as set in distro config with the license files as set by
8# LIC_FILES_CHKSUM.
9# TODO:
10# - There is a real issue revolving around license naming standards.
11
12LICENSE_DIRECTORY ??= "${DEPLOY_DIR}/licenses"
13LICSSTATEDIR = "${WORKDIR}/license-destdir/"
14
15# Create extra package with license texts and add it to RRECOMMENDS:${PN}
16LICENSE_CREATE_PACKAGE[type] = "boolean"
17LICENSE_CREATE_PACKAGE ??= "0"
18LICENSE_PACKAGE_SUFFIX ??= "-lic"
19LICENSE_FILES_DIRECTORY ??= "${datadir}/licenses/"
20
21LICENSE_DEPLOY_PATHCOMPONENT = "${SSTATE_PKGARCH}"
22LICENSE_DEPLOY_PATHCOMPONENT:class-cross = "native"
23LICENSE_DEPLOY_PATHCOMPONENT:class-native = "native"
24# Ensure the *value* of SSTATE_PKGARCH is captured as it is used in the output paths
25LICENSE_DEPLOY_PATHCOMPONENT[vardepvalue] += "${LICENSE_DEPLOY_PATHCOMPONENT}"
26
27addtask populate_lic after do_patch before do_build
28do_populate_lic[dirs] = "${LICSSTATEDIR}/${LICENSE_DEPLOY_PATHCOMPONENT}/${PN}"
29do_populate_lic[cleandirs] = "${LICSSTATEDIR}"
30
31python do_populate_lic() {
32    """
33    Populate LICENSE_DIRECTORY with licenses.
34    """
35    lic_files_paths = find_license_files(d)
36
37    # The base directory we wrangle licenses to
38    destdir = os.path.join(d.getVar('LICSSTATEDIR'), d.getVar('LICENSE_DEPLOY_PATHCOMPONENT'), d.getVar('PN'))
39    copy_license_files(lic_files_paths, destdir)
40    info = get_recipe_info(d)
41    with open(os.path.join(destdir, "recipeinfo"), "w") as f:
42        for key in sorted(info.keys()):
43            f.write("%s: %s\n" % (key, info[key]))
44    oe.qa.exit_if_errors(d)
45}
46
47# it would be better to copy them in do_install:append, but find_license_files is python
48python perform_packagecopy:prepend () {
49    enabled = oe.data.typed_value('LICENSE_CREATE_PACKAGE', d)
50    if d.getVar('CLASSOVERRIDE') == 'class-target' and enabled:
51        lic_files_paths = find_license_files(d)
52
53        # LICENSE_FILES_DIRECTORY starts with '/' so os.path.join cannot be used to join D and LICENSE_FILES_DIRECTORY
54        destdir = d.getVar('D') + os.path.join(d.getVar('LICENSE_FILES_DIRECTORY'), d.getVar('PN'))
55        copy_license_files(lic_files_paths, destdir)
56        add_package_and_files(d)
57}
58perform_packagecopy[vardeps] += "LICENSE_CREATE_PACKAGE"
59
60def get_recipe_info(d):
61    info = {}
62    info["PV"] = d.getVar("PV")
63    info["PR"] = d.getVar("PR")
64    info["LICENSE"] = d.getVar("LICENSE")
65    return info
66
67def add_package_and_files(d):
68    packages = d.getVar('PACKAGES')
69    files = d.getVar('LICENSE_FILES_DIRECTORY')
70    pn = d.getVar('PN')
71    pn_lic = "%s%s" % (pn, d.getVar('LICENSE_PACKAGE_SUFFIX', False))
72    if pn_lic in packages.split():
73        bb.warn("%s package already existed in %s." % (pn_lic, pn))
74    else:
75        # first in PACKAGES to be sure that nothing else gets LICENSE_FILES_DIRECTORY
76        d.setVar('PACKAGES', "%s %s" % (pn_lic, packages))
77        d.setVar('FILES:' + pn_lic, files)
78
79def copy_license_files(lic_files_paths, destdir):
80    import shutil
81    import errno
82
83    bb.utils.mkdirhier(destdir)
84    for (basename, path, beginline, endline) in lic_files_paths:
85        try:
86            src = path
87            dst = os.path.join(destdir, basename)
88            if os.path.exists(dst):
89                os.remove(dst)
90            if os.path.islink(src):
91                src = os.path.realpath(src)
92            canlink = os.access(src, os.W_OK) and (os.stat(src).st_dev == os.stat(destdir).st_dev) and beginline is None and endline is None
93            if canlink:
94                try:
95                    os.link(src, dst)
96                except OSError as err:
97                    if err.errno == errno.EXDEV:
98                        # Copy license files if hardlink is not possible even if st_dev is the
99                        # same on source and destination (docker container with device-mapper?)
100                        canlink = False
101                    else:
102                        raise
103                # Only chown if we did hardlink and we're running under pseudo
104                if canlink and os.environ.get('PSEUDO_DISABLED') == '0':
105                    os.chown(dst,0,0)
106            if not canlink:
107                begin_idx = max(0, int(beginline) - 1) if beginline is not None else None
108                end_idx = max(0, int(endline)) if endline is not None else None
109                if begin_idx is None and end_idx is None:
110                    shutil.copyfile(src, dst)
111                else:
112                    with open(src, 'rb') as src_f:
113                        with open(dst, 'wb') as dst_f:
114                            dst_f.write(b''.join(src_f.readlines()[begin_idx:end_idx]))
115
116        except Exception as e:
117            bb.warn("Could not copy license file %s to %s: %s" % (src, dst, e))
118
119def find_license_files(d):
120    """
121    Creates list of files used in LIC_FILES_CHKSUM and generic LICENSE files.
122    """
123    import shutil
124    import oe.license
125    from collections import defaultdict, OrderedDict
126
127    # All the license files for the package
128    lic_files = d.getVar('LIC_FILES_CHKSUM') or ""
129    pn = d.getVar('PN')
130    # The license files are located in S/LIC_FILE_CHECKSUM.
131    srcdir = d.getVar('S')
132    # Directory we store the generic licenses as set in the distro configuration
133    generic_directory = d.getVar('COMMON_LICENSE_DIR')
134    # List of basename, path tuples
135    lic_files_paths = []
136    # hash for keep track generic lics mappings
137    non_generic_lics = {}
138    # Entries from LIC_FILES_CHKSUM
139    lic_chksums = {}
140    license_source_dirs = []
141    license_source_dirs.append(generic_directory)
142    try:
143        additional_lic_dirs = d.getVar('LICENSE_PATH').split()
144        for lic_dir in additional_lic_dirs:
145            license_source_dirs.append(lic_dir)
146    except:
147        pass
148
149    class FindVisitor(oe.license.LicenseVisitor):
150        def visit_Str(self, node):
151            #
152            # Until I figure out what to do with
153            # the two modifiers I support (or greater = +
154            # and "with exceptions" being *
155            # we'll just strip out the modifier and put
156            # the base license.
157            find_licenses(node.s.replace("+", "").replace("*", ""))
158            self.generic_visit(node)
159
160        def visit_Constant(self, node):
161            find_licenses(node.value.replace("+", "").replace("*", ""))
162            self.generic_visit(node)
163
164    def find_licenses(license_type):
165        try:
166            bb.utils.mkdirhier(gen_lic_dest)
167        except:
168            pass
169        spdx_generic = None
170        license_source = None
171        # If the generic does not exist we need to check to see if there is an SPDX mapping to it,
172        # unless NO_GENERIC_LICENSE is set.
173        for lic_dir in license_source_dirs:
174            if not os.path.isfile(os.path.join(lic_dir, license_type)):
175                if d.getVarFlag('SPDXLICENSEMAP', license_type) != None:
176                    # Great, there is an SPDXLICENSEMAP. We can copy!
177                    bb.debug(1, "We need to use a SPDXLICENSEMAP for %s" % (license_type))
178                    spdx_generic = d.getVarFlag('SPDXLICENSEMAP', license_type)
179                    license_source = lic_dir
180                    break
181            elif os.path.isfile(os.path.join(lic_dir, license_type)):
182                spdx_generic = license_type
183                license_source = lic_dir
184                break
185
186        non_generic_lic = d.getVarFlag('NO_GENERIC_LICENSE', license_type)
187        if spdx_generic and license_source:
188            # we really should copy to generic_ + spdx_generic, however, that ends up messing the manifest
189            # audit up. This should be fixed in emit_pkgdata (or, we actually got and fix all the recipes)
190
191            lic_files_paths.append(("generic_" + license_type, os.path.join(license_source, spdx_generic),
192                                    None, None))
193
194            # The user may attempt to use NO_GENERIC_LICENSE for a generic license which doesn't make sense
195            # and should not be allowed, warn the user in this case.
196            if d.getVarFlag('NO_GENERIC_LICENSE', license_type):
197                oe.qa.handle_error("license-no-generic",
198                    "%s: %s is a generic license, please don't use NO_GENERIC_LICENSE for it." % (pn, license_type), d)
199
200        elif non_generic_lic and non_generic_lic in lic_chksums:
201            # if NO_GENERIC_LICENSE is set, we copy the license files from the fetched source
202            # of the package rather than the license_source_dirs.
203            lic_files_paths.append(("generic_" + license_type,
204                                    os.path.join(srcdir, non_generic_lic), None, None))
205            non_generic_lics[non_generic_lic] = license_type
206        else:
207            # Explicitly avoid the CLOSED license because this isn't generic
208            if license_type != 'CLOSED':
209                # And here is where we warn people that their licenses are lousy
210                oe.qa.handle_error("license-exists",
211                    "%s: No generic license file exists for: %s in any provider" % (pn, license_type), d)
212            pass
213
214    if not generic_directory:
215        bb.fatal("COMMON_LICENSE_DIR is unset. Please set this in your distro config")
216
217    for url in lic_files.split():
218        try:
219            (method, host, path, user, pswd, parm) = bb.fetch.decodeurl(url)
220            if method != "file" or not path:
221                raise bb.fetch.MalformedUrl()
222        except bb.fetch.MalformedUrl:
223            bb.fatal("%s: LIC_FILES_CHKSUM contains an invalid URL:  %s" % (d.getVar('PF'), url))
224        # We want the license filename and path
225        chksum = parm.get('md5', None)
226        beginline = parm.get('beginline')
227        endline = parm.get('endline')
228        lic_chksums[path] = (chksum, beginline, endline)
229
230    v = FindVisitor()
231    try:
232        v.visit_string(d.getVar('LICENSE'))
233    except oe.license.InvalidLicense as exc:
234        bb.fatal('%s: %s' % (d.getVar('PF'), exc))
235    except SyntaxError:
236        oe.qa.handle_error("license-syntax",
237            "%s: Failed to parse LICENSE: %s" % (d.getVar('PF'), d.getVar('LICENSE')), d)
238    # Add files from LIC_FILES_CHKSUM to list of license files
239    lic_chksum_paths = defaultdict(OrderedDict)
240    for path, data in sorted(lic_chksums.items()):
241        lic_chksum_paths[os.path.basename(path)][data] = (os.path.join(srcdir, path), data[1], data[2])
242    for basename, files in lic_chksum_paths.items():
243        if len(files) == 1:
244            # Don't copy again a LICENSE already handled as non-generic
245            if basename in non_generic_lics:
246                continue
247            data = list(files.values())[0]
248            lic_files_paths.append(tuple([basename] + list(data)))
249        else:
250            # If there are multiple different license files with identical
251            # basenames we rename them to <file>.0, <file>.1, ...
252            for i, data in enumerate(files.values()):
253                lic_files_paths.append(tuple(["%s.%d" % (basename, i)] + list(data)))
254
255    return lic_files_paths
256
257SSTATETASKS += "do_populate_lic"
258do_populate_lic[sstate-inputdirs] = "${LICSSTATEDIR}"
259do_populate_lic[sstate-outputdirs] = "${LICENSE_DIRECTORY}/"
260
261IMAGE_CLASSES:append = " license_image"
262
263python do_populate_lic_setscene () {
264    sstate_setscene(d)
265}
266addtask do_populate_lic_setscene
267