Skip to content

Commit 8ca847f

Browse files
committed
COMP: Modularize Python wrapping module creation logic
- Relocate Python LIMITED_API logic for modularity o Python configuration should be done in ITKSetPytnon3Vars.cmake. It was previously spread around in different parts of the ITK package which made tracking of behaviors difficult to monitor. - Simplify logic for ITK_USE_PYTHON_LIMITED_API o Avoid using a temporary variable use_python_limited_api_default to set the value of ITK_USE_PYTHON_LIMITED_API. o Prefer to set ITK_USE_PYTHON_LIMITED_API directly. o Moved Python LIMITED_API and package component logic to `ITKSetPython3Vars.cmake` for improved organization and maintainability. - Ensure single call to find_package( Python3 ) is used to simplify the logic and testing. Manually identify required Python3 COMPONENTS and give more descriptive error messages to help with tracking down the failing condition. This is particularly helpful when multiple versions of Python are installed (i.e. python3.13 from homebrew, python3.13 from pixi, python3.13 in standard OS install, etc). - Refactored Python LIMITED_API setup for improved modularity and reusability. - Adjusted `itk_end_wrap_module.cmake` to properly handle LIMITED_API settings. - Updated `ITKSetPython3Vars.cmake` to streamline Python version selection and configuration. - Improved maintainability by centralizing Python-related configuration and ensuring consistency across CMake files. - Fix linking of ITKCommon when including python support - Added a check in `ITKSetPython3Vars.cmake` to ensure CMake version is >= 3.26 when `ITK_USE_PYTHON_LIMITED_API` is enabled. - Provides clear error messaging to guide users to update CMake or disable the limited API configuration. - Updated `ITKSetPython3Vars.cmake` to append `Python3_FIND_ABI` values only for Linux (non-Apple, non-MSVC) systems. - Unset `Python3_FIND_ABI` for non-Linux platforms to prevent configuration issues. - Updated `ITKSetPython3Vars.cmake` to restrict `PYTHON_VERSION_MIN` and `PYTHON_VERSION_MAX` to the version of the specified `Python3_EXECUTABLE`. - Ensures compatibility and proper handling of Python LIMITED_API in wrapping process.
1 parent 78e5508 commit 8ca847f

File tree

2 files changed

+260
-124
lines changed

2 files changed

+260
-124
lines changed

CMake/ITKSetPython3Vars.cmake

