Skip to content

Commit b5c38e3

Browse files
Optimize building and packaging (#53)
* Remove custom bdist_wheel and prepare once OpenMP sources * Optimize openmp source extraction and building - Extract and apply patches once per build - Remove symlinks - Change pyomp to load the versioned libomptarget library * Clean up packaged files - Remove unnecessary manifest - Do not include package data, wheel builds will collect binaries from the build dir * Update conda test for correct filename
1 parent 88f7f76 commit b5c38e3

5 files changed

Lines changed: 118 additions & 110 deletions

File tree

MANIFEST.in

Lines changed: 0 additions & 1 deletion
This file was deleted.

buildscripts/conda-recipes/pyomp/meta.yaml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{% set version = environ.get('GIT_DESCRIBE_TAG', '0.0.0').lstrip('v') %}
22
{% set LLVM_VERSION = environ.get('LLVM_VERSION', '20.1.8') %}
3+
{% set LLVM_VERSION_MAJOR = LLVM_VERSION.split('.')[0] %}
4+
{% set LLVM_VERSION_MINOR = LLVM_VERSION.split('.')[1] %}
35

46
package:
57
name: pyomp
@@ -64,11 +66,11 @@ requirements:
6466

6567
test:
6668
commands:
67-
- test -f $SP_DIR/numba/openmp/libs/pass/libIntrinsicsOpenMP.dylib # [osx]
68-
- test -f $SP_DIR/numba/openmp/libs/pass/libIntrinsicsOpenMP.so # [linux]
69-
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget-amdgpu.bc # [linux]
70-
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget-nvptx.bc # [linux]
71-
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget.so # [linux]
69+
- test -f $SP_DIR/numba/openmp/libs/pass/libIntrinsicsOpenMP.dylib # [osx]
70+
- test -f $SP_DIR/numba/openmp/libs/pass/libIntrinsicsOpenMP.so # [linux]
71+
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget-amdgpu.bc # [linux]
72+
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget-nvptx.bc # [linux]
73+
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget.so.{{ LLVM_VERSION_MAJOR }}.{{ LLVM_VERSION_MINOR }} # [linux]
7274

7375
about:
7476
home: https://github.com/Python-for-HPC/PyOMP

pyproject.toml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,12 @@ Homepage = "https://github.com/Python-for-HPC/PyOMP"
2727
Issues = "https://github.com/Python-for-HPC/PyOMP/issues"
2828

2929
[tool.setuptools]
30-
include-package-data = true
30+
include-package-data = false
3131
package-dir = { "" = "src" }
3232

33-
# Use discovery for the numba.* namespace.
3433
[tool.setuptools.packages.find]
3534
where = ["src"]
36-
include = ["numba.openmp*"]
37-
38-
# Bundle the CMake-installed artifacts into the wheel.
39-
[tool.setuptools.package-data]
40-
"numba.openmp.libs" = ["pass/*", "openmp/**/*"]
35+
include = ["numba*"]
4136

4237
# setuptools-scm config
4338
[tool.setuptools_scm]

setup.py

Lines changed: 105 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from setuptools import setup, Extension
99
from setuptools import Command
1010
from setuptools.command.build_ext import build_ext
11-
from setuptools.command.bdist_wheel import bdist_wheel
1211

1312

1413
class 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-
3931
class 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

227239
def _check_true(env_var):
@@ -234,18 +246,15 @@ def _check_true(env_var):
234246
ext_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).
244252
if _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
)

src/numba/openmp/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ def _init():
9292
if sys_platform.startswith("darwin") or sys_platform.startswith("win32"):
9393
return
9494

95-
omptargetlib = libpath / "openmp" / "lib" / "libomptarget.so"
95+
llvm_major, llvm_minor, _ = ll.llvm_version_info
96+
omptargetlib = (
97+
libpath / "openmp" / "lib" / f"libomptarget.so.{llvm_major}.{llvm_minor}"
98+
)
9699
if omptargetlib.exists():
97100
if DEBUG_OPENMP >= 1:
98101
print("Found OpenMP target runtime library at", omptargetlib)

0 commit comments

Comments
 (0)