Hello,

On 19/10/2023 09:36:53+0200, Julien Stephan wrote:
> add support for PEP517 [1]
> 
> if a pyproject.toml file is found, use it to create the recipe,
> otherwise fallback to the old setup.py method.
> 
> [YOCTO #14737]
> 
> [1]: https://peps.python.org/pep-0517/
> 
> Signed-off-by: Julien Stephan <jstep...@baylibre.com>
> ---
>  .../lib/recipetool/create_buildsys_python.py  | 234 +++++++++++++++++-
>  1 file changed, 233 insertions(+), 1 deletion(-)
> 
> diff --git a/scripts/lib/recipetool/create_buildsys_python.py 
> b/scripts/lib/recipetool/create_buildsys_python.py
> index 69f6f5ca511..0b601d50a4b 100644
> --- a/scripts/lib/recipetool/create_buildsys_python.py
> +++ b/scripts/lib/recipetool/create_buildsys_python.py
> @@ -18,6 +18,7 @@ import os
>  import re
>  import sys
>  import subprocess
> +import toml

This fails on the autobuilders because we don't have the toml module installed 
so I guess you need to add a dependency.

>  from recipetool.create import RecipeHandler
>  
>  logger = logging.getLogger('recipetool')
> @@ -656,6 +657,235 @@ class PythonSetupPyRecipeHandler(PythonRecipeHandler):
>  
>          handled.append('buildsystem')
>  
> +class PythonPyprojectTomlRecipeHandler(PythonRecipeHandler):
> +    """Base class to support PEP517 and PEP518
> +
> +    PEP517 https://peps.python.org/pep-0517/#source-trees
> +    PEP518 https://peps.python.org/pep-0518/#build-system-table
> +    """
> +
> +    # PEP621: 
> https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
> +    # add only the ones that map to a BB var
> +    # potentially missing: optional-dependencies
> +    bbvar_map = {
> +        "name": "PN",
> +        "version": "PV",
> +        "Homepage": "HOMEPAGE",
> +        "description": "SUMMARY",
> +        "license": "LICENSE",
> +        "dependencies": "RDEPENDS:${PN}",
> +        "requires": "DEPENDS",
> +    }
> +
> +    replacements = [
> +        ("license", r" +$", ""),
> +        ("license", r"^ +", ""),
> +        ("license", r" ", "-"),
> +        ("license", r"^GNU-", ""),
> +        ("license", r"-[Ll]icen[cs]e(,?-[Vv]ersion)?", ""),
> +        ("license", r"^UNKNOWN$", ""),
> +        # Remove currently unhandled version numbers from these variables
> +        ("requires", r"\[[^\]]+\]$", ""),
> +        ("requires", r"^([^><= ]+).*", r"\1"),
> +        ("dependencies", r"\[[^\]]+\]$", ""),
> +        ("dependencies", r"^([^><= ]+).*", r"\1"),
> +    ]
> +
> +    build_backend_map = {
> +        "setuptools.build_meta": "python_setuptools_build_meta",
> +        "poetry.core.masonry.api": "python_poetry_core",
> +        "flit_core.buildapi": "python_flit_core",
> +    }
> +
> +    excluded_native_pkgdeps = [
> +        # already provided by python_setuptools_build_meta.bbclass
> +        "python3-setuptools-native",
> +        "python3-wheel-native",
> +        # already provided by python_poetry_core.bbclass
> +        "python3-poetry-core-native",
> +        # already provided by python_flit_core.bbclass
> +        "python3-flit-core-native",
> +    ]
> +
> +    # add here a list of known and often used packages and the corresponding 
> bitbake package
> +    known_deps_map = {
> +        "setuptools": "python3-setuptools",
> +        "wheel": "python3-wheel",
> +        "poetry-core": "python3-poetry-core",
> +        "flit_core": "python3-flit-core",
> +        "setuptools-scm": "python3-setuptools-scm",
> +    }
> +
> +    def __init__(self):
> +        pass
> +
> +    def process(self, srctree, classes, lines_before, lines_after, handled, 
> extravalues):
> +        info = {}
> +
> +        if 'buildsystem' in handled:
> +            return False
> +
> +        # Check for non-zero size setup.py files
> +        setupfiles = RecipeHandler.checkfiles(srctree, ["pyproject.toml"])
> +        for fn in setupfiles:
> +            if os.path.getsize(fn):
> +                break
> +        else:
> +            return False
> +
> +        setupscript = os.path.join(srctree, "pyproject.toml")
> +
> +        try:
> +            config = self.parse_pyproject_toml(setupscript)
> +            build_backend = config["build-system"]["build-backend"]
> +            if build_backend in self.build_backend_map:
> +                classes.append(self.build_backend_map[build_backend])
> +            else:
> +                logger.error(
> +                    "Unsupported build-backend: %s, cannot use 
> pyproject.toml. Will try to use legacy setup.py"
> +                    % build_backend
> +                )
> +                return False
> +
> +            licfile = ""
> +            if "project" in config:
> +                for field, values in config["project"].items():
> +                    if field == "license":
> +                        value = values.get("text", "")
> +                        if not value:
> +                            licfile = values.get("file", "")
> +                    elif isinstance(values, dict):
> +                        for k, v in values.items():
> +                            info[k] = v
> +                        continue
> +                    else:
> +                        value = values
> +
> +                    info[field] = value
> +
> +            # Grab the license value before applying replacements
> +            license_str = info.get("license", "").strip()
> +
> +            if license_str:
> +                for i, line in enumerate(lines_before):
> +                    if line.startswith("##LICENSE_PLACEHOLDER##"):
> +                        lines_before.insert(
> +                            i, "# NOTE: License in pyproject.toml is: %s" % 
> license_str
> +                        )
> +                        break
> +
> +            info["requires"] = config["build-system"]["requires"]
> +
> +            self.apply_info_replacements(info)
> +
> +            if "classifiers" in info:
> +                license = self.handle_classifier_license(
> +                    info["classifiers"], info.get("license", "")
> +                )
> +                if license:
> +                    if licfile:
> +                        lines = []
> +                        md5value = bb.utils.md5_file(os.path.join(srctree, 
> licfile))
> +                        lines.append('LICENSE = "%s"' % license)
> +                        lines.append(
> +                            'LIC_FILES_CHKSUM = "file://%s;md5=%s"'
> +                            % (licfile, md5value)
> +                        )
> +                        lines.append("")
> +
> +                        # Replace the placeholder so we get the values in 
> the right place in the recipe file
> +                        try:
> +                            pos = 
> lines_before.index("##LICENSE_PLACEHOLDER##")
> +                        except ValueError:
> +                            pos = -1
> +                        if pos == -1:
> +                            lines_before.extend(lines)
> +                        else:
> +                            lines_before[pos : pos + 1] = lines
> +
> +                        handled.append(("license", [license, licfile, 
> md5value]))
> +                    else:
> +                        info["license"] = license
> +
> +            provided_packages = self.parse_pkgdata_for_python_packages()
> +            provided_packages.update(self.known_deps_map)
> +            native_mapped_deps, native_unmapped_deps = set(), set()
> +            mapped_deps, unmapped_deps = set(), set()
> +
> +            if "requires" in info:
> +                for require in info["requires"]:
> +                    mapped = provided_packages.get(require)
> +
> +                    if mapped:
> +                        logger.error("Mapped %s to %s" % (require, mapped))
> +                        native_mapped_deps.add(mapped)
> +                    else:
> +                        logger.error("Could not map %s" % require)
> +                        native_unmapped_deps.add(require)
> +
> +                info.pop("requires")
> +
> +                if native_mapped_deps != set():
> +                    native_mapped_deps = {
> +                        item + "-native" for item in native_mapped_deps
> +                    }
> +                    native_mapped_deps -= set(self.excluded_native_pkgdeps)
> +                    if native_mapped_deps != set():
> +                        info["requires"] = " 
> ".join(sorted(native_mapped_deps))
> +
> +                if native_unmapped_deps:
> +                    lines_after.append("")
> +                    lines_after.append(
> +                        "# WARNING: We were unable to map the following 
> python package/module"
> +                    )
> +                    lines_after.append(
> +                        "# dependencies to the bitbake packages which 
> include them:"
> +                    )
> +                    lines_after.extend(
> +                        "#    {}".format(d) for d in 
> sorted(native_unmapped_deps)
> +                    )
> +
> +            if "dependencies" in info:
> +                for dependency in info["dependencies"]:
> +                    mapped = provided_packages.get(dependency)
> +                    if mapped:
> +                        logger.error("Mapped %s to %s" % (dependency, 
> mapped))
> +                        mapped_deps.add(mapped)
> +                    else:
> +                        logger.error("Could not map %s" % dependency)
> +                        unmapped_deps.add(dependency)
> +
> +                info.pop("dependencies")
> +
> +                if mapped_deps != set():
> +                    if mapped_deps != set():
> +                        info["dependencies"] = " ".join(sorted(mapped_deps))
> +
> +                if unmapped_deps:
> +                    lines_after.append("")
> +                    lines_after.append(
> +                        "# WARNING: We were unable to map the following 
> python package/module"
> +                    )
> +                    lines_after.append(
> +                        "# runtime dependencies to the bitbake packages 
> which include them:"
> +                    )
> +                    lines_after.extend(
> +                        "#    {}".format(d) for d in sorted(unmapped_deps)
> +                    )
> +
> +            self.map_info_to_bbvar(info, extravalues)
> +
> +            handled.append("buildsystem")
> +        except Exception:
> +            logger.exception("Failed to parse pyproject.toml")
> +            return False
> +
> +    def parse_pyproject_toml(self, setupscript):
> +        with open(setupscript, "r") as f:
> +            config = toml.load(f)
> +        return config
> +
> +
>  def gather_setup_info(fileobj):
>      parsed = ast.parse(fileobj.read(), fileobj.name)
>      visitor = SetupScriptVisitor()
> @@ -769,5 +999,7 @@ def has_non_literals(value):
>  
>  
>  def register_recipe_handlers(handlers):
> -    # We need to make sure this is ahead of the makefile fallback handler
> +    # We need to make sure these are ahead of the makefile fallback handler
> +    # and the pyproject.toml handler ahead of the setup.py handler
> +    handlers.append((PythonPyprojectTomlRecipeHandler(), 75))
>      handlers.append((PythonSetupPyRecipeHandler(), 70))
> -- 
> 2.42.0
> 

> 
> 
> 


-- 
Alexandre Belloni, co-owner and COO, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com
-=-=-=-=-=-=-=-=-=-=-=-
Links: You receive all messages sent to this group.
View/Reply Online (#189462): 
https://lists.openembedded.org/g/openembedded-core/message/189462
Mute This Topic: https://lists.openembedded.org/mt/102055999/21656
Group Owner: openembedded-core+ow...@lists.openembedded.org
Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub 
[arch...@mail-archive.com]
-=-=-=-=-=-=-=-=-=-=-=-

Reply via email to