From 45885c22a8cbfd1cefdae1652845226eeddfe568 Mon Sep 17 00:00:00 2001
From: Ben Cumming <bcumming@cscs.ch>
Date: Fri, 28 Feb 2020 11:25:24 +0100
Subject: [PATCH] pip / setuptools support (#977)

Add a `setup.py` for installing Arbor directly with pip/setuptools.

Implement a setuptools extension for CMake in `setup.py`. To be honest, I don't understand entirely how it works, but setuptools is obtuse enough that I don't feel at all guilty about this.

Define additional flags for optionally enabling GPUs, MPI, Vectorization and micro-architecture targets, for more adventurous users.

The documentation is updated with a "howto pip" for more casual users who don't want anything to do with CMake.

Fixes #958 .
---
 doc/install.rst    |  30 ++++----
 doc/python.rst     | 130 ++++++++++++++++++++++++++++------
 python/__init__.py |  15 ++--
 setup.py           | 172 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 307 insertions(+), 40 deletions(-)
 create mode 100644 setup.py

diff --git a/doc/install.rst b/doc/install.rst
index 8de0b760..fd665a42 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -29,10 +29,10 @@ with very few tools.
     =========== ============================================
     Git         To check out the code, minimum version 2.0.
     CMake       To set up the build, minimum version 3.9
-    compiler    A C++14 compiler. See `compilers <compilers_>`_.
+    compiler    A C++14 compiler. See `compilers <install-compilers_>`_.
     =========== ============================================
 
-.. _compilers:
+.. _install-compilers:
 
 Compilers
 ~~~~~~~~~
@@ -120,7 +120,7 @@ Python
 Arbor has a Python frontend, for which a minimum of Python 3.6 is required.
 In order to use MPI in combination with the python frontend the
 `mpi4py <https://mpi4py.readthedocs.io/en/stable/install.html#>`_
-Python package is recommended. See :ref:`pythonfrontend` for more information.
+Python package is recommended. See :ref:`install-python` for more information.
 
 
 Documentation
@@ -129,7 +129,7 @@ Documentation
 To build a local copy of the html documentation that you are reading now, you will need to
 install `Sphinx <http://www.sphinx-doc.org/en/master/>`_.
 
-.. _downloading:
+.. _install-downloading:
 
 Getting the Code
 ================
@@ -217,13 +217,13 @@ CMake parameters and flags, follow links to the more detailed descriptions below
         cmake -DARB_WITH_ASSERTIONS=ON -DCMAKE_BUILD_TYPE=debug
 
 .. topic:: `Release <buildtarget_>`_ mode (compiler optimizations enabled) with the default
-           compiler, optimized for the local `system architecture <architecture_>`_.
+           compiler, optimized for the local `system architecture <install-architecture_>`_.
 
     .. code-block:: bash
 
         cmake -DARB_ARCH=native
 
-.. topic:: `Release <buildtarget_>`_ mode with `Clang <compilers_>`_.
+.. topic:: `Release <buildtarget_>`_ mode with `Clang <install-compilers_>`_.
 
     .. code-block:: bash
 
@@ -231,13 +231,13 @@ CMake parameters and flags, follow links to the more detailed descriptions below
         export CXX=`which clang++`
         cmake
 
-.. topic:: `Release <buildtarget_>`_ mode for the `Haswell architecture <architecture_>`_ and `explicit vectorization <vectorize_>`_ of kernels.
+.. topic:: `Release <buildtarget_>`_ mode for the `Haswell architecture <install-architecture_>`_ and `explicit vectorization <install-vectorize_>`_ of kernels.
 
     .. code-block:: bash
 
         cmake -DARB_VECTORIZE=ON -DARB_ARCH=haswell
 
-.. topic:: `Release <buildtarget_>`_ mode with `explicit vectorization <vectorize_>`_, targeting the `Broadwell architecture <vectorize_>`_, with support for `P100 GPUs <gpu_>`_, and building with `GCC 6 <compilers_>`_.
+.. topic:: `Release <buildtarget_>`_ mode with `explicit vectorization <install-vectorize_>`_, targeting the `Broadwell architecture <install-vectorize_>`_, with support for `P100 GPUs <install-gpu_>`_, and building with `GCC 6 <install-compilers_>`_.
 
     .. code-block:: bash
 
@@ -245,7 +245,7 @@ CMake parameters and flags, follow links to the more detailed descriptions below
         export CXX=g++-6
         cmake -DARB_VECTORIZE=ON -DARB_ARCH=broadwell -DARB_WITH_GPU=ON
 
-.. topic:: `Release <buildtarget_>`_ mode with `explicit vectorization <vectorize_>`_, optimized for the `local system architecture <architecture_>`_ and `install <install_>`_ in ``/opt/arbor``
+.. topic:: `Release <buildtarget_>`_ mode with `explicit vectorization <install-vectorize_>`_, optimized for the `local system architecture <install-architecture_>`_ and `install <install_>`_ in ``/opt/arbor``
 
     .. code-block:: bash
 
@@ -264,7 +264,7 @@ with ``-g -O0`` flags), use the ``CMAKE_BUILD_TYPE`` CMake parameter.
 
     cmake -DCMAKE_BUILD_TYPE={debug,release}
 
