summaryrefslogtreecommitdiffstats
path: root/scripts/lib
diff options
context:
space:
mode:
authorJulien Stephan <jstephan@baylibre.com>2023-10-25 17:46:58 +0200
committerRichard Purdie <richard.purdie@linuxfoundation.org>2023-10-27 08:28:38 +0100
commitd4debbf5b7ff9e0f489530aa748c559d631eeb57 (patch)
tree964a40422cebd4ac5ca84f1c37fdad96c04af5da /scripts/lib
parente64e92f2de4b182d087d7d88326aa7d06f59776e (diff)
downloadpoky-d4debbf5b7ff9e0f489530aa748c559d631eeb57.tar.gz
recipetool/create_buildsys_python: add PEP517 support
Add basic support for PEP517 [1] for the 3 following backends that are supported by bitbake: - setuptools.build_meta - poetry.core.masonry.api - flit_core.buildapi If a pyproject.toml file is found, use it to create the recipe, otherwise fallback to the old setup.py method. Some projects can declare a minimal pyproject.toml file, and put all the metadata in setup.py/setup.cfg/requirements.txt .. theses cases are not handled. If a pyproject.toml file is found, assumes it has all necessary metadata. As for the old setup.py method, version numbers for dependencies are not handled. Some features may be missing, such as the extra dependencies. [YOCTO #14737] [1]: https://peps.python.org/pep-0517/ (From OE-Core rev: c7d8d15b2d0a9ecd210bd247fa0df31d9f458873) Signed-off-by: Julien Stephan <jstephan@baylibre.com> Signed-off-by: Luca Ceresoli <luca.ceresoli@bootlin.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'scripts/lib')
-rw-r--r--scripts/lib/recipetool/create_buildsys_python.py268
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
659class 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
659def gather_setup_info(fileobj): 923def 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
771def register_recipe_handlers(handlers): 1035def 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))