Skip to content

Commit ebe4c1e

Browse files
committed
Move CMake build to setup.py
- C++ binaries now built via setup.py instead of cibuildwheel config - Binaries are only built when needed (checks for existing files) - Enables building from source when no wheel is available (no need to git clone)
1 parent 86ac6c3 commit ebe4c1e

5 files changed

Lines changed: 181 additions & 25 deletions

File tree

.gitignore

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
1+
# General ignores
12
.DS_Store
23
.vscode
3-
*.pyc
44
*.swp
5-
6-
# Ignore all build artifacts
75
*.o
8-
*.a
9-
*.so
10-
*.so.*
11-
*.dylib
12-
CMakeCache.txt
6+
7+
# CMake artifacts
138
CMakeFiles/
14-
cmake_install.cmake
9+
CMakeCache.txt
1510
Makefile
11+
*.cmake
12+
*.a
13+
!CMakeLists.txt
14+
15+
# Python build artifacts
1616
build/
1717
dist/
1818
*.egg-info/
19-
wheelhouse/
20-
epigeec/_version.py
19+
__pycache__/
20+
_version.py
21+
*.pyc
2122

22-
# Ignore compiled binaries and libraries
23+
# Compiled binaries
2324
epigeec/bin/*
2425
epigeec/lib/*
26+
*.so
27+
*.so.*
28+
*.dylib
2529

2630
# Keep directory structure
2731
!epigeec/bin/.gitkeep

debug/Dockerfile.cmake

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,28 @@ FROM custom-manylinux_2_28_x86_64
33

44
LABEL description="Building epigeec binaries into custom-manylinux_2_28_x86_64 image."
55

6+
# Create a non-root user to match host user IDs
7+
8+
ARG USER_UID
9+
ARG USER_GID
10+
11+
RUN groupadd --gid ${USER_GID} dockeruser && \
12+
useradd --uid ${USER_UID} --gid ${USER_GID} -m dockeruser
13+
614
# Create work directory and copy source tree from build context
15+
# Docker always does COPY as root
716
WORKDIR /project
817
COPY . .
18+
RUN chown -R ${USER_UID}:${USER_GID} /project
919

10-
# Optional sanity check
11-
RUN echo "--- Source tree ---" && tree -L 2
20+
# Switch to non-root user, so cmake and make do not create root-owned files
21+
# Switch to non-root user
22+
USER dockeruser
1223

13-
# Clean, configure, and build
14-
RUN git clean -fdx && \
15-
cmake . && \
24+
# Build in a separate 'build/' directory (does not require .git, can't run git clean)
25+
RUN mkdir -p build && \
26+
cd build && \
27+
cmake .. && \
1628
make -j "$(nproc --all)"
1729

1830
# Default entrypoint

debug/build_image.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ export DOCKER_BUILDKIT=1
77
if [ "$1" == "yum" ]; then
88
build_file="debug/Dockerfile.yum"
99
tag="custom-manylinux_2_28_x86_64"
10+
cache=""
1011
elif [ "$1" == "cmake" ]; then
1112
build_file="debug/Dockerfile.cmake"
1213
tag="custom-manylinux_2_28_x86_64-epigeec-built"
14+
cache="--no-cache"
1315
else
1416
echo "Usage: $0 [yum|cmake]"
1517
exit 1
1618
fi
1719

18-
docker build -f $build_file -t $tag .
20+
docker build $cache --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) -f $build_file -t $tag .
1921

2022
echo "Custom manylinux image '$tag' built successfully."
2123
echo "You can now use this image in cibuildwheel or run it directly."

pyproject.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,15 @@ epigeec = [
5050
]
5151

5252
# --- CIBUILDWHEEL CONFIGURATION ---
53+
# Build wheels for multiple platforms and Python versions using cibuildwheel.
54+
# Depends on setup.py being properly configured for compiling the C++ programs.
55+
# 'before-all' installs system dependencies required for building the C++ extensions.
56+
# Then cibuildwheel calls the build backend (setuptools.build_meta) which triggers setup.py's CustomBuild class.
5357

5458
[tool.cibuildwheel]
5559
build = ["cp311-*", "cp312-*", "cp313-*"]
5660
skip = ["*-musllinux*", "*-manylinux_i686"] # ignore 32-bit Linux builds
5761

58-
# Define build commands as env var. Run once per architecture.
59-
environment = {EPIGEEC_BUILD = "git clean -fdx && cmake . && make -j $(nproc --all)"}
60-
6162
# Run tests only on some platforms (runs after each wheel is built)
6263
test-skip = [
6364
"*-macosx_*", # skip macOS tests
@@ -76,7 +77,6 @@ manylinux-aarch64-image = "manylinux_2_28"
7677
before-all = [
7778
"yum install -y epel-release", # Enable EPEL repository first, package names changed with manylinux_2_28
7879
"yum install -y hdf5-devel boost-devel",
79-
"bash -c \"$EPIGEEC_BUILD\"",
8080
]
8181

8282
[tool.cibuildwheel.macos]
@@ -85,7 +85,6 @@ environment = {CMAKE_PREFIX_PATH = "/opt/homebrew:/usr/local:/opt/local"}
8585

8686
before-all = [
8787
"brew install hdf5 boost libomp",
88-
"bash -c \"$EPIGEEC_BUILD\"",
8988
]
9089

9190
# --- LINTING CONFIGURATION ---

setup.py

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,145 @@
1+
"""Custom setup.py to build C++ components with CMake before packaging."""
2+
3+
import os
4+
import subprocess
5+
import sys
6+
from pathlib import Path
7+
18
from setuptools import setup
9+
from setuptools.command.build import build
10+
11+
12+
def is_binary(filepath: str | Path) -> bool:
13+
"""Check if a file is a binary executable."""
14+
path = str(filepath)
15+
if path.startswith("."):
16+
return False
17+
try:
18+
with open(path, "rb") as f:
19+
chunk = f.read(1024)
20+
if b"\0" in chunk:
21+
return True
22+
except Exception as err:
23+
print(f"Error checking if file is binary: {err}", file=sys.stderr)
24+
return False
25+
return False
26+
27+
28+
class CustomBuild(build):
29+
"""Custom build command that runs cmake and make."""
30+
31+
def run(self):
32+
"""Run cmake and make before the normal build."""
33+
bin_dir = Path("epigeec/bin")
34+
lib_dir = Path("epigeec/lib")
35+
36+
# Check if binaries exist
37+
has_binaries = False
38+
if bin_dir.exists():
39+
binaries = [b for b in bin_dir.glob("*") if is_binary(b)]
40+
has_binaries = len(binaries) > 0
41+
42+
if lib_dir.exists() and not has_binaries:
43+
libs = list(lib_dir.glob("*.so*"))
44+
has_binaries = len(libs) > 0
45+
46+
if not has_binaries:
47+
print("=" * 60, file=sys.stderr)
48+
print("Building C++ components with CMake...", file=sys.stderr)
49+
print("=" * 60, file=sys.stderr)
50+
51+
try:
52+
# Create build directory
53+
build_dir = Path("build_cmake")
54+
build_dir.mkdir(exist_ok=True)
55+
56+
# Run cmake (configure from build directory)
57+
result = subprocess.run(
58+
["cmake", ".."],
59+
cwd=build_dir,
60+
check=True,
61+
capture_output=True,
62+
text=True,
63+
)
64+
print(result.stdout, file=sys.stderr)
65+
66+
# Run make
67+
nproc = os.cpu_count() or 4
68+
result = subprocess.run(
69+
["cmake", "--build", ".", "-j", str(nproc)],
70+
cwd=build_dir,
71+
check=True,
72+
capture_output=True,
73+
text=True,
74+
)
75+
print(result.stdout, file=sys.stderr)
76+
77+
print("=" * 60, file=sys.stderr)
78+
print("C++ build complete!", file=sys.stderr)
79+
80+
# Verify build succeeded
81+
if bin_dir.exists():
82+
binaries = [b for b in bin_dir.glob("*") if is_binary(b)]
83+
print(f"Built {len(binaries)} binaries:", file=sys.stderr)
84+
for b in binaries:
85+
print(f" - {b.name}", file=sys.stderr)
86+
87+
print("=" * 60, file=sys.stderr)
88+
89+
except FileNotFoundError as e:
90+
print(
91+
f"""
92+
ERROR: Required build tool not found: {e}
93+
94+
Please install the required dependencies:
95+
- CMake
96+
- C++ compiler (gcc/g++)
97+
- HDF5 development libraries
98+
- Boost development libraries
99+
100+
On Ubuntu/Debian:
101+
sudo apt-get install cmake build-essential libhdf5-dev libboost-dev
102+
103+
On RHEL/Fedora:
104+
sudo yum install cmake gcc-c++ hdf5-devel boost-devel
105+
106+
On macOS:
107+
brew install cmake hdf5 boost libomp
108+
""",
109+
file=sys.stderr,
110+
)
111+
sys.exit(1)
112+
113+
except subprocess.CalledProcessError as e:
114+
print(
115+
f"""
116+
ERROR: Build failed with exit code {e.returncode}
117+
118+
STDOUT:
119+
{e.stdout}
120+
121+
STDERR:
122+
{e.stderr}
123+
124+
Please check that all dependencies are installed correctly.
125+
""",
126+
file=sys.stderr,
127+
)
128+
sys.exit(1)
129+
else:
130+
print("=" * 60, file=sys.stderr)
131+
print("C++ binaries already exist, skipping build.", file=sys.stderr)
132+
if bin_dir.exists():
133+
binaries = [b for b in bin_dir.glob("*") if is_binary(b)]
134+
for b in binaries:
135+
print(f" - {b.name}", file=sys.stderr)
136+
print("=" * 60, file=sys.stderr)
137+
138+
# Continue with normal build
139+
super().run()
140+
2141

3-
# This tells setuptools we have platform-specific binary content
4142
setup(
5-
has_ext_modules=lambda: True,
143+
cmdclass={"build": CustomBuild},
144+
has_ext_modules=lambda: True, # Makes wheel tag platform-specific
6145
)

0 commit comments

Comments
 (0)