Lines changed: 233 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,175 @@
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
2351
Interpreter
52+
Development # Needed for Modules/Core/Common/src/itkPyCommand.cxx
53+
Development.Module
54+
Development.SABIModule
55+
NumPy
56+
)
57+
set(ITK_WRAP_PYTHON_VERSION "${Python3_VERSION}")
58+
59+
# start section to define package components based on LIMITED_API support and choices
60+
set(_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION 3.11)
61+
62+
# Force ITK_WRAP_PYTHON_VERSION if SKBUILD_SABI_COMPONENT requests it
63+
string(
64+
FIND
65+
"${SKBUILD_SABI_COMPONENT}"
66+
"SABIModule"
67+
_SKBUILD_SABI_COMPONENT_REQUIRED
68+
)
69+
if(NOT DEFINED ITK_USE_PYTHON_LIMITED_API)
70+
if(
71+
(
72+
_SKBUILD_SABI_COMPONENT_REQUIRED
73+
GREATER
74+
-1
75+
)
76+
OR
77+
ITK_WRAP_PYTHON_VERSION
78+
VERSION_GREATER_EQUAL
79+
${_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION}
80+
)
81+
set(
82+
ITK_USE_PYTHON_LIMITED_API
83+
1
84+
CACHE BOOL
85+
"Configure Python's limited API for Python minor version compatibility."
86+
)
87+
else()
88+
set(
89+
ITK_USE_PYTHON_LIMITED_API
90+
0
91+
CACHE BOOL
92+
"Configure Python's limited API for Python minor version compatibility."
93+
)
94+
endif()
95+
mark_as_advanced(ITK_USE_PYTHON_LIMITED_API)
96+
endif()
97+
unset(_SKBUILD_SABI_COMPONENT_REQUIRED)
98+
if(ITK_USE_PYTHON_LIMITED_API)
99+
if(CMAKE_VERSION VERSION_LESS "3.26")
100+
message(
101+
FATAL_ERROR
102+
"CMake version ${CMAKE_VERSION} is too old for Python limited API wrapping: "
103+
"the FindPython Development.SABIModule component requires CMake >= 3.26. "
104+
"Either upgrade CMake or disable ITK_USE_PYTHON_LIMITED_API."
105+
)
106+
endif()
107+
108+
set(
109+
_python_find_components
110+
Interpreter
111+
Development # Needed for Modules/Core/Common/src/itkPyCommand.cxx
112+
Development.SABIModule
113+
)
114+
else()
115+
set(
116+
_python_find_components
117+
Interpreter
118+
Development # Needed for Modules/Core/Common/src/itkPyCommand.cxx
24119
Development.Module
25-
${SKBUILD_SABI_COMPONENT}
120+
)
121+
endif()
122+
if(BUILD_TESTING)
123+
# NumPy Required for testing ITK PythonTests, prefer to fail early if not installed
124+
list(APPEND _python_find_components NumPy)
125+
endif()
126+
set(_missing_required_component FALSE)
127+
find_package(
128+
Python3
129+
${ITK_WRAP_PYTHON_VERSION}
130+
COMPONENTS
131+
${_python_find_components}
26132
)
133+
set(ITK_WRAP_PYTHON_VERSION "${Python3_VERSION}")
134+
message(STATUS "Python3_FOUND=${Python3_FOUND}")
135+
foreach(_required_component ${_python_find_components})
136+
if(NOT Python3_${_required_component}_FOUND)
137+
message(
138+
STATUS
139+
" o Python3 Missing COMPONENT: Python3_${_required_component}_FOUND: ${Python3_${_required_component}_FOUND}"
140+
)
141+
set(_missing_required_component TRUE)
142+
else()
143+
message(
144+
STATUS
145+
" o Python3 Found COMPONENT: Python3_${_required_component}_FOUND: ${Python3_${_required_component}_FOUND}"
146+
)
147+
endif()
148+
endforeach()
149+
unset(_required_component)
150+
if(_missing_required_component)
151+
message(
152+
FATAL_ERROR
153+
"At least 1 required Python3 COMPONENT could not be found from : ${_python_find_components}
154+
in range ${PYTHON_VERSION_MIN}...${PYTHON_VERSION_MAX}:
155+
Python3_EXECUTABLE=:${Python3_EXECUTABLE}:
156+
ITK_WRAP_PYTHON_VERSION=:${ITK_WRAP_PYTHON_VERSION}:
157+
Python3_ROOT_DIR=:${Python3_ROOT_DIR}:
158+
---
159+
Python3_FOUND=${Python3_FOUND}
160+
Python3_Interpreter_FOUND=${Python3_Interpreter_FOUND}
161+
Python3_Compiler_FOUND=${Python3_Compiler_FOUND}
162+
Python3_Development_FOUND=${Python3_Development_FOUND}
163+
Python3_Development.Module_FOUND=${Python3_Development.Module_FOUND}
164+
Python3_Development.SABIModule_FOUND=${Python3_Development.SABIModule_FOUND}
165+
Python3_Development.Embed_FOUND=${Python3_Development.Embed_FOUND}
166+
Python3_NumPy_FOUND=${Python3_NumPy_FOUND}
167+
"
168+
)
169+
else()
170+
message(STATUS " o Python3_EXECUTABLE=${Python3_EXECUTABLE}")
171+
message(STATUS " o Python3_ROOT_DIR=${Python3_ROOT_DIR}")
172+
message(STATUS " o ITK_WRAP_PYTHON_VERSION=${ITK_WRAP_PYTHON_VERSION}")
173+
endif()
174+
unset(_missing_required_component)
175+
unset(_python_find_components)
176+
unset(_ITK_MINIMUM_SUPPORTED_LIMITED_API_VERSION)
177+
# end section to define package components based on LIMITED_API support and choices
27178
if(DEFINED _specified_Python3_EXECUTABLE)
28179
set(
29180
Python3_EXECUTABLE
@@ -33,35 +184,81 @@ if(PYTHON_DEVELOPMENT_REQUIRED)
33184
FORCE
34185
)
35186
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
187+
188+
if(NOT Python3_EXECUTABLE AND _specified_Python3_EXECUTABLE) # workaround for cases where FindPython3 fails to set correctly
189+
set(
190+
Python3_EXECUTABLE
191+
${_specified_Python3_EXECUTABLE}
192+
CACHE INTERNAL
193+
"Path to the Python interpreter"
194+
FORCE
195+
)
196+
endif()
197+
198+
# If a specific Python3_EXECUTABLE is provided by the user, try to infer
199+
# the corresponding Python3_ROOT_DIR for Unix/macOS/Linux so CMake's
200+
# FindPython3 locates the matching installation or virtual environment.
201+
# This is especially important for virtualenv/venv/conda environments.
202+
if(
203+
DEFINED
204+
Python3_EXECUTABLE
205+
AND
206+
NOT
207+
DEFINED
208+
Python3_ROOT_DIR
209+
AND
210+
(
211+
UNIX
212+
OR
213+
APPLE
214+
)
215+
AND
216+
NOT
217+
WIN32
56218
)
57-
endif()
219+
# First, try sys.prefix from the provided interpreter (works for venv/conda)
220+
execute_process(
221+
COMMAND
222+
"${Python3_EXECUTABLE}" -c "import sys; print(sys.prefix)"
223+
OUTPUT_VARIABLE _py_prefix
224+
ERROR_VARIABLE _py_prefix_err
225+
OUTPUT_STRIP_TRAILING_WHITESPACE
226+
)
227+
if(_py_prefix)
228+
file(TO_CMAKE_PATH "${_py_prefix}" _py_root_hint)
229+
endif()
230+
231+
# Fallback: parent of the interpreter's bin directory, e.g., /path/to/env
232+
# from /path/to/env/bin/python3
233+
if(NOT _py_root_hint)
234+
get_filename_component(_py_exe_dir "${Python3_EXECUTABLE}" DIRECTORY)
235+
get_filename_component(_py_root_hint "${_py_exe_dir}/.." REALPATH)
236+
endif()
58237

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)
238+
if(_py_root_hint)
239+
set(
240+
Python3_ROOT_DIR
241+
"${_py_root_hint}"
242+
CACHE PATH
243+
"Which installation or virtual environment of Python to use"
244+
FORCE
245+
)
246+
mark_as_advanced(Python3_ROOT_DIR)
247+
endif()
248+
unset(_py_prefix)
249+
unset(_py_prefix_err)
250+
unset(_py_exe_dir)
251+
unset(_py_root_hint)
252+
endif()
253+
if(Python3_ROOT_DIR)
254+
# Add user-visible cache entry if Python3_ROOT_DIR value is set
255+
set(
256+
Python3_ROOT_DIR
257+
${Python3_ROOT_DIR}
258+
CACHE PATH
259+
"Which installation or virtual environment of Python to use"
260+
FORCE
261+
)
262+
mark_as_advanced(Python3_ROOT_DIR)
263+
endif()
264+
endif()

0 commit comments

Comments
 (0)