[RFC,1/2] distutils3-legacy: fallback for missing setup.py

Message ID AM9PR09MB46426836B8FC0FACE562A084A8619@AM9PR09MB4642.eurprd09.prod.outlook.com
State New
Headers show
Series [RFC,1/2] distutils3-legacy: fallback for missing setup.py | expand

Commit Message

Konrad Weihmann Nov. 24, 2021, 8:15 p.m. UTC
add a bbclass to disutils3 that generates a fallback setup.py in case
there is no setup.py available in the source dir, but a setup.cfg.

Use the mapping provided by
https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
to translate the most essential items to legacy setuptools.setup
dictitonary.

Signed-off-by: Konrad Weihmann <kweihmann@outlook.com>
---
 meta/classes/distutils3-legacy.bbclass | 112 +++++++++++++++++++++++++
 meta/classes/distutils3.bbclass        |   1 +
 2 files changed, 113 insertions(+)
 create mode 100644 meta/classes/distutils3-legacy.bbclass

Comments

Tim Orling Nov. 24, 2021, 9:02 p.m. UTC | #1
On Wed, Nov 24, 2021 at 12:15 PM Konrad Weihmann <kweihmann@outlook.com>
wrote:

> add a bbclass to disutils3 that generates a fallback setup.py in case
> there is no setup.py available in the source dir, but a setup.cfg.
>

I’ll check this out later, but I do want to highlight that we will be
deprecating distutils bbclasses (and moving them to meta-python for
continuity).

https://bugzilla.yoctoproject.org/show_bug.cgi?id=14610


We can refactor this series into the refactored setuptools3.bbclass (sans
distutils). I will do this if we merge these changes.

https://git.yoctoproject.org/poky-contrib/log/?h=timo/nodistutils_14610

I’d like to hear what concerns others have.


