From 74ffc07ea75fe2237b278de095ba95cf843d08c6 Mon Sep 17 00:00:00 2001 From: Jarrett Johnson Date: Mon, 20 May 2024 08:37:07 -0400 Subject: [PATCH] Replace monkeypatch distutils with CMake setuptools Extensions --- CMakeLists.txt | 31 ++++++ INSTALL | 1 + contrib/champ/champ_module.c | 2 +- layer4/Cmd.cpp | 2 +- setup.py | 176 +++++++++++++++++++++++++++-------- 5 files changed, 173 insertions(+), 39 deletions(-) create mode 100644 CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..0620b6f5f --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.13) + +project(${TARGET_NAME}) + +set(CMAKE_VERBOSE_MAKEFILE on) + +add_library(${TARGET_NAME} SHARED ${ALL_SRC}) + +target_compile_options(${TARGET_NAME} PRIVATE ${ALL_COMP_ARGS}) + +set_target_properties(${TARGET_NAME} PROPERTIES SUFFIX ${SHARED_SUFFIX}) + +target_compile_features(${TARGET_NAME} PRIVATE cxx_std_17) + +set_target_properties(${TARGET_NAME} PROPERTIES PREFIX "") + +target_include_directories(${TARGET_NAME} PUBLIC ${ALL_INC_DIR}) + +target_link_directories(${TARGET_NAME} PUBLIC ${ALL_LIB_DIR}) + + +if(APPLE) + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -undefined dynamic_lookup") +endif() + +target_link_libraries(${TARGET_NAME} + ${ALL_LIB} + ${ALL_EXT_LINK} +) + +target_compile_definitions(${TARGET_NAME} PUBLIC ${ALL_DEF}) diff --git a/INSTALL b/INSTALL index 08a6776af..edfc56ad6 100644 --- a/INSTALL +++ b/INSTALL @@ -8,6 +8,7 @@ See also: http://pymolwiki.org/index.php/Linux_Install REQUIREMENTS - C++17 compiler (e.g. gcc 8+) + - CMake (3.13+) - Python 3.6+ - Pmw (Python Megawidgets) (optional, for legacy GUI/plugins) https://github.com/schrodinger/pmw-patched diff --git a/contrib/champ/champ_module.c b/contrib/champ/champ_module.c index 5b984b895..b8ed4aed0 100644 --- a/contrib/champ/champ_module.c +++ b/contrib/champ/champ_module.c @@ -1053,7 +1053,7 @@ static PyMethodDef champ_methods[] = { {NULL, NULL} /* sentinel */ }; -PyObject * PyInit__champ(void) +PyMODINIT_FUNC PyInit__champ(void) { static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, diff --git a/layer4/Cmd.cpp b/layer4/Cmd.cpp index 20b9ca263..d110a2ca9 100644 --- a/layer4/Cmd.cpp +++ b/layer4/Cmd.cpp @@ -6531,7 +6531,7 @@ static PyMethodDef Cmd_methods[] = { extern "C" { #endif -PyObject * PyInit__cmd(void) +PyMODINIT_FUNC PyInit__cmd(void) { static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, diff --git a/setup.py b/setup.py index eb18dd137..83111878b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# This script only applies if you are performing a Python Distutils-based +# This script only applies if you are performing a Python setuptools-based # installation of PyMOL. # # It may assume that all of PyMOL's external dependencies are @@ -9,12 +9,16 @@ import argparse import glob import os +import pathlib import re import sys +import sysconfig import shutil -from distutils.core import setup, Extension -from distutils.util import change_root +from setuptools import setup, Extension +from setuptools.command.build_ext import build_ext +from setuptools.command.build_py import build_py +from setuptools.command.install import install import create_shadertext @@ -33,7 +37,6 @@ class options: no_libxml = False no_glut = True use_msgpackc = 'guess' - help_distutils = False testing = False openvr = False use_openmp = 'no' if MAC else 'yes' @@ -58,8 +61,6 @@ class options: parser.add_argument('--use-msgpackc', choices=('c++11', 'c', 'guess', 'no'), help="c++11: use msgpack-c header-only library; c: link against " "shared library; no: disable fast MMTF load support") -parser.add_argument('--help-distutils', action="store_true", - help="show help for distutils options and exit") parser.add_argument('--testing', action="store_true", help="Build C-level tests") parser.add_argument('--openvr', dest='openvr', action='store_true') @@ -68,10 +69,7 @@ class options: help='Disable VMD molfile plugins (libnetcdf dependency)') options, sys.argv[1:] = parser.parse_known_args(namespace=options) -if options.help_distutils: - sys.argv.append("--help") - -if True: +if False: import monkeypatch_distutils monkeypatch_distutils.set_parallel_jobs(options.jobs) @@ -137,16 +135,104 @@ def guess_msgpackc(): return 'no' -# Important: import 'distutils.command' modules after monkeypatch_distutils -from distutils.command.build_ext import build_ext -from distutils.command.build_py import build_py -from distutils.command.install import install +class CMakeExtension(Extension): + + def __init__(self, + name, + sources, + include_dirs=[], + libraries=[], + library_dirs=[], + define_macros=[], + extra_link_args=[], + extra_compile_args=[]): + # don't invoke the original build_ext for this special extension + super().__init__(name, sources=[]) + self.sources = sources + self.include_dirs = include_dirs + self.libraries = libraries + self.library_dirs = library_dirs + self.define_macros = define_macros + self.extra_link_args = extra_link_args + self.extra_compile_args = extra_compile_args + class build_ext_pymol(build_ext): - def initialize_options(self): - build_ext.initialize_options(self) + def initialize_options(self) -> None: + super().initialize_options() if DEBUG and not WIN: - self.debug = True + self.debug = False + + def run(self): + for ext in self.extensions: + self.build_cmake(ext) + + def build_cmake(self, ext): + cwd = pathlib.Path().absolute() + + # these dirs will be created in build_py, so if you don't have + # any python sources to bundle, the dirs will be missing + name_split = ext.name.split('.') + target_name = name_split[-1] + build_temp = pathlib.Path(self.build_temp) / target_name + build_temp.mkdir(parents=True, exist_ok=True) + extdir = pathlib.Path(self.get_ext_fullpath(ext.name)) + extdirabs = extdir.absolute() + + extdir.parent.mkdir(parents=True, exist_ok=True) + + def concat_paths(paths): + return ''.join(path.replace('\\', '/') + ";" for path in paths) + + config = 'Debug' if DEBUG else 'Release' + lib_output_dir = str(extdir.parent.absolute()) + all_files = ext.sources + all_src = concat_paths(all_files) + all_defs = ''.join(mac[0] + ";" for mac in ext.define_macros) + all_libs = ''.join(f"{lib};" for lib in ext.libraries) + all_ext_link = ' '.join(ext.extra_link_args) + all_comp_args = ''.join(f"{arg};" for arg in ext.extra_compile_args) + all_lib_dirs = concat_paths(ext.library_dirs) + all_inc_dirs = concat_paths(ext.include_dirs) + + lib_mode = "RUNTIME" if WIN else "LIBRARY" + + shared_suffix = sysconfig.get_config_var('EXT_SUFFIX') + + cmake_args = [ + f"-DTARGET_NAME={target_name}", + f"-DCMAKE_{lib_mode}_OUTPUT_DIRECTORY={lib_output_dir}", + f"-DCMAKE_BUILD_TYPE={config}", + f"-DALL_INC_DIR={all_inc_dirs}", + f"-DALL_SRC={all_src}", + f"-DALL_DEF={all_defs}", + f"-DALL_LIB_DIR={all_lib_dirs}", + f"-DALL_LIB={all_libs}", + f"-DALL_COMP_ARGS={all_comp_args}", + f"-DALL_EXT_LINK={all_ext_link}", + f"-DSHARED_SUFFIX={shared_suffix}" + ] + + # example of build args + build_args = ['--config', config] + if not WIN: # Win /MP flag on compilation level + cpu_count = os.cpu_count() or 1 + build_args += [f'-j{cpu_count}'] + + os.chdir(str(build_temp)) + self.spawn(['cmake', str(cwd)] + cmake_args) + if not self.dry_run: + self.spawn(['cmake', '--build', '.'] + build_args) + + if WIN: + # Move up from VS release folder + cmake_lib_loc = pathlib.Path(lib_output_dir, "Release", f"{target_name}{shared_suffix}") + if cmake_lib_loc.exists(): + shutil.move(cmake_lib_loc, extdirabs) + + # Troubleshooting: if fail on line above then delete all possible + # temporary CMake files including "CMakeCache.txt" in top level dir. + os.chdir(str(cwd)) class build_py_pymol(build_py): @@ -175,10 +261,11 @@ def finalize_options(self): self.pymol_path = os.path.join( self.install_libbase, 'pymol', 'pymol_path') elif self.root is not None: - self.pymol_path = change_root(self.root, self.pymol_path) + self.pymol_path = install_pymol.change_root( + self.root, self.pymol_path) def run(self): - install.run(self) + super().run() self.install_pymol_path() if not self.no_launcher: @@ -304,7 +391,7 @@ def make_launch_script(self): '-Wno-char-subscripts', # optimizations "-Og" if DEBUG else "-O3", -] if not WIN else [] +] if not WIN else ["/MP"] ext_link_args = [] ext_objects = [] data_files = [] @@ -382,9 +469,9 @@ def make_launch_script(self): if options.osx_frameworks: ext_link_args += [ - "-framework", "OpenGL", + "-framework OpenGL", ] + (not options.no_glut) * [ - "-framework", "GLUT", + "-framework GLUT", ] def_macros += [ ("_PYMOL_OSX", None), @@ -527,22 +614,37 @@ def get_packages(base, parent='', r=None): for base in ['modules'] for x in get_packages(base)) +# Python includes +inc_dirs.append(sysconfig.get_paths()['include']) +inc_dirs.append(sysconfig.get_paths()['platinclude']) + +champ_inc_dirs = ['contrib/champ'] +champ_inc_dirs.append(sysconfig.get_paths()['include']) +champ_inc_dirs.append(sysconfig.get_paths()['platinclude']) + +if WIN: + # pyconfig.py forces linking against pythonXY.lib on MSVC + py_lib = pathlib.Path(sysconfig.get_paths()['stdlib']).parent / 'libs' + lib_dirs.append(str(py_lib)) + ext_modules += [ - Extension("pymol._cmd", - get_sources(pymol_src_dirs), - include_dirs=inc_dirs, - libraries=libs, - library_dirs=lib_dirs, - define_macros=def_macros, - extra_link_args=ext_link_args, - extra_compile_args=ext_comp_args, - extra_objects=ext_objects, - ), - - Extension("chempy.champ._champ", - get_sources(['contrib/champ']), - include_dirs=["contrib/champ"], - ), + CMakeExtension( + name="pymol._cmd", + sources=get_sources(pymol_src_dirs), + include_dirs=inc_dirs, + libraries=libs, + library_dirs=lib_dirs, + define_macros=def_macros, + extra_link_args=ext_link_args, + extra_compile_args=ext_comp_args, + ), + + CMakeExtension( + name="chempy.champ._champ", + sources=get_sources(['contrib/champ']), + include_dirs=champ_inc_dirs, + library_dirs=lib_dirs, + ), ] distribution = setup( # Distribution meta-data