diff --git a/.gitignore b/.gitignore index 2af7146ba2..4fd660b955 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ __pycache__/ .vscode/ *build* +*dist* *.egg-info -cmake/PybindWrap.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index d59e7a96a5..d48dd1964d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,6 @@ project(GTwrap VERSION 1.0) # ############################################################################## # General configuration -# Sets the path to the interface parser. -set(GTWRAP_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) - set(WRAP_PYTHON_VERSION "Default" CACHE STRING "The Python version to use for wrapping") @@ -24,13 +21,19 @@ else() set(SCRIPT_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lib/cmake") endif() -configure_file(cmake/PybindWrap.cmake.in - ${CMAKE_CURRENT_SOURCE_DIR}/cmake/PybindWrap.cmake @ONLY) -# Install scripts -install(FILES cmake/PybindWrap.cmake cmake/gtwrapConfig.cmake +# Install scripts to the standard CMake script directory. +install(FILES cmake/gtwrapConfig.cmake cmake/PybindWrap.cmake cmake/GtwrapUtils.cmake DESTINATION "${SCRIPT_INSTALL_DIR}/gtwrap") +# Install wrapping scripts as binaries to `CMAKE_INSTALL_PREFIX/bin` so they can +# be invoked for wrapping. +install(PROGRAMS scripts/pybind_wrap.py scripts/matlab_wrap.py TYPE BIN) + +# Install pybind11 directory to `CMAKE_INSTALL_PREFIX/lib/pybind11` This will +# allow the gtwrapConfig.cmake file to load it later. +install(DIRECTORY pybind11 TYPE LIB) + # ############################################################################## # Install the Python package find_package( @@ -46,7 +49,6 @@ else() set(_pip_args "--user") endif() -# We install in development mode (-e flag) so any updates to the package are -# automatically propagated. -execute_process(COMMAND ${Python_EXECUTABLE} -m pip install -e . ${_pip_args} +# Finally install the gtwrap python package. +execute_process(COMMAND ${Python_EXECUTABLE} setup.py install ${_pip_args} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/cmake/GtwrapUtils.cmake b/cmake/GtwrapUtils.cmake index a2f6f34f38..9c6b141a06 100644 --- a/cmake/GtwrapUtils.cmake +++ b/cmake/GtwrapUtils.cmake @@ -1,6 +1,6 @@ # Utilities to help with wrapping. -function(get_python_version) +macro(get_python_version) if(${CMAKE_VERSION} VERSION_LESS "3.12.0") # Use older version of cmake's find_python find_package(PythonInterp) @@ -37,23 +37,13 @@ function(get_python_version) "Cannot find Python interpreter. Please install Python>=3.6.") endif() - set(Python_VERSION_MAJOR - ${Python_VERSION_MAJOR} - PARENT_SCOPE) - set(Python_VERSION_MINOR - ${Python_VERSION_MINOR} - PARENT_SCOPE) - set(Python_VERSION_PATCH - ${Python_VERSION_PATCH} - PARENT_SCOPE) - endif() -endfunction() +endmacro() # Set the Python version for the wrapper and set the paths to the executable and # include/library directories. WRAP_PYTHON_VERSION can be "Default" or a # specific major.minor version. -function(gtwrap_get_python_version WRAP_PYTHON_VERSION) +macro(gtwrap_get_python_version WRAP_PYTHON_VERSION) # Unset these cached variables to avoid surprises when the python in the # current environment are different from the cached! unset(Python_EXECUTABLE CACHE) @@ -81,21 +71,4 @@ function(gtwrap_get_python_version WRAP_PYTHON_VERSION) EXACT REQUIRED) endif() - # Set variables' scope so we can access them from the calling function. - set(WRAP_PYTHON_VERSION - ${WRAP_PYTHON_VERSION} - PARENT_SCOPE) - set(Python_FOUND - ${Python_FOUND} - PARENT_SCOPE) - set(Python_EXECUTABLE - ${Python_EXECUTABLE} - PARENT_SCOPE) - set(Python_INCLUDE_DIRS - ${Python_INCLUDE_DIRS} - PARENT_SCOPE) - set(Python_LIBRARY_DIRS - ${Python_LIBRARY_DIRS} - PARENT_SCOPE) - -endfunction() +endmacro() diff --git a/cmake/PybindWrap.cmake.in b/cmake/PybindWrap.cmake similarity index 95% rename from cmake/PybindWrap.cmake.in rename to cmake/PybindWrap.cmake index 225c7d1a7a..8973151e4b 100644 --- a/cmake/PybindWrap.cmake.in +++ b/cmake/PybindWrap.cmake @@ -1,7 +1,5 @@ set(PYBIND11_PYTHON_VERSION ${WRAP_PYTHON_VERSION}) -add_subdirectory(@GTWRAP_SOURCE_DIR@/pybind11 pybind11) - # User-friendly Pybind11 wrapping and installing function. # Builds a Pybind11 module from the provided interface_header. # For example, for the interface header gtsam.h, this will @@ -40,7 +38,7 @@ function(pybind_wrap add_custom_command(OUTPUT ${generated_cpp} COMMAND ${PYTHON_EXECUTABLE} - @GTWRAP_SOURCE_DIR@/gtwrap/pybind_wrapper.py + ${CMAKE_INSTALL_FULL_BINDIR}/pybind_wrap.py --src ${interface_header} --out @@ -64,9 +62,9 @@ function(pybind_wrap # ~~~ add_custom_command(OUTPUT ${generated_cpp} DEPENDS ${interface_header} - @GTWRAP_SOURCE_DIR@/gtwrap/interface_parser.py - @GTWRAP_SOURCE_DIR@/gtwrap/pybind_wrapper.py - @GTWRAP_SOURCE_DIR@/gtwrap/template_instantiator.py + # @GTWRAP_SOURCE_DIR@/gtwrap/interface_parser.py + # @GTWRAP_SOURCE_DIR@/gtwrap/pybind_wrapper.py + # @GTWRAP_SOURCE_DIR@/gtwrap/template_instantiator.py APPEND) pybind11_add_module(${target} ${generated_cpp}) diff --git a/cmake/gtwrapConfig.cmake b/cmake/gtwrapConfig.cmake index e8472f62d5..61848b1243 100644 --- a/cmake/gtwrapConfig.cmake +++ b/cmake/gtwrapConfig.cmake @@ -10,5 +10,14 @@ else() set(SCRIPT_INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/lib/cmake") endif() +# Standard includes +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) +include(CMakeDependentOption) + +# Load the pybind11 code from the library installation path +add_subdirectory(${CMAKE_INSTALL_FULL_LIBDIR}/pybind11 pybind11) + +# Load all the CMake scripts from the standard location include(${SCRIPT_INSTALL_DIR}/gtwrap/PybindWrap.cmake) include(${SCRIPT_INSTALL_DIR}/gtwrap/GtwrapUtils.cmake) diff --git a/gtwrap/matlab_wrapper.py b/gtwrap/matlab_wrapper.py index 7fab510b04..88644c1780 100755 --- a/gtwrap/matlab_wrapper.py +++ b/gtwrap/matlab_wrapper.py @@ -1666,7 +1666,7 @@ def wrap(self): return self.content -def _generate_content(cc_content, path, verbose=False): +def generate_content(cc_content, path, verbose=False): """Generate files and folders from matlab wrapper content. Keyword arguments: @@ -1726,53 +1726,3 @@ def _debug(message): with open(path_to_file, 'w') as f: f.write(c[1]) - - -if __name__ == "__main__": - arg_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) - arg_parser.add_argument("--src", type=str, required=True, help="Input interface .h file.") - arg_parser.add_argument("--module_name", type=str, required=True, help="Name of the C++ class being wrapped.") - arg_parser.add_argument("--out", type=str, required=True, help="Name of the output folder.") - arg_parser.add_argument("--top_module_namespaces", - type=str, - default="", - help="C++ namespace for the top module, e.g. `ns1::ns2::ns3`. " - "Only the content within this namespace and its sub-namespaces " - "will be wrapped. The content of this namespace will be available at " - "the top module level, and its sub-namespaces' in the submodules.\n" - "For example, `import ` gives you access to a Python " - "`.Class` of the corresponding C++ `ns1::ns2::ns3::Class`" - ", and `from import ns4` gives you access to a Python " - "`ns4.Class` of the C++ `ns1::ns2::ns3::ns4::Class`. ") - arg_parser.add_argument("--ignore", - nargs='*', - type=str, - help="A space-separated list of classes to ignore. " - "Class names must include their full namespaces.") - args = arg_parser.parse_args() - - top_module_namespaces = args.top_module_namespaces.split("::") - if top_module_namespaces[0]: - top_module_namespaces = [''] + top_module_namespaces - - with open(args.src, 'r') as f: - content = f.read() - - if not os.path.exists(args.src): - os.mkdir(args.src) - - module = parser.Module.parseString(content) - - instantiator.instantiate_namespace_inplace(module) - - import sys - - print("Ignoring classes: {}".format(args.ignore), file=sys.stderr) - wrapper = MatlabWrapper(module=module, - module_name=args.module_name, - top_module_namespace=top_module_namespaces, - ignore_classes=args.ignore) - - cc_content = wrapper.wrap() - - _generate_content(cc_content, args.out) diff --git a/gtwrap/pybind_wrapper.py b/gtwrap/pybind_wrapper.py index a65eeae380..c0e88e37af 100755 --- a/gtwrap/pybind_wrapper.py +++ b/gtwrap/pybind_wrapper.py @@ -9,7 +9,6 @@ Code generator for wrapping a C++ module with Pybind11 Author: Duy Nguyen Ta, Fan Jiang, Matthew Sklar and Frank Dellaert """ -import argparse import re import textwrap @@ -319,76 +318,3 @@ def wrap(self): wrapped_namespace=wrapped_namespace, boost_class_export=boost_class_export, ) - - -def main(): - arg_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) - arg_parser.add_argument("--src", type=str, required=True, help="Input interface .h file") - arg_parser.add_argument( - "--module_name", - type=str, - required=True, - help="Name of the Python module to be generated and " - "used in the Python `import` statement.", - ) - arg_parser.add_argument( - "--out", - type=str, - required=True, - help="Name of the output pybind .cc file", - ) - arg_parser.add_argument( - "--use-boost", - action="store_true", - help="using boost's shared_ptr instead of std's", - ) - arg_parser.add_argument( - "--top_module_namespaces", - type=str, - default="", - help="C++ namespace for the top module, e.g. `ns1::ns2::ns3`. " - "Only the content within this namespace and its sub-namespaces " - "will be wrapped. The content of this namespace will be available at " - "the top module level, and its sub-namespaces' in the submodules.\n" - "For example, `import ` gives you access to a Python " - "`.Class` of the corresponding C++ `ns1::ns2::ns3::Class`" - "and `from import ns4` gives you access to a Python " - "`ns4.Class` of the C++ `ns1::ns2::ns3::ns4::Class`. ", - ) - arg_parser.add_argument( - "--ignore", - nargs='*', - type=str, - help="A space-separated list of classes to ignore. " - "Class names must include their full namespaces.", - ) - arg_parser.add_argument("--template", type=str, help="The module template file") - args = arg_parser.parse_args() - - top_module_namespaces = args.top_module_namespaces.split("::") - if top_module_namespaces[0]: - top_module_namespaces = [''] + top_module_namespaces - - with open(args.src, "r") as f: - content = f.read() - module = parser.Module.parseString(content) - instantiator.instantiate_namespace_inplace(module) - - with open(args.template, "r") as f: - template_content = f.read() - wrapper = PybindWrapper( - module=module, - module_name=args.module_name, - use_boost=args.use_boost, - top_module_namespaces=top_module_namespaces, - ignore_classes=args.ignore, - module_template=template_content, - ) - - cc_content = wrapper.wrap() - with open(args.out, "w") as f: - f.write(cc_content) - - -if __name__ == "__main__": - main() diff --git a/scripts/matlab_wrap.py b/scripts/matlab_wrap.py new file mode 100644 index 0000000000..232e934905 --- /dev/null +++ b/scripts/matlab_wrap.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +""" +Helper script to wrap C++ to Matlab. +This script is installed via CMake to the user's binary directory +and invoked during the wrapping by CMake. +""" + +import argparse +import os + +import gtwrap.interface_parser as parser +import gtwrap.template_instantiator as instantiator +from gtwrap.matlab_wrapper import MatlabWrapper, generate_content + +if __name__ == "__main__": + arg_parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + arg_parser.add_argument("--src", type=str, required=True, + help="Input interface .h file.") + arg_parser.add_argument("--module_name", type=str, required=True, + help="Name of the C++ class being wrapped.") + arg_parser.add_argument("--out", type=str, required=True, + help="Name of the output folder.") + arg_parser.add_argument( + "--top_module_namespaces", + type=str, + default="", + help="C++ namespace for the top module, e.g. `ns1::ns2::ns3`. " + "Only the content within this namespace and its sub-namespaces " + "will be wrapped. The content of this namespace will be available at " + "the top module level, and its sub-namespaces' in the submodules.\n" + "For example, `import ` gives you access to a Python " + "`.Class` of the corresponding C++ `ns1::ns2::ns3::Class`" + ", and `from import ns4` gives you access to a Python " + "`ns4.Class` of the C++ `ns1::ns2::ns3::ns4::Class`. ") + arg_parser.add_argument("--ignore", + nargs='*', + type=str, + help="A space-separated list of classes to ignore. " + "Class names must include their full namespaces.") + args = arg_parser.parse_args() + + top_module_namespaces = args.top_module_namespaces.split("::") + if top_module_namespaces[0]: + top_module_namespaces = [''] + top_module_namespaces + + with open(args.src, 'r') as f: + content = f.read() + + if not os.path.exists(args.src): + os.mkdir(args.src) + + module = parser.Module.parseString(content) + + instantiator.instantiate_namespace_inplace(module) + + import sys + + print("Ignoring classes: {}".format(args.ignore), file=sys.stderr) + wrapper = MatlabWrapper(module=module, + module_name=args.module_name, + top_module_namespace=top_module_namespaces, + ignore_classes=args.ignore) + + cc_content = wrapper.wrap() + + generate_content(cc_content, args.out) diff --git a/scripts/pybind_wrap.py b/scripts/pybind_wrap.py new file mode 100644 index 0000000000..e641cfaafe --- /dev/null +++ b/scripts/pybind_wrap.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +""" +Helper script to wrap C++ to Python with Pybind. +This script is installed via CMake to the user's binary directory +and invoked during the wrapping by CMake. +""" + +import argparse + +import gtwrap.interface_parser as parser +import gtwrap.template_instantiator as instantiator +from gtwrap.pybind_wrapper import PybindWrapper + + +def main(): + """Main runner.""" + arg_parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + arg_parser.add_argument( + "--src", + type=str, + required=True, + help="Input interface .i/.h file") + arg_parser.add_argument( + "--module_name", + type=str, + required=True, + help="Name of the Python module to be generated and " + "used in the Python `import` statement.", + ) + arg_parser.add_argument( + "--out", + type=str, + required=True, + help="Name of the output pybind .cc file", + ) + arg_parser.add_argument( + "--use-boost", + action="store_true", + help="using boost's shared_ptr instead of std's", + ) + arg_parser.add_argument( + "--top_module_namespaces", + type=str, + default="", + help="C++ namespace for the top module, e.g. `ns1::ns2::ns3`. " + "Only the content within this namespace and its sub-namespaces " + "will be wrapped. The content of this namespace will be available at " + "the top module level, and its sub-namespaces' in the submodules.\n" + "For example, `import ` gives you access to a Python " + "`.Class` of the corresponding C++ `ns1::ns2::ns3::Class`" + "and `from import ns4` gives you access to a Python " + "`ns4.Class` of the C++ `ns1::ns2::ns3::ns4::Class`. ", + ) + arg_parser.add_argument( + "--ignore", + nargs='*', + type=str, + help="A space-separated list of classes to ignore. " + "Class names must include their full namespaces.", + ) + arg_parser.add_argument("--template", type=str, + help="The module template file") + args = arg_parser.parse_args() + + top_module_namespaces = args.top_module_namespaces.split("::") + if top_module_namespaces[0]: + top_module_namespaces = [''] + top_module_namespaces + + with open(args.src, "r") as f: + content = f.read() + module = parser.Module.parseString(content) + instantiator.instantiate_namespace_inplace(module) + + with open(args.template, "r") as f: + template_content = f.read() + wrapper = PybindWrapper( + module=module, + module_name=args.module_name, + use_boost=args.use_boost, + top_module_namespaces=top_module_namespaces, + ignore_classes=args.ignore, + module_template=template_content, + ) + + cc_content = wrapper.wrap() + with open(args.out, "w") as f: + f.write(cc_content) + + +if __name__ == "__main__": + main()