1+ import copy
12import os
23import shutil
34import subprocess
45import sys
56from abc import ABC , abstractmethod
67from collections import OrderedDict
78from collections .abc import Callable
9+ from os import environ
810from pathlib import Path
911
1012from BuildManager import BuildManager
1921
2022
2123class BuildPythonInstanceBase (ABC ):
22- """
23- Abstract base class to build wheels for a single Python environment.
24-
25- Concrete subclasses implement platform-specific details by delegating to
26- injected helper functions. This avoids circular imports with the script
27- that defines those helpers.
24+ """Abstract base class to build wheels for a single Python environment.
25+
26+ Concrete subclasses implement platform-specific details (environment
27+ setup, wheel fixup, tarball creation) while this class provides the
28+ shared build orchestration, CMake configuration, and wheel-building
29+ logic.
30+
31+ Parameters
32+ ----------
33+ platform_env : str
34+ Platform/environment identifier (e.g. ``'manylinux228-py311'``).
35+ build_dir_root : Path
36+ Root directory for all build artifacts.
37+ package_env_config : dict
38+ Mutable configuration dictionary populated throughout the build.
39+ cleanup : bool
40+ Whether to remove intermediate build artifacts.
41+ build_itk_tarball_cache : bool
42+ Whether to create a reusable tarball of the ITK build tree.
43+ cmake_options : list[str]
44+ Extra ``-D`` options forwarded to CMake.
45+ windows_extra_lib_paths : list[str]
46+ Additional library paths for Windows wheel fixup (delvewheel).
47+ dist_dir : Path
48+ Output directory for built wheel files.
49+ module_source_dir : Path, optional
50+ Path to an external ITK remote module to build.
51+ module_dependencies_root_dir : Path, optional
52+ Directory where remote module dependencies are cloned.
53+ itk_module_deps : str, optional
54+ Colon-delimited dependency specifications for remote modules.
55+ skip_itk_build : bool, optional
56+ Skip the ITK C++ build step.
57+ skip_itk_wheel_build : bool, optional
58+ Skip the ITK wheel build step.
2859 """
2960
3061 def __init__ (
@@ -166,6 +197,7 @@ def __init__(
166197 )
167198
168199 def update_venv_itk_build_configurations (self ) -> None :
200+ """Set ``Python3_ROOT_DIR`` in ITK build configurations from venv info."""
169201 # Python3_EXECUTABLE, Python3_INCLUDE_DIR, and Python3_LIBRARY are validated
170202 # and resolved by find_package(Python3) in cmake/ITKPythonPackage_SuperBuild.cmake
171203 # when not already defined. Python3_ROOT_DIR is set here to guide that search.
@@ -248,6 +280,7 @@ def run(self) -> None:
248280 print ("=" * 80 )
249281
250282 def build_superbuild_support_components (self ):
283+ """Configure and build the superbuild support components (ITK source, TBB)."""
251284 # -----------------------------------------------------------------------
252285 # Build required components (optional local ITK source, TBB builds) used to populate the archive cache
253286
@@ -297,13 +330,22 @@ def build_superbuild_support_components(self):
297330 )
298331
299332 def fixup_wheels (self , lib_paths : str = "" ):
333+ """Apply platform-specific fixups to ``itk_core`` wheels for TBB linkage."""
300334 # TBB library fix-up (applies to itk_core wheel)
301335 tbb_wheel = "itk_core"
302336 for wheel in (self .build_dir_root / "dist" ).glob (f"{ tbb_wheel } *.whl" ):
303337 self .fixup_wheel (str (wheel ), lib_paths )
304338
305339 def final_wheel_import_test (self , installed_dist_dir : Path ):
306- self .echo_check_call (
340+ """Install and smoke-test all ITK wheels from *installed_dist_dir*.
341+
342+ Parameters
343+ ----------
344+ installed_dist_dir : Path
345+ Directory containing the built ``.whl`` files to install and
346+ verify.
347+ """
348+ exit_status = self .echo_check_call (
307349 [
308350 self .package_env_config ["PYTHON_EXECUTABLE" ],
309351 "-m" ,
@@ -316,7 +358,10 @@ def final_wheel_import_test(self, installed_dist_dir: Path):
316358 str (installed_dist_dir ),
317359 ]
318360 )
319- print ("Wheel successfully installed." )
361+ if exit_status == 0 :
362+ print ("Wheels successfully installed." )
363+ else :
364+ print (f"Failed to install wheels: { exit_status } " )
320365 # Basic imports
321366 self .echo_check_call (
322367 [self .package_env_config ["PYTHON_EXECUTABLE" ], "-c" , "import itk;" ]
@@ -392,6 +437,24 @@ def find_unix_exectable_paths(
392437 self ,
393438 venv_dir : Path ,
394439 ) -> tuple [str , str , str , str , str ]:
440+ """Resolve Python interpreter and virtualenv paths on Unix.
441+
442+ Parameters
443+ ----------
444+ venv_dir : Path
445+ Root of the Python virtual environment.
446+
447+ Returns
448+ -------
449+ tuple[str, str, str, str, str]
450+ ``(python_executable, python_include_dir, python_library,
451+ venv_bin_path, venv_base_dir)``.
452+
453+ Raises
454+ ------
455+ FileNotFoundError
456+ If the Python executable does not exist under *venv_dir*.
457+ """
395458 python_executable = venv_dir / "bin" / "python"
396459 if not python_executable .exists ():
397460 raise FileNotFoundError (f"Python executable not found: { python_executable } " )
@@ -426,48 +489,170 @@ def find_unix_exectable_paths(
426489 str (venv_dir ),
427490 )
428491
429- @abstractmethod
430492 def clone (self ):
431- # each subclass must implement this method that is used to clone itself
432- pass
493+ """Return a deep copy of this instance for building a dependency.
433494
434- @abstractmethod
435- def venv_paths (self ):
436- pass
495+ Uses ``self.__class__`` so the returned object is the same concrete
496+ subclass as the original (e.g. ``LinuxBuildPythonInstance``).
497+ """
498+ cls = self .__class__
499+ new = cls .__new__ (cls )
500+ new .__dict__ = copy .deepcopy (self .__dict__ )
501+ return new
502+
503+ def venv_paths (self ) -> None :
504+ """Populate ``venv_info_dict`` from the pixi-managed Python interpreter.
505+
506+ Default Unix implementation shared by Linux and macOS. Windows
507+ overrides this method with its own path conventions.
508+ """
509+ primary_python_base_dir = Path (
510+ self .package_env_config ["PYTHON_EXECUTABLE" ]
511+ ).parent .parent
512+ (
513+ python_executable ,
514+ python_include_dir ,
515+ python_library ,
516+ venv_bin_path ,
517+ venv_base_dir ,
518+ ) = self .find_unix_exectable_paths (primary_python_base_dir )
519+ self .venv_info_dict = {
520+ "python_executable" : python_executable ,
521+ "python_include_dir" : python_include_dir ,
522+ "python_library" : python_library ,
523+ "venv_bin_path" : venv_bin_path ,
524+ "venv_base_dir" : venv_base_dir ,
525+ "python_root_dir" : primary_python_base_dir ,
526+ }
437527
438528 @abstractmethod
439529 def fixup_wheel (
440530 self , filepath , lib_paths : str = "" , remote_module_wheel : bool = False
441531 ): # pragma: no cover - abstract
532+ """Apply platform-specific wheel repairs (auditwheel, delocate, delvewheel).
533+
534+ Parameters
535+ ----------
536+ filepath : str
537+ Path to the ``.whl`` file to fix.
538+ lib_paths : str, optional
539+ Additional library search paths (semicolon-delimited on Windows).
540+ remote_module_wheel : bool, optional
541+ True when fixing a wheel for an external remote module.
542+ """
442543 pass
443544
444545 @abstractmethod
445546 def build_tarball (self ):
547+ """Create a compressed archive of the ITK build tree for caching."""
446548 pass
447549
448- @abstractmethod
449- def post_build_cleanup (self ):
450- pass
550+ def post_build_cleanup (self ) -> None :
551+ """Remove intermediate build artifacts, leaving ``dist/`` intact.
552+
553+ Actions:
554+ - remove oneTBB-prefix (symlink or dir)
555+ - remove ITKPythonPackage/, tools/, _skbuild/, build/
556+ - remove top-level *.egg-info
557+ - remove ITK-* build tree and tarballs
558+ - if ITK_MODULE_PREQ is set, remove cloned module dirs
559+ """
560+ base = Path (self .package_env_config ["IPP_SOURCE_DIR" ])
561+
562+ def rm (tree_path : Path ):
563+ try :
564+ _remove_tree (tree_path )
565+ except Exception :
566+ pass
567+
568+ # 1) unlink oneTBB-prefix if it's a symlink or file
569+ tbb_prefix_dir = base / "oneTBB-prefix"
570+ try :
571+ if tbb_prefix_dir .is_symlink () or tbb_prefix_dir .is_file ():
572+ tbb_prefix_dir .unlink (missing_ok = True ) # type: ignore[arg-type]
573+ elif tbb_prefix_dir .exists ():
574+ rm (tbb_prefix_dir )
575+ except Exception :
576+ pass
577+
578+ # 2) standard build directories
579+ for rel in ("ITKPythonPackage" , "tools" , "_skbuild" , "build" ):
580+ rm (base / rel )
581+
582+ # 3) egg-info folders at top-level
583+ for p in base .glob ("*.egg-info" ):
584+ rm (p )
585+
586+ # 4) ITK build tree and tarballs
587+ target_arch = self .package_env_config ["ARCH" ]
588+ for p in base .glob (f"ITK-*-{ self .package_env_config } _{ target_arch } " ):
589+ rm (p )
590+
591+ # Tarballs
592+ for p in base .glob (f"ITKPythonBuilds-{ self .package_env_config } *.tar.zst" ):
593+ rm (p )
594+
595+ # 5) Optional module prerequisites cleanup (ITK_MODULE_PREQ)
596+ # Format: "InsightSoftwareConsortium/ITKModuleA@v1.0:Kitware/ITKModuleB@sha"
597+ itk_preq = self .package_env_config .get ("ITK_MODULE_PREQ" ) or environ .get (
598+ "ITK_MODULE_PREQ" , ""
599+ )
600+ if itk_preq :
601+ for entry in itk_preq .split (":" ):
602+ entry = entry .strip ()
603+ if not entry :
604+ continue
605+ try :
606+ module_name = entry .split ("@" , 1 )[0 ].split ("/" , 1 )[1 ]
607+ except Exception :
608+ continue
609+ rm (base / module_name )
451610
452611 @abstractmethod
453612 def prepare_build_env (self ) -> None : # pragma: no cover - abstract
613+ """Set up platform-specific build environment and CMake configurations.
614+
615+ Must populate ``self.venv_info_dict``, configure TBB settings,
616+ and set the ITK binary build directory in
617+ ``self.cmake_itk_source_build_configurations``.
618+ """
454619 pass
455620
456621 @abstractmethod
457622 def post_build_fixup (self ) -> None : # pragma: no cover - abstract
458- pass
623+ """Run platform-specific post-build wheel fixups.
459624
460- @abstractmethod
461- def final_import_test (self ) -> None : # pragma: no cover - abstract
625+ Called after all wheels are built but before the final import
626+ test. Typically invokes ``fixup_wheel`` or ``fixup_wheels``.
627+ """
462628 pass
463629
630+ def final_import_test (self ) -> None : # pragma: no cover
631+ """Install and smoke-test the built wheels."""
632+ self .final_wheel_import_test (installed_dist_dir = self .dist_dir )
633+
464634 @abstractmethod
465635 def discover_python_venvs (
466636 self , platform_os_name : str , platform_architechure : str
467637 ) -> list [str ]:
638+ """Return available Python environment names for the given platform.
639+
640+ Parameters
641+ ----------
642+ platform_os_name : str
643+ Operating system identifier.
644+ platform_architechure : str
645+ CPU architecture identifier.
646+
647+ Returns
648+ -------
649+ list[str]
650+ Sorted list of discovered environment names.
651+ """
468652 pass
469653
470654 def build_external_module_python_wheel (self ):
655+ """Build a wheel for an external ITK remote module via scikit-build-core."""
471656 self .module_source_dir = Path (self .module_source_dir )
472657 out_dir = self .module_source_dir / "dist"
473658 out_dir .mkdir (parents = True , exist_ok = True )
@@ -546,6 +731,14 @@ def build_external_module_python_wheel(self):
546731 if py_include :
547732 defs .set ("Python3_INCLUDE_DIR:PATH" , py_include )
548733
734+ # Pass Python library paths when explicitly known (Windows)
735+ py_library = self .venv_info_dict .get ("python_library" , "" )
736+ if py_library :
737+ defs .set ("Python3_LIBRARY:FILEPATH" , str (py_library ))
738+ py_sabi_library = self .venv_info_dict .get ("python_sabi_library" , "" )
739+ if py_sabi_library :
740+ defs .set ("Python3_SABI_LIBRARY:FILEPATH" , str (py_sabi_library ))
741+
549742 # Allow command-line cmake -D overrides to win last
550743 if self .cmake_cmdline_definitions :
551744 defs .update (self .cmake_cmdline_definitions .items ())
@@ -567,6 +760,7 @@ def build_external_module_python_wheel(self):
567760 self .fixup_wheel (str (wheel ), remote_module_wheel = True )
568761
569762 def build_itk_python_wheels (self ):
763+ """Build all ITK Python wheels listed in ``WHEEL_NAMES.txt``."""
570764 # Build wheels
571765 for wheel_name in self .wheel_names :
572766 print ("#" )
@@ -640,6 +834,7 @@ def build_itk_python_wheels(self):
640834 _remove_tree (bp / "Wrapping" / "Generators" / "CastXML" )
641835
642836 def build_wrapped_itk_cplusplus (self ):
837+ """Configure and build the ITK C++ libraries with Python wrapping."""
643838 # Clean up previous invocations
644839 if (
645840 self .cleanup
@@ -940,9 +1135,14 @@ def create_posix_tarball(self):
9401135 if issues :
9411136 print ("Compatibility warnings above - review before using in CI/CD" )
9421137
943- @abstractmethod
9441138 def get_pixi_environment_name (self ):
945- pass
1139+ """Return the pixi environment name for this build instance.
1140+
1141+ The pixi environment name is the same as the platform_env and
1142+ is related to the environment setups defined in pixi.toml
1143+ in the root of this git directory that contains these scripts.
1144+ """
1145+ return self .platform_env
9461146
9471147 def echo_check_call (
9481148 self ,
0 commit comments