129 lines
3.8 KiB
Python
129 lines
3.8 KiB
Python
"""Build script for Cider.
|
|
|
|
Usage:
|
|
pip install -e . # Editable install (dev)
|
|
pip install . # Regular install
|
|
python setup.py build_ext # Build C++ extension only
|
|
|
|
The C++ extension is built via CMake and installed into cider/lib/.
|
|
On Apple M4 and below, the C++ extension is skipped (INT8 TensorOps
|
|
require M5+). The pure-Python package still installs and provides
|
|
graceful fallback via is_available() → False.
|
|
"""
|
|
|
|
import os
|
|
import platform
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from setuptools import setup, Extension
|
|
from setuptools.command.build_ext import build_ext
|
|
|
|
|
|
def _detect_apple_chip() -> int:
|
|
"""Detect Apple Silicon generation. Returns chip number (4, 5, ...) or 0 if unknown."""
|
|
if platform.system() != "Darwin":
|
|
return 0
|
|
try:
|
|
brand = subprocess.run(
|
|
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
capture_output=True, text=True, timeout=5
|
|
).stdout.strip()
|
|
m = re.match(r"Apple M(\d+)", brand)
|
|
return int(m.group(1)) if m else 0
|
|
except Exception:
|
|
return 0
|
|
|
|
|
|
def _should_build_ext() -> bool:
|
|
"""Determine if C++ extension should be built."""
|
|
# Allow forcing via env var
|
|
force = os.environ.get("CIDER_FORCE_BUILD", "").lower()
|
|
if force in ("1", "true", "yes"):
|
|
return True
|
|
if force in ("0", "false", "no"):
|
|
return False
|
|
# Auto-detect: only build on M5+
|
|
chip = _detect_apple_chip()
|
|
if chip >= 5:
|
|
return True
|
|
if chip > 0:
|
|
print(f"[cider] Detected Apple M{chip} — skipping C++ extension (requires M5+)")
|
|
return False
|
|
# Unknown chip (cross-compile, CI, etc.) — try to build
|
|
return True
|
|
|
|
|
|
class CMakeBuild(build_ext):
|
|
def build_extension(self, ext):
|
|
src_dir = Path(__file__).parent.resolve()
|
|
|
|
# Resolve paths that cmake can't find in pip's isolated build env
|
|
import sysconfig
|
|
python_include = sysconfig.get_path("include")
|
|
|
|
try:
|
|
import nanobind
|
|
nanobind_dir = nanobind.cmake_dir()
|
|
except ImportError:
|
|
nanobind_dir = ""
|
|
|
|
cmake_args = [
|
|
f"-DPython_EXECUTABLE={sys.executable}",
|
|
f"-DPython_INCLUDE_DIR={python_include}",
|
|
"-DCMAKE_BUILD_TYPE=Release",
|
|
]
|
|
if nanobind_dir:
|
|
cmake_args.append(f"-Dnanobind_DIR={nanobind_dir}")
|
|
|
|
build_args = ["--config", "Release", "-j"]
|
|
|
|
# In-source build (cmake outputs .so to src_dir)
|
|
subprocess.check_call(
|
|
["cmake", "."] + cmake_args,
|
|
cwd=src_dir,
|
|
)
|
|
subprocess.check_call(
|
|
["cmake", "--build", "."] + build_args,
|
|
cwd=src_dir,
|
|
)
|
|
|
|
import shutil
|
|
|
|
# Copy .so to build_lib root (where setuptools expects the extension)
|
|
build_lib = Path(self.build_lib)
|
|
build_lib.mkdir(parents=True, exist_ok=True)
|
|
for f in src_dir.glob("_cider_prim*.so"):
|
|
shutil.copy2(f, build_lib)
|
|
|
|
# Also copy to cider/lib/ for runtime loading
|
|
output_lib_dir = build_lib / "cider" / "lib"
|
|
output_lib_dir.mkdir(parents=True, exist_ok=True)
|
|
for f in src_dir.glob("*.so"):
|
|
shutil.copy2(f, output_lib_dir)
|
|
for f in src_dir.glob("*.dylib"):
|
|
shutil.copy2(f, output_lib_dir)
|
|
|
|
# Write to source tree cider/lib/ so editable installs work
|
|
src_lib_dir = src_dir / "cider" / "lib"
|
|
src_lib_dir.mkdir(exist_ok=True)
|
|
for f in src_dir.glob("*.so"):
|
|
shutil.copy2(f, src_lib_dir)
|
|
for f in src_dir.glob("*.dylib"):
|
|
shutil.copy2(f, src_lib_dir)
|
|
|
|
|
|
if _should_build_ext():
|
|
ext_modules = [Extension("_cider_prim", sources=[])]
|
|
cmdclass = {"build_ext": CMakeBuild}
|
|
else:
|
|
ext_modules = []
|
|
cmdclass = {}
|
|
|
|
setup(
|
|
ext_modules=ext_modules,
|
|
cmdclass=cmdclass,
|
|
)
|