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