Skip to content

Commit

Permalink
Use wider-compatible LLVM-OpenMP, bundle libgfortran dylibs for mac…
Browse files Browse the repository at this point in the history
…OS wheels (pybamm-team#4092)

* Set `MACOSX_DEPLOYMENT_TARGET` for prerequisites

* Remove previously set environment variables

* Remove Homebrew libomp installations

* Ignore `pybamm-requires` OpenMP downloads

* Rework docs to add OpenMP instructions

* Download LLVM-OpenMP on macOS

* Remove unused compiler argument

* Fix rpath for libomp dylib at wheel repair

* Fix a failing link

* Revert "Fix rpath for libomp dylib at wheel repair"

This reverts commit 6fb8ef0.

* Fix RPATHs for SuiteSparse and libomp

* 11.1 for arm64 macOS and 11.0 for amd64 macOS

* Set up Fortran compiler via GHA

* Downgrade to gcc 12

* Try fix for macOS amd64 wheels first

* Build on macos-13 image instead

* Debug further, try building against 11.0

* And now build for macOS arm64

* Try building arm64 against 11.0

* Rename wheel from 11_1 to 11_0

* Cleanup and fix up commands

* Rename wheel in dest dir instead

* Cleanup plus references to SciPy license
  • Loading branch information
agriyakhetarpal authored and js1tr3 committed Aug 12, 2024
1 parent 4b564de commit 9ff552d
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 50 deletions.
70 changes: 50 additions & 20 deletions .github/workflows/publish_pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-12]
os: [ubuntu-latest, macos-13]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
Expand All @@ -109,15 +109,6 @@ jobs:
- name: Clone pybind11 repo (no history)
run: git clone --depth 1 --branch v2.11.1 https://github.com/pybind/pybind11.git

# sometimes gfortran cannot be found, so reinstall gcc just to be sure
- name: Install SuiteSparse and SUNDIALS on macOS
if: matrix.os == 'macos-12'
run: |
brew install graphviz libomp
brew reinstall gcc
python -m pip install cmake wget
python scripts/install_KLU_Sundials.py
- name: Build wheels on Linux
run: pipx run cibuildwheel --output-dir wheelhouse
if: matrix.os == 'ubuntu-latest'
Expand Down Expand Up @@ -153,18 +144,57 @@ jobs:
- name: Clone pybind11 repo (no history)
run: git clone --depth 1 --branch v2.11.1 https://github.com/pybind/pybind11.git

