summaryrefslogtreecommitdiffstats
path: root/scripts/lib/recipetool/create_buildsys_python.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/lib/recipetool/create_buildsys_python.py')
-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))