diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml new file mode 100644 index 0000000..be7d237 --- /dev/null +++ b/.github/workflows/wheel.yml @@ -0,0 +1,84 @@ +name: Wheel builder + +on: + pull_request: + push: + workflow_dispatch: + +jobs: + build_wheels: + + defaults: + run: + shell: bash -l {0} + + name: Build wheel ${{ matrix.python[0] }}-${{ matrix.buildplat[0] }} + runs-on: ${{ matrix.buildplat[0] }} + strategy: + fail-fast: false + matrix: + buildplat: + - [ubuntu-latest, manylinux_x86_64] + - [macos-13, macosx_x86_64] + - [macos-14, macosx_arm64] + - [windows-latest, win_amd64] + python: + - ["3.11", "cp311"] + - ["3.12", "cp312"] + + steps: + - name: Check out #${{ inputs.project }} + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python[0] }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python[0] }} + + - name: Build wheels for Linux + if: runner.os == 'Linux' + uses: pypa/cibuildwheel@v2.21.1 + env: + CIBW_BUILD: ${{ matrix.python[1] }}-${{ matrix.buildplat[1] }} + CIBW_BEFORE_BUILD: yum install -y gsl-devel && pip install -e . + with: + output-dir: wheelhouse + + - name: Build wheels for macOS + if: runner.os == 'macOS' + uses: pypa/cibuildwheel@v2.21.1 + env: + CIBW_BUILD: ${{ matrix.python[1] }}-${{ matrix.buildplat[1] }} + MACOSX_DEPLOYMENT_TARGET: 13.0 + CIBW_BEFORE_BUILD: brew install gsl && pip install -e . + with: + output-dir: wheelhouse + + - name: Set up conda for Windows + if: runner.os == 'Windows' + uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: gsl + auto-update-conda: true + environment-file: environment.yml + auto-activate-base: false + + - name: install gsl for Windows + if: runner.os == 'Windows' + run: | + conda config --set always_yes yes --set changeps1 no + conda install gsl + + - name: Build wheels for Windows + if: runner.os == 'Windows' + uses: pypa/cibuildwheel@v2.21.1 + env: + CIBW_BUILD: ${{ matrix.python[1] }}-${{ matrix.buildplat[1] }} + CONDA_PREFIX: ${{ env.CONDA_PREFIX }} + with: + output-dir: wheelhouse + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.python[0] }}-${{ matrix.buildplat[0] }} + path: ./wheelhouse/*.whl diff --git a/setup.py b/setup.py index 402249a..afe2a0c 100755 --- a/setup.py +++ b/setup.py @@ -11,97 +11,111 @@ import glob import os import re +import shutil import sys import warnings +from pathlib import Path from setuptools import Extension, setup +from setuptools.command.build_ext import build_ext # Use this version when git data are not available, like in git zip archive. # Update when tagging a new release. FALLBACK_VERSION = "1.4.3" -MYDIR = os.path.dirname(os.path.abspath(__file__)) +MYDIR = str(Path(__file__).parent.resolve()) # Helper functions ----------------------------------------------------------- def get_compiler_type(): - """find compiler used for building extensions.""" + """Find compiler used for building extensions.""" cc_arg = [a for a in sys.argv if a.startswith("--compiler=")] if cc_arg: - compiler_type = cc_arg[-1].split("=", 1)[1] - else: - from distutils.ccompiler import new_compiler + return cc_arg[-1].split("=", 1)[1] + from distutils.ccompiler import new_compiler - compiler_type = new_compiler().compiler_type - return compiler_type + return new_compiler().compiler_type def get_gsl_config(): """Return dictionary with paths to GSL library.""" - gslcfgpaths = [os.path.join(p, "gsl-config") for p in ([MYDIR] + os.environ["PATH"].split(os.pathsep))] - gslcfgpaths = [p for p in gslcfgpaths if os.path.isfile(p)] + gslcfgpaths = [Path(p) / "gsl-config" for p in ([MYDIR] + os.environ["PATH"].split(os.pathsep))] + gslcfgpaths = [p for p in gslcfgpaths if p.is_file()] rv = {"include_dirs": [], "library_dirs": []} if not gslcfgpaths: - wmsg = "Cannot find gsl-config in {!r} nor in system PATH." - warnings.warn(wmsg.format(MYDIR)) + warnings.warn(f"Cannot find gsl-config in {MYDIR} nor in system PATH.") return rv gslcfg = gslcfgpaths[0] - with open(gslcfg) as fp: - txt = fp.read() - mprefix = re.search("(?m)^prefix=(.+)", txt) + txt = gslcfg.read_text() + mprefix = re.search(r"(?m)^prefix=(.+)", txt) minclude = re.search(r"(?m)^[^#]*\s-I(\S+)", txt) mlibpath = re.search(r"(?m)^[^#]*\s-L(\S+)", txt) if not mprefix: - emsg = "Cannot find 'prefix=' line in {}." - raise RuntimeError(emsg.format(gslcfg)) - p = mprefix.group(1) - inc = minclude.group(1) if minclude else (p + "/include") - lib = mlibpath.group(1) if mlibpath else (p + "/lib") - rv["include_dirs"] += [inc] - rv["library_dirs"] += [lib] + raise RuntimeError(f"Cannot find 'prefix=' line in {gslcfg}.") + p = Path(mprefix.group(1)) + rv["include_dirs"].append(str(minclude.group(1) if minclude else p / "include")) + rv["library_dirs"].append(str(mlibpath.group(1) if mlibpath else p / "lib")) return rv def get_gsl_config_win(): """Return dictionary with paths to GSL library on Windows.""" - gsl_path = os.environ.get("GSL_PATH") + gsl_path = os.environ.get("GSL_PATH", "") if gsl_path: - inc = os.path.join(gsl_path, "include") - lib = os.path.join(gsl_path, "lib") + inc = Path(gsl_path) / "include" + lib = Path(gsl_path) / "lib" else: conda_prefix = os.environ.get("CONDA_PREFIX") if conda_prefix: - inc = os.path.join(conda_prefix, "Library", "include") - lib = os.path.join(conda_prefix, "Library", "lib") + inc = Path(conda_prefix) / "Library" / "include" + lib = Path(conda_prefix) / "Library" / "lib" else: raise EnvironmentError( "Neither GSL_PATH nor CONDA_PREFIX environment variables are set. " "Please ensure GSL is installed and GSL_PATH is correctly set." ) + return {"include_dirs": [str(inc)], "library_dirs": [str(lib)]} + + +class CustomBuildExt(build_ext): + def run(self): + super().run() + gsl_path = ( + Path(os.environ.get("GSL_PATH")) + if os.environ.get("GSL_PATH") + else Path(os.environ.get("CONDA_PREFIX", "")) / "Library" + ) + bin_path = gsl_path / "bin" + dest_path = Path(self.build_lib) / "diffpy" / "pdffit2" + dest_path.mkdir(parents=True, exist_ok=True) - return {"include_dirs": [inc], "library_dirs": [lib]} + for dll_file in bin_path.glob("gsl*.dll"): + shutil.copy(str(dll_file), str(dest_path)) # ---------------------------------------------------------------------------- -# compile and link options -define_macros = [] +# Compile and link options os_name = os.name if os_name == "nt": gcfg = get_gsl_config_win() else: gcfg = get_gsl_config() -include_dirs = [MYDIR] + gcfg["include_dirs"] -library_dirs = [] + if sys.platform == "darwin": libraries = [] else: libraries = ["gsl"] + +include_dirs = [MYDIR] + gcfg["include_dirs"] +library_dirs = [] +define_macros = [] extra_objects = [] extra_compile_args = [] extra_link_args = [] + compiler_type = get_compiler_type() if compiler_type in ("unix", "cygwin", "mingw32"): extra_compile_args = ["-std=c++11", "-Wall", "-Wno-write-strings", "-O3", "-funroll-loops", "-ffast-math"] @@ -114,7 +128,7 @@ def get_gsl_config_win(): library_dirs += gcfg["library_dirs"] # add optimization flags for other compilers if needed -# define extension arguments here +# Define extension arguments ext_kws = { "include_dirs": include_dirs, "libraries": libraries, @@ -126,7 +140,7 @@ def get_gsl_config_win(): } -# define extension here +# Define extensions def create_extensions(): ext = Extension("diffpy.pdffit2.pdffit2", glob.glob("src/extensions/**/*.cc"), **ext_kws) return [ext] @@ -134,6 +148,7 @@ def create_extensions(): setup_args = dict( ext_modules=[], + cmdclass={"build_ext": CustomBuildExt}, ) if __name__ == "__main__":