diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index aa45b6d..3f135ac 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -2,7 +2,6 @@ name: build-docker-image on: workflow_dispatch: - pull_request: push: branches: - main diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea24e1d..8e66789 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,28 +3,29 @@ name: continuous-integration on: [push, pull_request] jobs: - test-package: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.10', '3.11', '3.12'] + test-package: + runs-on: ubuntu-latest + container: ghcr.io/${{ github.repository }}/ci-image:latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12"] - steps: - - name: Check out repository - uses: actions/checkout@v4 + steps: + - name: Check out repository + uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} - - name: Install package and its dependencies - run: pip install -e .[dev] + - name: Install package and its dependencies + run: python3 -m pip install -e .[dev] - - name: Run pytest - run: pytest -v --cov --cov-report json + - name: Run pytest + run: pytest -v --cov --cov-report json - - name: Upload coverage report - uses: codecov/codecov-action@v3 - with: - flags: python-${{ matrix.python-version }} + - name: Upload coverage report + uses: codecov/codecov-action@v3 + with: + flags: python-${{ matrix.python-version }} diff --git a/Dockerfile b/Dockerfile index 098e53b..ff6d738 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,4 @@ -# Use Ubuntu as the base image -FROM ubuntu:latest AS base +FROM alpine:latest AS base -# Install build-essential, cmake, and OpenBLAS -RUN apt-get update && \ - apt-get install -y build-essential cmake libopenblas-dev +RUN apk update && \ + apk add --no-cache build-base cmake openblas-dev python3 diff --git a/cleedpy/cleed/CMakeLists.txt b/cleedpy/cleed/CMakeLists.txt index bb11827..1139401 100644 --- a/cleedpy/cleed/CMakeLists.txt +++ b/cleedpy/cleed/CMakeLists.txt @@ -6,8 +6,8 @@ set(CMAKE_C_STANDARD 90) set(CMAKE_C_STANDARD_REQUIRED True) # Set the output directories -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) # Where to find CMake modules list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) diff --git a/cleedpy/interface/cleed.py b/cleedpy/interface/cleed.py index c3ba4eb..1e6dfe0 100644 --- a/cleedpy/interface/cleed.py +++ b/cleedpy/interface/cleed.py @@ -1,7 +1,8 @@ import math -from ctypes import POINTER, Structure, c_char_p, c_double, c_int, cdll +import pathlib as pl +import platform +from ctypes import CDLL, POINTER, Structure, c_char_p, c_double, c_int, cdll from dataclasses import dataclass -from pathlib import Path from ..config import ( AtomParametersStructured, @@ -188,9 +189,29 @@ def generate_crystal_structure(inp: AtomParametersVariants) -> Crystal: raise ValueError("Not implemented") -def call_cleed(): - path = Path(__file__).parent.parent / "cleed" / "build" / "lib" / "libcleed.dylib" - lib = cdll.LoadLibrary(str(path)) +def get_cleed_lib() -> CDLL: + lib_path = pl.Path(__file__).parent.parent / "cleed" / "lib" + + lib_exts = { + "Windows": ".dll", + "Darwin": ".dylib", + "Linux": ".so", + } + + try: + cleed_lib = ( + (lib_path / "libcleed").with_suffix(lib_exts[platform.system()]).as_posix() + ) + except KeyError as err: + raise ValueError( + f"Platform {platform.system()} not supported by cleedpy" + ) from err + + return cdll.LoadLibrary(cleed_lib) + + +def call_cleed() -> None: + lib = get_cleed_lib() lib.my_test_function.argtypes = [c_int, c_int, POINTER(Crystal)] lib.my_test_function.restype = c_int diff --git a/pyproject.toml b/pyproject.toml index aa77348..b1d7c48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61.2"] +requires = ["setuptools>=61.2", "cmake>=3.5"] build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..543f31a --- /dev/null +++ b/setup.py @@ -0,0 +1,77 @@ +import os +import pathlib as pl +import platform +import subprocess +import sys + +from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext + + +class CMakeExtension(Extension): + def __init__(self, name, source_dir=""): + Extension.__init__(self, name, sources=[]) + self.source_dir = pl.Path(source_dir).absolute() + + +class CMakeBuild(build_ext): + def run(self): + try: + subprocess.check_output(["cmake", "--version"]) + except subprocess.CalledProcessError as err: + raise RuntimeError( + "CMake must be installed to build the following extensions: " + + ", ".join(e.name for e in self.extensions) + ) from err + + for ext in self.extensions: + self.build_extension(ext) + + def build_extension(self, ext: CMakeExtension): + ext_dir = pl.Path(self.get_ext_fullpath(ext.name)).parent.absolute() + + cmake_args = ["-DPYTHON_EXECUTABLE=" + sys.executable] + + cfg = "Debug" if self.debug else "Release" + build_args = ["--config", cfg] + + if platform.system() == "Windows": + cmake_args += [f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={ext_dir}"] + if sys.maxsize > 2**32: + # We are on a Win 64-bit system + cmake_args += ["-A", "x64"] + build_args += ["--", "/m"] + else: + # These flags are passed as-is to the underlying build tool (make) + build_args += ["--", "-j2"] + + env = os.environ.copy() + env["CXXFLAGS"] = '{} -DVERSION_INFO="{}"'.format( + env.get("CXXFLAGS", ""), self.distribution.get_version() + ) + + build_dir = pl.Path(self.build_temp) + build_dir.mkdir(parents=True, exist_ok=True) + + try: + subprocess.check_call( + ["cmake", "-S", ext.source_dir.as_posix(), "-B", "."] + cmake_args, + cwd=build_dir, + env=env, + ) + subprocess.check_call(["cmake", "--build", "."] + build_args, cwd=build_dir) + except subprocess.CalledProcessError as err: + raise RuntimeError(f"CMake error: {err}") from err + + +setup( + name="cleedpy", + ext_modules=[ + CMakeExtension( + "cleed", (pl.Path(__file__).parent / "cleedpy" / "cleed").as_posix() + ) + ], + cmdclass={"build_ext": CMakeBuild}, + # The following includes *any* library file found in the package directory + package_data={"cleedpy": ["**/*.dylib", "**/*.so", "**/*.dll"]}, +)