88from setuptools import setup , Extension
99from setuptools import Command
1010from setuptools .command .build_ext import build_ext
11- from setuptools .command .bdist_wheel import bdist_wheel
1211
1312
1413class CleanCommand (Command ):
@@ -29,34 +28,21 @@ def run(self):
2928 shutil .rmtree (egg_info , ignore_errors = True )
3029
3130
32- class CustomBdistWheel (bdist_wheel ):
33- def run (self ):
34- # Ensure all build steps are run before bdist_wheel
35- self .run_command ("build_ext" )
36- super ().run ()
37-
38-
3931class CMakeExtension (Extension ):
4032 def __init__ (
4133 self ,
4234 name ,
4335 * ,
36+ setup = None ,
4437 source_dir = None ,
4538 install_dir = None ,
46- url = None ,
47- sha256 = None ,
4839 cmake_args = [],
4940 ):
5041 # Don't invoke the original build_ext for this special extension.
5142 super ().__init__ (name , sources = [])
52- if source_dir and url :
53- raise ValueError (
54- "CMakeExtension should have either a source_dir or a url, not both."
55- )
43+ self .setup = setup
5644 self .source_dir = source_dir
5745 self .install_dir = install_dir
58- self .url = url
59- self .sha256 = sha256
6046 self .cmake_args = cmake_args
6147
6248
@@ -81,9 +67,17 @@ def _build_cmake(self, ext: CMakeExtension):
8167 shutil .rmtree (build_dir , ignore_errors = True )
8268 build_dir .mkdir (parents = True , exist_ok = True )
8369
84- lib_dir = Path (
85- self .get_finalized_command ("build_py" ).get_package_dir ("numba.openmp.libs" )
86- )
70+ if ext .setup :
71+ ext .setup .setup ()
72+
73+ if self .inplace :
74+ lib_dir = Path (
75+ self .get_finalized_command ("build_py" ).get_package_dir (
76+ "numba.openmp.libs"
77+ )
78+ )
79+ else :
80+ lib_dir = Path (self .build_lib ) / "numba/openmp/libs"
8781
8882 extra_cmake_args = self ._env_toolchain_args (ext )
8983 # Set RPATH.
@@ -135,6 +129,10 @@ def _build_cmake(self, ext: CMakeExtension):
135129 include_dir = install_dir / "lib/cmake"
136130 if include_dir .exists ():
137131 shutil .rmtree (include_dir )
132+ # Remove symlinks in the install directory to avoid copies.
133+ for file in install_dir .rglob ("*" ):
134+ if file .is_symlink ():
135+ file .unlink ()
138136
139137 def _env_toolchain_args (self , ext ):
140138 args = []
@@ -149,79 +147,93 @@ def _env_toolchain_args(self, ext):
149147 return args
150148
151149
152- def _prepare_source_openmp (sha256 = None ):
150+ class PrepareOpenMP :
151+ setup_done = False
153152 LLVM_VERSION = os .environ .get ("LLVM_VERSION" , None )
154- assert LLVM_VERSION is not None , "LLVM_VERSION environment variable must be set."
155- url = f"https://github.com/llvm/llvm-project/releases/download/llvmorg-{ LLVM_VERSION } /llvm-project-{ LLVM_VERSION } .src.tar.xz"
156-
157- tmp = Path ("_downloads/libomp" ) / f"llvm-project-{ LLVM_VERSION } .tar.gz"
158- tmp .parent .mkdir (parents = True , exist_ok = True )
159-
160- # Download the source tarball if it does not exist.
161- if not tmp .exists ():
162- print (f"Downloading llvm-project version { LLVM_VERSION } url:" , url )
163- with urllib .request .urlopen (url ) as r :
164- with tmp .open ("wb" ) as f :
165- f .write (r .read ())
166- else :
167- print (f"Using downloaded llvm-project at { tmp } " )
168-
169- if sha256 :
170- import hashlib
171-
172- hasher = hashlib .sha256 ()
173- with tmp .open ("rb" ) as f :
174- hasher .update (f .read ())
175- if hasher .hexdigest () != sha256 :
176- raise ValueError (f"SHA256 mismatch for { url } " )
177-
178- print ("Extracting llvm-project..." )
179- with tarfile .open (tmp ) as tf :
180- # The root dir llvm-project-20.1.8.src
181- root_name = tf .getnames ()[0 ]
182-
183- # Extract only needed subdirectories
184- members = [
185- m
186- for m in tf .getmembers ()
187- if m .name .startswith (f"{ root_name } /openmp/" )
188- or m .name .startswith (f"{ root_name } /offload/" )
189- or m .name .startswith (f"{ root_name } /runtimes/" )
190- or m .name .startswith (f"{ root_name } /cmake/" )
191- or m .name .startswith (f"{ root_name } /llvm/cmake/" )
192- or m .name .startswith (f"{ root_name } /llvm/utils/" )
193- or m .name .startswith (f"{ root_name } /libc/" )
194- ]
195-
196- parentdir = tmp .parent
197- # Base arguments for extractall.
198- kwargs = {"path" : parentdir , "members" : members }
199-
200- # Check if data filter is available.
201- if hasattr (tarfile , "data_filter" ):
202- # If this exists, the 'filter' argument is guaranteed to work
203- kwargs ["filter" ] = "data"
204-
205- tf .extractall (** kwargs )
206-
207- source_dir = parentdir / root_name
208- print ("Extracted llvm-project to:" , source_dir )
209-
210- print ("Applying patches to llvm-project..." )
211- for patch in sorted (
212- Path (f"src/numba/openmp/libs/openmp/patches/{ LLVM_VERSION } " )
213- .absolute ()
214- .glob ("*.patch" )
215- ):
216- print ("applying patch" , patch )
217- subprocess .run (
218- ["patch" , "-p1" , "-i" , str (patch )],
219- cwd = source_dir ,
220- check = True ,
221- stdin = subprocess .DEVNULL ,
153+
154+ @classmethod
155+ def setup (cls ):
156+ if not cls .setup_done :
157+ cls ._prepare_source_openmp ()
158+ cls .setup_done = True
159+
160+ @classmethod
161+ def get_source_dir (cls ):
162+ return Path (
163+ f"_downloads/libomp/llvm-project-{ cls .LLVM_VERSION } .src/runtimes"
164+ ).absolute ()
165+
166+ @classmethod
167+ def _prepare_source_openmp (cls ):
168+ assert cls .LLVM_VERSION is not None , (
169+ "LLVM_VERSION environment variable must be set."
222170 )
171+ url = f"https://github.com/llvm/llvm-project/releases/download/llvmorg-{ cls .LLVM_VERSION } /llvm-project-{ cls .LLVM_VERSION } .src.tar.xz"
223172
224- return f"{ source_dir } /runtimes"
173+ tmp = Path ("_downloads/libomp" ) / f"llvm-project-{ cls .LLVM_VERSION } .tar.gz"
174+ tmp .parent .mkdir (parents = True , exist_ok = True )
175+
176+ # Download the source tarball if it does not exist.
177+ if not tmp .exists ():
178+ print (
179+ f"Downloading llvm-project version { cls .LLVM_VERSION } url:" ,
180+ url ,
181+ flush = True ,
182+ )
183+ with urllib .request .urlopen (url ) as r :
184+ with tmp .open ("wb" ) as f :
185+ f .write (r .read ())
186+ else :
187+ print (f"Using downloaded llvm-project at { tmp } " , flush = True )
188+
189+ print ("Extracting llvm-project..." , flush = True )
190+ root_dir = Path (f"_downloads/libomp/llvm-project-{ cls .LLVM_VERSION } .src" )
191+ if not root_dir .exists ():
192+ with tarfile .open (tmp ) as tf :
193+ # Extract only needed subdirectories
194+ root_name = f"llvm-project-{ cls .LLVM_VERSION } .src"
195+
196+ # Use prefix tuple + any() for faster membership testing
197+ prefixes = (
198+ f"{ root_name } /openmp/" ,
199+ f"{ root_name } /offload/" ,
200+ f"{ root_name } /runtimes/" ,
201+ f"{ root_name } /cmake/" ,
202+ f"{ root_name } /llvm/cmake/" ,
203+ f"{ root_name } /llvm/utils/" ,
204+ f"{ root_name } /libc/" ,
205+ )
206+
207+ include_members = [
208+ m
209+ for m in tf .getmembers ()
210+ if any (m .name .startswith (p ) for p in prefixes )
211+ ]
212+
213+ parentdir = tmp .parent
214+ # Base arguments for extractall.
215+ kwargs = {"path" : parentdir , "members" : include_members }
216+
217+ # Check if data filter is available.
218+ if hasattr (tarfile , "data_filter" ):
219+ # If this exists, the 'filter' argument is guaranteed to work
220+ kwargs ["filter" ] = "data"
221+
222+ tf .extractall (** kwargs )
223+
224+ print ("Applying patches to llvm-project..." , flush = True )
225+ for patch in sorted (
226+ Path (f"src/numba/openmp/libs/openmp/patches/{ cls .LLVM_VERSION } " )
227+ .absolute ()
228+ .glob ("*.patch" )
229+ ):
230+ print ("applying patch" , patch , flush = True )
231+ subprocess .run (
232+ ["patch" , "-p1" , "-N" , "-i" , str (patch )],
233+ cwd = root_dir ,
234+ check = True ,
235+ stdin = subprocess .DEVNULL ,
236+ )
225237
226238
227239def _check_true (env_var ):
@@ -234,18 +246,15 @@ def _check_true(env_var):
234246ext_modules = [CMakeExtension ("pass" , source_dir = "src/numba/openmp/libs/pass" )]
235247
236248
237- # Prepare source directory if either bundled libomp or libomptarget is enabled.
238- if _check_true ("ENABLE_BUNDLED_LIBOMP" ) or _check_true ("ENABLE_BUNDLED_LIBOMPTARGET" ):
239- openmp_source_dir = _prepare_source_openmp ()
240-
241249# Optionally enable bundled libomp build via ENABLE_BUNDLED_LIBOMP=1. We want
242250# to avoid bundling for conda builds to avoid duplicate OpenMP runtime conflicts
243251# (e.g., numba 0.62+ and libopenblas already require llvm-openmp).
244252if _check_true ("ENABLE_BUNDLED_LIBOMP" ):
245253 ext_modules .append (
246254 CMakeExtension (
247255 "libomp" ,
248- source_dir = openmp_source_dir ,
256+ setup = PrepareOpenMP ,
257+ source_dir = PrepareOpenMP .get_source_dir (),
249258 install_dir = "openmp" ,
250259 cmake_args = [
251260 "-DOPENMP_STANDALONE_BUILD=ON" ,
@@ -265,7 +274,8 @@ def _check_true(env_var):
265274 ext_modules .append (
266275 CMakeExtension (
267276 "libomptarget" ,
268- source_dir = openmp_source_dir ,
277+ setup = PrepareOpenMP ,
278+ source_dir = PrepareOpenMP .get_source_dir (),
269279 install_dir = "openmp" ,
270280 cmake_args = [
271281 "-DOPENMP_STANDALONE_BUILD=ON" ,
@@ -283,6 +293,5 @@ def _check_true(env_var):
283293 cmdclass = {
284294 "clean" : CleanCommand ,
285295 "build_ext" : BuildCMakeExt ,
286- ** ({"bdist_wheel" : CustomBdistWheel } if CustomBdistWheel else {}),
287296 },
288297)
0 commit comments