Skip to content

Commit 587a06a

Browse files
committed
Use CMake FindPython3 for reliable library discovery
- Use CMake 3.18+ built-in FindPython3 module - Pass Python library path as PYTHON_LIBRARY_PATH compile definition - Use CMake-discovered path for dlopen with fallback to pattern-based This ensures the correct Python library is loaded with RTLD_GLOBAL on all platforms including FreeBSD and free-threaded Python.
1 parent 90b4e0d commit 587a06a

2 files changed

Lines changed: 59 additions & 66 deletions

File tree

c_src/CMakeLists.txt

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cmake_minimum_required(VERSION 3.14 FATAL_ERROR)
1+
cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
22

33
project(ErlangPythonNIF C)
44

@@ -51,50 +51,41 @@ endif()
5151
include(FindErlang)
5252
include_directories(${ERLANG_ERTS_INCLUDE_PATH})
5353

54-
# Find Python - use the PYTHON_CONFIG env variable if set
54+
# Find Python using CMake's built-in FindPython3
55+
# Use PYTHON_CONFIG env variable to hint which Python to use
5556
if(DEFINED ENV{PYTHON_CONFIG})
56-
set(Python3_CONFIG "$ENV{PYTHON_CONFIG}")
57+
# Extract prefix from python-config for hinting
5758
execute_process(
58-
COMMAND ${Python3_CONFIG} --prefix
59-
OUTPUT_VARIABLE Python3_PREFIX
59+
COMMAND $ENV{PYTHON_CONFIG} --prefix
60+
OUTPUT_VARIABLE Python3_ROOT_DIR
6061
OUTPUT_STRIP_TRAILING_WHITESPACE
6162
)
62-
execute_process(
63-
COMMAND ${Python3_CONFIG} --includes
64-
OUTPUT_VARIABLE Python3_INCLUDE_FLAGS
65-
OUTPUT_STRIP_TRAILING_WHITESPACE
66-
)
67-
execute_process(
68-
COMMAND ${Python3_CONFIG} --ldflags --embed
69-
OUTPUT_VARIABLE Python3_LINK_FLAGS
70-
OUTPUT_STRIP_TRAILING_WHITESPACE
71-
RESULT_VARIABLE Python3_EMBED_RESULT
72-
)
73-
if(NOT Python3_EMBED_RESULT EQUAL 0)
74-
execute_process(
75-
COMMAND ${Python3_CONFIG} --ldflags
76-
OUTPUT_VARIABLE Python3_LINK_FLAGS
77-
OUTPUT_STRIP_TRAILING_WHITESPACE
78-
)
79-
endif()
80-
81-
# Extract include directories from flags
82-
string(REGEX MATCHALL "-I[^ ]+" Python3_INCLUDE_DIRS_LIST "${Python3_INCLUDE_FLAGS}")
83-
string(REGEX REPLACE "-I" "" Python3_INCLUDE_DIRS_LIST "${Python3_INCLUDE_DIRS_LIST}")
84-
85-
message(STATUS "Using Python config: ${Python3_CONFIG}")
86-
message(STATUS "Python includes: ${Python3_INCLUDE_FLAGS}")
87-
message(STATUS "Python link flags: ${Python3_LINK_FLAGS}")
88-
else()
89-
# Use CMake's FindPython3
90-
find_package(Python3 REQUIRED COMPONENTS Development)
91-
set(Python3_INCLUDE_DIRS_LIST ${Python3_INCLUDE_DIRS})
92-
set(Python3_LINK_FLAGS "${Python3_LIBRARIES}")
63+
set(Python3_FIND_STRATEGY LOCATION)
9364
endif()
9465

66+
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
67+
68+
message(STATUS "Python3 executable: ${Python3_EXECUTABLE}")
69+
message(STATUS "Python3 version: ${Python3_VERSION}")
70+
message(STATUS "Python3 include dirs: ${Python3_INCLUDE_DIRS}")
71+
message(STATUS "Python3 libraries: ${Python3_LIBRARIES}")
72+
message(STATUS "Python3 library: ${Python3_LIBRARY}")
73+
9574
# Create the NIF shared library
9675
add_library(py_nif MODULE py_nif.c)
9776