-..  _architecture:
+..  _install-architecture:
 
 Architecture
 ------------
@@ -306,7 +306,7 @@ and `ARM options <https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html>`_.
      # IBM Power8
      cmake -DARB_ARCH=power8
 
-..  _vectorize:
+..  _install-vectorize:
 
 Vectorization
 -------------
@@ -324,7 +324,7 @@ With this flag set, the library will use architecture-specific vectorization int
 to implement these kernels. Arbor currently has vectorization support for x86 architectures
 with AVX, AVX2 or AVX512 ISA extensions, and for ARM architectures with support for AArch64 NEON intrinsics (first available on ARMv8-A).
 
-.. _gpu:
+.. _install-gpu:
 
 GPU Backend
 -----------
@@ -355,7 +355,7 @@ example:
     Arbor supports and has been tested on the Kepler (K20 & K80), Pascal (P100) and Volta (V100) GPUs
 
 
-.. _pythonfrontend:
+.. _install-python:
 
 Python Frontend
 ----------------
@@ -461,6 +461,8 @@ on your target system that are not covered here, please make an issue on the
 Arbor `Github issues <https://github.com/arbor-sim/arbor/issues>`_ page.
 We will do our best to help you directly, and update this guide to help other users.
 
+.. _install-mpi:
+
 MPI
 ---
 
@@ -730,7 +732,7 @@ CMake Git Submodule Warnings
 ----------------------------
 
 When running CMake, warnings like the following indicate that the Git submodules
-need to be `updated <downloading_>`_.
+need to be `updated <install-downloading_>`_.
 
 .. code-block:: none
 
diff --git a/doc/python.rst b/doc/python.rst
index 94ef8d82..63c57f2c 100644
--- a/doc/python.rst
+++ b/doc/python.rst
@@ -9,15 +9,13 @@ The getting started guides will introduce Arbor via the Python interface.
 
 To test that Arbor is available, try the following in a `Python 3 <python2_>`_ interpreter:
 
-.. container:: example-code
+.. code-block:: python
 
-    .. code-block:: python
-
-        >>> import arbor
-        >>> print(arbor.__config__)
-        {'mpi': True, 'mpi4py': True, 'gpu': False, 'version': '0.2.3-dev'}
-        >>> print(arbor.__version__)
-        0.2.3-dev
+    >>> import arbor
+    >>> print(arbor.__config__)
+    {'mpi': True, 'mpi4py': True, 'gpu': False, 'version': '0.2.3-dev'}
+    >>> print(arbor.__version__)
+    0.2.3-dev
 
 The dictionary ``arbor.__config__`` contains information about the Arbor installation.
 This can be used to check that Arbor supports features that you require to run your model,
@@ -25,20 +23,11 @@ or even to dynamically decide how to run a model.
 Single cell models like do not require parallelism like
 that provided by MPI or GPUs, so the ``'mpi'`` and ``'gpu'`` fields can be ``False``.
 