> Use the mapping provided by
> https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
> to translate the most essential items to legacy setuptools.setup
> dictitonary.
>
> Signed-off-by: Konrad Weihmann <kweihmann@outlook.com>
> ---
>  meta/classes/distutils3-legacy.bbclass | 112 +++++++++++++++++++++++++
>  meta/classes/distutils3.bbclass        |   1 +
>  2 files changed, 113 insertions(+)
>  create mode 100644 meta/classes/distutils3-legacy.bbclass
>
> diff --git a/meta/classes/distutils3-legacy.bbclass
> b/meta/classes/distutils3-legacy.bbclass
> new file mode 100644
> index 0000000000..266d30138f
> --- /dev/null
> +++ b/meta/classes/distutils3-legacy.bbclass
> @@ -0,0 +1,112 @@
> +# Helper to create a trimmed down setup.py from information found in
> +# setup.cfg, in case there is no setup.py shipped with the sources
> +
> +# this functionality can be safely removed once the pypa community
> +# comes up with a safe replacement for the functionality found in
> distutils3.bbclass
> +
> +def distutils_legacy_package_name(d):
> +    # use pypi name or fall back to BPN
> +    return d.getVar("PYPI_PACKAGE") or d.getVar('BPN').replace('python-',
> '').replace('python3-', '')
> +
> +DISTUTILS_LEGACY_VERSION ?= "${PV}"
> +DISTUTILS_LEGACY_NAME ?= "${@distutils_legacy_package_name(d)}"
> +
> +python do_create_setup_py_legacy() {
> +    import os
> +
> +    if os.path.exists(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
> "setup.py")):
> +        return
> +
> +    from configparser import ConfigParser, NoOptionError, NoSectionError,
> ParsingError
> +    import re
> +
> +    config = ConfigParser()
> +    try:
> +        config.read(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
> "setup.cfg"))
> +    except FileNotFoundError:
> +        return
> +
> +    def _strip(x):
> +        return re.sub(r"\s|\t|\n", "", x)
> +
> +    def get_section(section):
> +        try:
> +            return dict(config.items(section=section))
> +        except (NoSectionError, ParsingError):
> +            return None
> +
> +    def get_option(section, option):
> +        try:
> +            return config.get(section=section, option=option)
> +        except (NoOptionError, NoSectionError, ParsingError):
> +            return None
> +
> +    def extract_bool(section, option, default):
> +        _option = get_option(section, option)
> +        if _option is None:
> +            return default
> +        return bool(_strip(_option))
> +
> +    def extract_str(section, option, default):
> +        _option = get_option(section, option)
> +        if _option is None:
> +            return default
> +        return _strip(_option)
> +
> +    def extract_dict_vallist(section, default, delim=""):
> +        _section = get_section(section)
> +        if _section is None:
> +            return default
> +        return {_strip(k): [_strip(x) for x in re.split(delim, v)] if
> delim else [ _strip(v) ] for k, v in _section.items()}
> +
> +    def extract_dict(section, default):
> +        _section = get_section(section)
> +        if _section is None:
> +            return default
> +        return {_strip(k): _strip(v) for k, v in _section.items()}
> +
> +    def extract_list(section, option, default, delim):
> +        _option = get_option(section, option)
> +        if _option is None:
> +            return default
> +        bb.warn("%s:%s -> %s" % (section, option, _option))
> +        _listitems = re.split(delim, _option) if delim else [_option]
> +        return [_strip(x) for x in _listitems]
> +
> +    def quote(x):
> +        return '"%s"' % x
> +
> +    _pkginfo = {
> +        "entry_points": extract_dict_vallist("options.entry_points", {}),
> +        "include_package_data": extract_bool("options",
> "include_package_data", False),
> +        "name": quote(extract_str("options", "name",
> d.getVar("DISTUTILS_LEGACY_NAME"))),
> +        "package_data": extract_dict_vallist("options.package_data", {},
> r"\s+|,"),
> +        "packages": extract_list("options", "packages", [], ""),
> +        "version": quote(d.getVar("DISTUTILS_LEGACY_VERSION")),
> +        "zip_safe": extract_bool("options", "zip_safe", False),
> +        "install_requires": extract_list("options", "install_requires",
> [], r"\t+|\n+"),
> +        "python_requires": quote(extract_str("options",
> "python_requires", ">0.0")),
> +        "package_dir": extract_dict("package_dir", {}),
> +        "py_modules": extract_list("options", "py_modules", [], r"\s+|,"),
> +    }
> +
> +    # In case packages is using :find module
> +    # we need to look for top level directories containing a __init__.py
> +    if _pkginfo["packages"] == ["find:"]:
> +        # top level search dir can be adjusted by options.packages.find
> option
> +        _path = extract_str("options.packages.find", "where", "")
> +        _pkginfo["packages"] = set(x.name for x in
> os.scandir(os.path.join(
> +            d.getVar("DISTUTILS_SETUP_PATH"), _path)) if os.path.isdir(x)
> and os.path.exists(os.path.join(x, "__init__.py")))
> +
> +    with open(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"), "setup.py"),
> "w") as o:
> +        o.write("import setuptools\n")
> +        o.write("setuptools.setup(\n")
> +        for k, v in _pkginfo.items():
> +            o.write("%s = %s,\n" % (str(k), str(v)))
> +        o.write(")")
> +
> +
> +}
> +
> +do_create_setup_py_legacy[doc] = "Create a fallback version of legacy
> setup.py if not existing"
> +addtask do_create_setup_py_legacy before do_configure after do_patch
> do_prepare_recipe_sysroot
> diff --git a/meta/classes/distutils3.bbclass
> b/meta/classes/distutils3.bbclass
> index be645d37bd..f26f0d5184 100644
> --- a/meta/classes/distutils3.bbclass
> +++ b/meta/classes/distutils3.bbclass
> @@ -1,4 +1,5 @@
>  inherit distutils3-base
> +inherit distutils3-legacy
>
>  B = "${WORKDIR}/build"
>  distutils_do_configure[cleandirs] = "${B}"
> --
> 2.25.1
>
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#158741):
> https://lists.openembedded.org/g/openembedded-core/message/158741
> Mute This Topic: https://lists.openembedded.org/mt/87289428/924729
> Group Owner: openembedded-core+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-core/unsub [
> ticotimo@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
>
Konrad Weihmann Nov. 25, 2021, 10:14 a.m. UTC | #2
On 24.11.21 22:02, Tim Orling wrote:
> 
> 
> On Wed, Nov 24, 2021 at 12:15 PM Konrad Weihmann <kweihmann@outlook.com 
> <mailto:kweihmann@outlook.com>> wrote:
> 
>     add a bbclass to disutils3 that generates a fallback setup.py in case
>     there is no setup.py available in the source dir, but a setup.cfg.
> 
> 
> I’ll check this out later, but I do want to highlight that we will be 
> deprecating distutils bbclasses (and moving them to meta-python for 
> continuity).
> 
> https://bugzilla.yoctoproject.org/show_bug.cgi?id=14610 
> <https://bugzilla.yoctoproject.org/show_bug.cgi?id=14610>

Not sure if we should support it at all after the rework is done.
Most of the python modules that haven't made at least the move to 
setup.cfg will stop working with py3.12 anyway - putting some pressure 
on the respective upstream to migrate.
So what is the benefit in moving distutils-support to meta-python (which 
btw bundles poky/meta to openembedded-layer - a move that is discouraged 
by a couple of projects I came across so far)?

I mean the patches currently in the works likely will affect kirkstone+ 
releases only, right? so current already released branches will remain 
unaffected by this switch - or is there a plan to even backport this 
change to other releases?

> 
> 
> We can refactor this series into the refactored setuptools3.bbclass 
> (sans distutils). I will do this if we merge these changes.
> 
> https://git.yoctoproject.org/poky-contrib/log/?h=timo/nodistutils_14610 
> <https://git.yoctoproject.org/poky-contrib/log/?h=timo/nodistutils_14610>

Fine for me - I would be also be fine, if we could use my patch for an 
intermediate period, till the removal of distutils is done.

> 
> I’d like to hear what concerns others have.
> 
> 
>     Use the mapping provided by
>     https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
>     <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>
>     to translate the most essential items to legacy setuptools.setup
>     dictitonary.
> 
>     Signed-off-by: Konrad Weihmann <kweihmann@outlook.com
>     <mailto:kweihmann@outlook.com>>
>     ---
>       meta/classes/distutils3-legacy.bbclass | 112 +++++++++++++++++++++++++
>       meta/classes/distutils3.bbclass        |   1 +
>       2 files changed, 113 insertions(+)
>       create mode 100644 meta/classes/distutils3-legacy.bbclass
> 
>     diff --git a/meta/classes/distutils3-legacy.bbclass
>     b/meta/classes/distutils3-legacy.bbclass
>     new file mode 100644
>     index 0000000000..266d30138f
>     --- /dev/null
>     +++ b/meta/classes/distutils3-legacy.bbclass
>     @@ -0,0 +1,112 @@
>     +# Helper to create a trimmed down setup.py from information found in
>     +# setup.cfg, in case there is no setup.py shipped with the sources
>     +
>     +# this functionality can be safely removed once the pypa community
>     +# comes up with a safe replacement for the functionality found in
>     distutils3.bbclass
>     +
>     +def distutils_legacy_package_name(d):
>     +    # use pypi name or fall back to BPN
>     +    return d.getVar("PYPI_PACKAGE") or
>     d.getVar('BPN').replace('python-', '').replace('python3-', '')
>     +
>     +DISTUTILS_LEGACY_VERSION ?= "${PV}"
>     +DISTUTILS_LEGACY_NAME ?= "${@distutils_legacy_package_name(d)}"
>     +
>     +python do_create_setup_py_legacy() {
>     +    import os
>     +
>     +    if
>     os.path.exists(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
>     "setup.py")):
>     +        return
>     +
>     +    from configparser import ConfigParser, NoOptionError,
>     NoSectionError, ParsingError
>     +    import re
>     +
>     +    config = ConfigParser()
>     +    try:
>     +        config.read(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
>     "setup.cfg"))
>     +    except FileNotFoundError:
>     +        return
>     +
>     +    def _strip(x):
>     +        return re.sub(r"\s|\t|\n", "", x)
>     +
>     +    def get_section(section):
>     +        try:
>     +            return dict(config.items(section=section))
>     +        except (NoSectionError, ParsingError):
>     +            return None
>     +
>     +    def get_option(section, option):
>     +        try:
>     +            return config.get(section=section, option=option)
>     +        except (NoOptionError, NoSectionError, ParsingError):
>     +            return None
>     +
>     +    def extract_bool(section, option, default):
>     +        _option = get_option(section, option)
>     +        if _option is None:
>     +            return default
>     +        return bool(_strip(_option))
>     +
>     +    def extract_str(section, option, default):
>     +        _option = get_option(section, option)
>     +        if _option is None:
>     +            return default
>     +        return _strip(_option)
>     +
>     +    def extract_dict_vallist(section, default, delim=""):
>     +        _section = get_section(section)
>     +        if _section is None:
>     +            return default
>     +        return {_strip(k): [_strip(x) for x in re.split(delim, v)]
>     if delim else [ _strip(v) ] for k, v in _section.items()}
>     +
>     +    def extract_dict(section, default):
>     +        _section = get_section(section)
>     +        if _section is None:
>     +            return default
>     +        return {_strip(k): _strip(v) for k, v in _section.items()}
>     +
>     +    def extract_list(section, option, default, delim):
>     +        _option = get_option(section, option)
>     +        if _option is None:
>     +            return default
>     +        bb.warn("%s:%s -> %s" % (section, option, _option))
>     +        _listitems = re.split(delim, _option) if delim else [_option]
>     +        return [_strip(x) for x in _listitems]
>     +
>     +    def quote(x):
>     +        return '"%s"' % x
>     +
>     +    _pkginfo = {
>     +        "entry_points":
>     extract_dict_vallist("options.entry_points", {}),
>     +        "include_package_data": extract_bool("options",
>     "include_package_data", False),
>     +        "name": quote(extract_str("options", "name",
>     d.getVar("DISTUTILS_LEGACY_NAME"))),
>     +        "package_data":
>     extract_dict_vallist("options.package_data", {}, r"\s+|,"),
>     +        "packages": extract_list("options", "packages", [], ""),
>     +        "version": quote(d.getVar("DISTUTILS_LEGACY_VERSION")),
>     +        "zip_safe": extract_bool("options", "zip_safe", False),
>     +        "install_requires": extract_list("options",
>     "install_requires", [], r"\t+|\n+"),
>     +        "python_requires": quote(extract_str("options",
>     "python_requires", ">0.0")),
>     +        "package_dir": extract_dict("package_dir", {}),
>     +        "py_modules": extract_list("options", "py_modules", [],
>     r"\s+|,"),
>     +    }
>     +
>     +    # In case packages is using :find module
>     +    # we need to look for top level directories containing a
>     __init__.py
>     +    if _pkginfo["packages"] == ["find:"]:
>     +        # top level search dir can be adjusted by
>     options.packages.find option
>     +        _path = extract_str("options.packages.find", "where", "")
>     +        _pkginfo["packages"] = set(x.name <http://x.name> for x in
>     os.scandir(os.path.join(
>     +            d.getVar("DISTUTILS_SETUP_PATH"), _path)) if
>     os.path.isdir(x) and os.path.exists(os.path.join(x, "__init__.py")))
>     +
>     +    with open(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"),
>     "setup.py"), "w") as o:
>     +        o.write("import setuptools\n")
>     +        o.write("setuptools.setup(\n")
>     +        for k, v in _pkginfo.items():
>     +            o.write("%s = %s,\n" % (str(k), str(v)))
>     +        o.write(")")
>     +
>     +
>     +}
>     +
>     +do_create_setup_py_legacy[doc] = "Create a fallback version of
>     legacy setup.py if not existing"
>     +addtask do_create_setup_py_legacy before do_configure after
>     do_patch do_prepare_recipe_sysroot
>     diff --git a/meta/classes/distutils3.bbclass
>     b/meta/classes/distutils3.bbclass
>     index be645d37bd..f26f0d5184 100644
>     --- a/meta/classes/distutils3.bbclass
>     +++ b/meta/classes/distutils3.bbclass
>     @@ -1,4 +1,5 @@
>       inherit distutils3-base
>     +inherit distutils3-legacy
> 
>       B = "${WORKDIR}/build"
>       distutils_do_configure[cleandirs] = "${B}"
>     -- 
>     2.25.1
> 
> 
>     -=-=-=-=-=-=-=-=-=-=-=-
>     Links: You receive all messages sent to this group.
>     View/Reply Online (#158741):
>     https://lists.openembedded.org/g/openembedded-core/message/158741
>     <https://lists.openembedded.org/g/openembedded-core/message/158741>
>     Mute This Topic: https://lists.openembedded.org/mt/87289428/924729
>     <https://lists.openembedded.org/mt/87289428/924729>
>     Group Owner: openembedded-core+owner@lists.openembedded.org
>     <mailto:openembedded-core%2Bowner@lists.openembedded.org>
>     Unsubscribe:
>     https://lists.openembedded.org/g/openembedded-core/unsub
>     <https://lists.openembedded.org/g/openembedded-core/unsub>
>     [ticotimo@gmail.com <mailto:ticotimo@gmail.com>]
>     -=-=-=-=-=-=-=-=-=-=-=-
>

Patch

diff --git a/meta/classes/distutils3-legacy.bbclass b/meta/classes/distutils3-legacy.bbclass
new file mode 100644
index 0000000000..266d30138f
--- /dev/null
+++ b/meta/classes/distutils3-legacy.bbclass
@@ -0,0 +1,112 @@ 
+# Helper to create a trimmed down setup.py from information found in
+# setup.cfg, in case there is no setup.py shipped with the sources
+
+# this functionality can be safely removed once the pypa community
+# comes up with a safe replacement for the functionality found in distutils3.bbclass
+
+def distutils_legacy_package_name(d):
+    # use pypi name or fall back to BPN
+    return d.getVar("PYPI_PACKAGE") or d.getVar('BPN').replace('python-', '').replace('python3-', '')
+
+DISTUTILS_LEGACY_VERSION ?= "${PV}"
+DISTUTILS_LEGACY_NAME ?= "${@distutils_legacy_package_name(d)}"
+
+python do_create_setup_py_legacy() {
+    import os 
+
+    if os.path.exists(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"), "setup.py")):
+        return
+
+    from configparser import ConfigParser, NoOptionError, NoSectionError, ParsingError
+    import re
+
+    config = ConfigParser()
+    try:
+        config.read(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"), "setup.cfg"))
+    except FileNotFoundError:
+        return
+
+    def _strip(x):
+        return re.sub(r"\s|\t|\n", "", x)
+
+    def get_section(section):
+        try:
+            return dict(config.items(section=section))
+        except (NoSectionError, ParsingError):
+            return None
+
+    def get_option(section, option):
+        try:
+            return config.get(section=section, option=option)
+        except (NoOptionError, NoSectionError, ParsingError):
+            return None
+
+    def extract_bool(section, option, default):
+        _option = get_option(section, option)
+        if _option is None:
+            return default
+        return bool(_strip(_option))
+
+    def extract_str(section, option, default):
+        _option = get_option(section, option)
+        if _option is None:
+            return default
+        return _strip(_option)
+
+    def extract_dict_vallist(section, default, delim=""):
+        _section = get_section(section)
+        if _section is None:
+            return default
+        return {_strip(k): [_strip(x) for x in re.split(delim, v)] if delim else [ _strip(v) ] for k, v in _section.items()}
+
+    def extract_dict(section, default):
+        _section = get_section(section)
+        if _section is None:
+            return default
+        return {_strip(k): _strip(v) for k, v in _section.items()}
+
+    def extract_list(section, option, default, delim):
+        _option = get_option(section, option)
+        if _option is None:
+            return default
+        bb.warn("%s:%s -> %s" % (section, option, _option))
+        _listitems = re.split(delim, _option) if delim else [_option]
+        return [_strip(x) for x in _listitems]
+
+    def quote(x):
+        return '"%s"' % x
+
+    _pkginfo = {
+        "entry_points": extract_dict_vallist("options.entry_points", {}),
+        "include_package_data": extract_bool("options", "include_package_data", False),
+        "name": quote(extract_str("options", "name", d.getVar("DISTUTILS_LEGACY_NAME"))),
+        "package_data": extract_dict_vallist("options.package_data", {}, r"\s+|,"),
+        "packages": extract_list("options", "packages", [], ""),
+        "version": quote(d.getVar("DISTUTILS_LEGACY_VERSION")),
+        "zip_safe": extract_bool("options", "zip_safe", False),
+        "install_requires": extract_list("options", "install_requires", [], r"\t+|\n+"),
+        "python_requires": quote(extract_str("options", "python_requires", ">0.0")),
+        "package_dir": extract_dict("package_dir", {}),
+        "py_modules": extract_list("options", "py_modules", [], r"\s+|,"),
+    }
+
+    # In case packages is using :find module
+    # we need to look for top level directories containing a __init__.py
+    if _pkginfo["packages"] == ["find:"]:
+        # top level search dir can be adjusted by options.packages.find option
+        _path = extract_str("options.packages.find", "where", "")
+        _pkginfo["packages"] = set(x.name for x in os.scandir(os.path.join(
+            d.getVar("DISTUTILS_SETUP_PATH"), _path)) if os.path.isdir(x) and os.path.exists(os.path.join(x, "__init__.py")))
+
+    with open(os.path.join(d.getVar("DISTUTILS_SETUP_PATH"), "setup.py"), "w") as o:
+        o.write("import setuptools\n")
+        o.write("setuptools.setup(\n")
+        for k, v in _pkginfo.items():
+            o.write("%s = %s,\n" % (str(k), str(v)))
+        o.write(")")
+
+    
+}
+
+do_create_setup_py_legacy[doc] = "Create a fallback version of legacy setup.py if not existing"
+addtask do_create_setup_py_legacy before do_configure after do_patch do_prepare_recipe_sysroot
diff --git a/meta/classes/distutils3.bbclass b/meta/classes/distutils3.bbclass
index be645d37bd..f26f0d5184 100644
--- a/meta/classes/distutils3.bbclass
+++ b/meta/classes/distutils3.bbclass
@@ -1,4 +1,5 @@ 
 inherit distutils3-base
+inherit distutils3-legacy
 
 B = "${WORKDIR}/build"
 distutils_do_configure[cleandirs] = "${B}"