77+
# Pass Python library path to NIF for dlopen
78+
# Python3_LIBRARY is the full path to the Python shared library
79+
if(Python3_LIBRARY)
80+
target_compile_definitions(py_nif PRIVATE PYTHON_LIBRARY_PATH="${Python3_LIBRARY}")
81+
message(STATUS "Using Python library path for dlopen: ${Python3_LIBRARY}")
82+
elseif(Python3_LIBRARIES)
83+
# Fallback to first library in the list
84+
list(GET Python3_LIBRARIES 0 Python3_FIRST_LIB)
85+
target_compile_definitions(py_nif PRIVATE PYTHON_LIBRARY_PATH="${Python3_FIRST_LIB}")
86+
message(STATUS "Using Python library path for dlopen: ${Python3_FIRST_LIB}")
87+
endif()
88+
9889
# Set output name
9990
set_target_properties(py_nif PROPERTIES
10091
PREFIX ""
@@ -104,7 +95,7 @@ set_target_properties(py_nif PROPERTIES
10495
# Include directories
10596
target_include_directories(py_nif PRIVATE
10697
${ERLANG_ERTS_INCLUDE_PATH}
107-
${Python3_INCLUDE_DIRS_LIST}
98+
${Python3_INCLUDE_DIRS}
10899
)
109100

110101
# Compiler flags
@@ -140,13 +131,7 @@ elseif(UNIX)
140131
endif()
141132

142133
# Link Python
143-
if(DEFINED ENV{PYTHON_CONFIG})
144-
# Parse the link flags and add them properly
145-
separate_arguments(Python3_LINK_FLAGS_LIST UNIX_COMMAND "${Python3_LINK_FLAGS}")
146-
target_link_libraries(py_nif PRIVATE ${Python3_LINK_FLAGS_LIST})
147-
else()
148-
target_link_libraries(py_nif PRIVATE Python3::Python)
149-
endif()
134+
target_link_libraries(py_nif PRIVATE Python3::Python)
150135

151136
# Threads
152137
find_package(Threads REQUIRED)

c_src/py_nif.c

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -471,34 +471,42 @@ static ERL_NIF_TERM nif_py_init(ErlNifEnv *env, int argc, const ERL_NIF_TERM arg
471471
* extension modules can find Python symbols when dynamically loaded.
472472
* Without this, modules like _socket.so fail with "undefined symbol: PyByteArray_Type" */
473473
{
474-
char libpython[256];
475474
void *handle = NULL;
476475

476+
#ifdef PYTHON_LIBRARY_PATH
477+
/* Use CMake-discovered library path (most reliable) */
478+
handle = dlopen(PYTHON_LIBRARY_PATH, RTLD_NOW | RTLD_GLOBAL);
479+
#endif
480+
481+
/* Fallback: try pattern-based discovery if CMake path didn't work */
482+
if (!handle) {
483+
char libpython[256];
477484
#ifdef Py_GIL_DISABLED
478-
/* Free-threaded Python has 't' suffix in library name (e.g., libpython3.13t.so) */
479-
const char *patterns[] = {
480-
"libpython%d.%dt.so.1.0", /* Linux free-threaded with full version */
481-
"libpython%d.%dt.so", /* Linux/FreeBSD free-threaded */
482-
"libpython%d.%dt.so.1", /* Some systems free-threaded */
483-
"libpython%d.%d.so.1.0", /* Fallback: Linux with full version */
484-
"libpython%d.%d.so", /* Fallback: Linux/FreeBSD */
485-
"libpython%d.%d.so.1", /* Fallback: Some systems */
486-
NULL
487-
};
485+
/* Free-threaded Python has 't' suffix in library name (e.g., libpython3.13t.so) */
486+
const char *patterns[] = {
487+
"libpython%d.%dt.so.1.0", /* Linux free-threaded with full version */
488+
"libpython%d.%dt.so", /* Linux/FreeBSD free-threaded */
489+
"libpython%d.%dt.so.1", /* Some systems free-threaded */
490+
"libpython%d.%d.so.1.0", /* Fallback: Linux with full version */
491+
"libpython%d.%d.so", /* Fallback: Linux/FreeBSD */
492+
"libpython%d.%d.so.1", /* Fallback: Some systems */
493+
NULL
494+
};
488495
#else
489-
/* Standard Python library names */
490-
const char *patterns[] = {
491-
"libpython%d.%d.so.1.0", /* Linux with full version */
492-
"libpython%d.%d.so", /* Linux/FreeBSD */
493-
"libpython%d.%d.so.1", /* Some systems */
494-
NULL
495-
};
496+
/* Standard Python library names */
497+
const char *patterns[] = {
498+
"libpython%d.%d.so.1.0", /* Linux with full version */
499+
"libpython%d.%d.so", /* Linux/FreeBSD */
500+
"libpython%d.%d.so.1", /* Some systems */
501+
NULL
502+
};
496503
#endif
497504

498-
for (int i = 0; patterns[i] && !handle; i++) {
499-
snprintf(libpython, sizeof(libpython), patterns[i],
500-
PY_MAJOR_VERSION, PY_MINOR_VERSION);
501-
handle = dlopen(libpython, RTLD_NOW | RTLD_GLOBAL);
505+
for (int i = 0; patterns[i] && !handle; i++) {
506+
snprintf(libpython, sizeof(libpython), patterns[i],
507+
PY_MAJOR_VERSION, PY_MINOR_VERSION);
508+
handle = dlopen(libpython, RTLD_NOW | RTLD_GLOBAL);
509+
}
502510
}
503511
/* It's OK if this fails - the symbols might already be global */
504512
}

0 commit comments

Comments
 (0)