-Installing
--------------
-
-Before starting Arbor needs to be installed with the Python interface enabled,
-following the :ref:`Python configuration <pythonfrontend>` in
-the
-:ref:`installation guide <installarbor>`.
-
-
 Performance
 --------------
 
-The Python interface can incur significant performance overheads relative to C++
-during the *model building* phase, however simulation performance is be the same
+The Python interface can incur significant memory and runtime overheads relative to C++
+during the *model building* phase, however simulation performance is the same
 for both interfaces.
 
 .. _python2:
@@ -50,6 +39,105 @@ Python 2 reached `end of life <https://pythonclock.org/>`_ in January 2020.
 Arbor only officially supports Python 3.6 and later, and all examples in the
 documentation are in Python 3. While it is possible to install and run Arbor
 using Python 2.7 by setting the ``PYTHON_EXECUTABLE`` variable when
-configuring CMake, support is only provided for using Arbor with Python 3.6
-and later.
+:ref:`configuring CMake <install-python>`, support is only provided for using
+Arbor with Python 3.6 and later.
+
+Installing
+-------------
+
+Before starting Arbor needs to be installed with the Python interface enabled.
+The easiest way to get started with the Python interface is to install Arbor using
+`pip <https://packaging.python.org/tutorials/installing-packages>`_.
+
+Installing with pip requires two steps:
+**(1)** Obtain Arbor source code from GitHub;
+**(2)** then use pip to compile and install the Arbor package in one shot.
+
+.. code-block:: bash
+
+    git clone https://github.com/arbor-sim/arbor.git --recursive
+    # use pip (recommended)
+    pip3 install ./arbor
+    # use setuptools and python directly
+    python3 install ./arbor/setup.py
+
+This will install Arbor as a site-wide package with only multi-threading enabled.
+
+To enable more advanced forms of parallelism, the following flags can optionally
+be passed to the pip install command:
+
+* ``--mpi``: enable mpi support (requires MPI library).
+* ``--gpu``: enable nvidia cuda support (requires cudaruntime and nvcc).
+* ``--vec``: enable vectorization. This might require carefully choosing the ``--arch`` flag.
+* ``--arch``: cpu micro-architecture to target. By default this is set to ``native``.
+
+If calling ``setup.py`` the flags must to come after ``install`` on the command line,
+and if being passed to pip they must be passed via ``--install-option``. The examples
+below demonstrate this for both pip and ``setup.py``, with pip being our recommend method.
+
+Vanilla install with no additional options/features enabled:
+
+.. code-block:: bash
+
+    pip3 install ./arbor
+    python3 ./arbor/setup.py install
+
+Enable MPI support. This might require loading an MPI module or setting the ``CC`` and ``CXX``
+:ref:`environment variables <install-mpi>`.
+
+.. code-block:: bash
+
+    pip3 install --install-option='--mpi' ./arbor
+    python3 ./arbor/setup.py install --mpi
+
+Compile with :ref:`vectorization <install-vectorize>` on a system with SkyLake
+:ref:`architecture <install-architecture>`:
+
+.. code-block:: bash
+
+    pip3 install --install-option='--vec' --install-option='--arch=skylake' ./arbor
+    python3 ./arbor/setup.py install --vec --arch=skylake
+
+Compile with support for NVIDIA GPUs. This requires that the :ref:`CUDA toolkit <install-gpu>`
+is installed and the CUDA compiler nvcc is available:
+
+.. code-block:: bash
+
+    pip3 install --install-option='--gpu' ./arbor
+    python3 ./arbor/setup.py install --gpu
+
+.. Note::
+    Installation takes a while because pip has to compile the Arbor C++ library and
+    wrapper, which takes a few minutes. Pass the ``--verbose`` flag to pip
+    to see the individual steps being preformed if concerned that progress
+    is halting.
+
+.. Note::
+    Detailed instructions on how to install using CMake are in the
+    :ref:`Python configuration <install-python>` section of the
+    :ref:`installation guide <installarbor>`.
+    CMake is recommended for developers, integration with package managers such as
+    Spack and EasyBuild, and users who require fine grained control over compilation
+    and installation.
+
+.. Note::
+    If there is an error installing with pip you want to report,
+    run pip with the ``--verbose`` flag, and attach the output (along with
+    the pip command itself) to a ticket on the
+    `Arbor GitHub <https://github.com/arbor-sim/arbor/issues>`_.
+    For example, ``pip3 install --install-option='--mpi' --verbose .``.
+
+Dependencies
+^^^^^^^^^^^^^
+
+If a downstream dependency of Arbor that requires Arbor be built with
+a specific feature enabled, use ``requirements.txt`` to
+`define the constraints <https://pip.pypa.io/en/stable/reference/pip_install/#per-requirement-overrides>`_.
+For example, a package that depends on `arbor` would version 0.3 or later
+with MPI support would add the following to its requirements.
+
+.. code-block:: python
+
+    arbor >= 0.3 --install-option='--gpu' \
+                 --install-option='--mpi'
 
