Require pybind 2.6 from environment for gmxapi Python package extension module.
[alexxy/gromacs.git] / python_packaging / src / CMakeLists.txt
index 79e5e0cb2b28b659b884d5714156a52c90902c87..418185e1891bdbbe4c851c55f6cea0a7cf37b535 100644 (file)
@@ -1,7 +1,7 @@
 #
 # This file is part of the GROMACS molecular simulation package.
 #
-# Copyright (c) 2019, by the GROMACS development team, led by
+# Copyright (c) 2019,2020,2021, by the GROMACS development team, led by
 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
 # and including many others, as listed in the AUTHORS file in the
 # top-level source directory and at http://www.gromacs.org.
 # To help us fund GROMACS development, we humbly ask that you cite
 # the research papers on the package. Check out http://www.gromacs.org.
 
-# This CMakeLists.txt allows source distributions of the gmxapi Python package
-# to rely on scikit-build for support of various Python packaging systems. The
-# simplest use case is to allow the `setup.py` file to invoke skbuild to
-# configure and run CMake. CMake could be invoked directly by the user or a
-# parent package, but the Python distribution would not be packaged automatically.
-# Reference https://redmine.gromacs.org/issues/2896 for additional discussion.
-cmake_minimum_required(VERSION 3.9.6)
+# This CMakeLists.txt is not intended to be used directly, but either through
+# setup.py or as an inclusion of the full GROMACS project.
+# See https://manual.gromacs.org/current/gmxapi/userguide/install.html for more.
+cmake_minimum_required(VERSION 3.16.3)
 
 # This needs to be set before project() in order to pick up toolchain files
 #list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake)
 
-# OS X deployment target should be >=10.9 for modern C++ compatibility.
+# OS X deployment target should be >=10.14 for modern C++ compatibility.
 # Reference https://scikit-build.readthedocs.io/en/latest/generators.html#macosx
 # and https://github.com/MacPython/wiki/wiki/Spinning-wheels
-set(CMAKE_OSX_DEPLOYMENT_TARGET 10.9 CACHE STRING
-    "OS X deployment target below 10.9 does not use modern standard library"
-    FORCE)
+set(CMAKE_OSX_DEPLOYMENT_TARGET 10.14 CACHE STRING
+    "OS X deployment target below 10.14 does not use modern standard library")
 set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING
     "OS X should build Python package for 64-bit architecture"
     FORCE)
 
-project(gmxapi VERSION 0.1.0)
+# Note that this is the gmxapi._gmxapi Python bindings package version,
+# not the C++ API version. It is not essential that it match the pure Python
+# package version, but is likely to do so.
+project(gmxapi)
 
 # Check if Python package is being built directly or via add_subdirectory
 set(GMXAPI_MASTER_PROJECT OFF)
 if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR)
     set(GMXAPI_MASTER_PROJECT ON)
+    if (NOT Python3_FIND_STRATEGY)
+        # If the user provides a hint for the Python installation with Python3_ROOT_DIR,
+        # prevent FindPython3 from overriding the choice with a newer Python version
+        # when CMP0094 is set to OLD.
+        set(Python3_FIND_STRATEGY LOCATION)
+    endif ()
+    if(NOT Python3_FIND_VIRTUALENV)
+        # We advocate using Python venvs to manage package availability, so by default
+        # we want to preferentially discover user-space software.
+        set(Python3_FIND_VIRTUALENV FIRST)
+    endif()
 endif()
 
-set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 
 # Only interpret if() arguments as variables or keywords when unquoted.
 cmake_policy(SET CMP0054 NEW)
 # honor the language standard settings for try_compile()
 cmake_policy(SET CMP0067 NEW)
+if(POLICY CMP0074) #3.12
+    # Allow gmxapi_ROOT hint.
+    cmake_policy(SET CMP0074 NEW)
+endif()
+
+find_package(Python3 3.7 COMPONENTS Interpreter Development)
+find_package(pybind11 2.6 CONFIG)
+# If we are not running through setup.py, we may need to look for the pybind11 headers.
+if (NOT pybind11_FOUND)
+    execute_process(
+        COMMAND
+        "${Python3_EXECUTABLE}" -c
+        "import pybind11; print(pybind11.get_cmake_dir())"
+        OUTPUT_VARIABLE _tmp_dir
+        OUTPUT_STRIP_TRAILING_WHITESPACE COMMAND_ECHO STDOUT)
+    list(APPEND CMAKE_PREFIX_PATH "${_tmp_dir}")
+    find_package(pybind11 2.6 CONFIG)
+endif ()
+if (NOT pybind11_FOUND)
+    message(FATAL_ERROR "Python package build dependencies not found with interpreter ${Python3_EXECUTABLE}. "
+            "See https://manual.gromacs.org/current/gmxapi/userguide/install.html")
+endif ()
 
 if(GMXAPI_MASTER_PROJECT)
