2 # This file is part of the GROMACS molecular simulation package.
4 # Copyright (c) 2019,2020,2021, by the GROMACS development team, led by
5 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
6 # and including many others, as listed in the AUTHORS file in the
7 # top-level source directory and at http://www.gromacs.org.
9 # GROMACS is free software; you can redistribute it and/or
10 # modify it under the terms of the GNU Lesser General Public License
11 # as published by the Free Software Foundation; either version 2.1
12 # of the License, or (at your option) any later version.
14 # GROMACS is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 # Lesser General Public License for more details.
19 # You should have received a copy of the GNU Lesser General Public
20 # License along with GROMACS; if not, see
21 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
22 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 # If you want to redistribute modifications to GROMACS, please
25 # consider that scientific software is very special. Version
26 # control is crucial - bugs must be traceable. We will be happy to
27 # consider code for inclusion in the official distribution, but
28 # derived work must not be called official GROMACS. Details are found
29 # in the README & COPYING files - if they are missing, get the
30 # official version at http://www.gromacs.org.
32 # To help us fund GROMACS development, we humbly ask that you cite
33 # the research papers on the package. Check out http://www.gromacs.org.
35 # Note: most of this file is borrowed from
36 # https://github.com/pybind/cmake_example/commit/31bc276d91985c9bb94e2b4ec12f3fd528971f2c
38 """Python setuptools script to build and install the gmxapi Python interface.
40 The `gmxapi` package requires an existing GROMACS installation, version 2020 or higher.
41 To specify the GROMACS installation to use, provide a GMXTOOLCHAINDIR
42 environment variable when running setup.py or `pip`.
45 GMXTOOLCHAINDIR=/path/to/gromacs/share/cmake/gromacs pip install gmxapi
47 See https://manual.gromacs.org/current/gmxapi/userguide/install.html for more information.
58 from setuptools import setup, Extension
59 from setuptools.command.build_ext import build_ext
63 # gmxapi does not officially support Windows environments because GROMACS does not have automated testing
64 # infrastructure to verify correct functionality. However, we can try to be friendly or prepare for a possible future
65 # in which we can support more platforms.
66 # Convert distutils Windows platform specifiers to CMake -A arguments
75 # A CMakeExtension needs a sourcedir instead of a file list.
76 # The name must be the _single_ output extension from the CMake build.
77 class CMakeExtension(Extension):
78 def __init__(self, name, sourcedir=""):
79 Extension.__init__(self, name, sources=[])
80 self.sourcedir = os.path.abspath(sourcedir)
83 class CMakeBuild(build_ext):
84 """Derived distutils Command for build_extension.
86 See https://github.com/pybind/cmake_example for the current version
87 of the sample project from which this is borrowed.
89 def build_extension(self, ext):
92 extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
94 # required for auto-detection & inclusion of auxiliary "native" libs
95 if not extdir.endswith(os.path.sep):
98 debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug
99 cfg = "Debug" if debug else "Release"
101 # CMake lets you override the generator - we need to check this.
102 # Can be set with Conda-Build, for example.
103 cmake_generator = os.environ.get("CMAKE_GENERATOR", "")
106 "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={}".format(extdir),
107 "-DCMAKE_BUILD_TYPE={}".format(cfg), # not used on MSVC, but no harm
110 # Adding CMake arguments set as environment variable
111 # (needed e.g. to build for ARM OSx on conda-forge)
112 if "CMAKE_ARGS" in os.environ:
113 cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item]
115 if self.compiler.compiler_type != "msvc":
116 # Using Ninja-build since it a) is available as a wheel and b)
117 # multithreads automatically. MSVC would require all variables be
118 # exported for Ninja to pick it up, which is a little tricky to do.
119 # Users can override the generator with CMAKE_GENERATOR in CMake
121 if not cmake_generator:
123 import ninja # noqa: F401
125 cmake_args += ["-GNinja"]
131 # Single config generators are handled "normally"
132 single_config = any(x in cmake_generator for x in {"NMake", "Ninja"})
134 # CMake allows an arch-in-generator style for backward compatibility
135 contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"})
137 # Specify the arch if using MSVC generator, but only if it doesn't
138 # contain a backward-compatibility arch spec already in the
140 if not single_config and not contains_arch:
141 cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]]
143 # Multi-config generators have a different way to specify configs
144 if not single_config:
146 "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}".format(cfg.upper(), extdir)
148 build_args += ["--config", cfg]
150 if sys.platform.startswith("darwin"):
151 # Cross-compile support for macOS - respect ARCHFLAGS if set
152 archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", ""))
154 cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))]
156 # Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level
157 # across all generators.
158 if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
159 # self.parallel is a Python 3 only way to set parallel jobs by hand
160 # using -j in the build_ext call, not supported by pip or PyPA-build.
161 if hasattr(self, "parallel") and self.parallel:
163 build_args += ["-j{}".format(self.parallel)]
165 if not os.path.exists(self.build_temp):
166 os.makedirs(self.build_temp)
168 update_gromacs_client_cmake_args(cmake_args)
171 for arg in cmake_args:
172 if arg.upper().startswith('-DPYBIND11_ROOT'):
175 pybind_root = pybind11.get_cmake_dir()
177 cmake_args.append(f'-Dpybind11_ROOT={pybind_root}')
179 subprocess.check_call(
180 ["cmake", ext.sourcedir] + cmake_args, cwd=self.build_temp
182 subprocess.check_call(
183 ["cmake", "--build", "."] + build_args, cwd=self.build_temp
187 def _find_first_gromacs_suffix(directory):
188 dir_contents = os.listdir(directory)
189 for entry in dir_contents:
190 if entry.startswith('gromacs'):
191 return entry.strip('gromacs')
194 def update_gromacs_client_cmake_args(args: typing.List[str]):
195 """Try to convert information from command line environment to usable client CMake stuff.
197 This function compartmentalizes details that are likely to evolve with issues
198 https://gitlab.com/gromacs/gromacs/-/issues/3273
200 https://gitlab.com/gromacs/gromacs/-/issues/3279
202 See linked issues for more discussion or to join in the conversation.
204 has_gmxapi_dir = False
207 if arg.upper().startswith('-DGMXAPI_DIR'):
208 gmxapi_DIR = arg.split('=')[1]
210 has_gmxapi_dir = True
212 if not has_gmxapi_dir:
213 gmxapi_DIR = os.getenv('gmxapi_DIR')
215 # Infer from GMXRC exports, if available.
216 gmxapi_DIR = os.getenv('GROMACS_DIR')
218 has_toolchain_file = False
221 if arg.upper().startswith('-DCMAKE_TOOLCHAIN_FILE'):
222 gmx_toolchain = arg.split('=')[1]
224 has_toolchain_file = True
226 if has_toolchain_file and has_gmxapi_dir:
229 gmx_toolchain_dir = os.getenv('GMXTOOLCHAINDIR')
231 if gmx_toolchain_dir:
232 warnings.warn('Overriding GMXTOOLCHAINDIR environment variable because CMAKE_TOOLCHAIN_FILE CMake '
233 'variable was specified.')
234 gmx_toolchain_dir = os.path.dirname(gmx_toolchain)
236 if gmx_toolchain_dir is None:
237 # Try to guess from standard GMXRC environment variables.
238 if gmxapi_DIR is not None:
239 if os.path.exists(gmxapi_DIR) and os.path.isdir(gmxapi_DIR):
240 share_cmake = os.path.join(gmxapi_DIR, 'share', 'cmake')
241 suffix = _find_first_gromacs_suffix(share_cmake)
242 if suffix is not None:
243 gmx_toolchain_dir = os.path.join(share_cmake, 'gromacs' + suffix)
245 if gmx_toolchain_dir is None:
247 raise GmxapiInstallError('Could not configure for GROMACS installation. Provide GMXTOOLCHAINDIR.')
249 suffix = os.path.basename(gmx_toolchain_dir).strip('gromacs')
250 gmx_toolchain = os.path.abspath(os.path.join(gmx_toolchain_dir, 'gromacs-toolchain' + suffix + '.cmake'))
253 # Example: given /usr/local/gromacs/share/cmake/gromacs/gromacs-toolchain.cmake,
254 # we would want /usr/local/gromacs.
255 # Note that we could point more directly to the gmxapi-config.cmake but,
256 # so far, we have relied on CMake automatically looking into
257 # <package>_DIR/share/cmake/<package>/ for such a file.
258 # We would need a slightly different behavior for packages that link against
259 # libgromacs directly, as sample_restraint currently does.
260 gmxapi_DIR = os.path.join(os.path.dirname(gmx_toolchain), '..', '..', '..')
262 gmxapi_DIR = os.path.abspath(gmxapi_DIR)
264 if not os.path.exists(gmxapi_DIR) or not os.path.isdir(gmxapi_DIR):
266 raise GmxapiInstallError('Please set a valid gmxapi_DIR.')
268 if gmxapi_DIR != os.path.commonpath([gmxapi_DIR, gmx_toolchain]):
269 raise GmxapiInstallError('GROMACS toolchain file {} is not in gmxapi_DIR {}'.format(
274 if not has_gmxapi_dir:
275 args.append(f'-Dgmxapi_ROOT={gmxapi_DIR}')
276 if not has_toolchain_file:
277 args.append(f'-DCMAKE_TOOLCHAIN_FILE={gmx_toolchain}')
280 class GmxapiInstallError(Exception):
281 """Error processing setup.py for gmxapi Python package."""
284 ext_modules=[CMakeExtension("gmxapi._gmxapi")],
285 cmdclass={"build_ext": CMakeBuild},