distutils icon indicating copy to clipboard operation
distutils copied to clipboard

[BUG] self.linker_ex: TypeError: 'NoneType' object is not subscriptable

Open daniel-larraz opened this issue 1 year ago • 20 comments

setuptools version

setuptools==72.2.0

Python version

Python 3.8.16 (PyPy 7.3.11)

OS

Ubuntu 20.04

Additional environment information

This issue occurs only when I run setuptools with PyPy; it works fine with CPython. Downgrading to setuptools version 72.1.0 resolves the issue for both PyPy and CPython.

Description

When building a Cython extension that depends on a shared library, setuptools triggers a TypeError during the linking process.

Expected behavior

setuptools doesn't fail.

How to Reproduce

Create a file named hello_world.cpp:

// hello_world.cpp
#include <iostream>

extern "C" void print_hello() {
    std::cout << "Hello, World!" << std::endl;
}

Create a file named hello_world.h:

// hello_world.h
#ifndef HELLO_WORLD_H
#define HELLO_WORLD_H

extern "C" void print_hello();

#endif // HELLO_WORLD_H

On Linux, compile this into a shared library using:

g++ -c -fPIC hello_world.cpp -o hello_world.o
g++ -shared hello_world.o -o libhello_world.so

Create a file named hello.pyx:

# hello.pyx
cdef extern from "hello_world.h":
    void print_hello()

def call_print_hello():
    print_hello()

Create a setup.py file to build the Cython extension:

# setup.py
from setuptools import setup, Extension
from Cython.Build import cythonize

extensions = [
    Extension(
        "hello",
        ["hello.pyx"],
        language="c++",
        include_dirs=["."],
        library_dirs=["."],
        libraries=["hello_world"],
        extra_compile_args=["-std=c++11"]
    )
]

setup(
    name="hello",
    ext_modules=cythonize(extensions),
)

Run the following command in the terminal:

python setup.py build_ext --inplace

Output