-    find_package(gmxapi 0.0.8 REQUIRED
+    find_package(gmxapi 0.2 REQUIRED
                  HINTS "$ENV{GROMACS_DIR}"
                  )
+    if (gmxapi_VERSION VERSION_LESS 0.2.1)
+        message(WARNING "Your GROMACS installation does not support custom MD plugins. "
+                "If you need this feature, please install GROMACS 2021.3 or higher.")
+    endif ()
+else()
+    # Building as part of the GROMACS master project. GROMACS CMake logic should
+    # not be processing this unless Python3 was appropriately detected.
+    if (NOT Python3_FOUND)
+        message(FATAL_ERROR "Error in CMake script. Please report GROMACS bug.")
+    endif ()
+
+    get_target_property(gmxapi_VERSION gmxapi VERSION)
 endif()
+
 if(gmxapi_FOUND)
     set(_suffix "")
     # GROMACS master branch and development branches may have divergent
@@ -84,33 +129,42 @@ if(gmxapi_FOUND)
     if(gmxapi_EXPERIMENTAL)
         set(_suffix " (unofficial)")
     endif()
-    message(STATUS "Found gmxapi version ${gmxapi_VERSION}${_suffix}")
 endif()
 
-option(GMXAPI_USE_BUNDLED_PYBIND
-       "Use pybind11 headers bundled with this repository. If OFF, CMake does `find_package(pybind11)`."
-       ON)
-if(GMXAPI_USE_BUNDLED_PYBIND)
-    add_subdirectory(external/pybind)
-else()
-    # Reference https://redmine.gromacs.org/issues/2896
-    find_package(pybind11 2.2 REQUIRED)
-endif()
+message(STATUS "Configuring Python package for gmxapi version ${gmxapi_VERSION}${_suffix}")
 
-set(GMXAPI_PYTHON_EXTENSION_SOURCES
-    gmxapi/module.cpp
-    gmxapi/export_context.cpp
-    gmxapi/export_exceptions.cpp
-    gmxapi/export_system.cpp
-    gmxapi/export_tprfile.cpp
-    gmxapi/pycontext.cpp
-    gmxapi/pysystem.cpp
-    )
+# The Gromacs::gmxapi target could be imported from an existing installation or
+# provided as an alias target within the GROMACS build tree.
+if (NOT TARGET Gromacs::gmxapi)
+    message(FATAL_ERROR "Cannot build Python package without GROMACS gmxapi support.")
+endif ()
+
+# TODO(#3279): Provide user hints for mpi4py installation.
+# Note that neither the Python package nor the Gromacs::gmxapi CMake target are
+# built with MPI in any case, but they _should_ be built with a C++ compiler
+# that is compatible with the available MPI compiler wrappers, and technically
+# _that_ is what we want to help the user identify when installing mpi4py, even
+# if libgromacs is not built with MPI support either.
+# For convenience, it is fine if libgmxapi and _gmxapi are built with the mpi
+# compiler wrapper.
 
 pybind11_add_module(_gmxapi
-                    ${GMXAPI_PYTHON_EXTENSION_SOURCES}
+                    gmxapi/module.cpp
+                    gmxapi/export_context.cpp
+                    gmxapi/export_exceptions.cpp
+                    gmxapi/export_system.cpp
+                    gmxapi/export_tprfile.cpp
+                    gmxapi/pycontext.cpp
+                    gmxapi/pysystem.cpp
                     )
 
+if (gmxapi_VERSION VERSION_GREATER_EQUAL 0.2.1)
+    target_sources(_gmxapi PRIVATE gmxapi/launch_021.cpp)
+else()
+    message(WARNING "Found an old gmxapi library version. Please consider updating your GROMACS installation.")
+    target_sources(_gmxapi PRIVATE gmxapi/launch_020.cpp)
+endif()
+
 target_include_directories(_gmxapi PRIVATE
                            ${CMAKE_CURRENT_SOURCE_DIR}/gmxapi
                            ${CMAKE_CURRENT_BINARY_DIR}/gmxapi
@@ -119,66 +173,89 @@ target_include_directories(_gmxapi PRIVATE
 # RPATH management: make sure build artifacts can find GROMACS library.
 set_target_properties(_gmxapi PROPERTIES SKIP_BUILD_RPATH FALSE)
 
-# Python sources (*.py) will be packaged by scikit-build and setuptools.
-# Note that library targets are built in CMAKE_LIBRARY_OUTPUT_DIRECTORY if not otherwise specified.
-# This may be an unexpected location, whether inherited from the GROMACS build tree
-# or the SKBUILD framework. Note, also, that when scikit-build is invoked with setup.py,
-# the CMake build takes place in a subdirectory of ./_skbuild/.
-set(GMXAPI_PYTHON_STAGING_DIR ${CMAKE_CURRENT_BINARY_DIR}/gmxapi_staging)
-set_target_properties(_gmxapi PROPERTIES
-                      LIBRARY_OUTPUT_DIRECTORY ${GMXAPI_PYTHON_STAGING_DIR}/gmxapi)
-
-# scikit-build sets SKBUILD when running Python packaging tools through setup.py
-# (e.g. with pip)
-if(SKBUILD)
-    # The Python module is being built for a GROMACS installation.
+if(GMXAPI_MASTER_PROJECT)
+    # TODO: This requirement is probably overly restrictive.
+    find_package(GROMACS 2021 REQUIRED
+                 NAMES gromacs gromacs_mpi
+                 HINTS "$ENV{GROMACS_DIR}"
+                 )
+endif()
+
+# Get details of GROMACS installation needed by the Python package at run time.
+
+# Get the MPI capability.
+get_target_property(_gmx_mpi Gromacs::gmxapi MPI)
+if (${_gmx_mpi} STREQUAL "library")
+    set(_gmx_mpi_type "\"library\"")
+elseif(${_gmx_mpi} STREQUAL "tmpi")
+    set(_gmx_mpi_type "\"tmpi\"")
+elseif(${_gmx_mpi} STREQUAL "none")
+    set(_gmx_mpi_type "null")
+else()
+    message(FATAL_ERROR "Unrecognized gmxapi MPI value: ${_gmx_mpi}")
+endif ()
+unset(_gmx_mpi)
+# Get the path of the command line entry point and binary install directory.
+if (NOT TARGET Gromacs::gmx)
+    message(FATAL_ERROR "GROMACS command line tool not found.")
+endif ()
+get_target_property(_gmx_executable_imported Gromacs::gmx IMPORTED)
+if (_gmx_executable_imported)
+    get_target_property(_gmx_executable Gromacs::gmx LOCATION)
+    get_filename_component(_gmx_bindir ${_gmx_executable} DIRECTORY)
+    message(STATUS "Imported ${_gmx_bindir} executable.")
+    unset(_gmx_executable_imported)
+else()
+    get_target_property(_gmx_bindir Gromacs::gmx RUNTIME_OUTPUT_DIRECTORY)
+    get_target_property(_gmx_executable Gromacs::gmx OUTPUT_NAME)
+    set(_gmx_executable "${_gmx_bindir}/${_gmx_executable}")
+    message(STATUS "Using ${_gmx_executable} from build tree.")
+endif ()
+if (NOT _gmx_bindir OR NOT _gmx_executable)
+    message(FATAL_ERROR "Could not get path for gmx wrapper binary.")
+endif ()
+configure_file(gmxapi/gmxconfig.json.in gmxapi/gmxconfig.json)
+unset(_gmx_executable)
+unset(_gmx_bindir)
+unset(_gmx_mpi_type)
+
+if (GMXAPI_MASTER_PROJECT)
     set_target_properties(_gmxapi PROPERTIES BUILD_WITH_INSTALL_RPATH TRUE)
     set_target_properties(_gmxapi PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE)
     target_link_libraries(_gmxapi PRIVATE Gromacs::gmxapi)
-    # By default, scikit-build expects the library to be installed into a directory
-    # named for the Python package as in setup.py.
-    install(TARGETS _gmxapi LIBRARY DESTINATION gmxapi)
+    # The Python setup.py sets CMAKE_LIBRARY_OUTPUT_DIRECTORY and will be looking for generated files there.
+    file(COPY ${CMAKE_CURRENT_BINARY_DIR}/gmxapi/gmxconfig.json
+         DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
 else()
-    # The Python module is being built against GROMACS in its build tree.
-    # Note: we do not have plans to install the staged package when SKBUILD != TRUE
-    set_target_properties(_gmxapi PROPERTIES BUILD_WITH_INSTALL_RPATH FALSE)
-    target_link_libraries(_gmxapi PRIVATE Gromacs::gmxapi)
-
-    # TODO: Determine packaging and installation cases and implementation.
-    # Reference https://redmine.gromacs.org/issues/2896 for additional discussion.
-    # Currently, CMake should be run by scikit-build through setup.py for proper Python packaging.
-    # We don't want to install by default in the outer scope of the GROMACS
-    # CMake procedure because we could end up trying to install to a system directory
-    # the user did not intend, or a user might install a Python-installation-specific
-    # package into an overly generic GROMACS path.
+    # The rest of the logic in this conditional is to support the GMX_PYTHON_PACKAGE option
+    # for testing the gmxapi Python packages within a full GROMACS project build_command and.
+    # for building full GROMACS project documentation.
 
+    set(GMXAPI_PYTHON_STAGING_DIR ${CMAKE_CURRENT_BINARY_DIR}/gmxapi_staging)
     # Instead, we should probably build a source package and alert the user of its location.
     # We can use CMake to call the Python packaging tools to create an 'sdist'
     # source distribution archive to be installed in the GROMACS installation
     # destination. We can use the build directory as the working directory for
     # easier clean-up, as well.
-    # TODO: (ref issue #2896) Build and install 'sdist' with GROMACS.
+    # TODO: (ref Issue #2896) Build and install 'sdist' with GROMACS.
 
+    # The Python module is being built against GROMACS in its build tree, so we will not install.
+    set_target_properties(_gmxapi PROPERTIES BUILD_WITH_INSTALL_RPATH FALSE)
+    target_link_libraries(_gmxapi PRIVATE Gromacs::gmxapi)
     # However, we can still produce an importable package for documentation builds and
     # basic testing in ${CMAKE_CURRENT_BINARY_DIR}/gmxapi_staging
-    if(CMAKE_VERSION VERSION_LESS 3.12)
-        file(GLOB_RECURSE _py_sources
-             ${CMAKE_CURRENT_SOURCE_DIR}/gmxapi/*.py)
-    else()
-        # CONFIGURE_DEPENDS appears in CMake 3.12 and can help to more robustly detect
-        # the need to update anything depending on the staged package.
-        file(GLOB_RECURSE _py_sources
-             CONFIGURE_DEPENDS
-             ${CMAKE_CURRENT_SOURCE_DIR}/gmxapi/*.py)
-    endif()
+    set_target_properties(_gmxapi PROPERTIES
+                          LIBRARY_OUTPUT_DIRECTORY ${GMXAPI_PYTHON_STAGING_DIR}/gmxapi)
+    file(GLOB_RECURSE _py_sources
+         CONFIGURE_DEPENDS
+         ${CMAKE_CURRENT_SOURCE_DIR}/gmxapi/*.py)
     foreach(_package_file IN LISTS _py_sources)
         get_filename_component(_absolute_dir ${_package_file} DIRECTORY)
         file(RELATIVE_PATH _relative_dir ${CMAKE_CURRENT_SOURCE_DIR} ${_absolute_dir})
-        file(COPY ${_package_file} DESTINATION gmxapi_staging/${_relative_dir})
+        file(COPY ${_package_file} DESTINATION ${GMXAPI_PYTHON_STAGING_DIR}/${_relative_dir})
     endforeach()
     file(COPY setup.py CMakeLists.txt DESTINATION ${GMXAPI_PYTHON_STAGING_DIR})
-    # Set CMake variable pybind11_DIR to ${CMAKE_CURRENT_SOURCE_DIR}/external/pybind/tools
-    # if re-invoking CMake (including via Python setuptools) for the files in gmxapi_staging.
+    file(COPY ${CMAKE_CURRENT_BINARY_DIR}/gmxapi/gmxconfig.json DESTINATION ${GMXAPI_PYTHON_STAGING_DIR}/gmxapi)
 
     # Unit test and build docs using PYTHONPATH=$CMAKE_CURRENT_BINARY_DIR/gmxapi_staging
     set_target_properties(_gmxapi PROPERTIES staging_dir ${GMXAPI_PYTHON_STAGING_DIR})
@@ -190,5 +267,7 @@ endif()
 # to the `check` target. Normal usage is to first install the Python package,
 # then run `pytest` on the `tests` directory. Refer to gmxapi package documentation.
 if(NOT GMXAPI_MASTER_PROJECT)
-    add_subdirectory(test)
+       if (BUILD_TESTING)
+               add_subdirectory(test)
+       endif()
 endif()