- name: Install SuiteSparse and SUNDIALS on macOS
run: |
brew install graphviz libomp
brew reinstall gcc
python -m pip install cmake pipx
python scripts/install_KLU_Sundials.py
- name: Build wheels on macOS arm64
run: python -m pipx run cibuildwheel --output-dir wheelhouse
run: |
python -m pip install cibuildwheel
python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BEFORE_BUILD: python -m pip install cmake casadi setuptools wheel delocate
CIBW_REPAIR_WHEEL_COMMAND: delocate-listdeps {wheel} && delocate-wheel -v -w {dest_dir} {wheel}
MACOSX_DEPLOYMENT_TARGET: 11.0
# Sourced from
# License: BSD-3-Clause
# https://github.com/scipy/scipy/blob/f2d4775e7762fad984f8f0acd8227c725ff21630/tools/wheels/cibw_before_build_macos.sh#L23-L49
CIBW_BEFORE_ALL_MACOS: |
set -e -x
# download gfortran with proper macos minimum version (11.0)
curl -L https://github.com/isuruf/gcc/releases/download/gcc-11.3.0-2/gfortran-darwin-arm64-native.tar.gz -o gfortran.tar.gz
GFORTRAN_SHA256=$(shasum --algorithm 256 gfortran.tar.gz)
KNOWN_SHA256="84364eee32ba843d883fb8124867e2bf61a0cd73b6416d9897ceff7b85a24604 gfortran.tar.gz"
if [ "$GFORTRAN_SHA256" != "$KNOWN_SHA256" ]; then
echo "SHA256 mismatch for gfortran.tar.gz"
echo "expected: $KNOWN_SHA256"
echo "got: $GFORTRAN_SHA256"
exit 1
fi
mkdir -p gfortran_installed/
tar -xv -C gfortran_installed/ -f gfortran.tar.gz
export FC=$(pwd)/gfortran_installed/gfortran-darwin-arm64-native/bin/gfortran
export PATH=$(pwd)/gfortran_installed/gfortran-darwin-arm64-native/bin:$PATH
# link libgfortran.5.dylib, libgfortran.dylib, libquadmath.0.dylib, libquadmath.dylib, libgcc_s.1.1.dylib
# and place them in $HOME/.local/lib, and then change rpath
# note: libgcc_s.1.dylib not present for arm64, skip for now
# to $HOME/.local/lib
mkdir -p $HOME/.local/lib
lib_dir=$(pwd)/gfortran_installed/gfortran-darwin-arm64-native/lib
for lib in libgfortran.5.dylib libgfortran.dylib libquadmath.0.dylib libquadmath.dylib libgcc_s.1.1.dylib; do
cp $lib_dir/$lib $HOME/.local/lib/
install_name_tool -id $HOME/.local/lib/$lib $HOME/.local/lib/$lib
done
export SDKROOT=${SDKROOT:-$(xcrun --show-sdk-path)}
python -m pip install cmake wget
python scripts/install_KLU_Sundials.py
CIBW_BEFORE_BUILD_MACOS: python -m pip install --upgrade pip cmake casadi setuptools wheel delocate
# Use higher macOS target for now: https://github.com/casadi/casadi/issues/3698
CIBW_REPAIR_WHEEL_COMMAND_MACOS: |
delocate-listdeps {wheel} && delocate-wheel -v -w {dest_dir} {wheel} --require-target-macos-version 11.1
for file in {dest_dir}/*.whl; do mv "$file" "${file//macosx_11_1/macosx_11_0}"; done
CIBW_TEST_COMMAND: python -c "import pybamm; pybamm.IDAKLUSolver()"

- name: Upload wheels for macOS arm64
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,7 @@ results/

# tests
test_callback.log

# openmp downloads
install_KLU_Sundials/openmp-*
install_KLU_Sundials/usr/
1 change: 1 addition & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# a list of links/files to be ignored by lychee link checker (see workflow file)
https://github.com/DrTimothyAldenDavis/SuiteSparse/archive/v%7BSUITESPARSE_VERSION%7D.tar.gz
https://github.com/LLNL/sundials/releases/download/v%7BSUNDIALS_VERSION%7D/sundials-%7BSUNDIALS_VERSION%7D.tar.gz
https://mac.r-project.org/openmp/openmp-%7BOPENMP_VERSION%7D-darwin20-Release.tar.gz

# Errors in docs/source/user_guide/getting_started.md
file:///home/runner/work/PyBaMM/PyBaMM/docs/source/user_guide/api_docs
Expand Down
17 changes: 9 additions & 8 deletions docs/source/user_guide/installation/install-from-source.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ You can install the above with
Where ``X`` is the version sub-number.

.. tab:: MacOS
.. tab:: macOS

.. code:: bash
brew install python openblas gcc gfortran graphviz libomp cmake pandoc
brew install python openblas gcc gfortran graphviz cmake pandoc
.. note::

Expand Down Expand Up @@ -82,8 +82,9 @@ If you are running windows, you can simply skip this section and jump to :ref:`p
# in the PyBaMM/ directory
nox -s pybamm-requires
This will download, compile and install the SuiteSparse and SUNDIALS libraries.
Both libraries are installed in ``~/.local``.
This will download, compile and install the SuiteSparse and SUNDIALS (with OpenMP) libraries
and the ``pybind11`` headers.
SuiteSparse and SUNDIALS are installed in ``~/.local`` by default.

For users requiring more control over the installation process, the ``pybamm-requires`` session supports additional command-line arguments:

Expand All @@ -110,7 +111,7 @@ If you'd rather do things yourself,

1. Make sure you have CMake installed
2. Compile and install SuiteSparse (PyBaMM only requires the ``KLU`` component).
3. Compile and install SUNDIALS.
3. Compile and install SUNDIALS with `OpenMP support <https://mac.r-project.org/openmp/>`_.
4. Clone the pybind11 repository in the ``PyBaMM/`` directory (make sure the directory is named ``pybind11``).


Expand Down Expand Up @@ -315,12 +316,12 @@ source files to your current directory.
``ValueError: Integrator name ida does not exist``, or
``ValueError: Integrator name cvode does not exist``.

**Solution:** This could mean that you have not installed
``scikits.odes`` correctly, check the instructions given above and make
**Solution:** This could mean that you have not linked to
SUNDIALS correctly, check the instructions given above and make
sure each command was successful.

One possibility is that you have not set your ``LD_LIBRARY_PATH`` to
point to the sundials library, type ``echo $LD_LIBRARY_PATH`` and make
point to the SUNDIALS library, type ``echo $LD_LIBRARY_PATH`` and make
sure one of the directories printed out corresponds to where the
SUNDIALS libraries are located.

Expand Down
101 changes: 79 additions & 22 deletions scripts/install_KLU_Sundials.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,35 @@

SUITESPARSE_VERSION = "6.0.3"
SUNDIALS_VERSION = "6.5.0"

SUITESPARSE_URL = f"https://github.com/DrTimothyAldenDavis/SuiteSparse/archive/v{SUITESPARSE_VERSION}.tar.gz"
SUNDIALS_URL = f"https://github.com/LLNL/sundials/releases/download/v{SUNDIALS_VERSION}/sundials-{SUNDIALS_VERSION}.tar.gz"

SUITESPARSE_CHECKSUM = (
"7111b505c1207f6f4bd0be9740d0b2897e1146b845d73787df07901b4f5c1fb7"
)
SUNDIALS_CHECKSUM = "4e0b998dff292a2617e179609b539b511eb80836f5faacf800e688a886288502"

# universal binaries for macOS 11.0 and later; sourced from https://mac.r-project.org/openmp/
OPENMP_VERSION = "16.0.4"
OPENMP_URL = (
f"https://mac.r-project.org/openmp/openmp-{OPENMP_VERSION}-darwin20-Release.tar.gz"
)
OPENMP_CHECKSUM = "a763f0bdc9115c4f4933accc81f514f3087d56d6528778f38419c2a0d2231972"


DEFAULT_INSTALL_DIR = os.path.join(os.getenv("HOME"), ".local")


def safe_remove_dir(path):
if os.path.exists(path):
shutil.rmtree(path)
"""Remove a directory or file if it exists."""
try:
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
shutil.rmtree(path)
except Exception as e:
print(f"Error while removing {path}: {e}")


def install_suitesparse(download_dir):
Expand All @@ -44,11 +61,11 @@ def install_suitesparse(download_dir):
env = os.environ.copy()
for libdir in ["SuiteSparse_config", "AMD", "COLAMD", "BTF", "KLU"]:
build_dir = os.path.join(suitesparse_src, libdir)
# We want to ensure that libsuitesparseconfig.dylib is not repeated in
# multiple paths at the time of wheel repair. Therefore, it should not be
# built with an RPATH since it is copied to the install prefix.
# Set an RPATH in order for libsuitesparseconfig.dylib to find libomp.dylib
if libdir == "SuiteSparse_config":
env["CMAKE_OPTIONS"] = f"-DCMAKE_INSTALL_PREFIX={install_dir}"
env["CMAKE_OPTIONS"] = (
f"-DCMAKE_INSTALL_PREFIX={install_dir} -DCMAKE_INSTALL_RPATH={install_dir}/lib"
)
else:
# For AMD, COLAMD, BTF and KLU; do not set a BUILD RPATH but use an
# INSTALL RPATH in order to ensure that the dynamic libraries are found
Expand Down Expand Up @@ -86,20 +103,9 @@ def install_sundials(download_dir, install_dir):
# try to find OpenMP on mac
if platform.system() == "Darwin":
# flags to find OpenMP on mac
if platform.processor() == "arm":
OpenMP_C_FLAGS = (
"-Xpreprocessor -fopenmp -I/opt/homebrew/opt/libomp/include"
)
OpenMP_C_LIB_NAMES = "omp"
OpenMP_omp_LIBRARY = "/opt/homebrew/opt/libomp/lib/libomp.dylib"
elif platform.processor() == "i386":
OpenMP_C_FLAGS = "-Xpreprocessor -fopenmp -I/usr/local/opt/libomp/include"
OpenMP_C_LIB_NAMES = "omp"
OpenMP_omp_LIBRARY = "/usr/local/opt/libomp/lib/libomp.dylib"
else:
raise NotImplementedError(
f"Unsupported processor architecture: {platform.processor()}. Only 'arm' and 'i386' architectures are supported."
)
OpenMP_C_FLAGS = f"-Xpreprocessor -fopenmp -lomp -L{os.path.join(KLU_LIBRARY_DIR)} -I{os.path.join(KLU_INCLUDE_DIR)}"
OpenMP_C_LIB_NAMES = "omp"
OpenMP_omp_LIBRARY = os.path.join(KLU_LIBRARY_DIR, "libomp.dylib")

cmake_args += [
"-DOpenMP_C_FLAGS=" + OpenMP_C_FLAGS,
Expand All @@ -123,6 +129,47 @@ def install_sundials(download_dir, install_dir):
subprocess.run(make_cmd, cwd=build_dir, check=True)


# Relevant for macOS only because recent Xcode Clang versions do not include OpenMP headers.
# Other compilers (e.g. GCC) include the OpenMP specification by default.
def set_up_openmp(download_dir, install_dir):
print("-" * 10, "Extracting OpenMP archive", "-" * 40)

openmp_dir = f"openmp-{OPENMP_VERSION}"
openmp_src = os.path.join(download_dir, openmp_dir)

# extract OpenMP archive
with tarfile.open(
os.path.join(download_dir, f"{openmp_dir}-darwin20-Release.tar.gz")
) as tar:
tar.extractall(openmp_src)

# create directories
os.makedirs(os.path.join(install_dir, "lib"), exist_ok=True)
os.makedirs(os.path.join(install_dir, "include"), exist_ok=True)

# copy files
shutil.copy(
os.path.join(openmp_src, "usr", "local", "lib", "libomp.dylib"),
os.path.join(install_dir, "lib"),
)
for file in os.listdir(os.path.join(openmp_src, "usr", "local", "include")):
shutil.copy(
os.path.join(openmp_src, "usr", "local", "include", file),
os.path.join(install_dir, "include"),
)

# fix rpath; for some reason the downloaded dylib has an absolute path
# to /usr/local/lib/, so use self-referential rpath
subprocess.check_call(
[
"install_name_tool",
"-id",
"@rpath/libomp.dylib",
f"{os.path.join(install_dir, 'lib', 'libomp.dylib')}",
]
)


def check_libraries_installed(install_dir):
# Define the directories to check for SUNDIALS and SuiteSparse libraries
lib_dirs = [install_dir]
Expand Down Expand Up @@ -170,7 +217,7 @@ def check_libraries_installed(install_dir):
suitesparse_files = [file + ".dylib" for file in suitesparse_files]
else:
raise NotImplementedError(
f"Unsupported operating system: {platform.system()}. This script currently supports only Linux and macOS."
f"Unsupported operating system: {platform.system()}. This script supports only Linux and macOS."
)

suitesparse_lib_found = True
Expand All @@ -192,6 +239,16 @@ def check_libraries_installed(install_dir):
return sundials_lib_found, suitesparse_lib_found


def check_openmp_installed_on_macos(install_dir):
openmp_lib_found = isfile(join(install_dir, "lib", "libomp.dylib"))
openmp_headers_found = isfile(join(install_dir, "include", "omp.h"))
if not openmp_lib_found or not openmp_headers_found:
print("libomp.dylib or omp.h not found. Proceeding with OpenMP installation.")
else:
print(f"libomp.dylib and omp.h found in {install_dir}.")
return openmp_lib_found


def calculate_sha256(file_path):
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
Expand Down Expand Up @@ -267,7 +324,7 @@ def parallel_download(urls, download_dir):

# Get installation location
parser = argparse.ArgumentParser(
description="Download, compile and install Sundials and SuiteSparse."
description="Download, compile and install SUNDIALS and SuiteSparse."
)
parser.add_argument(
"--force",
Expand Down

0 comments on commit 9ff552d

Please sign in to comment.