diff options
Diffstat (limited to 'scripts/lib')
| -rw-r--r-- | scripts/lib/recipetool/create_buildsys_python.py | 268 |
1 files changed, 267 insertions, 1 deletions
diff --git a/scripts/lib/recipetool/create_buildsys_python.py b/scripts/lib/recipetool/create_buildsys_python.py index 69f6f5ca51..9e7f22c0db 100644 --- a/scripts/lib/recipetool/create_buildsys_python.py +++ b/scripts/lib/recipetool/create_buildsys_python.py | |||
| @@ -656,6 +656,270 @@ class PythonSetupPyRecipeHandler(PythonRecipeHandler): | |||
| 656 | 656 | ||
| 657 | handled.append('buildsystem') | 657 | handled.append('buildsystem') |
| 658 | 658 | ||
| 659 | class PythonPyprojectTomlRecipeHandler(PythonRecipeHandler): | ||
| 660 | """Base class to support PEP517 and PEP518 | ||
| 661 | |||
| 662 | PEP517 https://peps.python.org/pep-0517/#source-trees | ||
| 663 | PEP518 https://peps.python.org/pep-0518/#build-system-table | ||
| 664 | """ | ||
| 665 | # bitbake currently support the 3 following backends | ||
| 666 | build_backend_map = { | ||
| 667 | "setuptools.build_meta": "python_setuptools_build_meta", | ||
| 668 | "poetry.core.masonry.api": "python_poetry_core", | ||
| 669 | "flit_core.buildapi": "python_flit_core", | ||
| 670 | } | ||
| 671 | |||
| 672 | # setuptools.build_meta and flit declare project metadata into the "project" section of pyproject.toml | ||
| 673 | # according to PEP-621: https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#declaring-project-metadata | ||
| 674 | # while poetry uses the "tool.poetry" section according to its official documentation: https://python-poetry.org/docs/pyproject/ | ||
| 675 | # keys from "project" and "tool.poetry" sections are almost the same except for the HOMEPAGE which is "homepage" for tool.poetry | ||
| 676 | # and "Homepage" for "project" section. So keep both | ||
| 677 | bbvar_map = { | ||
| 678 | "name": "PN", | ||
| 679 | "version": "PV", | ||
| 680 | "Homepage": "HOMEPAGE", | ||
| 681 | "homepage": "HOMEPAGE", | ||
| 682 | "description": "SUMMARY", | ||
| 683 | "license": "LICENSE", | ||
| 684 | "dependencies": "RDEPENDS:${PN}", | ||
| 685 | "requires": "DEPENDS", | ||
| 686 | } | ||
| 687 | |||
| 688 | replacements = [ | ||
| 689 | ("license", r" +$", ""), | ||
| 690 | ("license", r"^ +", ""), | ||
| 691 | ("license", r" ", "-"), | ||
| 692 | ("license", r"^GNU-", ""), | ||
| 693 | ("license", r"-[Ll]icen[cs]e(,?-[Vv]ersion)?", ""), | ||
| 694 | ("license", r"^UNKNOWN$", ""), | ||
| 695 | # Remove currently unhandled version numbers from these variables | ||
| 696 | ("requires", r"\[[^\]]+\]$", ""), | ||
| 697 | ("requires", r"^([^><= ]+).*", r"\1"), | ||
| 698 | ("dependencies", r"\[[^\]]+\]$", ""), | ||
| 699 | ("dependencies", r"^([^><= ]+).*", r"\1"), | ||
| 700 | ] | ||
| 701 | |||
| 702 | excluded_native_pkgdeps = [ | ||
| 703 | # already provided by python_setuptools_build_meta.bbclass | ||
| 704 | "python3-setuptools-native", | ||
| 705 | "python3-wheel-native", | ||
| 706 | # already provided by python_poetry_core.bbclass | ||
| 707 | "python3-poetry-core-native", | ||
| 708 | # already provided by python_flit_core.bbclass | ||
| 709 | "python3-flit-core-native", | ||
| 710 | ] | ||
| 711 | |||
| 712 | # add here a list of known and often used packages and the corresponding bitbake package | ||
| 713 | known_deps_map = { | ||
| 714 | "setuptools": "python3-setuptools", | ||
| 715 | "wheel": "python3-wheel", | ||
| 716 | "poetry-core": "python3-poetry-core", | ||
| 717 | "flit_core": "python3-flit-core", | ||
| 718 | "setuptools-scm": "python3-setuptools-scm", | ||
| 719 | } | ||
| 720 | |||
| 721 | def __init__(self): | ||
| 722 | pass | ||
| 723 | |||
| 724 | def process(self, srctree, classes, lines_before, lines_after, handled, extravalues): | ||
| 725 | info = {} | ||
| 726 | |||
| 727 | if 'buildsystem' in handled: | ||
| 728 | return False | ||
| 729 | |||
| 730 | # Check for non-zero size setup.py files | ||
| 731 | setupfiles = RecipeHandler.checkfiles(srctree, ["pyproject.toml"]) | ||
| 732 | for fn in setupfiles: | ||
| 733 | if os.path.getsize(fn): | ||
| 734 | break | ||
| 735 | else: | ||
| 736 | return False | ||
| 737 | |||
| 738 | setupscript = os.path.join(srctree, "pyproject.toml") | ||
| 739 | |||
| 740 | try: | ||
| 741 | try: | ||
| 742 | import tomllib | ||
| 743 | except ImportError: | ||
| 744 | try: | ||
| 745 | import tomli as tomllib | ||
| 746 | except ImportError: | ||
| 747 | logger.exception("Neither 'tomllib' nor 'tomli' could be imported. Please use python3.11 or above or install tomli module") | ||
| 748 | return False | ||
| 749 | except Exception: | ||
| 750 | logger.exception("Failed to parse pyproject.toml") | ||
| 751 | return False | ||
| 752 | |||
| 753 | with open(setupscript, "rb") as f: | ||
| 754 | config = tomllib.load(f) | ||
| 755 | build_backend = config["build-system"]["build-backend"] | ||
| 756 | if build_backend in self.build_backend_map: | ||
| 757 | classes.append(self.build_backend_map[build_backend]) | ||
| 758 | else: | ||
| 759 | logger.error( | ||
| 760 | "Unsupported build-backend: %s, cannot use pyproject.toml. Will try to use legacy setup.py" | ||
| 761 | % build_backend | ||
| 762 | ) | ||
| 763 | return False | ||
| 764 | |||
| 765 | licfile = "" | ||
| 766 | |||
| 767 | if build_backend == "poetry.core.masonry.api": | ||
| 768 | if "tool" in config and "poetry" in config["tool"]: | ||
| 769 | metadata = config["tool"]["poetry"] | ||
| 770 | else: | ||
| 771 | if "project" in config: | ||
| 772 | metadata = config["project"] | ||
| 773 | |||
| 774 | if metadata: | ||
| 775 | for field, values in metadata.items(): | ||
| 776 | if field == "license": | ||
| 777 | # For setuptools.build_meta and flit, licence is a table | ||
| 778 | # but for poetry licence is a string | ||
| 779 | if build_backend == "poetry.core.masonry.api": | ||
| 780 | value = values | ||
| 781 | else: | ||
| 782 | value = values.get("text", "") | ||
| 783 | if not value: | ||
| 784 | licfile = values.get("file", "") | ||
| 785 | continue | ||
| 786 | elif field == "dependencies" and build_backend == "poetry.core.masonry.api": | ||
| 787 | # For poetry backend, "dependencies" section looks like: | ||
| 788 | # [tool.poetry.dependencies] | ||
| 789 | # requests = "^2.13.0" | ||
| 790 | # requests = { version = "^2.13.0", source = "private" } | ||
| 791 | # See https://python-poetry.org/docs/master/pyproject/#dependencies-and-dependency-groups for more details | ||
| 792 | # This class doesn't handle versions anyway, so we just get the dependencies name here and construct a list | ||
| 793 | value = [] | ||
| 794 | for k in values.keys(): | ||
| 795 | value.append(k) | ||
| 796 | elif isinstance(values, dict): | ||
| 797 | for k, v in values.items(): | ||
| 798 | info[k] = v | ||
| 799 | continue | ||
| 800 | else: | ||
| 801 | value = values | ||
| 802 | |||
| 803 | info[field] = value | ||
| 804 | |||
| 805 | # Grab the license value before applying replacements | ||
| 806 | license_str = info.get("license", "").strip() | ||
| 807 | |||
| 808 | if license_str: | ||
| 809 | for i, line in enumerate(lines_before): | ||
| 810 | if line.startswith("##LICENSE_PLACEHOLDER##"): | ||
| 811 | lines_before.insert( | ||
| 812 | i, "# NOTE: License in pyproject.toml is: %s" % license_str | ||
| 813 | ) | ||
| 814 | break | ||
| 815 | |||
| 816 | info["requires"] = config["build-system"]["requires"] | ||
| 817 | |||
| 818 | self.apply_info_replacements(info) | ||
| 819 | |||
| 820 | if "classifiers" in info: | ||
| 821 | license = self.handle_classifier_license( | ||
| 822 | info["classifiers"], info.get("license", "") | ||
| 823 | ) | ||
| 824 | if license: | ||
| 825 | if licfile: | ||
| 826 | lines = [] | ||
| 827 | md5value = bb.utils.md5_file(os.path.join(srctree, licfile)) | ||
| 828 | lines.append('LICENSE = "%s"' % license) | ||
| 829 | lines.append( | ||
| 830 | 'LIC_FILES_CHKSUM = "file://%s;md5=%s"' | ||
| 831 | % (licfile, md5value) | ||
| 832 | ) | ||
| 833 | lines.append("") | ||
| 834 | |||
| 835 | # Replace the placeholder so we get the values in the right place in the recipe file | ||
| 836 | try: | ||
| 837 | pos = lines_before.index("##LICENSE_PLACEHOLDER##") | ||
| 838 | except ValueError: | ||
| 839 | pos = -1 | ||
| 840 | if pos == -1: | ||
| 841 | lines_before.extend(lines) | ||
| 842 | else: | ||
| 843 | lines_before[pos : pos + 1] = lines | ||
| 844 | |||
| 845 | handled.append(("license", [license, licfile, md5value])) | ||
| 846 | else: | ||
| 847 | info["license"] = license | ||
| 848 | |||
| 849 | provided_packages = self.parse_pkgdata_for_python_packages() | ||
| 850 | provided_packages.update(self.known_deps_map) | ||
| 851 | native_mapped_deps, native_unmapped_deps = set(), set() | ||
| 852 | mapped_deps, unmapped_deps = set(), set() | ||
| 853 | |||
| 854 | if "requires" in info: | ||
| 855 | for require in info["requires"]: | ||
| 856 | mapped = provided_packages.get(require) | ||
| 857 | |||
| 858 | if mapped: | ||
| 859 | logger.debug("Mapped %s to %s" % (require, mapped)) | ||
| 860 | native_mapped_deps.add(mapped) | ||
| 861 | else: | ||
| 862 | logger.debug("Could not map %s" % require) | ||
| 863 | native_unmapped_deps.add(require) | ||
| 864 | |||
| 865 | info.pop("requires") | ||
| 866 | |||
| 867 | if native_mapped_deps != set(): | ||
| 868 | native_mapped_deps = { | ||
| 869 | item + "-native" for item in native_mapped_deps | ||
| 870 | } | ||
| 871 | native_mapped_deps -= set(self.excluded_native_pkgdeps) | ||
| 872 | if native_mapped_deps != set(): | ||
| 873 | info["requires"] = " ".join(sorted(native_mapped_deps)) | ||
| 874 | |||
| 875 | if native_unmapped_deps: | ||
| 876 | lines_after.append("") | ||
| 877 | lines_after.append( | ||
| 878 | "# WARNING: We were unable to map the following python package/module" | ||
| 879 | ) | ||
| 880 | lines_after.append( | ||
| 881 | "# dependencies to the bitbake packages which include them:" | ||
| 882 | ) | ||
| 883 | lines_after.extend( | ||
| 884 | "# {}".format(d) for d in sorted(native_unmapped_deps) | ||
| 885 | ) | ||
| 886 | |||
| 887 | if "dependencies" in info: | ||
| 888 | for dependency in info["dependencies"]: | ||
| 889 | mapped = provided_packages.get(dependency) | ||
| 890 | if mapped: | ||
| 891 | logger.debug("Mapped %s to %s" % (dependency, mapped)) | ||
| 892 | mapped_deps.add(mapped) | ||
| 893 | else: | ||
| 894 | logger.debug("Could not map %s" % dependency) | ||
| 895 | unmapped_deps.add(dependency) | ||
| 896 | |||
| 897 | info.pop("dependencies") | ||
| 898 | |||
| 899 | if mapped_deps != set(): | ||
| 900 | if mapped_deps != set(): | ||
| 901 | info["dependencies"] = " ".join(sorted(mapped_deps)) | ||
| 902 | |||
| 903 | if unmapped_deps: | ||
| 904 | lines_after.append("") | ||
| 905 | lines_after.append( | ||
| 906 | "# WARNING: We were unable to map the following python package/module" | ||
| 907 | ) | ||
| 908 | lines_after.append( | ||
| 909 | "# runtime dependencies to the bitbake packages which include them:" | ||
| 910 | ) | ||
| 911 | lines_after.extend( | ||
| 912 | "# {}".format(d) for d in sorted(unmapped_deps) | ||
| 913 | ) | ||
| 914 | |||
| 915 | self.map_info_to_bbvar(info, extravalues) | ||
| 916 | |||
| 917 | handled.append("buildsystem") | ||
| 918 | except Exception: | ||
| 919 | logger.exception("Failed to correctly handle pyproject.toml, falling back to another method") | ||
| 920 | return False | ||
| 921 | |||
| 922 | |||
| 659 | def gather_setup_info(fileobj): | 923 | def gather_setup_info(fileobj): |
| 660 | parsed = ast.parse(fileobj.read(), fileobj.name) | 924 | parsed = ast.parse(fileobj.read(), fileobj.name) |
| 661 | visitor = SetupScriptVisitor() | 925 | visitor = SetupScriptVisitor() |
| @@ -769,5 +1033,7 @@ def has_non_literals(value): | |||
| 769 | 1033 | ||
| 770 | 1034 | ||
| 771 | def register_recipe_handlers(handlers): | 1035 | def register_recipe_handlers(handlers): |
| 772 | # We need to make sure this is ahead of the makefile fallback handler | 1036 | # We need to make sure these are ahead of the makefile fallback handler |
| 1037 | # and the pyproject.toml handler ahead of the setup.py handler | ||
| 1038 | handlers.append((PythonPyprojectTomlRecipeHandler(), 75)) | ||
| 773 | handlers.append((PythonSetupPyRecipeHandler(), 70)) | 1039 | handlers.append((PythonSetupPyRecipeHandler(), 70)) |
