Skip to content

Commit 41f436a

Browse files
authored
Merge pull request #5594 from InsightSoftwareConsortium/modernize-python3-module-build
COMP: Modernize Python3 module build
2 parents 54ee564 + 4784176 commit 41f436a

File tree

8 files changed

+271
-269
lines changed

8 files changed

+271
-269
lines changed

CMake/ITKSetPython3Vars.cmake

Lines changed: 232 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,174 @@
66
# Additionally, setting Python3_EXECUTABLE can be used to set the Python version explicitly, but
77
# may become less reliable with newer versions of CMake (as opposed setting FindPython3 HINTS). Current
88
# implementation gives preference to active virtualenvs.
9-
109
cmake_policy(SET CMP0094 NEW) # makes FindPython3 prefer activated virtualenv Python to latest version
1110
set(PYTHON_VERSION_MIN 3.9)
1211
set(PYTHON_VERSION_MAX 3.999)
13-
14-
if(PYTHON_DEVELOPMENT_REQUIRED)
15-
if(DEFINED Python3_EXECUTABLE) # if already specified
16-
set(_specified_Python3_EXECUTABLE ${Python3_EXECUTABLE})
12+
if(DEFINED Python3_EXECUTABLE) # if already specified
13+
set(_specified_Python3_EXECUTABLE ${Python3_EXECUTABLE})
14+
# If a specific Python executable is provided, lock the Python version range
15+
# to the exact version of that executable so FindPython3 searches that version.
16+
execute_process(
17+
COMMAND
18+
"${_specified_Python3_EXECUTABLE}" -c
19+
"import sys; print('{}.{}'.format(sys.version_info[0], sys.version_info[1]))"
20+
OUTPUT_VARIABLE _specified_Python3_VERSION_MM
21+
ERROR_VARIABLE _specified_Python3_VERSION_MM_ERR
22+
OUTPUT_STRIP_TRAILING_WHITESPACE
23+
)
24+
if(_specified_Python3_VERSION_MM)
25+
set(PYTHON_VERSION_MIN ${_specified_Python3_VERSION_MM})
26+
set(PYTHON_VERSION_MAX ${_specified_Python3_VERSION_MM})
1727
endif()
28+
unset(_specified_Python3_VERSION_MM)
29+
unset(_specified_Python3_VERSION_MM_ERR)
30+
endif()
31+
32+
# Tested in cmake 3.26-4.21. Python3_FIND_ABI causes the Development COMPONENTS to not be found
33+
unset(Python3_FIND_ABI)
34+
35+
if(NOT PYTHON_DEVELOPMENT_REQUIRED)
36+
# if not PYTHON_DEVELOPMENT_REQUIRED, just find some version of
37+
# Python (don't need to be as specific)
38+
find_package(
39+
Python3
40+
${PYTHON_VERSION_MIN}...${PYTHON_VERSION_MAX}
41+
COMPONENTS
42+
Interpreter
43+
)
44+
set(ITK_WRAP_PYTHON_VERSION "ITK_WRAP_PYTHON=OFF")
45+
else()
1846
# set(Python3_FIND_REGISTRY LAST) # default is FIRST. Do we need/want this?
1947
find_package(
2048
Python3
2149
${PYTHON_VERSION_MIN}...${PYTHON_VERSION_MAX}
2250
COMPONENTS
51+
Interpreter
52+
# NOTE: dockcross build environments do not supply
53+
# `Development` python-dev resources
54+
Development.Module
55+
Development.SABIModule
56+
NumPy
57+
)
58+
set(ITK_WRAP_PYTHON_VERSION "${Python3_VERSION}")
59+
60+
# start section to define package components based on LIMITED_API support and choices
61+
set(_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION 3.11)
62+
63+
# Force ITK_WRAP_PYTHON_VERSION if SKBUILD_SABI_COMPONENT requests it
64+
string(
65+
FIND
66+
"${SKBUILD_SABI_COMPONENT}"
67+
"SABIModule"
68+
_SKBUILD_SABI_COMPONENT_REQUIRED
69+
)
70+
if(NOT DEFINED ITK_USE_PYTHON_LIMITED_API)
71+
if(
72+
(
73+
_SKBUILD_SABI_COMPONENT_REQUIRED
74+
GREATER
75+
-1
76+
)
77+
OR
78+
ITK_WRAP_PYTHON_VERSION
79+
VERSION_GREATER_EQUAL
80+
${_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION}
81+
)
82+
set(
83+
ITK_USE_PYTHON_LIMITED_API
84+
1
85+
CACHE BOOL
86+
"Configure Python's limited API for Python minor version compatibility."
87+
)
88+
else()
89+
set(
90+
ITK_USE_PYTHON_LIMITED_API
91+
0
92+
CACHE BOOL
93+
"Configure Python's limited API for Python minor version compatibility."
94+
)
95+
endif()
96+
mark_as_advanced(ITK_USE_PYTHON_LIMITED_API)
97+
endif()
98+
unset(_SKBUILD_SABI_COMPONENT_REQUIRED)
99+
if(ITK_USE_PYTHON_LIMITED_API)
100+
if(CMAKE_VERSION VERSION_LESS "3.26")
101+
message(
102+
FATAL_ERROR
103+
"CMake version ${CMAKE_VERSION} is too old for Python limited API wrapping: "
104+
"the FindPython Development.SABIModule component requires CMake >= 3.26. "
105+
"Either upgrade CMake or disable ITK_USE_PYTHON_LIMITED_API."
106+
)
107+
endif()
108+
109+
set(
110+
_python_find_components
111+
Interpreter
112+
Development.SABIModule
113+
)
114+
else()
115+
set(
116+
_python_find_components
23117
Interpreter
24118
Development.Module
25-
${SKBUILD_SABI_COMPONENT}
119+
)
120+
endif()
121+
if(BUILD_TESTING)
122+
# NumPy Required for testing ITK PythonTests, prefer to fail early if not installed
123+
list(APPEND _python_find_components NumPy)
124+
endif()
125+
set(_missing_required_component FALSE)
126+
find_package(
127+
Python3
128+
${ITK_WRAP_PYTHON_VERSION}
129+
COMPONENTS
130+
${_python_find_components}
26131
)
132+
set(ITK_WRAP_PYTHON_VERSION "${Python3_VERSION}")
133+
message(STATUS "Python3_FOUND=${Python3_FOUND}")
134+
foreach(_required_component ${_python_find_components})
135+
if(NOT Python3_${_required_component}_FOUND)
136+
message(
137+
STATUS
138+
" o Python3 Missing COMPONENT: Python3_${_required_component}_FOUND: ${Python3_${_required_component}_FOUND}"
139+
)
140+
set(_missing_required_component TRUE)
141+
else()
142+
message(
143+
STATUS
144+
" o Python3 Found COMPONENT: Python3_${_required_component}_FOUND: ${Python3_${_required_component}_FOUND}"
145+
)
146+
endif()
147+
endforeach()
148+
unset(_required_component)
149+
if(_missing_required_component)
150+
message(
151+
FATAL_ERROR
152+
"At least 1 required Python3 COMPONENT could not be found from : ${_python_find_components}
153+
in range ${PYTHON_VERSION_MIN}...${PYTHON_VERSION_MAX}:
154+
Python3_EXECUTABLE=:${Python3_EXECUTABLE}:
155+
ITK_WRAP_PYTHON_VERSION=:${ITK_WRAP_PYTHON_VERSION}:
156+
Python3_ROOT_DIR=:${Python3_ROOT_DIR}:
157+
---
158+
Python3_FOUND=${Python3_FOUND}
159+
Python3_Interpreter_FOUND=${Python3_Interpreter_FOUND}
160+
Python3_Compiler_FOUND=${Python3_Compiler_FOUND}
161+
Python3_Development_FOUND=${Python3_Development_FOUND}
162+
Python3_Development.Module_FOUND=${Python3_Development.Module_FOUND}
163+
Python3_Development.SABIModule_FOUND=${Python3_Development.SABIModule_FOUND}
164+
Python3_Development.Embed_FOUND=${Python3_Development.Embed_FOUND}
165+
Python3_NumPy_FOUND=${Python3_NumPy_FOUND}
166+
"
167+
)
168+
else()
169+
message(STATUS " o Python3_EXECUTABLE=${Python3_EXECUTABLE}")
170+
message(STATUS " o Python3_ROOT_DIR=${Python3_ROOT_DIR}")
171+
message(STATUS " o ITK_WRAP_PYTHON_VERSION=${ITK_WRAP_PYTHON_VERSION}")
172+
endif()
173+
unset(_missing_required_component)
174+
unset(_python_find_components)
175+
unset(_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION)
176+
# end section to define package components based on LIMITED_API support and choices
27177
if(DEFINED _specified_Python3_EXECUTABLE)
28178
set(
29179
Python3_EXECUTABLE
@@ -33,35 +183,81 @@ if(PYTHON_DEVELOPMENT_REQUIRED)
33183
FORCE
34184
)
35185
endif()
36-
else() # if not PYTHON_DEVELOPMENT_REQUIRED, just find some version of Python (don't need to be as specific)
37-
find_package(
38-
Python3
39-
${PYTHON_VERSION_MIN}...${PYTHON_VERSION_MAX}
40-
COMPONENTS
41-
Interpreter
42-
)
43-
endif()
44-
if(ITK_WRAP_PYTHON)
45-
set(ITK_WRAP_PYTHON_VERSION "${Python3_VERSION}")
46-
else()
47-
set(ITK_WRAP_PYTHON_VERSION "ITK_WRAP_PYTHON=OFF")
48-
endif()
49-
if(NOT Python3_EXECUTABLE AND _specified_Python3_EXECUTABLE) # workaround for cases where FindPython3 fails to set correctly
50-
set(
51-
Python3_EXECUTABLE
52-
${_specified_Python3_EXECUTABLE}
53-
CACHE INTERNAL
54-
"Path to the Python interpreter"
55-
FORCE
186+
187+
if(NOT Python3_EXECUTABLE AND _specified_Python3_EXECUTABLE) # workaround for cases where FindPython3 fails to set correctly
188+
set(
189+
Python3_EXECUTABLE
190+
${_specified_Python3_EXECUTABLE}
191+
CACHE INTERNAL
192+
"Path to the Python interpreter"
193+
FORCE
194+
)
195+
endif()
196+
197+
# If a specific Python3_EXECUTABLE is provided by the user, try to infer
198+
# the corresponding Python3_ROOT_DIR for Unix/macOS/Linux so CMake's
199+
# FindPython3 locates the matching installation or virtual environment.
200+
# This is especially important for virtualenv/venv/conda environments.
201+
if(
202+
DEFINED
203+
Python3_EXECUTABLE
204+
AND
205+
NOT
206+
DEFINED
207+
Python3_ROOT_DIR
208+
AND
209+
(
210+
UNIX
211+
OR
212+
APPLE
213+
)
214+
AND
215+
NOT
216+
WIN32
56217
)
57-
endif()
218+
# First, try sys.prefix from the provided interpreter (works for venv/conda)
219+
execute_process(
220+
COMMAND
221+
"${Python3_EXECUTABLE}" -c "import sys; print(sys.prefix)"
222+
OUTPUT_VARIABLE _py_prefix
223+
ERROR_VARIABLE _py_prefix_err
224+
OUTPUT_STRIP_TRAILING_WHITESPACE
225+
)
226+
if(_py_prefix)
227+
file(TO_CMAKE_PATH "${_py_prefix}" _py_root_hint)
228+
endif()
229+
230+
# Fallback: parent of the interpreter's bin directory, e.g., /path/to/env
231+
# from /path/to/env/bin/python3
232+
if(NOT _py_root_hint)
233+
get_filename_component(_py_exe_dir "${Python3_EXECUTABLE}" DIRECTORY)
234+
get_filename_component(_py_root_hint "${_py_exe_dir}/.." REALPATH)
235+
endif()
58236

59-
# Add user-visible cache entry
60-
set(
61-
Python3_ROOT_DIR
62-
${Python3_ROOT_DIR}
63-
CACHE PATH
64-
"Which installation or virtual environment of Python to use"
65-
FORCE
66-
)
67-
mark_as_advanced(Python3_ROOT_DIR)
237+
if(_py_root_hint)
238+
set(
239+
Python3_ROOT_DIR
240+
"${_py_root_hint}"
241+
CACHE PATH
242+
"Which installation or virtual environment of Python to use"
243+
FORCE
244+
)
245+
mark_as_advanced(Python3_ROOT_DIR)
246+
endif()
247+
unset(_py_prefix)
248+
unset(_py_prefix_err)
249+
unset(_py_exe_dir)
250+
unset(_py_root_hint)
251+
endif()
252+
if(Python3_ROOT_DIR)
253+
# Add user-visible cache entry if Python3_ROOT_DIR value is set
254+
set(
255+
Python3_ROOT_DIR
256+
${Python3_ROOT_DIR}
257+
CACHE PATH
258+
"Which installation or virtual environment of Python to use"
259+
FORCE
260+
)
261+
mark_as_advanced(Python3_ROOT_DIR)
262+
endif()
263+
endif()

0 commit comments

Comments
 (0)