Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setuptools 64 broke editable build fallback when no compiler is found. #8458

Closed
CaselIT opened this issue Aug 29, 2022 · 25 comments
Closed

setuptools 64 broke editable build fallback when no compiler is found. #8458

CaselIT opened this issue Aug 29, 2022 · 25 comments
Labels
setup issues related to installation and setup sqlalchemy.ext extension modules, most of which are ORM related
Milestone

Comments

@CaselIT
Copy link
Member

CaselIT commented Aug 29, 2022

Currently running pip install -e . on a machine without compiler breaks the build.
This is most likely due to pypa/setuptools#3488

step to reproduce:

apt update && apt install git
git clone --depth=1 https://github.com/sqlalchemy/sqlalchemy.git
cd sqlalchemy
pip install -e .
The output is something like
× Building editable for SQLAlchemy (pyproject.toml) did not run successfully.
  │ exit code: 1
  ╰─> [108 lines of output]
      running editable_wheel
      creating /tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy.egg-info
      writing /tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy.egg-info/PKG-INFO
      writing dependency_links to /tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy.egg-info/dependency_links.txt
      writing requirements to /tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy.egg-info/requires.txt
      writing top-level names to /tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy.egg-info/top_level.txt
      writing manifest file '/tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy.egg-info/SOURCES.txt'
      reading manifest file '/tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy.egg-info/SOURCES.txt'
      reading manifest template 'MANIFEST.in'
      warning: no files found matching '*.html' under directory 'doc'
      warning: no files found matching '*.css' under directory 'doc'
      warning: no files found matching '*.js' under directory 'doc'
      warning: no previously-included files found matching 'lib/sqlalchemy/cyextension/*.c'
      warning: no previously-included files found matching 'lib/sqlalchemy/cyextension/*.so'
      no previously-included directories found matching 'doc/build/output'
      adding license file 'LICENSE'
      writing manifest file '/tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy.egg-info/SOURCES.txt'
      creating '/tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy-2.0.0b1.dev0.dist-info'
      adding license file "LICENSE" (matched pattern "LICENSE")
      creating /tmp/pip-wheel-dex41m14/tmpr9ru2lxp/SQLAlchemy-2.0.0b1.dev0.dist-info/WHEEL
      /tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/command/install.py:34: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.
        warnings.warn(
      running build_py
      running build_ext
      cythoning lib/sqlalchemy/cyextension/collections.pyx to lib/sqlalchemy/cyextension/collections.c
      cythoning lib/sqlalchemy/cyextension/immutabledict.pyx to lib/sqlalchemy/cyextension/immutabledict.c
      cythoning lib/sqlalchemy/cyextension/processors.pyx to lib/sqlalchemy/cyextension/processors.c
      cythoning lib/sqlalchemy/cyextension/resultproxy.pyx to lib/sqlalchemy/cyextension/resultproxy.c
      cythoning lib/sqlalchemy/cyextension/util.pyx to lib/sqlalchemy/cyextension/util.c
      building 'sqlalchemy.cyextension.collections' extension
      creating /tmp/tmpjgd8_e6s.build-temp/lib
      creating /tmp/tmpjgd8_e6s.build-temp/lib/sqlalchemy
      creating /tmp/tmpjgd8_e6s.build-temp/lib/sqlalchemy/cyextension
      gcc -pthread -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/usr/local/include/python3.11 -c lib/sqlalchemy/cyextension/collections.c -o /tmp/tmpjgd8_e6s.build-temp/lib/sqlalchemy/cyextension/collections.o
      Traceback (most recent call last):
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/spawn.py", line 57, in spawn
          proc = subprocess.Popen(cmd, env=env)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/usr/local/lib/python3.11/subprocess.py", line 1022, in __init__
          self._execute_child(args, executable, preexec_fn, close_fds,
        File "/usr/local/lib/python3.11/subprocess.py", line 1899, in _execute_child
          raise child_exception_type(errno_num, err_msg, err_filename)
      FileNotFoundError: [Errno 2] No such file or directory: 'gcc'

      The above exception was the direct cause of the following exception:

      Traceback (most recent call last):
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/unixccompiler.py", line 186, in _compile
          self.spawn(compiler_so + cc_args + [src, '-o', obj] + extra_postargs)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/ccompiler.py", line 1007, in spawn
          spawn(cmd, dry_run=self.dry_run, **kwargs)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/spawn.py", line 63, in spawn
          raise DistutilsExecError(
      distutils.errors.DistutilsExecError: command 'gcc' failed: No such file or directory

      During handling of the above exception, another exception occurred:

      Traceback (most recent call last):
        File "<string>", line 84, in build_extension
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/command/build_ext.py", line 547, in build_extension
          objects = self.compiler.compile(
                    ^^^^^^^^^^^^^^^^^^^^^^
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/ccompiler.py", line 599, in compile
          self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/unixccompiler.py", line 188, in _compile
          raise CompileError(msg)
      distutils.errors.CompileError: command 'gcc' failed: No such file or directory

      The above exception was the direct cause of the following exception:

      Traceback (most recent call last):
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 140, in run
          self._create_wheel_file(bdist_wheel)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 330, in _create_wheel_file
          files, mapping = self._run_build_commands(dist_name, unpacked, lib, tmp)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 261, in _run_build_commands
          self._run_build_subcommands()
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/command/editable_wheel.py", line 288, in _run_build_subcommands
          self.run_command(name)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/cmd.py", line 319, in run_command
          self.distribution.run_command(command)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/dist.py", line 1217, in run_command
          super().run_command(command)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/dist.py", line 992, in run_command
          cmd_obj.run()
        File "<string>", line 78, in run
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/Cython/Distutils/old_build_ext.py", line 186, in run
          _build_ext.build_ext.run(self)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/command/build_ext.py", line 346, in run
          self.build_extensions()
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/Cython/Distutils/old_build_ext.py", line 195, in build_extensions
          _build_ext.build_ext.build_extensions(self)
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/command/build_ext.py", line 466, in build_extensions
          self._build_extensions_serial()
        File "/tmp/pip-build-env-njvb0gv5/overlay/lib/python3.11/site-packages/setuptools/_distutils/command/build_ext.py", line 492, in _build_extensions_serial
          self.build_extension(ext)
        File "<string>", line 86, in build_extension
      BuildFailed
      error: Support for editable installs via PEP 660 was recently introduced
      in `setuptools`. If you are seeing this error, please report to:

      https://github.com/pypa/setuptools/issues

      Meanwhile you can try the legacy behavior by setting an
      environment variable and trying to install again:

      SETUPTOOLS_ENABLE_FEATURES="legacy-editable"
      [end of output]

At the moment the normal install is working, so running pip install . works, but it may be the next thing that breaks.

From what I gather the issue is that the build is not run in a subprocess, so the raised exception is SystemExit. Catching SystemExit and retrying in that case does not fix the issue

@CaselIT CaselIT added sqlalchemy.ext extension modules, most of which are ORM related setup issues related to installation and setup labels Aug 29, 2022
@CaselIT CaselIT added this to the 1.4.x milestone Aug 29, 2022
@zzzeek
Copy link
Member

zzzeek commented Aug 29, 2022

have you tested on 1.4? I notice cython in the script output above.

@zzzeek
Copy link
Member

zzzeek commented Aug 29, 2022

also can you clarify that this is only with -e from a source install, not a regaulr pip with the .tar.gz? if so, this is nearly a non-issue

@CaselIT
Copy link
Member Author

CaselIT commented Aug 29, 2022

yes, it's the same, the issue is not cython, but the fact that we try compiling then fallback to pure python

@CaselIT
Copy link
Member Author

CaselIT commented Aug 29, 2022

also can you clarify that this is only with -e from a source install, not a regaulr pip with the .tar.gz? if so, this is nearly a non-issue

correct, normal, non editable install works ok

@CaselIT
Copy link
Member Author

CaselIT commented Aug 29, 2022

this patch fixes it, (should probably be improved in checking better the system exit case)

diff --git a/setup.py b/setup.py
index 604958922..847897d58 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,7 @@
 import os
+from pathlib import Path
 import platform
+import shutil
 import sys

 from setuptools import __version__
@@ -126,8 +128,17 @@ def run_setup(with_cext):
             )

         kwargs["ext_modules"] = []
-
-    setup(cmdclass=cmdclass, distclass=Distribution, **kwargs)
+    dist_dir = None
+    if "--dist-dir" in sys.argv:
+        kwargs["script_args"] = script_args = sys.argv[1:]
+        index = script_args.index("--dist-dir") + 1
+        dist_dir = script_args[index]
+        script_args[index] = dist_dir + str(with_cext)
+    setup(cmdclass=cmdclass, distclass=Distribution, **kwargs)
+    if dist_dir:
+        if Path(dist_dir).is_dir():
+            shutil.rmtree(dist_dir)
+        Path(dist_dir + str(with_cext)).rename(dist_dir)


 if not cpython:
@@ -152,10 +163,8 @@ elif os.environ.get("DISABLE_SQLALCHEMY_CEXT"):
         "Plain-Python build succeeded.",
     )
 else:
-    try:
-        run_setup(True)
-    except BuildFailed as exc:

+    def retry(exc):
         if os.environ.get("REQUIRE_SQLALCHEMY_CEXT"):
             status_msgs(
                 "NOTE: Cython extension build is required because "
@@ -179,3 +188,10 @@ else:
             "speedups are not enabled.",
             "Plain-Python build succeeded.",
         )
+
+    try:
+        run_setup(True)
+    except BuildFailed as exc:
+        retry(exc)
+    except SystemExit as exc:
+        retry(exc)

that said, it's very hacky and it's likely not overly robust.

Mike do you think it makes sense opening an issue on setuptools to ask for advide?

@zzzeek
Copy link
Member

zzzeek commented Aug 29, 2022

we are already subclassing the build_ext command, so I need to understand the problem better before I would know if we have any chance of them helping us. are the same run() / build_extension() methods being called? what's --dist-dir ?

@CaselIT
Copy link
Member Author

CaselIT commented Aug 29, 2022

The change seems to be how the editable build is run. Now it works more or less like this:

  • a subprocess is created
  • the build requirements are installed like in a normal install in a virtual env
  • setup.py is called with egg_info command
  • setup.py is called with dist_info command
  • setup.py is called with editable_wheel command (here it has the --dist-dir folder
  • if a call to setup fails system exit is called, it no longer propagates the original error

@zzzeek
Copy link
Member

zzzeek commented Aug 30, 2022

OK exit it called because...it's a subprocess? What im trying to understand is in the parent process, are we still inside of build_extension() ? because if their spawned process has an issue and an error code, they should still propagate an exception we can catch locally? what happens in the parent process?

@CaselIT
Copy link
Member Author

CaselIT commented Aug 30, 2022

The commands are mostly run using this: https://github.com/pypa/setuptools/blob/ba3995e5705a22e13bb5d2231ac22c77e4417747/setuptools/_distutils/core.py#L193-L217
that basically re-raises errors as system exit.

because if their spawned process has an issue and an error code, they should still propagate an exception we can catch locally? what happens in the parent process?

it exits.

The main change I gather is that before what pip install -e . did was basically call setup.py that would then call setup to run.
Not pip calls the pep660 machinary that uses setup.py similarly to a library, running it multiple times with different commands to create a source dist, then a wheel from it, then instal the wheel. all this is done in a subprocess, so what setup.py sees is a system exit eception

again I'm not 100% of the above, sice I have not read the pep 660 change. Maybe at the next meeting we can better speak about it.

Personally I think that something like we are doing with setup.py (ie trying to build extensions and falling back on pure python) is not something they are supporting, at least for editable builds, with the current changes and we should instead use something else. I don't know what the something else is, so I would appreciate guidance on it

@zzzeek
Copy link
Member

zzzeek commented Aug 30, 2022

The commands are mostly run using this: https://github.com/pypa/setuptools/blob/ba3995e5705a22e13bb5d2231ac22c77e4417747/setuptools/_distutils/core.py#L193-L217 that basically re-raises errors as system exit.

because if their spawned process has an issue and an error code, they should still propagate an exception we can catch locally? what happens in the parent process?

it exits.

OK that's bug on their end maybe? We need an exception that can be caught. Does it exit via SystemExit that we can catch? (edit: or maybe we just change our approach)

Personally I think that something like we are doing with setup.py (ie trying to build extensions and falling back on pure python) is not something they are supporting, at least for editable builds,

I'm sure it's not!

with the current changes and we should instead use something else. I don't know what the something else is, so I would appreciate guidance on it

does cython have any guidance for this? Maybe we can just check up front for valid cython install before anything happens, then we just run either native or non-native. if native breaks, they have a broken cython install and that's not our problem, we are just differentiating between "no build tools or cython installed" vs "build tools and cython are present".

I am sure there's a way to test for this that's "good enough".

@CaselIT
Copy link
Member Author

CaselIT commented Aug 30, 2022

does cython have any guidance for this? Maybe we can just check up front for valid cython install before anything happens, then we just run either native or non-native. if native breaks, they have a broken cython install and that's not our problem, we are just differentiating between "no build tools or cython installed" vs "build tools and cython are present".

I am sure there's a way to test for this that's "good enough".

I think this is a viable thing.

I think that the supported way would be doing something like this https://setuptools.pypa.io/en/latest/build_meta.html#dynamic-build-dependencies-and-other-build-meta-tweaks
With pep660 there is now also build_editable and the other get_requires_*.
I think we are supposed to customize things there, but I would like their opinion on this to avoid hacking things together and see them break at the next update

@zzzeek
Copy link
Member

zzzeek commented Aug 30, 2022

OK can you find out from them

@sqla-tester
Copy link
Collaborator

Mike Bayer has proposed a fix for this issue in the main branch:

propose removal of "build fallback" https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/4062

@zzzeek
Copy link
Member

zzzeek commented Aug 31, 2022

as discussed in the meeting I think we will be fine if we just remove everything from our setup.py and just build normally. It's only windows users that really hit this issue and they will have a wheel file on pypi.

@gordthompson
Copy link
Member

FWIW, this workaround lets me build editable on Windows with setuptools 65.3.0:

(venv) PS C:\Users\Gord\git\sqla-gerrit> $Env:DISABLE_SQLALCHEMY_CEXT = "1"
(venv) PS C:\Users\Gord\git\sqla-gerrit> $Env:SETUPTOOLS_ENABLE_FEATURES = "legacy-editable"
(venv) PS C:\Users\Gord\git\sqla-gerrit> pip install -e .

@CaselIT
Copy link
Member Author

CaselIT commented Aug 31, 2022

this $Env:SETUPTOOLS_ENABLE_FEATURES = "legacy-editable" should not be needed

@CaselIT
Copy link
Member Author

CaselIT commented Sep 2, 2022

Commit 282d86d has updated the setup on the main branch.

I'll leave it open since I plan to update also on the 1.4 setup, that has a similar functionality but is using the c files

@zzzeek
Copy link
Member

zzzeek commented Sep 2, 2022

Do we want to do this for 1.4? I was thinking not. the change to setup.py is dramatic, it's plausible that people have CI /packaging setups that are finely tuned to our current setup.py. people have not reported this as an issue

@CaselIT
Copy link
Member Author

CaselIT commented Sep 2, 2022

from an installer prospective nothing has changed. Unless they where parsing the error message in case of a failed compilation?

@zzzeek
Copy link
Member

zzzeek commented Sep 2, 2022

an example of a change is, if the C compilation fails for just one module, the other three still build. now you have a SQLAlchemy installation where three of the four C libraries are present, and the other is pure Python. That's a combination for which we have no test support. In 1.4 it would likely work fine, but for example in 2.0, the thing I did here and then here would cause the library to not be importable, since we only tested that "cyextension.util" were working.

1.4 is very deep into its release series so IMO it's important we don't do any non-critical architectural changes at this point.

@CaselIT
Copy link
Member Author

CaselIT commented Sep 2, 2022

In 1.4 it would likely work fine, but for example in 2.0, the thing I did here and then here would cause the library to not be importable, since we only tested that "cyextension.util" were working.

Right, I think we need to change that part since the current setup.py could leave things broken.
We probably need to import all cython extensions there. Shame we can't share the list in setup.py too

@zzzeek
Copy link
Member

zzzeek commented Sep 2, 2022

In 1.4 it would likely work fine, but for example in 2.0, the thing I did here and then here would cause the library to not be importable, since we only tested that "cyextension.util" were working.

Right, I think we need to change that part since the current setup.py could leave things broken. We probably need to import all cython extensions there. Shame we can't share the list in setup.py too

yes agree, in _has_cy.py we just import all the cython extensions instead of just the one.

my point is just that in 1.4 we have something else going on, and past very early 1.4 releases, major reworks of that stuff are not warranted unless there were some blocking issue, like a new setuptools release that flat out wouldn't build at all, something like that.

@CaselIT
Copy link
Member Author

CaselIT commented Sep 2, 2022

ok for 1.4, we can keep as is until we don't have a reason to change it.

Any idea on how to share the list of cython modules so _has_cy and setup can share it?

@zzzeek
Copy link
Member

zzzeek commented Sep 2, 2022

I dunno, you could scan the directory, it might be better just to make a unit test that tests things, like scans the directory and makes sure has_cy loaded them all, something like that

@zzzeek
Copy link
Member

zzzeek commented Sep 3, 2022

fixed in 282d86d

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
setup issues related to installation and setup sqlalchemy.ext extension modules, most of which are ORM related
Projects
None yet
Development

No branches or pull requests

4 participants