blob: 23625f0104ff0fbba6f758ee8a9f8d458444b843 [file] [log] [blame]
Patrick Williams92b42cb2022-09-03 06:53:57 -05001#
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
21addtask populate_lic after do_patch before do_build
22do_populate_lic[dirs] = "${LICSSTATEDIR}/${PN}"
23do_populate_lic[cleandirs] = "${LICSSTATEDIR}"
24
25python do_populate_lic() {
26 """
27 Populate LICENSE_DIRECTORY with licenses.
28 """
29 lic_files_paths = find_license_files(d)
30
31 # The base directory we wrangle licenses to
32 destdir = os.path.join(d.getVar('LICSSTATEDIR'), d.getVar('PN'))
33 copy_license_files(lic_files_paths, destdir)
34 info = get_recipe_info(d)
35 with open(os.path.join(destdir, "recipeinfo"), "w") as f:
36 for key in sorted(info.keys()):
37 f.write("%s: %s\n" % (key, info[key]))
38 oe.qa.exit_if_errors(d)
39}
40
41PSEUDO_IGNORE_PATHS .= ",${@','.join(((d.getVar('COMMON_LICENSE_DIR') or '') + ' ' + (d.getVar('LICENSE_PATH') or '') + ' ' + d.getVar('COREBASE') + '/meta/COPYING').split())}"
42# it would be better to copy them in do_install:append, but find_license_filesa is python
43python perform_packagecopy:prepend () {
44 enabled = oe.data.typed_value('LICENSE_CREATE_PACKAGE', d)
45 if d.getVar('CLASSOVERRIDE') == 'class-target' and enabled:
46 lic_files_paths = find_license_files(d)
47
48 # LICENSE_FILES_DIRECTORY starts with '/' so os.path.join cannot be used to join D and LICENSE_FILES_DIRECTORY
49 destdir = d.getVar('D') + os.path.join(d.getVar('LICENSE_FILES_DIRECTORY'), d.getVar('PN'))
50 copy_license_files(lic_files_paths, destdir)
51 add_package_and_files(d)
52}
53perform_packagecopy[vardeps] += "LICENSE_CREATE_PACKAGE"
54
55def get_recipe_info(d):
56 info = {}
57 info["PV"] = d.getVar("PV")
58 info["PR"] = d.getVar("PR")
59 info["LICENSE"] = d.getVar("LICENSE")
60 return info
61
62def add_package_and_files(d):
63 packages = d.getVar('PACKAGES')
64 files = d.getVar('LICENSE_FILES_DIRECTORY')
65 pn = d.getVar('PN')
66 pn_lic = "%s%s" % (pn, d.getVar('LICENSE_PACKAGE_SUFFIX', False))
67 if pn_lic in packages.split():
68 bb.warn("%s package already existed in %s." % (pn_lic, pn))
69 else:
70 # first in PACKAGES to be sure that nothing else gets LICENSE_FILES_DIRECTORY
71 d.setVar('PACKAGES', "%s %s" % (pn_lic, packages))
72 d.setVar('FILES:' + pn_lic, files)
73
74def copy_license_files(lic_files_paths, destdir):
75 import shutil
76 import errno
77
78 bb.utils.mkdirhier(destdir)
79 for (basename, path, beginline, endline) in lic_files_paths:
80 try:
81 src = path
82 dst = os.path.join(destdir, basename)
83 if os.path.exists(dst):
84 os.remove(dst)
85 if os.path.islink(src):
86 src = os.path.realpath(src)
87 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
88 if canlink:
89 try:
90 os.link(src, dst)
91 except OSError as err:
92 if err.errno == errno.EXDEV:
93 # Copy license files if hardlink is not possible even if st_dev is the
94 # same on source and destination (docker container with device-mapper?)
95 canlink = False
96 else:
97 raise
98 # Only chown if we did hardlink and we're running under pseudo
99 if canlink and os.environ.get('PSEUDO_DISABLED') == '0':
100 os.chown(dst,0,0)
101 if not canlink:
102 begin_idx = max(0, int(beginline) - 1) if beginline is not None else None
103 end_idx = max(0, int(endline)) if endline is not None else None
104 if begin_idx is None and end_idx is None:
105 shutil.copyfile(src, dst)
106 else:
107 with open(src, 'rb') as src_f:
108 with open(dst, 'wb') as dst_f:
109 dst_f.write(b''.join(src_f.readlines()[begin_idx:end_idx]))
110
111 except Exception as e:
112 bb.warn("Could not copy license file %s to %s: %s" % (src, dst, e))
113
114def find_license_files(d):
115 """
116 Creates list of files used in LIC_FILES_CHKSUM and generic LICENSE files.
117 """
118 import shutil
119 import oe.license
120 from collections import defaultdict, OrderedDict
121
122 # All the license files for the package
123 lic_files = d.getVar('LIC_FILES_CHKSUM') or ""
124 pn = d.getVar('PN')
125 # The license files are located in S/LIC_FILE_CHECKSUM.
126 srcdir = d.getVar('S')
127 # Directory we store the generic licenses as set in the distro configuration
128 generic_directory = d.getVar('COMMON_LICENSE_DIR')
129 # List of basename, path tuples
130 lic_files_paths = []
131 # hash for keep track generic lics mappings
132 non_generic_lics = {}
133 # Entries from LIC_FILES_CHKSUM
134 lic_chksums = {}
135 license_source_dirs = []
136 license_source_dirs.append(generic_directory)
137 try:
138 additional_lic_dirs = d.getVar('LICENSE_PATH').split()
139 for lic_dir in additional_lic_dirs:
140 license_source_dirs.append(lic_dir)
141 except:
142 pass
143
144 class FindVisitor(oe.license.LicenseVisitor):
145 def visit_Str(self, node):
146 #
147 # Until I figure out what to do with
148 # the two modifiers I support (or greater = +
149 # and "with exceptions" being *
150 # we'll just strip out the modifier and put
151 # the base license.
152 find_license(node.s.replace("+", "").replace("*", ""))
153 self.generic_visit(node)
154
155 def visit_Constant(self, node):
156 find_license(node.value.replace("+", "").replace("*", ""))
157 self.generic_visit(node)
158
159 def find_license(license_type):
160 try:
161 bb.utils.mkdirhier(gen_lic_dest)
162 except:
163 pass
164 spdx_generic = None
165 license_source = None
166 # If the generic does not exist we need to check to see if there is an SPDX mapping to it,
167 # unless NO_GENERIC_LICENSE is set.
168 for lic_dir in license_source_dirs:
169 if not os.path.isfile(os.path.join(lic_dir, license_type)):
170 if d.getVarFlag('SPDXLICENSEMAP', license_type) != None:
171 # Great, there is an SPDXLICENSEMAP. We can copy!
172 bb.debug(1, "We need to use a SPDXLICENSEMAP for %s" % (license_type))
173 spdx_generic = d.getVarFlag('SPDXLICENSEMAP', license_type)
174 license_source = lic_dir
175 break
176 elif os.path.isfile(os.path.join(lic_dir, license_type)):
177 spdx_generic = license_type
178 license_source = lic_dir
179 break
180
181 non_generic_lic = d.getVarFlag('NO_GENERIC_LICENSE', license_type)
182 if spdx_generic and license_source:
183 # we really should copy to generic_ + spdx_generic, however, that ends up messing the manifest
184 # audit up. This should be fixed in emit_pkgdata (or, we actually got and fix all the recipes)
185
186 lic_files_paths.append(("generic_" + license_type, os.path.join(license_source, spdx_generic),
187 None, None))
188
189 # The user may attempt to use NO_GENERIC_LICENSE for a generic license which doesn't make sense
190 # and should not be allowed, warn the user in this case.
191 if d.getVarFlag('NO_GENERIC_LICENSE', license_type):
192 oe.qa.handle_error("license-no-generic",
193 "%s: %s is a generic license, please don't use NO_GENERIC_LICENSE for it." % (pn, license_type), d)
194
195 elif non_generic_lic and non_generic_lic in lic_chksums:
196 # if NO_GENERIC_LICENSE is set, we copy the license files from the fetched source
197 # of the package rather than the license_source_dirs.
198 lic_files_paths.append(("generic_" + license_type,
199 os.path.join(srcdir, non_generic_lic), None, None))
200 non_generic_lics[non_generic_lic] = license_type
201 else:
202 # Explicitly avoid the CLOSED license because this isn't generic
203 if license_type != 'CLOSED':
204 # And here is where we warn people that their licenses are lousy
205 oe.qa.handle_error("license-exists",
206 "%s: No generic license file exists for: %s in any provider" % (pn, license_type), d)
207 pass
208
209 if not generic_directory:
210 bb.fatal("COMMON_LICENSE_DIR is unset. Please set this in your distro config")
211
212 for url in lic_files.split():
213 try:
214 (method, host, path, user, pswd, parm) = bb.fetch.decodeurl(url)
215 if method != "file" or not path:
216 raise bb.fetch.MalformedUrl()
217 except bb.fetch.MalformedUrl:
218 bb.fatal("%s: LIC_FILES_CHKSUM contains an invalid URL: %s" % (d.getVar('PF'), url))
219 # We want the license filename and path
220 chksum = parm.get('md5', None)
221 beginline = parm.get('beginline')
222 endline = parm.get('endline')
223 lic_chksums[path] = (chksum, beginline, endline)
224
225 v = FindVisitor()
226 try:
227 v.visit_string(d.getVar('LICENSE'))
228 except oe.license.InvalidLicense as exc:
229 bb.fatal('%s: %s' % (d.getVar('PF'), exc))
230 except SyntaxError:
231 oe.qa.handle_error("license-syntax",
Andrew Geissler028142b2023-05-05 11:29:21 -0500232 "%s: Failed to parse LICENSE: %s" % (d.getVar('PF'), d.getVar('LICENSE')), d)
Patrick Williams92b42cb2022-09-03 06:53:57 -0500233 # Add files from LIC_FILES_CHKSUM to list of license files
234 lic_chksum_paths = defaultdict(OrderedDict)
235 for path, data in sorted(lic_chksums.items()):
236 lic_chksum_paths[os.path.basename(path)][data] = (os.path.join(srcdir, path), data[1], data[2])
237 for basename, files in lic_chksum_paths.items():
238 if len(files) == 1:
239 # Don't copy again a LICENSE already handled as non-generic
240 if basename in non_generic_lics:
241 continue
242 data = list(files.values())[0]
243 lic_files_paths.append(tuple([basename] + list(data)))
244 else:
245 # If there are multiple different license files with identical
246 # basenames we rename them to <file>.0, <file>.1, ...
247 for i, data in enumerate(files.values()):
248 lic_files_paths.append(tuple(["%s.%d" % (basename, i)] + list(data)))
249
250 return lic_files_paths
251
252def return_spdx(d, license):
253 """
254 This function returns the spdx mapping of a license if it exists.
255 """
256 return d.getVarFlag('SPDXLICENSEMAP', license)
257
258def canonical_license(d, license):
259 """
260 Return the canonical (SPDX) form of the license if available (so GPLv3
261 becomes GPL-3.0-only) or the passed license if there is no canonical form.
262 """
263 return d.getVarFlag('SPDXLICENSEMAP', license) or license
264
265def expand_wildcard_licenses(d, wildcard_licenses):
266 """
267 There are some common wildcard values users may want to use. Support them
268 here.
269 """
270 licenses = set(wildcard_licenses)
271 mapping = {
272 "AGPL-3.0*" : ["AGPL-3.0-only", "AGPL-3.0-or-later"],
273 "GPL-3.0*" : ["GPL-3.0-only", "GPL-3.0-or-later"],
274 "LGPL-3.0*" : ["LGPL-3.0-only", "LGPL-3.0-or-later"],
275 }
276 for k in mapping:
277 if k in wildcard_licenses:
278 licenses.remove(k)
279 for item in mapping[k]:
280 licenses.add(item)
281
282 for l in licenses:
283 if l in oe.license.obsolete_license_list():
284 bb.fatal("Error, %s is an obsolete license, please use an SPDX reference in INCOMPATIBLE_LICENSE" % l)
285 if "*" in l:
286 bb.fatal("Error, %s is an invalid license wildcard entry" % l)
287
288 return list(licenses)
289
290def incompatible_license_contains(license, truevalue, falsevalue, d):
291 license = canonical_license(d, license)
292 bad_licenses = (d.getVar('INCOMPATIBLE_LICENSE') or "").split()
293 bad_licenses = expand_wildcard_licenses(d, bad_licenses)
294 return truevalue if license in bad_licenses else falsevalue
295
296def incompatible_pkg_license(d, dont_want_licenses, license):
297 # Handles an "or" or two license sets provided by
298 # flattened_licenses(), pick one that works if possible.
299 def choose_lic_set(a, b):
300 return a if all(oe.license.license_ok(canonical_license(d, lic),
301 dont_want_licenses) for lic in a) else b
302
303 try:
304 licenses = oe.license.flattened_licenses(license, choose_lic_set)
305 except oe.license.LicenseError as exc:
306 bb.fatal('%s: %s' % (d.getVar('P'), exc))
307
308 incompatible_lic = []
309 for l in licenses:
310 license = canonical_license(d, l)
311 if not oe.license.license_ok(license, dont_want_licenses):
312 incompatible_lic.append(license)
313
314 return sorted(incompatible_lic)
315
316def incompatible_license(d, dont_want_licenses, package=None):
317 """
318 This function checks if a recipe has only incompatible licenses. It also
319 take into consideration 'or' operand. dont_want_licenses should be passed
320 as canonical (SPDX) names.
321 """
322 import oe.license
323 license = d.getVar("LICENSE:%s" % package) if package else None
324 if not license:
325 license = d.getVar('LICENSE')
326
327 return incompatible_pkg_license(d, dont_want_licenses, license)
328
329def check_license_flags(d):
330 """
331 This function checks if a recipe has any LICENSE_FLAGS that
332 aren't acceptable.
333
334 If it does, it returns the all LICENSE_FLAGS missing from the list
335 of acceptable license flags, or all of the LICENSE_FLAGS if there
336 is no list of acceptable flags.
337
338 If everything is is acceptable, it returns None.
339 """
340
341 def license_flag_matches(flag, acceptlist, pn):
342 """
343 Return True if flag matches something in acceptlist, None if not.
344
345 Before we test a flag against the acceptlist, we append _${PN}
346 to it. We then try to match that string against the
347 acceptlist. This covers the normal case, where we expect
348 LICENSE_FLAGS to be a simple string like 'commercial', which
349 the user typically matches exactly in the acceptlist by
350 explicitly appending the package name e.g 'commercial_foo'.
351 If we fail the match however, we then split the flag across
352 '_' and append each fragment and test until we either match or
353 run out of fragments.
354 """
355 flag_pn = ("%s_%s" % (flag, pn))
356 for candidate in acceptlist:
357 if flag_pn == candidate:
358 return True
359
360 flag_cur = ""
361 flagments = flag_pn.split("_")
362 flagments.pop() # we've already tested the full string
363 for flagment in flagments:
364 if flag_cur:
365 flag_cur += "_"
366 flag_cur += flagment
367 for candidate in acceptlist:
368 if flag_cur == candidate:
369 return True
370 return False
371
372 def all_license_flags_match(license_flags, acceptlist):
373 """ Return all unmatched flags, None if all flags match """
374 pn = d.getVar('PN')
375 split_acceptlist = acceptlist.split()
376 flags = []
377 for flag in license_flags.split():
378 if not license_flag_matches(flag, split_acceptlist, pn):
379 flags.append(flag)
380 return flags if flags else None
381
382 license_flags = d.getVar('LICENSE_FLAGS')
383 if license_flags:
384 acceptlist = d.getVar('LICENSE_FLAGS_ACCEPTED')
385 if not acceptlist:
386 return license_flags.split()
387 unmatched_flags = all_license_flags_match(license_flags, acceptlist)
388 if unmatched_flags:
389 return unmatched_flags
390 return None
391
392def check_license_format(d):
393 """
394 This function checks if LICENSE is well defined,
395 Validate operators in LICENSES.
396 No spaces are allowed between LICENSES.
397 """
398 pn = d.getVar('PN')
399 licenses = d.getVar('LICENSE')
400 from oe.license import license_operator, license_operator_chars, license_pattern
401
402 elements = list(filter(lambda x: x.strip(), license_operator.split(licenses)))
403 for pos, element in enumerate(elements):
404 if license_pattern.match(element):
405 if pos > 0 and license_pattern.match(elements[pos - 1]):
406 oe.qa.handle_error('license-format',
407 '%s: LICENSE value "%s" has an invalid format - license names ' \
408 'must be separated by the following characters to indicate ' \
409 'the license selection: %s' %
410 (pn, licenses, license_operator_chars), d)
411 elif not license_operator.match(element):
412 oe.qa.handle_error('license-format',
413 '%s: LICENSE value "%s" has an invalid separator "%s" that is not ' \
414 'in the valid list of separators (%s)' %
415 (pn, licenses, element, license_operator_chars), d)
416
417SSTATETASKS += "do_populate_lic"
418do_populate_lic[sstate-inputdirs] = "${LICSSTATEDIR}"
419do_populate_lic[sstate-outputdirs] = "${LICENSE_DIRECTORY}/"
420
421IMAGE_CLASSES:append = " license_image"
422
423python do_populate_lic_setscene () {
424 sstate_setscene(d)
425}
426addtask do_populate_lic_setscene