DEV Community

AJ Kerrigan
AJ Kerrigan

Posted on • Edited on

Homebrew, pyenv, ctypes... oh my!

Background

I usually have a number of Python versions, environments and tools in play. I generally use pyenv to manage Python versions, and Homebrew to manage pyenv.

Skipping to the End

If you've come across this post because you're having problems installing Python via Homebrew-managed pyenv, you have my sympathies. And in case it "just works" for you, feel free to give something like this a try:

CC="$(brew --prefix gcc)/bin/gcc-11" \
pyenv install --verbose 3.10.0
Enter fullscreen mode Exit fullscreen mode

If you're interested in the circuitous road of troubleshooting and exploration that led to that command, you're welcome to keep reading and come along for the ride. Nice to have company :).

The Problem

After successfully installing Python 3.10 with pyenv, I found that I couldn't pip install packages with C extension modules. They complained about _ctypes being missing. And trying to import ctypes from the Python REPL confirmed the issue:

Python 3.10.0 (default, Dec 31 2021, 16:09:32) [GCC 10.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/tmp/python-build.20211231145343.269699/Python-3.10.0/Lib/ctypes/__init__.py", line 8, in <module>
    from _ctypes import Union, Structure, Array
ModuleNotFoundError: No module named '_ctypes'
Enter fullscreen mode Exit fullscreen mode

Looking at the logs of my pyenv install command, I found this error:

*** WARNING: renaming "_ctypes" since importing it failed: libffi.so.8: cannot open shared object file: No such file or directory
Enter fullscreen mode Exit fullscreen mode

Which took things a bit further and pointed at an underlying problem. Now, I've seen that issue (or one of its kin) before, and it usually means something like "go muck with LDFLAGS until it works". And after a bit of searching I found a number of relevant resources with plenty of suggestions:

Along with the always-helpful suggested build environment information from the pyenv wiki.

That was plenty of information to fuel a few cycles of "try stuff and see what happens".

The Solution?

Exploring those linked issues was productive, but my Python environment was still broken. I also wasn't sure why my previous pyenv-installed versions of Python could import ctypes just fine.

But I noticed that many of the suggested fixes worked because they took homebrew out of the equation. What I really wanted was to use the build tooling Homebrew already had installed. And so I thought "Why am I passing Homebrew paths in for CFLAGS and LDFLAGS but still letting pyenv install use my system gcc?". So I tried:

CC="$(brew --prefix gcc)/bin/gcc-11" \
CFLAGS="$(pkg-config --cflags libffi)" \
LDFLAGS="$(pkg-config --libs libffi)" \
pyenv install --verbose 3.10.0
Enter fullscreen mode Exit fullscreen mode

And it worked! I commented about it in this issue because I suspected other folks with a similar problem might benefit from the same solution. Then I went back to whatever the heck I was trying to do with Python in the first place :).

The Itch

The "solution" got my Python 3.10 environments working perfectly, but it also felt unsatisfying. I had some hunches about why it worked, but I wasn't really sure. And then I saw...

  • A few other folks hit the same issue, and note that my suggestion worked for them
  • A really great post from Julia Evans debugging a very similar issue

Which made me think, "This is a great problem to explore during the last week of December".

Isolating the Useful Change

I had been thinking that since adding a custom CC effectively fixed my Python build, the CFLAGS and LDFLAGS bits might be unnecessary. So I tried without them:

CC="$(brew --prefix gcc)/bin/gcc-11" \
pyenv install --verbose 3.10.0
Enter fullscreen mode Exit fullscreen mode

And yes, that still worked. But why?

One Level Down

The next step was to drop down a level and use python-build to set up side-by-side build directories: one working, one broken. To get a broken one, I used:

python-build --keep --verbose 3.10.0 ./py310
Enter fullscreen mode Exit fullscreen mode

And for a working one:

CC="$(brew --prefix gcc)/bin/gcc-11" \
python-build --keep --verbose 3.10.0 ./py310-2
Enter fullscreen mode Exit fullscreen mode

From the broken directory, I could run make and see the ctypes warning. I could also see the object file renamed to ..._failed.so:

❯ fd "ctypes.*so"
build/lib.linux-x86_64-3.10/_ctypes.cpython-310-x86_64-linux-gnu_failed.so
build/lib.linux-x86_64-3.10/_ctypes_test.cpython-310-x86_64-linux-gnu.so
Enter fullscreen mode Exit fullscreen mode

Compared to the working directory which showed the expected file names:

❯ fd "ctypes.*so"
build/lib.linux-x86_64-3.10/_ctypes.cpython-310-x86_64-linux-gnu.so
build/lib.linux-x86_64-3.10/_ctypes_test.cpython-310-x86_64-linux-gnu.so
Enter fullscreen mode Exit fullscreen mode

The earlier error messages mentioned an error finding libffi, and ldd helped highlight and reproduce that. From my broken build, I see "not found":

> ldd build/lib.linux-x86_64-3.10/_ctypes.cpython-310-x86_64-linux-gnu_failed.so
        linux-vdso.so.1 (0x00007ffd97fbf000)
        libffi.so.8 => not found
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f478258f000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f478256c000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f478237a000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f47825d8000)
Enter fullscreen mode Exit fullscreen mode

While the working file references homebrew's libffi:

> ldd build/lib.linux-x86_64-3.10/_ctypes.cpython-310-x86_64-linux-gnu.so
        linux-vdso.so.1 (0x00007ffcd35fb000)
        libffi.so.8 => /home/linuxbrew/.linuxbrew/lib/libffi.so.8 (0x00007f306da2d000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f306da08000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f306d9e5000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f306d7f3000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f306dc5f000)
Enter fullscreen mode Exit fullscreen mode

I was providing custom CFLAGS and LDFLAGS options that I thought would avoid this issue, but obviously that didn't do it. What was that custom CC doing that the CFLAGS/LDFLAGS change didn't do?

Another Level Down

I looked in the python build log to get the gcc command that built _ctypes.cpython-310-x86_64-linux-gnu.so. It looked something like this (noise alert):

gcc -pthread -fPIC -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -I/home/linuxbrew/.linuxbrew/opt/zlib -I/home/linuxbrew/.linuxbrew/opt/zlib -std=c99 -Wextra -Wno-unused-result -Wno-unused-parameter -Wno-missing-field-initializers -Werror=implicit-function-declaration -fvisibility=hidden -I./Include/internal -I/home/linuxbrew/.linuxbrew/Cellar/libffi/3.4.2/include -I./Include -I. -I/home/linuxbrew/.linuxbrew/opt/readline/include -I/home/aj/code/scratch/pyenv-builds/./py310/include -I/home/linuxbrew/.linuxbrew/include -I/usr/include/x86_64-linux-gnu -I/usr/local/include -I/tmp/python-build.20211231172024.425603/Python-3.10.0/Include -I/tmp/python-build.20211231172024.425603/Python-3.10.0 -c /tmp/python-build.20211231172024.425603/Python-3.10.0/Modules/_ctypes/stgdict.c -o build/temp.linux-x86_64-3.10/tmp/python-build.20211231172024.425603/Python-3.10.0/Modules/_ctypes/stgdict.o -DPy_BUILD_CORE_MODULE -DHAVE_FFI_PREP_CIF_VAR=1 -DHAVE_FFI_PREP_CLOSURE_LOC=1 -DHAVE_FFI_CLOSURE_ALLOC=1
gcc -pthread -shared -L/home/linuxbrew/.linuxbrew/opt/readline/lib -L/home/aj/code/scratch/pyenv-builds/./py310/lib -L/home/linuxbrew/.linuxbrew/lib build/temp.linux-x86_64-3.10/tmp/python-build.20211231172024.425603/Python-3.10.0/Modules/_ctypes/_ctypes.o build/temp.linux-x86_64-3.10/tmp/python-build.20211231172024.425603/Python-3.10.0/Modules/_ctypes/callbacks.o build/temp.linux-x86_64-3.10/tmp/python-build.20211231172024.425603/Python-3.10.0/Modules/_ctypes/callproc.o build/temp.linux-x86_64-3.10/tmp/python-build.20211231172024.425603/Python-3.10.0/Modules/_ctypes/cfield.o build/temp.linux-x86_64-3.10/tmp/python-build.20211231172024.425603/Python-3.10.0/Modules/_ctypes/stgdict.o -L/home/linuxbrew/.linuxbrew/opt/readline/lib -L/home/aj/code/scratch/pyenv-builds/./py310/lib -L/home/linuxbrew/.linuxbrew/lib -L/usr/lib/x86_64-linux-gnu -L/usr/local/lib -lffi -ldl -o build/lib.linux-x86_64-3.10/_ctypes.cpython-310-x86_64-linux-gnu.so
Enter fullscreen mode Exit fullscreen mode

