diff options
Diffstat (limited to 'scripts/lib/recipetool/create.py')
-rw-r--r-- | scripts/lib/recipetool/create.py | 368 |
1 files changed, 99 insertions, 269 deletions
diff --git a/scripts/lib/recipetool/create.py b/scripts/lib/recipetool/create.py index 4f6e01c639..ef0ba974a9 100644 --- a/scripts/lib/recipetool/create.py +++ b/scripts/lib/recipetool/create.py | |||
@@ -18,6 +18,8 @@ from urllib.parse import urlparse, urldefrag, urlsplit | |||
18 | import hashlib | 18 | import hashlib |
19 | import bb.fetch2 | 19 | import bb.fetch2 |
20 | logger = logging.getLogger('recipetool') | 20 | logger = logging.getLogger('recipetool') |
21 | from oe.license import tidy_licenses | ||
22 | from oe.license_finder import find_licenses | ||
21 | 23 | ||
22 | tinfoil = None | 24 | tinfoil = None |
23 | plugins = None | 25 | plugins = None |
@@ -389,9 +391,6 @@ def reformat_git_uri(uri): | |||
389 | parms.update({('protocol', 'ssh')}) | 391 | parms.update({('protocol', 'ssh')}) |
390 | elif (scheme == "http" or scheme == 'https' or scheme == 'ssh') and not ('protocol' in parms): | 392 | elif (scheme == "http" or scheme == 'https' or scheme == 'ssh') and not ('protocol' in parms): |
391 | parms.update({('protocol', scheme)}) | 393 | parms.update({('protocol', scheme)}) |
392 | # We assume 'master' branch if not set | ||
393 | if not 'branch' in parms: | ||
394 | parms.update({('branch', 'master')}) | ||
395 | # Always append 'git://' | 394 | # Always append 'git://' |
396 | fUrl = bb.fetch2.encodeurl(('git', host, path, user, pswd, parms)) | 395 | fUrl = bb.fetch2.encodeurl(('git', host, path, user, pswd, parms)) |
397 | return fUrl | 396 | return fUrl |
@@ -426,6 +425,36 @@ def create_recipe(args): | |||
426 | storeTagName = '' | 425 | storeTagName = '' |
427 | pv_srcpv = False | 426 | pv_srcpv = False |
428 | 427 | ||
428 | handled = [] | ||
429 | classes = [] | ||
430 | |||
431 | # Find all plugins that want to register handlers | ||
432 | logger.debug('Loading recipe handlers') | ||
433 | raw_handlers = [] | ||
434 | for plugin in plugins: | ||
435 | if hasattr(plugin, 'register_recipe_handlers'): | ||
436 | plugin.register_recipe_handlers(raw_handlers) | ||
437 | # Sort handlers by priority | ||
438 | handlers = [] | ||
439 | for i, handler in enumerate(raw_handlers): | ||
440 | if isinstance(handler, tuple): | ||
441 | handlers.append((handler[0], handler[1], i)) | ||
442 | else: | ||
443 | handlers.append((handler, 0, i)) | ||
444 | handlers.sort(key=lambda item: (item[1], -item[2]), reverse=True) | ||
445 | for handler, priority, _ in handlers: | ||
446 | logger.debug('Handler: %s (priority %d)' % (handler.__class__.__name__, priority)) | ||
447 | setattr(handler, '_devtool', args.devtool) | ||
448 | handlers = [item[0] for item in handlers] | ||
449 | |||
450 | fetchuri = None | ||
451 | for handler in handlers: | ||
452 | if hasattr(handler, 'process_url'): | ||
453 | ret = handler.process_url(args, classes, handled, extravalues) | ||
454 | if 'url' in handled and ret: | ||
455 | fetchuri = ret | ||
456 | break | ||
457 | |||
429 | if os.path.isfile(source): | 458 | if os.path.isfile(source): |
430 | source = 'file://%s' % os.path.abspath(source) | 459 | source = 'file://%s' % os.path.abspath(source) |
431 | 460 | ||
@@ -434,11 +463,12 @@ def create_recipe(args): | |||
434 | if re.match(r'https?://github.com/[^/]+/[^/]+/archive/.+(\.tar\..*|\.zip)$', source): | 463 | if re.match(r'https?://github.com/[^/]+/[^/]+/archive/.+(\.tar\..*|\.zip)$', source): |
435 | logger.warning('github archive files are not guaranteed to be stable and may be re-generated over time. If the latter occurs, the checksums will likely change and the recipe will fail at do_fetch. It is recommended that you point to an actual commit or tag in the repository instead (using the repository URL in conjunction with the -S/--srcrev option).') | 464 | logger.warning('github archive files are not guaranteed to be stable and may be re-generated over time. If the latter occurs, the checksums will likely change and the recipe will fail at do_fetch. It is recommended that you point to an actual commit or tag in the repository instead (using the repository URL in conjunction with the -S/--srcrev option).') |
436 | # Fetch a URL | 465 | # Fetch a URL |
437 | fetchuri = reformat_git_uri(urldefrag(source)[0]) | 466 | if not fetchuri: |
467 | fetchuri = reformat_git_uri(urldefrag(source)[0]) | ||
438 | if args.binary: | 468 | if args.binary: |
439 | # Assume the archive contains the directory structure verbatim | 469 | # Assume the archive contains the directory structure verbatim |
440 | # so we need to extract to a subdirectory | 470 | # so we need to extract to a subdirectory |
441 | fetchuri += ';subdir=${BP}' | 471 | fetchuri += ';subdir=${BPN}' |
442 | srcuri = fetchuri | 472 | srcuri = fetchuri |
443 | rev_re = re.compile(';rev=([^;]+)') | 473 | rev_re = re.compile(';rev=([^;]+)') |
444 | res = rev_re.search(srcuri) | 474 | res = rev_re.search(srcuri) |
@@ -481,6 +511,9 @@ def create_recipe(args): | |||
481 | storeTagName = params['tag'] | 511 | storeTagName = params['tag'] |
482 | params['nobranch'] = '1' | 512 | params['nobranch'] = '1' |
483 | del params['tag'] | 513 | del params['tag'] |
514 | # Assume 'master' branch if not set | ||
515 | if scheme in ['git', 'gitsm'] and 'branch' not in params and 'nobranch' not in params: | ||
516 | params['branch'] = 'master' | ||
484 | fetchuri = bb.fetch2.encodeurl((scheme, network, path, user, passwd, params)) | 517 | fetchuri = bb.fetch2.encodeurl((scheme, network, path, user, passwd, params)) |
485 | 518 | ||
486 | tmpparent = tinfoil.config_data.getVar('BASE_WORKDIR') | 519 | tmpparent = tinfoil.config_data.getVar('BASE_WORKDIR') |
@@ -497,7 +530,7 @@ def create_recipe(args): | |||
497 | if ftmpdir and args.keep_temp: | 530 | if ftmpdir and args.keep_temp: |
498 | logger.info('Fetch temp directory is %s' % ftmpdir) | 531 | logger.info('Fetch temp directory is %s' % ftmpdir) |
499 | 532 | ||
500 | dirlist = scriptutils.filter_src_subdirs(srctree) | 533 | dirlist = os.listdir(srctree) |
501 | logger.debug('Directory listing (excluding filtered out):\n %s' % '\n '.join(dirlist)) | 534 | logger.debug('Directory listing (excluding filtered out):\n %s' % '\n '.join(dirlist)) |
502 | if len(dirlist) == 1: | 535 | if len(dirlist) == 1: |
503 | singleitem = os.path.join(srctree, dirlist[0]) | 536 | singleitem = os.path.join(srctree, dirlist[0]) |
@@ -530,10 +563,9 @@ def create_recipe(args): | |||
530 | # Remove HEAD reference point and drop remote prefix | 563 | # Remove HEAD reference point and drop remote prefix |
531 | get_branch = [x.split('/', 1)[1] for x in get_branch if not x.startswith('origin/HEAD')] | 564 | get_branch = [x.split('/', 1)[1] for x in get_branch if not x.startswith('origin/HEAD')] |
532 | if 'master' in get_branch: | 565 | if 'master' in get_branch: |
533 | # If it is master, we do not need to append 'branch=master' as this is default. | ||
534 | # Even with the case where get_branch has multiple objects, if 'master' is one | 566 | # Even with the case where get_branch has multiple objects, if 'master' is one |
535 | # of them, we should default take from 'master' | 567 | # of them, we should default take from 'master' |
536 | srcbranch = '' | 568 | srcbranch = 'master' |
537 | elif len(get_branch) == 1: | 569 | elif len(get_branch) == 1: |
538 | # If 'master' isn't in get_branch and get_branch contains only ONE object, then store result into 'srcbranch' | 570 | # If 'master' isn't in get_branch and get_branch contains only ONE object, then store result into 'srcbranch' |
539 | srcbranch = get_branch[0] | 571 | srcbranch = get_branch[0] |
@@ -546,8 +578,8 @@ def create_recipe(args): | |||
546 | # Since we might have a value in srcbranch, we need to | 578 | # Since we might have a value in srcbranch, we need to |
547 | # recontruct the srcuri to include 'branch' in params. | 579 | # recontruct the srcuri to include 'branch' in params. |
548 | scheme, network, path, user, passwd, params = bb.fetch2.decodeurl(srcuri) | 580 | scheme, network, path, user, passwd, params = bb.fetch2.decodeurl(srcuri) |
549 | if srcbranch: | 581 | if scheme in ['git', 'gitsm']: |
550 | params['branch'] = srcbranch | 582 | params['branch'] = srcbranch or 'master' |
551 | 583 | ||
552 | if storeTagName and scheme in ['git', 'gitsm']: | 584 | if storeTagName and scheme in ['git', 'gitsm']: |
553 | # Check srcrev using tag and check validity of the tag | 585 | # Check srcrev using tag and check validity of the tag |
@@ -606,8 +638,7 @@ def create_recipe(args): | |||
606 | splitline = line.split() | 638 | splitline = line.split() |
607 | if len(splitline) > 1: | 639 | if len(splitline) > 1: |
608 | if splitline[0] == 'origin' and scriptutils.is_src_url(splitline[1]): | 640 | if splitline[0] == 'origin' and scriptutils.is_src_url(splitline[1]): |
609 | srcuri = reformat_git_uri(splitline[1]) | 641 | srcuri = reformat_git_uri(splitline[1]) + ';branch=master' |
610 | srcsubdir = 'git' | ||
611 | break | 642 | break |
612 | 643 | ||
613 | if args.src_subdir: | 644 | if args.src_subdir: |
@@ -639,8 +670,6 @@ def create_recipe(args): | |||
639 | # We'll come back and replace this later in handle_license_vars() | 670 | # We'll come back and replace this later in handle_license_vars() |
640 | lines_before.append('##LICENSE_PLACEHOLDER##') | 671 | lines_before.append('##LICENSE_PLACEHOLDER##') |
641 | 672 | ||
642 | handled = [] | ||
643 | classes = [] | ||
644 | 673 | ||
645 | # FIXME This is kind of a hack, we probably ought to be using bitbake to do this | 674 | # FIXME This is kind of a hack, we probably ought to be using bitbake to do this |
646 | pn = None | 675 | pn = None |
@@ -678,8 +707,10 @@ def create_recipe(args): | |||
678 | if not srcuri: | 707 | if not srcuri: |
679 | lines_before.append('# No information for SRC_URI yet (only an external source tree was specified)') | 708 | lines_before.append('# No information for SRC_URI yet (only an external source tree was specified)') |
680 | lines_before.append('SRC_URI = "%s"' % srcuri) | 709 | lines_before.append('SRC_URI = "%s"' % srcuri) |
710 | shown_checksums = ["%ssum" % s for s in bb.fetch2.SHOWN_CHECKSUM_LIST] | ||
681 | for key, value in sorted(checksums.items()): | 711 | for key, value in sorted(checksums.items()): |
682 | lines_before.append('SRC_URI[%s] = "%s"' % (key, value)) | 712 | if key in shown_checksums: |
713 | lines_before.append('SRC_URI[%s] = "%s"' % (key, value)) | ||
683 | if srcuri and supports_srcrev(srcuri): | 714 | if srcuri and supports_srcrev(srcuri): |
684 | lines_before.append('') | 715 | lines_before.append('') |
685 | lines_before.append('# Modify these as desired') | 716 | lines_before.append('# Modify these as desired') |
@@ -691,7 +722,7 @@ def create_recipe(args): | |||
691 | srcpvprefix = 'svnr' | 722 | srcpvprefix = 'svnr' |
692 | else: | 723 | else: |
693 | srcpvprefix = scheme | 724 | srcpvprefix = scheme |
694 | lines_before.append('PV = "%s+%s${SRCPV}"' % (realpv or '1.0', srcpvprefix)) | 725 | lines_before.append('PV = "%s+%s"' % (realpv or '1.0', srcpvprefix)) |
695 | pv_srcpv = True | 726 | pv_srcpv = True |
696 | if not args.autorev and srcrev == '${AUTOREV}': | 727 | if not args.autorev and srcrev == '${AUTOREV}': |
697 | if os.path.exists(os.path.join(srctree, '.git')): | 728 | if os.path.exists(os.path.join(srctree, '.git')): |
@@ -705,7 +736,7 @@ def create_recipe(args): | |||
705 | if srcsubdir and not args.binary: | 736 | if srcsubdir and not args.binary: |
706 | # (for binary packages we explicitly specify subdir= when fetching to | 737 | # (for binary packages we explicitly specify subdir= when fetching to |
707 | # match the default value of S, so we don't need to set it in that case) | 738 | # match the default value of S, so we don't need to set it in that case) |
708 | lines_before.append('S = "${WORKDIR}/%s"' % srcsubdir) | 739 | lines_before.append('S = "${UNPACKDIR}/%s"' % srcsubdir) |
709 | lines_before.append('') | 740 | lines_before.append('') |
710 | 741 | ||
711 | if pkgarch: | 742 | if pkgarch: |
@@ -719,25 +750,6 @@ def create_recipe(args): | |||
719 | if args.npm_dev: | 750 | if args.npm_dev: |
720 | extravalues['NPM_INSTALL_DEV'] = 1 | 751 | extravalues['NPM_INSTALL_DEV'] = 1 |
721 | 752 | ||
722 | # Find all plugins that want to register handlers | ||
723 | logger.debug('Loading recipe handlers') | ||
724 | raw_handlers = [] | ||
725 | for plugin in plugins: | ||
726 | if hasattr(plugin, 'register_recipe_handlers'): | ||
727 | plugin.register_recipe_handlers(raw_handlers) | ||
728 | # Sort handlers by priority | ||
729 | handlers = [] | ||
730 | for i, handler in enumerate(raw_handlers): | ||
731 | if isinstance(handler, tuple): | ||
732 | handlers.append((handler[0], handler[1], i)) | ||
733 | else: | ||
734 | handlers.append((handler, 0, i)) | ||
735 | handlers.sort(key=lambda item: (item[1], -item[2]), reverse=True) | ||
736 | for handler, priority, _ in handlers: | ||
737 | logger.debug('Handler: %s (priority %d)' % (handler.__class__.__name__, priority)) | ||
738 | setattr(handler, '_devtool', args.devtool) | ||
739 | handlers = [item[0] for item in handlers] | ||
740 | |||
741 | # Apply the handlers | 753 | # Apply the handlers |
742 | if args.binary: | 754 | if args.binary: |
743 | classes.append('bin_package') | 755 | classes.append('bin_package') |
@@ -746,9 +758,14 @@ def create_recipe(args): | |||
746 | for handler in handlers: | 758 | for handler in handlers: |
747 | handler.process(srctree_use, classes, lines_before, lines_after, handled, extravalues) | 759 | handler.process(srctree_use, classes, lines_before, lines_after, handled, extravalues) |
748 | 760 | ||
761 | # native and nativesdk classes are special and must be inherited last | ||
762 | # If present, put them at the end of the classes list | ||
763 | classes.sort(key=lambda c: c in ("native", "nativesdk")) | ||
764 | |||
749 | extrafiles = extravalues.pop('extrafiles', {}) | 765 | extrafiles = extravalues.pop('extrafiles', {}) |
750 | extra_pn = extravalues.pop('PN', None) | 766 | extra_pn = extravalues.pop('PN', None) |
751 | extra_pv = extravalues.pop('PV', None) | 767 | extra_pv = extravalues.pop('PV', None) |
768 | run_tasks = extravalues.pop('run_tasks', "").split() | ||
752 | 769 | ||
753 | if extra_pv and not realpv: | 770 | if extra_pv and not realpv: |
754 | realpv = extra_pv | 771 | realpv = extra_pv |
@@ -809,7 +826,8 @@ def create_recipe(args): | |||
809 | extraoutdir = os.path.join(os.path.dirname(outfile), pn) | 826 | extraoutdir = os.path.join(os.path.dirname(outfile), pn) |
810 | bb.utils.mkdirhier(extraoutdir) | 827 | bb.utils.mkdirhier(extraoutdir) |
811 | for destfn, extrafile in extrafiles.items(): | 828 | for destfn, extrafile in extrafiles.items(): |
812 | shutil.move(extrafile, os.path.join(extraoutdir, destfn)) | 829 | fn = destfn.format(pn=pn, pv=realpv) |
830 | shutil.move(extrafile, os.path.join(extraoutdir, fn)) | ||
813 | 831 | ||
814 | lines = lines_before | 832 | lines = lines_before |
815 | lines_before = [] | 833 | lines_before = [] |
@@ -824,7 +842,7 @@ def create_recipe(args): | |||
824 | line = line.replace(realpv, '${PV}') | 842 | line = line.replace(realpv, '${PV}') |
825 | if pn: | 843 | if pn: |
826 | line = line.replace(pn, '${BPN}') | 844 | line = line.replace(pn, '${BPN}') |
827 | if line == 'S = "${WORKDIR}/${BPN}-${PV}"': | 845 | if line == 'S = "${UNPACKDIR}/${BPN}-${PV}"' or 'tmp-recipetool-' in line: |
828 | skipblank = True | 846 | skipblank = True |
829 | continue | 847 | continue |
830 | elif line.startswith('SRC_URI = '): | 848 | elif line.startswith('SRC_URI = '): |
@@ -870,8 +888,10 @@ def create_recipe(args): | |||
870 | outlines.append('') | 888 | outlines.append('') |
871 | outlines.extend(lines_after) | 889 | outlines.extend(lines_after) |
872 | 890 | ||
891 | outlines = [ line.rstrip('\n') +"\n" for line in outlines] | ||
892 | |||
873 | if extravalues: | 893 | if extravalues: |
874 | _, outlines = oe.recipeutils.patch_recipe_lines(outlines, extravalues, trailing_newline=False) | 894 | _, outlines = oe.recipeutils.patch_recipe_lines(outlines, extravalues, trailing_newline=True) |
875 | 895 | ||
876 | if args.extract_to: | 896 | if args.extract_to: |
877 | scriptutils.git_convert_standalone_clone(srctree) | 897 | scriptutils.git_convert_standalone_clone(srctree) |
@@ -887,7 +907,7 @@ def create_recipe(args): | |||
887 | log_info_cond('Source extracted to %s' % args.extract_to, args.devtool) | 907 | log_info_cond('Source extracted to %s' % args.extract_to, args.devtool) |
888 | 908 | ||
889 | if outfile == '-': | 909 | if outfile == '-': |
890 | sys.stdout.write('\n'.join(outlines) + '\n') | 910 | sys.stdout.write(''.join(outlines) + '\n') |
891 | else: | 911 | else: |
892 | with open(outfile, 'w') as f: | 912 | with open(outfile, 'w') as f: |
893 | lastline = None | 913 | lastline = None |
@@ -895,9 +915,14 @@ def create_recipe(args): | |||
895 | if not lastline and not line: | 915 | if not lastline and not line: |
896 | # Skip extra blank lines | 916 | # Skip extra blank lines |
897 | continue | 917 | continue |
898 | f.write('%s\n' % line) | 918 | f.write('%s' % line) |
899 | lastline = line | 919 | lastline = line |
900 | log_info_cond('Recipe %s has been created; further editing may be required to make it fully functional' % outfile, args.devtool) | 920 | log_info_cond('Recipe %s has been created; further editing may be required to make it fully functional' % outfile, args.devtool) |
921 | tinfoil.modified_files() | ||
922 | |||
923 | for task in run_tasks: | ||
924 | logger.info("Running task %s" % task) | ||
925 | tinfoil.build_file_sync(outfile, task) | ||
901 | 926 | ||
902 | if tempsrc: | 927 | if tempsrc: |
903 | if args.keep_temp: | 928 | if args.keep_temp: |
@@ -920,23 +945,32 @@ def split_value(value): | |||
920 | else: | 945 | else: |
921 | return value | 946 | return value |
922 | 947 | ||
948 | def fixup_license(value): | ||
949 | # Ensure licenses with OR starts and ends with brackets | ||
950 | if '|' in value: | ||
951 | return '(' + value + ')' | ||
952 | return value | ||
953 | |||
923 | def handle_license_vars(srctree, lines_before, handled, extravalues, d): | 954 | def handle_license_vars(srctree, lines_before, handled, extravalues, d): |
924 | lichandled = [x for x in handled if x[0] == 'license'] | 955 | lichandled = [x for x in handled if x[0] == 'license'] |
925 | if lichandled: | 956 | if lichandled: |
926 | # Someone else has already handled the license vars, just return their value | 957 | # Someone else has already handled the license vars, just return their value |
927 | return lichandled[0][1] | 958 | return lichandled[0][1] |
928 | 959 | ||
929 | licvalues = guess_license(srctree, d) | 960 | licvalues = find_licenses(srctree, d) |
930 | licenses = [] | 961 | licenses = [] |
931 | lic_files_chksum = [] | 962 | lic_files_chksum = [] |
932 | lic_unknown = [] | 963 | lic_unknown = [] |
933 | lines = [] | 964 | lines = [] |
934 | if licvalues: | 965 | if licvalues: |
935 | for licvalue in licvalues: | 966 | for licvalue in licvalues: |
936 | if not licvalue[0] in licenses: | 967 | license = licvalue[0] |
937 | licenses.append(licvalue[0]) | 968 | lics = tidy_licenses(fixup_license(license)) |
969 | lics = [lic for lic in lics if lic not in licenses] | ||
970 | if len(lics): | ||
971 | licenses.extend(lics) | ||
938 | lic_files_chksum.append('file://%s;md5=%s' % (licvalue[1], licvalue[2])) | 972 | lic_files_chksum.append('file://%s;md5=%s' % (licvalue[1], licvalue[2])) |
939 | if licvalue[0] == 'Unknown': | 973 | if license == 'Unknown': |
940 | lic_unknown.append(licvalue[1]) | 974 | lic_unknown.append(licvalue[1]) |
941 | if lic_unknown: | 975 | if lic_unknown: |
942 | lines.append('#') | 976 | lines.append('#') |
@@ -945,9 +979,7 @@ def handle_license_vars(srctree, lines_before, handled, extravalues, d): | |||
945 | for licfile in lic_unknown: | 979 | for licfile in lic_unknown: |
946 | lines.append('# %s' % licfile) | 980 | lines.append('# %s' % licfile) |
947 | 981 | ||
948 | extra_license = split_value(extravalues.pop('LICENSE', [])) | 982 | extra_license = tidy_licenses(extravalues.pop('LICENSE', '')) |
949 | if '&' in extra_license: | ||
950 | extra_license.remove('&') | ||
951 | if extra_license: | 983 | if extra_license: |
952 | if licenses == ['Unknown']: | 984 | if licenses == ['Unknown']: |
953 | licenses = extra_license | 985 | licenses = extra_license |
@@ -988,7 +1020,7 @@ def handle_license_vars(srctree, lines_before, handled, extravalues, d): | |||
988 | lines.append('# instead of &. If there is any doubt, check the accompanying documentation') | 1020 | lines.append('# instead of &. If there is any doubt, check the accompanying documentation') |
989 | lines.append('# to determine which situation is applicable.') | 1021 | lines.append('# to determine which situation is applicable.') |
990 | 1022 | ||
991 | lines.append('LICENSE = "%s"' % ' & '.join(licenses)) | 1023 | lines.append('LICENSE = "%s"' % ' & '.join(sorted(licenses, key=str.casefold))) |
992 | lines.append('LIC_FILES_CHKSUM = "%s"' % ' \\\n '.join(lic_files_chksum)) | 1024 | lines.append('LIC_FILES_CHKSUM = "%s"' % ' \\\n '.join(lic_files_chksum)) |
993 | lines.append('') | 1025 | lines.append('') |
994 | 1026 | ||
@@ -1005,228 +1037,15 @@ def handle_license_vars(srctree, lines_before, handled, extravalues, d): | |||
1005 | handled.append(('license', licvalues)) | 1037 | handled.append(('license', licvalues)) |
1006 | return licvalues | 1038 | return licvalues |
1007 | 1039 | ||
1008 | def get_license_md5sums(d, static_only=False, linenumbers=False): | 1040 | def split_pkg_licenses(licvalues, packages, outlines, fallback_licenses=None, pn='${PN}'): |
1009 | import bb.utils | ||
1010 | import csv | ||
1011 | md5sums = {} | ||
1012 | if not static_only and not linenumbers: | ||
1013 | # Gather md5sums of license files in common license dir | ||
1014 | commonlicdir = d.getVar('COMMON_LICENSE_DIR') | ||
1015 | for fn in os.listdir(commonlicdir): | ||
1016 | md5value = bb.utils.md5_file(os.path.join(commonlicdir, fn)) | ||
1017 | md5sums[md5value] = fn | ||
1018 | |||
1019 | # The following were extracted from common values in various recipes | ||
1020 | # (double checking the license against the license file itself, not just | ||
1021 | # the LICENSE value in the recipe) | ||
1022 | |||
1023 | # Read license md5sums from csv file | ||
1024 | scripts_path = os.path.dirname(os.path.realpath(__file__)) | ||
1025 | for path in (d.getVar('BBPATH').split(':') | ||
1026 | + [os.path.join(scripts_path, '..', '..')]): | ||
1027 | csv_path = os.path.join(path, 'lib', 'recipetool', 'licenses.csv') | ||
1028 | if os.path.isfile(csv_path): | ||
1029 | with open(csv_path, newline='') as csv_file: | ||
1030 | fieldnames = ['md5sum', 'license', 'beginline', 'endline', 'md5'] | ||
1031 | reader = csv.DictReader(csv_file, delimiter=',', fieldnames=fieldnames) | ||
1032 | for row in reader: | ||
1033 | if linenumbers: | ||
1034 | md5sums[row['md5sum']] = ( | ||
1035 | row['license'], row['beginline'], row['endline'], row['md5']) | ||
1036 | else: | ||
1037 | md5sums[row['md5sum']] = row['license'] | ||
1038 | |||
1039 | return md5sums | ||
1040 | |||
1041 | def crunch_license(licfile): | ||
1042 | ''' | ||
1043 | Remove non-material text from a license file and then check | ||
1044 | its md5sum against a known list. This works well for licenses | ||
1045 | which contain a copyright statement, but is also a useful way | ||
1046 | to handle people's insistence upon reformatting the license text | ||
1047 | slightly (with no material difference to the text of the | ||
1048 | license). | ||
1049 | ''' | ||
1050 | |||
1051 | import oe.utils | ||
1052 | |||
1053 | # Note: these are carefully constructed! | ||
1054 | license_title_re = re.compile(r'^#*\(? *(This is )?([Tt]he )?.{0,15} ?[Ll]icen[sc]e( \(.{1,10}\))?\)?[:\.]? ?#*$') | ||
1055 | license_statement_re = re.compile(r'^((This (project|software)|.{1,10}) is( free software)? (released|licen[sc]ed)|(Released|Licen[cs]ed)) under the .{1,10} [Ll]icen[sc]e:?$') | ||
1056 | copyright_re = re.compile('^ *[#\*]* *(Modified work |MIT LICENSED )?Copyright ?(\([cC]\))? .*$') | ||
1057 | disclaimer_re = re.compile('^ *\*? ?All [Rr]ights [Rr]eserved\.$') | ||
1058 | email_re = re.compile('^.*<[\w\.-]*@[\w\.\-]*>$') | ||
1059 | header_re = re.compile('^(\/\**!?)? ?[\-=\*]* ?(\*\/)?$') | ||
1060 | tag_re = re.compile('^ *@?\(?([Ll]icense|MIT)\)?$') | ||
1061 | url_re = re.compile('^ *[#\*]* *https?:\/\/[\w\.\/\-]+$') | ||
1062 | |||
1063 | crunched_md5sums = {} | ||
1064 | |||
1065 | # common licenses | ||
1066 | crunched_md5sums['89f3bf322f30a1dcfe952e09945842f0'] = 'Apache-2.0' | ||
1067 | crunched_md5sums['13b6fe3075f8f42f2270a748965bf3a1'] = 'BSD-0-Clause' | ||
1068 | crunched_md5sums['ba87a7d7c20719c8df4b8beed9b78c43'] = 'BSD-2-Clause' | ||
1069 | crunched_md5sums['7f8892c03b72de419c27be4ebfa253f8'] = 'BSD-3-Clause' | ||
1070 | crunched_md5sums['21128c0790b23a8a9f9e260d5f6b3619'] = 'BSL-1.0' | ||
1071 | crunched_md5sums['975742a59ae1b8abdea63a97121f49f4'] = 'EDL-1.0' | ||
1072 | crunched_md5sums['5322cee4433d84fb3aafc9e253116447'] = 'EPL-1.0' | ||
1073 | crunched_md5sums['6922352e87de080f42419bed93063754'] = 'EPL-2.0' | ||
1074 | crunched_md5sums['793475baa22295cae1d3d4046a3a0ceb'] = 'GPL-2.0-only' | ||
1075 | crunched_md5sums['ff9047f969b02c20f0559470df5cb433'] = 'GPL-2.0-or-later' | ||
1076 | crunched_md5sums['ea6de5453fcadf534df246e6cdafadcd'] = 'GPL-3.0-only' | ||
1077 | crunched_md5sums['b419257d4d153a6fde92ddf96acf5b67'] = 'GPL-3.0-or-later' | ||
1078 | crunched_md5sums['228737f4c49d3ee75b8fb3706b090b84'] = 'ISC' | ||
1079 | crunched_md5sums['c6a782e826ca4e85bf7f8b89435a677d'] = 'LGPL-2.0-only' | ||
1080 | crunched_md5sums['32d8f758a066752f0db09bd7624b8090'] = 'LGPL-2.0-or-later' | ||
1081 | crunched_md5sums['4820937eb198b4f84c52217ed230be33'] = 'LGPL-2.1-only' | ||
1082 | crunched_md5sums['db13fe9f3a13af7adab2dc7a76f9e44a'] = 'LGPL-2.1-or-later' | ||
1083 | crunched_md5sums['d7a0f2e4e0950e837ac3eabf5bd1d246'] = 'LGPL-3.0-only' | ||
1084 | crunched_md5sums['abbf328e2b434f9153351f06b9f79d02'] = 'LGPL-3.0-or-later' | ||
1085 | crunched_md5sums['eecf6429523cbc9693547cf2db790b5c'] = 'MIT' | ||
1086 | crunched_md5sums['b218b0e94290b9b818c4be67c8e1cc82'] = 'MIT-0' | ||
1087 | crunched_md5sums['ddc18131d6748374f0f35a621c245b49'] = 'Unlicense' | ||
1088 | crunched_md5sums['51f9570ff32571fc0a443102285c5e33'] = 'WTFPL' | ||
1089 | |||
1090 | # The following two were gleaned from the "forever" npm package | ||
1091 | crunched_md5sums['0a97f8e4cbaf889d6fa51f84b89a79f6'] = 'ISC' | ||
1092 | # https://github.com/waffle-gl/waffle/blob/master/LICENSE.txt | ||
1093 | crunched_md5sums['50fab24ce589d69af8964fdbfe414c60'] = 'BSD-2-Clause' | ||
1094 | # https://github.com/spigwitmer/fakeds1963s/blob/master/LICENSE | ||
1095 | crunched_md5sums['88a4355858a1433fea99fae34a44da88'] = 'GPLv2' | ||
1096 | # http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt | ||
1097 | crunched_md5sums['063b5c3ebb5f3aa4c85a2ed18a31fbe7'] = 'GPLv2' | ||
1098 | # https://github.com/FFmpeg/FFmpeg/blob/master/COPYING.LGPLv2.1 | ||
1099 | crunched_md5sums['7f5202f4d44ed15dcd4915f5210417d8'] = 'LGPLv2.1' | ||
1100 | # unixODBC-2.3.4 COPYING | ||
1101 | crunched_md5sums['3debde09238a8c8e1f6a847e1ec9055b'] = 'LGPLv2.1' | ||
1102 | # https://github.com/FFmpeg/FFmpeg/blob/master/COPYING.LGPLv3 | ||
1103 | crunched_md5sums['f90c613c51aa35da4d79dd55fc724ceb'] = 'LGPLv3' | ||
1104 | # https://raw.githubusercontent.com/eclipse/mosquitto/v1.4.14/epl-v10 | ||
1105 | crunched_md5sums['efe2cb9a35826992b9df68224e3c2628'] = 'EPL-1.0' | ||
1106 | |||
1107 | # https://raw.githubusercontent.com/jquery/esprima/3.1.3/LICENSE.BSD | ||
1108 | crunched_md5sums['80fa7b56a28e8c902e6af194003220a5'] = 'BSD-2-Clause' | ||
1109 | # https://raw.githubusercontent.com/npm/npm-install-checks/master/LICENSE | ||
1110 | crunched_md5sums['e659f77bfd9002659e112d0d3d59b2c1'] = 'BSD-2-Clause' | ||
1111 | # https://raw.githubusercontent.com/silverwind/default-gateway/4.2.0/LICENSE | ||
1112 | crunched_md5sums['4c641f2d995c47f5cb08bdb4b5b6ea05'] = 'BSD-2-Clause' | ||
1113 | # https://raw.githubusercontent.com/tad-lispy/node-damerau-levenshtein/v1.0.5/LICENSE | ||
1114 | crunched_md5sums['2b8c039b2b9a25f0feb4410c4542d346'] = 'BSD-2-Clause' | ||
1115 | # https://raw.githubusercontent.com/terser/terser/v3.17.0/LICENSE | ||
1116 | crunched_md5sums['8bd23871802951c9ad63855151204c2c'] = 'BSD-2-Clause' | ||
1117 | # https://raw.githubusercontent.com/alexei/sprintf.js/1.0.3/LICENSE | ||
1118 | crunched_md5sums['008c22318c8ea65928bf730ddd0273e3'] = 'BSD-3-Clause' | ||
1119 | # https://raw.githubusercontent.com/Caligatio/jsSHA/v3.2.0/LICENSE | ||
1120 | crunched_md5sums['0e46634a01bfef056892949acaea85b1'] = 'BSD-3-Clause' | ||
1121 | # https://raw.githubusercontent.com/d3/d3-path/v1.0.9/LICENSE | ||
1122 | crunched_md5sums['b5f72aef53d3b2b432702c30b0215666'] = 'BSD-3-Clause' | ||
1123 | # https://raw.githubusercontent.com/feross/ieee754/v1.1.13/LICENSE | ||
1124 | crunched_md5sums['a39327c997c20da0937955192d86232d'] = 'BSD-3-Clause' | ||
1125 | # https://raw.githubusercontent.com/joyent/node-extsprintf/v1.3.0/LICENSE | ||
1126 | crunched_md5sums['721f23a96ff4161ca3a5f071bbe18108'] = 'MIT' | ||
1127 | # https://raw.githubusercontent.com/pvorb/clone/v0.2.0/LICENSE | ||
1128 | crunched_md5sums['b376d29a53c9573006b9970709231431'] = 'MIT' | ||
1129 | # https://raw.githubusercontent.com/andris9/encoding/v0.1.12/LICENSE | ||
1130 | crunched_md5sums['85d8a977ee9d7c5ab4ac03c9b95431c4'] = 'MIT-0' | ||
1131 | # https://raw.githubusercontent.com/faye/websocket-driver-node/0.7.3/LICENSE.md | ||
1132 | crunched_md5sums['b66384e7137e41a9b1904ef4d39703b6'] = 'Apache-2.0' | ||
1133 | # https://raw.githubusercontent.com/less/less.js/v4.1.1/LICENSE | ||
1134 | crunched_md5sums['b27575459e02221ccef97ec0bfd457ae'] = 'Apache-2.0' | ||
1135 | # https://raw.githubusercontent.com/microsoft/TypeScript/v3.5.3/LICENSE.txt | ||
1136 | crunched_md5sums['a54a1a6a39e7f9dbb4a23a42f5c7fd1c'] = 'Apache-2.0' | ||
1137 | # https://raw.githubusercontent.com/request/request/v2.87.0/LICENSE | ||
1138 | crunched_md5sums['1034431802e57486b393d00c5d262b8a'] = 'Apache-2.0' | ||
1139 | # https://raw.githubusercontent.com/dchest/tweetnacl-js/v0.14.5/LICENSE | ||
1140 | crunched_md5sums['75605e6bdd564791ab698fca65c94a4f'] = 'Unlicense' | ||
1141 | # https://raw.githubusercontent.com/stackgl/gl-mat3/v2.0.0/LICENSE.md | ||
1142 | crunched_md5sums['75512892d6f59dddb6d1c7e191957e9c'] = 'Zlib' | ||
1143 | |||
1144 | lictext = [] | ||
1145 | with open(licfile, 'r', errors='surrogateescape') as f: | ||
1146 | for line in f: | ||
1147 | # Drop opening statements | ||
1148 | if copyright_re.match(line): | ||
1149 | continue | ||
1150 | elif disclaimer_re.match(line): | ||
1151 | continue | ||
1152 | elif email_re.match(line): | ||
1153 | continue | ||
1154 | elif header_re.match(line): | ||
1155 | continue | ||
1156 | elif tag_re.match(line): | ||
1157 | continue | ||
1158 | elif url_re.match(line): | ||
1159 | continue | ||
1160 | elif license_title_re.match(line): | ||
1161 | continue | ||
1162 | elif license_statement_re.match(line): | ||
1163 | continue | ||
1164 | # Strip comment symbols | ||
1165 | line = line.replace('*', '') \ | ||
1166 | .replace('#', '') | ||
1167 | # Unify spelling | ||
1168 | line = line.replace('sub-license', 'sublicense') | ||
1169 | # Squash spaces | ||
1170 | line = oe.utils.squashspaces(line.strip()) | ||
1171 | # Replace smart quotes, double quotes and backticks with single quotes | ||
1172 | line = line.replace(u"\u2018", "'").replace(u"\u2019", "'").replace(u"\u201c","'").replace(u"\u201d", "'").replace('"', '\'').replace('`', '\'') | ||
1173 | # Unify brackets | ||
1174 | line = line.replace("{", "[").replace("}", "]") | ||
1175 | if line: | ||
1176 | lictext.append(line) | ||
1177 | |||
1178 | m = hashlib.md5() | ||
1179 | try: | ||
1180 | m.update(' '.join(lictext).encode('utf-8')) | ||
1181 | md5val = m.hexdigest() | ||
1182 | except UnicodeEncodeError: | ||
1183 | md5val = None | ||
1184 | lictext = '' | ||
1185 | license = crunched_md5sums.get(md5val, None) | ||
1186 | return license, md5val, lictext | ||
1187 | |||
1188 | def guess_license(srctree, d): | ||
1189 | import bb | ||
1190 | md5sums = get_license_md5sums(d) | ||
1191 | |||
1192 | licenses = [] | ||
1193 | licspecs = ['*LICEN[CS]E*', 'COPYING*', '*[Ll]icense*', 'LEGAL*', '[Ll]egal*', '*GPL*', 'README.lic*', 'COPYRIGHT*', '[Cc]opyright*', 'e[dp]l-v10'] | ||
1194 | skip_extensions = (".html", ".js", ".json", ".svg", ".ts") | ||
1195 | licfiles = [] | ||
1196 | for root, dirs, files in os.walk(srctree): | ||
1197 | for fn in files: | ||
1198 | if fn.endswith(skip_extensions): | ||
1199 | continue | ||
1200 | for spec in licspecs: | ||
1201 | if fnmatch.fnmatch(fn, spec): | ||
1202 | fullpath = os.path.join(root, fn) | ||
1203 | if not fullpath in licfiles: | ||
1204 | licfiles.append(fullpath) | ||
1205 | for licfile in licfiles: | ||
1206 | md5value = bb.utils.md5_file(licfile) | ||
1207 | license = md5sums.get(md5value, None) | ||
1208 | if not license: | ||
1209 | license, crunched_md5, lictext = crunch_license(licfile) | ||
1210 | if lictext and not license: | ||
1211 | license = 'Unknown' | ||
1212 | logger.info("Please add the following line for '%s' to a 'lib/recipetool/licenses.csv' " \ | ||
1213 | "and replace `Unknown` with the license:\n" \ | ||
1214 | "%s,Unknown" % (os.path.relpath(licfile, srctree), md5value)) | ||
1215 | if license: | ||
1216 | licenses.append((license, os.path.relpath(licfile, srctree), md5value)) | ||
1217 | |||
1218 | # FIXME should we grab at least one source file with a license header and add that too? | ||
1219 | |||
1220 | return licenses | ||
1221 | |||
1222 | def split_pkg_licenses(licvalues, packages, outlines, fallback_licenses=[], pn='${PN}'): | ||
1223 | """ | 1041 | """ |
1224 | Given a list of (license, path, md5sum) as returned by guess_license(), | 1042 | Given a list of (license, path, md5sum) as returned by match_licenses(), |
1225 | a dict of package name to path mappings, write out a set of | 1043 | a dict of package name to path mappings, write out a set of |
1226 | package-specific LICENSE values. | 1044 | package-specific LICENSE values. |
1227 | """ | 1045 | """ |
1228 | pkglicenses = {pn: []} | 1046 | pkglicenses = {pn: []} |
1229 | for license, licpath, _ in licvalues: | 1047 | for license, licpath, _ in licvalues: |
1048 | license = fixup_license(license) | ||
1230 | for pkgname, pkgpath in packages.items(): | 1049 | for pkgname, pkgpath in packages.items(): |
1231 | if licpath.startswith(pkgpath + '/'): | 1050 | if licpath.startswith(pkgpath + '/'): |
1232 | if pkgname in pkglicenses: | 1051 | if pkgname in pkglicenses: |
@@ -1239,13 +1058,24 @@ def split_pkg_licenses(licvalues, packages, outlines, fallback_licenses=[], pn=' | |||
1239 | pkglicenses[pn].append(license) | 1058 | pkglicenses[pn].append(license) |
1240 | outlicenses = {} | 1059 | outlicenses = {} |
1241 | for pkgname in packages: | 1060 | for pkgname in packages: |
1242 | license = ' '.join(list(set(pkglicenses.get(pkgname, ['Unknown'])))) or 'Unknown' | 1061 | # Assume AND operator between license files |
1243 | if license == 'Unknown' and pkgname in fallback_licenses: | 1062 | license = ' & '.join(list(set(pkglicenses.get(pkgname, ['Unknown'])))) or 'Unknown' |
1063 | if license == 'Unknown' and fallback_licenses and pkgname in fallback_licenses: | ||
1244 | license = fallback_licenses[pkgname] | 1064 | license = fallback_licenses[pkgname] |
1065 | licenses = tidy_licenses(license) | ||
1066 | license = ' & '.join(licenses) | ||
1245 | outlines.append('LICENSE:%s = "%s"' % (pkgname, license)) | 1067 | outlines.append('LICENSE:%s = "%s"' % (pkgname, license)) |
1246 | outlicenses[pkgname] = license.split() | 1068 | outlicenses[pkgname] = licenses |
1247 | return outlicenses | 1069 | return outlicenses |
1248 | 1070 | ||
1071 | def generate_common_licenses_chksums(common_licenses, d): | ||
1072 | lic_files_chksums = [] | ||
1073 | for license in tidy_licenses(common_licenses): | ||
1074 | licfile = '${COMMON_LICENSE_DIR}/' + license | ||
1075 | md5value = bb.utils.md5_file(d.expand(licfile)) | ||
1076 | lic_files_chksums.append('file://%s;md5=%s' % (licfile, md5value)) | ||
1077 | return lic_files_chksums | ||
1078 | |||
1249 | def read_pkgconfig_provides(d): | 1079 | def read_pkgconfig_provides(d): |
1250 | pkgdatadir = d.getVar('PKGDATA_DIR') | 1080 | pkgdatadir = d.getVar('PKGDATA_DIR') |
1251 | pkgmap = {} | 1081 | pkgmap = {} |
@@ -1376,7 +1206,7 @@ def register_commands(subparsers): | |||
1376 | parser_create.add_argument('-B', '--srcbranch', help='Branch in source repository if fetching from an SCM such as git (default master)') | 1206 | parser_create.add_argument('-B', '--srcbranch', help='Branch in source repository if fetching from an SCM such as git (default master)') |
1377 | parser_create.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)') | 1207 | parser_create.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)') |
1378 | parser_create.add_argument('--npm-dev', action="store_true", help='For npm, also fetch devDependencies') | 1208 | parser_create.add_argument('--npm-dev', action="store_true", help='For npm, also fetch devDependencies') |
1209 | parser_create.add_argument('--no-pypi', action="store_true", help='Do not inherit pypi class') | ||
1379 | parser_create.add_argument('--devtool', action="store_true", help=argparse.SUPPRESS) | 1210 | parser_create.add_argument('--devtool', action="store_true", help=argparse.SUPPRESS) |
1380 | parser_create.add_argument('--mirrors', action="store_true", help='Enable PREMIRRORS and MIRRORS for source tree fetching (disabled by default).') | 1211 | parser_create.add_argument('--mirrors', action="store_true", help='Enable PREMIRRORS and MIRRORS for source tree fetching (disabled by default).') |
1381 | parser_create.set_defaults(func=create_recipe) | 1212 | parser_create.set_defaults(func=create_recipe) |
1382 | |||