1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7ROOTFS_LICENSE_DIR = "${IMAGE_ROOTFS}/usr/share/common-licenses"
8
9# This requires LICENSE_CREATE_PACKAGE=1 to work too
10COMPLEMENTARY_GLOB[lic-pkgs] = "*-lic"
11
12python() {
13    if not oe.data.typed_value('LICENSE_CREATE_PACKAGE', d):
14        features = set(oe.data.typed_value('IMAGE_FEATURES', d))
15        if 'lic-pkgs' in features:
16            bb.error("'lic-pkgs' in IMAGE_FEATURES but LICENSE_CREATE_PACKAGE not enabled to generate -lic packages")
17}
18
19python write_package_manifest() {
20    # Get list of installed packages
21    license_image_dir = d.expand('${LICENSE_DIRECTORY}/${SSTATE_PKGARCH}/${IMAGE_NAME}')
22    bb.utils.mkdirhier(license_image_dir)
23    from oe.rootfs import image_list_installed_packages
24    from oe.utils import format_pkg_list
25
26    pkgs = image_list_installed_packages(d)
27    output = format_pkg_list(pkgs)
28    with open(os.path.join(license_image_dir, 'package.manifest'), "w+") as package_manifest:
29        package_manifest.write(output)
30}
31
32python license_create_manifest() {
33    import oe.packagedata
34    from oe.rootfs import image_list_installed_packages
35
36    build_images_from_feeds = d.getVar('BUILD_IMAGES_FROM_FEEDS')
37    if build_images_from_feeds == "1":
38        return 0
39
40    pkg_dic = {}
41    for pkg in sorted(image_list_installed_packages(d)):
42        pkg_info = os.path.join(d.getVar('PKGDATA_DIR'),
43                                'runtime-reverse', pkg)
44        pkg_name = os.path.basename(os.readlink(pkg_info))
45
46        pkg_dic[pkg_name] = oe.packagedata.read_pkgdatafile(pkg_info)
47        if not "LICENSE" in pkg_dic[pkg_name].keys():
48            pkg_lic_name = "LICENSE:" + pkg_name
49            pkg_dic[pkg_name]["LICENSE"] = pkg_dic[pkg_name][pkg_lic_name]
50
51    rootfs_license_manifest = os.path.join(d.getVar('LICENSE_DIRECTORY'),
52                        d.getVar('SSTATE_PKGARCH'), d.getVar('IMAGE_NAME'), 'license.manifest')
53    write_license_files(d, rootfs_license_manifest, pkg_dic, rootfs=True)
54}
55
56def write_license_files(d, license_manifest, pkg_dic, rootfs=True):
57    import re
58    import stat
59
60    bad_licenses = (d.getVar("INCOMPATIBLE_LICENSE") or "").split()
61    bad_licenses = expand_wildcard_licenses(d, bad_licenses)
62    pkgarchs = d.getVar("SSTATE_ARCHS").split()
63    pkgarchs.reverse()
64
65    exceptions = (d.getVar("INCOMPATIBLE_LICENSE_EXCEPTIONS") or "").split()
66    with open(license_manifest, "w") as license_file:
67        for pkg in sorted(pkg_dic):
68            remaining_bad_licenses = oe.license.apply_pkg_license_exception(pkg, bad_licenses, exceptions)
69            incompatible_licenses = incompatible_pkg_license(d, remaining_bad_licenses, pkg_dic[pkg]["LICENSE"])
70            if incompatible_licenses:
71                bb.fatal("Package %s cannot be installed into the image because it has incompatible license(s): %s" %(pkg, ' '.join(incompatible_licenses)))
72            else:
73                incompatible_licenses = incompatible_pkg_license(d, bad_licenses, pkg_dic[pkg]["LICENSE"])
74                if incompatible_licenses:
75                    oe.qa.handle_error('license-incompatible', "Including %s with incompatible license(s) %s into the image, because it has been allowed by exception list." %(pkg, ' '.join(incompatible_licenses)), d)
76            try:
77                (pkg_dic[pkg]["LICENSE"], pkg_dic[pkg]["LICENSES"]) = \
78                    oe.license.manifest_licenses(pkg_dic[pkg]["LICENSE"],
79                    remaining_bad_licenses, canonical_license, d)
80            except oe.license.LicenseError as exc:
81                bb.fatal('%s: %s' % (d.getVar('P'), exc))
82
83            if not "IMAGE_MANIFEST" in pkg_dic[pkg]:
84                # Rootfs manifest
85                license_file.write("PACKAGE NAME: %s\n" % pkg)
86                license_file.write("PACKAGE VERSION: %s\n" % pkg_dic[pkg]["PV"])
87                license_file.write("RECIPE NAME: %s\n" % pkg_dic[pkg]["PN"])
88                license_file.write("LICENSE: %s\n\n" % pkg_dic[pkg]["LICENSE"])
89
90                # If the package doesn't contain any file, that is, its size is 0, the license
91                # isn't relevant as far as the final image is concerned. So doing license check
92                # doesn't make much sense, skip it.
93                if pkg_dic[pkg]["PKGSIZE:%s" % pkg] == "0":
94                    continue
95            else:
96                # Image manifest
97                license_file.write("RECIPE NAME: %s\n" % pkg_dic[pkg]["PN"])
98                license_file.write("VERSION: %s\n" % pkg_dic[pkg]["PV"])
99                license_file.write("LICENSE: %s\n" % pkg_dic[pkg]["LICENSE"])
100                license_file.write("FILES: %s\n\n" % pkg_dic[pkg]["FILES"])
101
102            for lic in pkg_dic[pkg]["LICENSES"]:
103                for pkgarch in pkgarchs:
104                    lic_file = os.path.join(d.getVar('LICENSE_DIRECTORY'),
105                                            pkgarch,
106                                            pkg_dic[pkg]["PN"], "generic_%s" %
107                                            re.sub(r'\+', '', lic))
108                    if os.path.exists(lic_file):
109                        break
110                # add explicity avoid of CLOSED license because isn't generic
111                if lic == "CLOSED":
112                   continue
113
114                if not os.path.exists(lic_file):
115                    oe.qa.handle_error('license-file-missing',
116                                       "The license listed %s was not in the "\
117                                       "licenses collected for recipe %s"
118                                       % (lic, pkg_dic[pkg]["PN"]), d)
119    oe.qa.exit_if_errors(d)
120
121    # Two options here:
122    # - Just copy the manifest
123    # - Copy the manifest and the license directories
124    # With both options set we see a .5 M increase in core-image-minimal
125    copy_lic_manifest = d.getVar('COPY_LIC_MANIFEST')
126    copy_lic_dirs = d.getVar('COPY_LIC_DIRS')
127    if rootfs and copy_lic_manifest == "1":
128        rootfs_license_dir = d.getVar('ROOTFS_LICENSE_DIR')
129        bb.utils.mkdirhier(rootfs_license_dir)
130        rootfs_license_manifest = os.path.join(rootfs_license_dir,
131                os.path.split(license_manifest)[1])
132        if not os.path.exists(rootfs_license_manifest):
133            oe.path.copyhardlink(license_manifest, rootfs_license_manifest)
134
135        if copy_lic_dirs == "1":
136            for pkg in sorted(pkg_dic):
137                pkg_rootfs_license_dir = os.path.join(rootfs_license_dir, pkg)
138                bb.utils.mkdirhier(pkg_rootfs_license_dir)
139                for pkgarch in pkgarchs:
140                    pkg_license_dir = os.path.join(d.getVar('LICENSE_DIRECTORY'),
141                                                   pkgarch, pkg_dic[pkg]["PN"])
142                    if os.path.exists(pkg_license_dir):
143                        break
144                if not os.path.exists(pkg_license_dir ):
145                    bb.fatal("Couldn't find license information for dependency %s" % pkg)
146
147                pkg_manifest_licenses = [canonical_license(d, lic) \
148                        for lic in pkg_dic[pkg]["LICENSES"]]
149
150                licenses = os.listdir(pkg_license_dir)
151                for lic in licenses:
152                    pkg_license = os.path.join(pkg_license_dir, lic)
153                    pkg_rootfs_license = os.path.join(pkg_rootfs_license_dir, lic)
154
155                    if re.match(r"^generic_.*$", lic):
156                        generic_lic = canonical_license(d,
157                                re.search(r"^generic_(.*)$", lic).group(1))
158
159                        # Do not copy generic license into package if isn't
160                        # declared into LICENSES of the package.
161                        if not re.sub(r'\+$', '', generic_lic) in \
162                                [re.sub(r'\+', '', lic) for lic in \
163                                 pkg_manifest_licenses]:
164                            continue
165
166                        if oe.license.license_ok(generic_lic,
167                                bad_licenses) == False:
168                            continue
169
170                        # Make sure we use only canonical name for the license file
171                        generic_lic_file = "generic_%s" % generic_lic
172                        rootfs_license = os.path.join(rootfs_license_dir, generic_lic_file)
173                        if not os.path.exists(rootfs_license):
174                            oe.path.copyhardlink(pkg_license, rootfs_license)
175
176                        if not os.path.exists(pkg_rootfs_license):
177                            os.symlink(os.path.join('..', generic_lic_file), pkg_rootfs_license)
178                    else:
179                        if (oe.license.license_ok(canonical_license(d,
180                                lic), bad_licenses) == False or
181                                os.path.exists(pkg_rootfs_license)):
182                            continue
183
184                        oe.path.copyhardlink(pkg_license, pkg_rootfs_license)
185            # Fixup file ownership and permissions
186            for walkroot, dirs, files in os.walk(rootfs_license_dir):
187                for f in files:
188                    p = os.path.join(walkroot, f)
189                    os.lchown(p, 0, 0)
190                    if not os.path.islink(p):
191                        os.chmod(p, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
192                for dir in dirs:
193                    p = os.path.join(walkroot, dir)
194                    os.lchown(p, 0, 0)
195                    os.chmod(p, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
196
197write_license_files[vardepsexclude] = "SSTATE_ARCHS"
198
199def license_deployed_manifest(d):
200    """
201    Write the license manifest for the deployed recipes.
202    The deployed recipes usually includes the bootloader
203    and extra files to boot the target.
204    """
205
206    dep_dic = {}
207    man_dic = {}
208    lic_dir = d.getVar("LICENSE_DIRECTORY")
209    pkgarchs = d.getVar("SSTATE_ARCHS").split()
210    pkgarchs.reverse()
211
212    dep_dic = get_deployed_dependencies(d)
213    for dep in dep_dic.keys():
214        man_dic[dep] = {}
215        # It is necessary to mark this will be used for image manifest
216        man_dic[dep]["IMAGE_MANIFEST"] = True
217        man_dic[dep]["PN"] = dep
218        man_dic[dep]["FILES"] = \
219            " ".join(get_deployed_files(dep_dic[dep]))
220
221        for pkgarch in pkgarchs:
222            licfile = os.path.join(lic_dir, pkgarch, dep, "recipeinfo")
223            if os.path.exists(licfile):
224                break
225        if not os.path.exists(licfile):
226            bb.fatal("Couldn't find license information for dependency %s" % dep)
227        with open(licfile, "r") as f:
228            for line in f.readlines():
229                key,val = line.split(": ", 1)
230                man_dic[dep][key] = val[:-1]
231
232    lic_manifest_dir = os.path.join(d.getVar('LICENSE_DIRECTORY'), d.getVar('SSTATE_PKGARCH'),
233                                    d.getVar('IMAGE_NAME'))
234    bb.utils.mkdirhier(lic_manifest_dir)
235    image_license_manifest = os.path.join(lic_manifest_dir, 'image_license.manifest')
236    write_license_files(d, image_license_manifest, man_dic, rootfs=False)
237
238    link_name = d.getVar('IMAGE_LINK_NAME')
239    if link_name:
240        lic_manifest_symlink_dir = os.path.join(d.getVar('LICENSE_DIRECTORY'), d.getVar('SSTATE_PKGARCH'),
241                                    link_name)
242        # remove old symlink
243        if os.path.islink(lic_manifest_symlink_dir):
244            os.unlink(lic_manifest_symlink_dir)
245
246        # create the image dir symlink
247        if lic_manifest_dir != lic_manifest_symlink_dir:
248            os.symlink(lic_manifest_dir, lic_manifest_symlink_dir)
249
250license_deployed_manifest[vardepsexclude] = "SSTATE_ARCHS"
251
252def get_deployed_dependencies(d):
253    """
254    Get all the deployed dependencies of an image
255    """
256
257    deploy = {}
258    # Get all the dependencies for the current task (rootfs).
259    taskdata = d.getVar("BB_TASKDEPDATA", False)
260    pn = d.getVar("PN")
261    depends = list(set([dep[0] for dep
262                    in list(taskdata.values())
263                    if not dep[0].endswith("-native") and not dep[0] == pn]))
264
265    # To verify what was deployed it checks the rootfs dependencies against
266    # the SSTATE_MANIFESTS for "deploy" task.
267    # The manifest file name contains the arch. Because we are not running
268    # in the recipe context it is necessary to check every arch used.
269    sstate_manifest_dir = d.getVar("SSTATE_MANIFESTS")
270    archs = list(set(d.getVar("SSTATE_ARCHS").split()))
271    for dep in depends:
272        for arch in archs:
273            sstate_manifest_file = os.path.join(sstate_manifest_dir,
274                    "manifest-%s-%s.deploy" % (arch, dep))
275            if os.path.exists(sstate_manifest_file):
276                deploy[dep] = sstate_manifest_file
277                break
278
279    return deploy
280get_deployed_dependencies[vardepsexclude] = "BB_TASKDEPDATA SSTATE_ARCHS"
281
282def get_deployed_files(man_file):
283    """
284    Get the files deployed from the sstate manifest
285    """
286
287    dep_files = []
288    excluded_files = []
289    with open(man_file, "r") as manifest:
290        all_files = manifest.read()
291    for f in all_files.splitlines():
292        if ((not (os.path.islink(f) or os.path.isdir(f))) and
293                not os.path.basename(f) in excluded_files):
294            dep_files.append(os.path.basename(f))
295    return dep_files
296
297ROOTFS_POSTPROCESS_COMMAND:prepend = "write_package_manifest license_create_manifest "
298do_rootfs[recrdeptask] += "do_populate_lic"
299
300python do_populate_lic_deploy() {
301    license_deployed_manifest(d)
302    oe.qa.exit_if_errors(d)
303}
304
305addtask populate_lic_deploy before do_build after do_image_complete
306do_populate_lic_deploy[recrdeptask] += "do_populate_lic do_deploy"
307
308python license_qa_dead_symlink() {
309    import os
310
311    for root, dirs, files in os.walk(d.getVar('ROOTFS_LICENSE_DIR')):
312        for file in files:
313            full_path = root + "/" + file
314            if os.path.islink(full_path) and not os.path.exists(full_path):
315                bb.error("broken symlink: " + full_path)
316}
317IMAGE_QA_COMMANDS += "license_qa_dead_symlink"
318