There's a lot going on there, but as a test I ran the same command with all the same command line options, but using Homebrew's gcc-11 instead of my system gcc. And... that still worked. That was another helpful input.

Finally An Answer

Once I found that I could fail or succeed to build _ctypes.cpython-310-x86_64-linux-gnu.so by varying only the gcc version, I ran both versions of gcc with the --verbose option to look for more clues. And I found my answer in the form of gcc specs. The verbose logs from my system gcc included this bit:

Using built-in specs.
Enter fullscreen mode Exit fullscreen mode

While the Homebrew gcc log included:

Reading specs from /home/linuxbrew/.linuxbrew/Cellar/gcc/11.2.0_3/lib/gcc/11/gcc/x86_64-pc-linux-gnu/11/specs
Enter fullscreen mode Exit fullscreen mode

And peeking into /home/linuxbrew/.linuxbrew/Cellar/gcc/11.2.0_3/lib/gcc/11/gcc/x86_64-pc-linux-gnu/11/specs revealed some custom options:

*cpp_unique_options:
+ -isysroot /home/linuxbrew/.linuxbrew/nonexistent -idirafter /home/linuxbrew/.linuxbrew/include -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include

*link_libgcc:
+ -L/home/linuxbrew/.linuxbrew/lib/gcc/11 -L/home/linuxbrew/.linuxbrew/lib

*link:
+ --dynamic-linker /home/linuxbrew/.linuxbrew/lib/ld.so -rpath /home/linuxbrew/.linuxbrew/lib/gcc/11

*homebrew_rpath:
-rpath /home/linuxbrew/.linuxbrew/lib
Enter fullscreen mode Exit fullscreen mode

I've never messed around with gcc spec files to be honest. But it sure looks like that's what was causing my Homebrew gcc to work where my system gcc didn't.

In Other, Fewer Words...

My condensed takeaway after all of this exploration and troubleshooting is:

I'm trying to install Python using Homebrew-managed build dependencies. Those tools know where their pieces live and how they should work together. I should get out of their way and let them work. By pointing at Homebrew's gcc with pyenv install, I'm letting pyenv operate in the world it knows. The world where it was born.

Lingering Questions

There are some things I'm still not sure about. If you're reading this, you are a lovely human and a patient angel. And maybe you can help shed additional light on my busted system? Notably...

  • My Homebrew-managed pyenv on this system previously installed Python versions from 3.6 through 3.9 without any issues and without requiring any CC-fiddling. Did something change with Homebrew or pyenv, or is this possibly something specific to Python 3.10?
  • The options in the gcc spec look similar to the CFLAGS and LDFLAGS I was trying... is there some other magic combination of flags that would have allowed my system gcc to find everything it needed?

I may come back to these questions myself in a later troubleshooting phase. But I suspect I'm missing something obvious that someone else would catch at a glance!

Top comments (4)

Collapse
 
islerfab profile image
Fabio

I unfortunately cannot answer your final questions, but I'd like to point out that I made an account here (finally) just to give you a big fat THANK YOU - you've solved something I got stuck on for a whole afternoon (because the Python debugger in VSCode won't work without ctypes...).
I am also using the asdf build like @michaelhays .
Have a great day!

Collapse
 
ajkerrigan profile image
AJ Kerrigan

That makes my day @islerfab, thanks for sharing! I was hoping that if other people were in frantic googling mode, they'd find something in here useful :).

Collapse
 
michaelhays profile image
Michael Hays

You rock AJ! Also worked for my asdf Python :)

Collapse
 
ajkerrigan profile image
AJ Kerrigan

Ah good call, I didn't think of asdf but I guess it's the same concept so that makes sense. Glad you're sorted out too, cheers! :)