diff --git a/.github/workflows/CI_CCBlade.yml b/.github/workflows/CI_CCBlade.yml index 6ef5137..e8e414f 100644 --- a/.github/workflows/CI_CCBlade.yml +++ b/.github/workflows/CI_CCBlade.yml @@ -5,85 +5,110 @@ on: [push, pull_request] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - build_conda: - name: Build (${{ matrix.os }} Python ${{ matrix.python-version }}) + build_pip: + name: Pip Build (${{ matrix.os }}) - ${{ matrix.python-version }} runs-on: ${{ matrix.os }} - defaults: run: shell: bash -l {0} strategy: - fail-fast: False + fail-fast: false #true matrix: - os: ["ubuntu-latest", "windows-latest"] - python-version: ["3.8", "3.9", "3.10"] + os: ["ubuntu-latest", "macOS-latest", "windows-latest"] + python-version: ["3.9", "3.10", "3.11"] steps: + - name: Setup GNU Fortran + # if: false == contains( matrix.os, 'windows') + uses: awvwgk/setup-fortran@v1 #modflowpy/install-intelfortran-action@v1 # + - name: checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + id: cp + with: + python-version: ${{ matrix.python-version }} + update-environment: true + + #- name: Setup tmate session + # if: contains( matrix.os, 'windows') + # uses: mxschmitt/action-tmate@v3 - # Official way to do miniconda, but it messes with the worker environment and shell - - name: Install miniconda - uses: conda-incubator/setup-miniconda@v2 + - name: Pip Install CCBlade + env: + MESON_ARGS: -Dpython_target=${{ steps.cp.outputs.python-path }} + run: | + '${{ steps.cp.outputs.python-path }}' -m pip install -v -e .[test] + + - name: Test run + run: | + cd test + '${{ steps.cp.outputs.python-path }}' -m pytest . + + + build_conda: + name: Conda Build (${{ matrix.os }}) - ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash -el {0} + + strategy: + fail-fast: false #true + matrix: + os: ["ubuntu-latest", "macOS-latest", "windows-latest"] + python-version: ["3.9", "3.10", "3.11"] + + steps: + - name: checkout repository + uses: actions/checkout@v4 + + - uses: conda-incubator/setup-miniconda@v2 # https://github.com/marketplace/actions/setup-miniconda with: - miniconda-version: "latest" - channels: conda-forge + #mamba-version: "*" + miniforge-version: "latest" auto-update-conda: true python-version: ${{ matrix.python-version }} environment-file: environment.yml activate-environment: test auto-activate-base: false - # This is a less official, but more lightweight way to do miniconda - #- name: Install miniconda - # uses: s-weigand/setup-conda@v1 - # # https://github.com/marketplace/actions/setup-conda - # with: - # update-conda: true - # python-version: ${{ matrix.python-version }} - # conda-channels: conda-forge - # activate-conda: true - # - #- name: Update environment - # run: | - # conda env update --file environment.yml - - # Install compilers - #- name: Add compilers - # run: | - # conda install compilers - - # Install dependencies of WISDEM specific to linux/mac - #- name: Add dependencies linux specific - # if: false == contains( matrix.os, 'windows') - # run: | - # conda install ninja - # conda init bash - - # Install dependencies of WISDEM specific to windows - name: Add dependencies windows specific if: contains( matrix.os, 'windows') run: | conda install -y m2w64-toolchain libpython + + - name: Add dependencies mac specific + if: contains( matrix.os, 'mac') + run: | + conda install -y compilers + gfortran --version + + # Install + - name: Debug + run: | + conda list + printenv + #- name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 + # with: + # detached: true + # if: contains( matrix.os, 'windows') + # Install - name: Conda Install CCBlade + env: + MESON_ARGS: "" run: | python setup.py develop - # Peek - #- name: Library name - # run: | - # ls - # echo "BREAK" - # ls ccblade - # echo "BREAK" - # ls meson_build/ccblade - - # Run tests - - name: Conda Run pytest + - name: Test run run: | - pytest test + cd test + pytest . diff --git a/setup.py b/setup.py index 478e459..538130d 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,9 @@ #!/usr/bin/env python -# encoding: utf-8 - -# setup.py -# only if building in place: ``python setup.py build_ext --inplace`` import os -import re -import platform import shutil +import platform import setuptools -import subprocess +from setuptools.command.build_ext import build_ext ####### # This forces wheels to be platform specific @@ -25,93 +20,79 @@ class BinaryDistribution(Distribution): def has_ext_modules(foo): return True ####### - - -def run_meson_build(staging_dir): - prefix = os.path.join(os.getcwd(), staging_dir) - purelibdir = "." - - # check if meson extra args are specified - meson_args = "" - if "MESON_ARGS" in os.environ: - meson_args = os.environ["MESON_ARGS"] - # A weird add-on on mac github action runners needs to be removed - if meson_args.find("buildtype") >= 0: meson_args = "" - - if platform.system() == "Windows": - if not "FC" in os.environ: - os.environ["FC"] = "gfortran" - if not "CC" in os.environ: - os.environ["CC"] = "gcc" - - # configure - meson_path = shutil.which("meson") - if meson_path is None: - raise OSError("The meson command cannot be found on the system") - - meson_call = [meson_path, "setup", staging_dir, "--wipe", - f"--prefix={prefix}", f"-Dpython.purelibdir={purelibdir}", - f"-Dpython.platlibdir={purelibdir}", meson_args] - meson_call = [m for m in meson_call if m != ""] - print(meson_call) - p1 = subprocess.run(meson_call, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - os.makedirs(staging_dir, exist_ok=True) - setup_log = os.path.join(staging_dir, "setup.log") - with open(setup_log, "wb") as f: - f.write(p1.stdout) - if p1.returncode != 0: - with open(setup_log, "r") as f: - print(f.read()) - raise OSError(meson_call, f"The meson setup command failed! Check the log at {setup_log} for more information.") - - # build - meson_call = [meson_path, "compile", "-vC", staging_dir] - meson_call = [m for m in meson_call if m != ""] - print(meson_call) - p2 = subprocess.run(meson_call, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - compile_log = os.path.join(staging_dir, "compile.log") - with open(compile_log, "wb") as f: - f.write(p2.stdout) - if p2.returncode != 0: - with open(compile_log, "r") as f: - print(f.read()) - raise OSError(meson_call, f"The meson compile command failed! Check the log at {compile_log} for more information.") - +this_dir = os.path.abspath(os.path.dirname(__file__)) +staging_dir = os.path.join(this_dir, "meson_build") +build_dir = os.path.join(this_dir, "build") def copy_shared_libraries(): build_path = os.path.join(staging_dir, "ccblade") for root, _dirs, files in os.walk(build_path): - for file in files: - # move ccblade to just under staging_dir - if file.endswith((".so", ".lib", ".pyd", ".pdb", ".dylib", ".dll", ".mod")): - if ".so.p" in root or ".pyd.p" in root: # excludes intermediate object files - continue - file_path = os.path.join(root, file) - new_path = str(file_path) - match = re.search(staging_dir, new_path) - new_path = new_path[match.span()[1] + 1 :] + for f in files: + if f.endswith((".so", ".lib", ".pyd", ".pdb", ".dylib", ".dll")): + file_path = os.path.join(root, f) + new_path = str(file_path).replace(staging_dir + os.sep, "") print(f"Copying build file {file_path} -> {new_path}") - shutil.move(file_path, new_path) - + shutil.copy(file_path, new_path) +####### +class MesonExtension(setuptools.Extension): + + def __init__(self, name, sourcedir="", **kwa): + setuptools.Extension.__init__(self, name, sources=[], **kwa) + self.sourcedir = os.path.abspath(sourcedir) + +class MesonBuildExt(build_ext): + + def copy_extensions_to_source(self): + newext = [] + for ext in self.extensions: + if isinstance(ext, MesonExtension): continue + newext.append( ext ) + self.extensions = newext + super().copy_extensions_to_source() + + def build_extension(self, ext): + if not isinstance(ext, MesonExtension): + super().build_extension(ext) + + else: + + # Ensure that Meson is present and working + try: + self.spawn(["meson", "--version"]) + except OSError: + raise RuntimeError("Cannot find meson executable") + + # check if meson extra args are specified + meson_args = "" + if "MESON_ARGS" in os.environ: + meson_args = os.environ["MESON_ARGS"] + + if platform.system() == "Windows": + if "FC" not in os.environ: + os.environ["FC"] = "gfortran" + if "CC" not in os.environ: + os.environ["CC"] = "gcc" + + purelibdir = "." + configure_call = ["meson", "setup", staging_dir, "--wipe", + f"-Dpython.purelibdir={purelibdir}", f"--prefix={build_dir}", + f"-Dpython.platlibdir={purelibdir}"] + meson_args.split() + configure_call = [m for m in configure_call if m.strip() != ""] + print(configure_call) + + build_call = ["meson", "compile", "-vC", staging_dir] + print(build_call) + + self.build_temp = build_dir + + self.spawn(configure_call) + self.spawn(build_call) + copy_shared_libraries() + + if __name__ == "__main__": - # This is where the meson build system will install to, it is then - # used as the sources for setuptools - staging_dir = "meson_build" - - # this keeps the meson build system from running more than once - if "dist" not in str(os.path.abspath(__file__)): - cwd = os.getcwd() - run_meson_build(staging_dir) - os.chdir(cwd) - copy_shared_libraries() - - init_file = os.path.join("ccblade", "__init__.py") - #__version__ = re.findall( - # r"""__version__ = ["']+([0-9\.]*)["']+""", - # open(init_file).read(), - #)[0] - - setuptools.setup(cmdclass={'bdist_wheel': bdist_wheel}, distclass=BinaryDistribution) - -#os.environ['NPY_DISTUTILS_APPEND_FLAGS'] = '1' + setuptools.setup(cmdclass={"bdist_wheel": bdist_wheel, "build_ext": MesonBuildExt}, + distclass=BinaryDistribution, + ext_modules=[ MesonExtension("ccblade", this_dir) ], + )