diff --git a/python/__init__.py b/python/__init__.py
index dd137a74..0a4ac0bb 100644
--- a/python/__init__.py
+++ b/python/__init__.py
@@ -6,10 +6,15 @@
 
 from ._arbor import *
 
-import os
+# Parse VERSION file for the Arbor version string.
+def get_version():
+    import os
+    here = os.path.abspath(os.path.dirname(__file__))
+    with open(os.path.join(here, 'VERSION')) as version_file:
+        return version_file.read().strip()
 
-here = os.path.abspath(os.path.dirname(__file__))
-with open(os.path.join(here, 'VERSION')) as version_file:
-    __version__ = version_file.read().strip()
+__version__ = get_version()
+__config__  = config()
 
-__config__ = config()
+# Remove get_version from arbor module.
+del get_version
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..30214847
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,172 @@
+import os
+import sys
+import setuptools
+import pathlib
+from setuptools import Extension
+from setuptools.command.build_ext import build_ext
+from setuptools.command.install import install
+import subprocess
+
+# VERSION is in the same path as setup.py
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'VERSION')) as version_file:
+    version_ = version_file.read().strip()
+
+def check_cmake():
+    try:
+        out = subprocess.check_output(['cmake', '--version'])
+        return True
+    except OSError:
+        return False
+
+# Extend the command line options available to the install phase.
+# These arguments must come after `install` on the command line, e.g.:
+#    python3 setup.py install --mpi --arch=skylake
+#    pip3 install --install-option '--mpi' --install-option '--arch=skylake' .
+class install_command(install):
+    user_options = install.user_options + [
+        ('mpi',   None, 'enable mpi support (requires MPI library)'),
+        ('gpu',   None, 'enable nvidia cuda support (requires cudaruntime and nvcc)'),
+        ('vec',   None, 'enable vectorization'),
+        ('arch=', None, 'cpu architecture, e.g. haswell, skylake, armv8-a'),
+    ]
+
+    def initialize_options(self):
+        install.initialize_options(self)
+        self.mpi  = None
+        self.gpu  = None
+        self.arch = None
+        self.vec  = None
+
+    def finalize_options(self):
+        install.finalize_options(self)
+
+    def run(self):
+        # The options are stored in a dictionary cl_opt, with the following keys:
+        #   'mpi'  : build with MPI support (boolean).
+        #   'gpu'  : build with CUDA support (boolean).
+        #   'vec'  : generate SIMD vectorized kernels for CPU micro-architecture (boolean).
+        #   'arch' : target CPU micro-architecture (string).
+        global cl_opt
+        cl_opt = {
+            'mpi' : self.mpi is not None,
+            'gpu' : self.gpu is not None,
+            'vec' : self.vec is not None,
+            'arch': "native" if self.arch is None else self.arch
+        }
+        install.run(self)
+
+class cmake_extension(Extension):
+    def __init__(self, name):
+        Extension.__init__(self, name, sources=[])
+
+class cmake_build(build_ext):
+    def run(self):
+        if not check_cmake():
+            raise RuntimeError('CMake is not available. CMake 3.12 is required.')
+
+        # cl_opt contains the command line arguments passed to install, via
+        # --install-option if using pip.
+        # pip skips building wheels when --install-option flags are set.
+        # However, when no --install-options are passed, it runs build_ext
+        # without running install_command, required to create and set cl_opt.
+        # This hack works around this. I think that the upshot of this is
+        # that only wheels built with default configuration will be possible.
+
+        if 'cl_opt' not in globals():
+            cl_opt = {
+                    'mpi': False,
+                    'gpu': False,
+                    'vec': False,
+                    'arch': 'native'
+            }
+
+        # The path where CMake will be configured and Arbor will be built.
+        build_directory = os.path.abspath(self.build_temp)
+        # The path where the package will be copied after building.
+        lib_directory = os.path.abspath(self.build_lib)
+        # The path where the Python package will be compiled.
+        source_path = build_directory + '/python/arbor'
+        # Where to copy the package after it is built, so that whatever the next phase is
+        # can copy it into the target 'prefix' path.
+        dest_path = lib_directory + '/arbor'
+
+        cmake_args = [
+            '-DARB_WITH_PYTHON=on',
+            '-DPYTHON_EXECUTABLE=' + sys.executable,
+            '-DARB_WITH_MPI={}'.format('on' if cl_opt['mpi'] else 'off'),
+            '-DARB_WITH_GPU={}'.format('on' if cl_opt['gpu'] else 'off'),
+            '-DARB_VECTORIZE={}'.format('on' if cl_opt['vec'] else 'off'),
+            '-DARB_ARCH={}'.format(cl_opt['arch']),
+        ]
+
+        print('-'*5, 'command line options: {}'.format(cl_opt))
+        print('-'*5, 'cmake arguments: {}'.format(cmake_args))
+
+        cfg = 'Debug' if self.debug else 'Release'
+        build_args = ['--config', cfg]
+
+        cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg]
+
+        # Assuming Makefiles
+        build_args += ['--', '-j4']
+
+        self.build_args = build_args
+
+        env = os.environ.copy()
+        env['CXXFLAGS'] = '{}'.format(env.get('CXXFLAGS', ''))
+        if not os.path.exists(self.build_temp):
+            os.makedirs(self.build_temp)
+
+        # CMakeLists.txt is in the same directory as this setup.py file
+        cmake_list_dir = os.path.abspath(os.path.dirname(__file__))
+        print('-'*20, 'Configure CMake')
+        subprocess.check_call(['cmake', cmake_list_dir] + cmake_args,
+                              cwd=self.build_temp, env=env)
+
+        print('-'*20, 'Build')
+        cmake_cmd = ['cmake', '--build', '.'] + self.build_args
+        subprocess.check_call(cmake_cmd,
+                              cwd=self.build_temp)
+
+        # Copy from build path to some other place from whence it will later be installed.
+        # ... or something like that
+        # ... setuptools is an enigma monkey patched on a mystery
+        if not os.path.exists(dest_path):
+            os.makedirs(dest_path, exist_ok=True)
+        self.copy_tree(source_path, dest_path)
+
+setuptools.setup(
+    name='arbor',
+    version=version_,
+    python_requires='>=3.6',
+
+    install_requires=[],
+    setup_requires=[],
+    zip_safe=False,
+    ext_modules=[cmake_extension('arbor')],
+    cmdclass={
+        'build_ext': cmake_build,
+        'install':   install_command,
+    },
+
+    author='The lovely Arbor devs.',
+    url='https://github.com/arbor-sim/arbor',
+    description='High performance simulation of networks of multicompartment neurons.',
+    long_description='',
+    classifiers=[
+        'Development Status :: 4 - Beta', # Upgrade to "5 - Production/Stable" on release of v0.3
+        'Intended Audience :: Science/Research',
+        'Topic :: Scientific/Engineering :: Build Tools',
+        'License :: OSI Approved :: BSD License'
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+    ],
+    project_urls={
+        'Source': 'https://github.com/arbor-sim/arbor',
+        'Documentation': 'https://arbor.readthedocs.io',
+        'Bug Reports': 'https://github.com/arbor-sim/arbor/issues',
+    },
+)
+
-- 
GitLab