diff options
Diffstat (limited to 'scripts/lib/recipetool/create_buildsys_python.py')
-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)) |