Traceback (most recent call last):
  File "setup.py", line 17, in <module>
    setup(
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/__init__.py", line 108, in setup
    return distutils.core.setup(**attrs)
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/core.py", line 184, in setup
    return run_commands(dist)
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/core.py", line 200, in run_commands
    dist.run_commands()
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/dist.py", line 964, in run_commands
    self.run_command(cmd)
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/dist.py", line 945, in run_command
    super().run_command(command)
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/dist.py", line 983, in run_command
    cmd_obj.run()
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/command/build_ext.py", line 93, in run
    _build_ext.run(self)
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/command/build_ext.py", line 359, in run
    self.build_extensions()
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/command/build_ext.py", line 476, in build_extensions
    self._build_extensions_serial()
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/command/build_ext.py", line 502, in _build_extensions_serial
    self.build_extension(ext)
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/command/build_ext.py", line 254, in build_extension
    _build_ext.build_extension(self, ext)
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/Cython/Distutils/build_ext.py", line 135, in build_extension
    super(build_ext, self).build_extension(ext)
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/command/build_ext.py", line 581, in build_extension
    self.compiler.link_shared_object(
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/ccompiler.py", line 757, in link_shared_object
    self.link(
  File "/home/daniel/.pyenv/versions/pypy3.8-7.3.11/lib/pypy3.8/site-packages/setuptools/_distutils/unixccompiler.py", line 269, in link
    self.linker_exe
TypeError: 'NoneType' object is not subscriptable (key slice(None, None, None))

daniel-larraz avatar Aug 15 '24 08:08 daniel-larraz

We are seeing the same problem with building Matplotlib wheels, though not directly. It's actually failing on kiwisolver, but this is a bit annoying to work around, since the setuptools install is under cibuildwheel's test command + build isolation.

A bisect points to 0ab156c3e12054503a0dac0ce172e241262881f4, which is not too surprising, I think. Bisecting distutils itself points to pypa/distutils@2c937116cc0dcd9b26b6070e89a3dc5dcbedc2ae

QuLogic avatar Aug 16 '24 23:08 QuLogic

It looks like PyPy only just added LDCXXSHARED (and there's been no releases with it) but the fetch from sysconfig to distutils's linker_so_cxx does not have any handling of that. Something simple like this might work:

diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index fbdd5d73..19ea6aa0 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -321,6 +321,9 @@ def customize_compiler(compiler):
         )
 
         cxxflags = cflags
+        if ldcxxshared is None:
+            # Older versions of PyPy do not set LDCXXSHARED.
+            ldcxxshared = ldshared
 
         if 'CC' in os.environ:
             newcc = os.environ['CC']

QuLogic avatar Aug 17 '24 00:08 QuLogic

Actually, looking at the commit again, it appears that CygwinCCompiler and UnixCCompiler both have a default for linker_so_cxx, but it's customize_compiler that overwrites it. So instead of trying to set the default in customize_compiler, it should just not set it, perhaps:

diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index fbdd5d73..29a2ba36 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -362,11 +362,13 @@ def customize_compiler(compiler):
             compiler_cxx=cxx_cmd,
             compiler_so_cxx=cxx_cmd + ' ' + ccshared,
             linker_so=ldshared,
-            linker_so_cxx=ldcxxshared,
             linker_exe=cc,
             linker_exe_cxx=cxx,
             archiver=archiver,
         )
+        if ldcxxshared is not None:
+            # Older versions of PyPy do not set LDCXXSHARED.
+            compile.set_executable('linker_so_cxx', ldcxxshared)
 
         if 'RANLIB' in os.environ and compiler.executables.get('ranlib', None):
             compiler.set_executables(ranlib=os.environ['RANLIB'])

QuLogic avatar Aug 17 '24 00:08 QuLogic

Thank you @QuLogic for the investigation.

@jaraco , @lazka do you think this patch is viable in the pypa/distutils repository? Should we move this issue there?

abravalheri avatar Aug 17 '24 07:08 abravalheri

The patches above need a few more None checks for CXXFLAGS and LDCXXSHARED cases, but yeah, moving to distutils sounds good to me.

I wonder why CI didn't catch this.

lazka avatar Aug 17 '24 07:08 lazka

See #228 where this behavior was added. That PR and the one that preceded it were tricky to get right, especially due to the nuances of manipulating the sysconfig variables and the different locations where those variables are defined.

I see now how that PR caused a regression. Prior to this change, distutils and setuptools would treat C++ like C, only honoring the C configs and environment and disregarding the C++ settings. With the change, it now honors the C++ settings for C++ extensions, but there are some environments that don't yet have C++ settings configured. In that case, we want distutils to fall back to the old behavior (C settings).

I wonder why CI didn't catch this.

We should definitely determine the answer. If the current tests don't cover the condition (and it seems they don't), we'll want to manifest a test that does capture the missed expectation.

jaraco avatar Aug 18 '24 14:08 jaraco

Looking at the original PR, test_customize_compiler does not set LDCXXSHARED, but it only checks compiler_cxx, and none of the new compiler_so_cxx, linker_so_cxx, linker_exe_cxx, so it neither confirms that the environment variable works, nor confirms that the fallback works.

QuLogic avatar Aug 22 '24 00:08 QuLogic

It looks like PyPy only just added LDCXXSHARED (and there's been no releases with it)

This has now been released in last week's PyPy3.10 v7.3.17:

  • https://pypy.org/posts/2024/08/pypy-v7317-release.html
  • https://github.com/pypy/pypy/releases/tag/release-pypy3.10-v7.3.17

GitHub Actions is still on 7.3.16:

  • https://github.com/ultrajson/ultrajson/actions/runs/10651820147/job/29524844616#step:3:14

Reported: https://github.com/actions/setup-python/issues/936

hugovk avatar Sep 02 '24 06:09 hugovk

Could you add check-latest: true to the setup-python action options? I don't know how expensive it is, in any case you can comment it out once the image updates.

mattip avatar Sep 02 '24 20:09 mattip

Thanks, that does force 7.3.17 to be installed and now it passes for Ubuntu but fails for macOS with a different error:

ld: unknown options: -Bsymbolic-function

https://github.com/hugovk/ultrajson/actions/runs/10676841128

hugovk avatar Sep 03 '24 05:09 hugovk

I wonder where that is coming from. As far as I can tell, it is not here (distutils) nor in PyPy. Is it an xcode default?

mattip avatar Sep 03 '24 09:09 mattip

So is there a solution to this?

RuABraun avatar Feb 26 '25 22:02 RuABraun

PyPy has quite a few releases since it added LDCXXSHARED. Is this still happening?

mattip avatar Feb 27 '25 12:02 mattip

PyPy has quite a few releases since it added LDCXXSHARED. Is this still happening?

Based on the linked commits above as recent as Feb 4, it sounds like it may still be happening.

I wonder why CI didn't catch this.

We should definitely determine the answer. If the current tests don't cover the condition (and it seems they don't), we'll want to manifest a test that does capture the missed expectation.

Looking at the original PR, test_customize_compiler does not set LDCXXSHARED, but it only checks compiler_cxx, and none of the new compiler_so_cxx, linker_so_cxx, linker_exe_cxx, so it neither confirms that the environment variable works, nor confirms that the fallback works.

At the very least, it'd be great if someone could craft a patch to the tests that will capture the missed expectation, so that distutils' own tests can confirm that the issue is fixed and won't regress.

Perhaps @QuLogic or @sciyoshi would be willing to develop such a test?

jaraco avatar Mar 16 '25 16:03 jaraco

I don't understand the problem: what is missing in PyPy?

mattip avatar Mar 16 '25 20:03 mattip

This commit was intended to fix this issue on PyPy but it was backed out due to issues on OSX I believe. The relevant comment and discussion is here: https://github.com/pypa/distutils/pull/228#issuecomment-2265478504

sciyoshi avatar Mar 16 '25 23:03 sciyoshi

Specifically https://github.com/pypa/distutils/pull/228#issuecomment-2265537941 where we thought this was an acceptable limitation (C++ on PyPy), but I suppose that's not the case.

sciyoshi avatar Mar 16 '25 23:03 sciyoshi

Sorry, I missed the earlier discussion in this issue referencing these issues already.

sciyoshi avatar Mar 16 '25 23:03 sciyoshi

I can confirm however that the issue no longer occurs with newer versions PyPy (tested PyPy 3.10 v7.3.19).

sciyoshi avatar Mar 16 '25 23:03 sciyoshi

@RuABraun could you provide more information since this should have been solved. What version of PyPy are you using, what does your invocation look like, what platform are you using?

mattip avatar Mar 17 '25 05:03 mattip

Thanks, that does force 7.3.17 to be installed and now it passes for Ubuntu but fails for macOS with a different error:

ld: unknown options: -Bsymbolic-function

hugovk/ultrajson/actions/runs/10676841128

This is still happening on macOS with PyPy 7.3.19 (Python 3.11.11):

  • https://github.com/ultrajson/ultrajson/actions/runs/14185861876/job/39740941480?pr=660
  • https://github.com/ultrajson/ultrajson/pull/660

hugovk avatar Apr 01 '25 08:04 hugovk

I changed the values of LD*SHARED for macOS on pypy3.11 in the latest HEAD to use clang-compatible parameters.

mattip avatar Apr 02 '25 16:04 mattip

It sounds as if there's nothing more to do here. I'd still love for distutils to have a test that captures the failure. If someone wishes to author one, I'd readily accept it.

jaraco avatar Jul 19 '25 12:07 jaraco