diff --git a/.gitmodules b/.gitmodules index 80ec1e2e..2ecbc5fe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,7 @@ url = https://github.com/cppit/libclangmm [submodule "tiny-process-library"] path = tiny-process-library - url = https://github.com/eidheim/tiny-process-library + url = https://gitlab.com/eidheim/tiny-process-library +[submodule "pybind11"] + path = pybind11 + url = https://github.com/pybind/pybind11.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 35b2373b..a00b1f57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,7 @@ set(BUILD_TESTING_SAVED ${BUILD_TESTING}) set(BUILD_TESTING OFF CACHE BOOL "Disable sub-project tests" FORCE) add_subdirectory(libclangmm) add_subdirectory(tiny-process-library) +add_subdirectory(pybind11) set(BUILD_TESTING ${BUILD_TESTING_SAVED} CACHE BOOL "Set to previous value" FORCE) find_package(Boost 1.54 COMPONENTS system filesystem serialization REQUIRED) @@ -69,6 +70,8 @@ else() set(LIBLLDB_LIBRARIES "") message("liblldb not found. Building juCi++ without debugging support") endif() +find_package(PythonLibs 3 REQUIRED) +pkg_check_modules(PYGOBJECT pygobject-3.0 REQUIRED) # For both src and tests targets include_directories( @@ -78,6 +81,8 @@ include_directories( ${LIBCLANG_INCLUDE_DIRS} ${ASPELL_INCLUDE_DIR} ${LIBGIT2_INCLUDE_DIRS} + ${PYTHON_INCLUDE_DIRS} + ${PYGOBJECT_INCLUDE_DIRS} ) add_subdirectory("src") diff --git a/README.md b/README.md index c235929f..4b6cea8b 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ See [enhancements](https://github.com/cppit/jucipp/labels/enhancement) for plann * lldb * libgit2 * [libclangmm](http://github.com/cppit/libclangmm/) (downloaded directly with git --recursive, no need to install) -* [tiny-process-library](http://github.com/eidheim/tiny-process-library/) (downloaded directly with git --recursive, no need to install) +* [tiny-process-library](http://gitlab.com/eidheim/tiny-process-library/) (downloaded directly with git --recursive, no need to install) ## Installation See [installation guide](docs/install.md). diff --git a/ci/update_ci.sh b/ci/update_ci.sh index 93ebb0e0..ef8ab197 100755 --- a/ci/update_ci.sh +++ b/ci/update_ci.sh @@ -32,11 +32,11 @@ function windows () { if [ "$PLATFORM" == "x86" ]; then arch=i686 fi - sh -c "pacman -S --noconfirm git mingw-w64-${arch}-cmake make mingw-w64-${arch}-toolchain mingw-w64-${arch}-clang mingw-w64-${arch}-gtkmm3 mingw-w64-${arch}-gtksourceviewmm3 mingw-w64-${arch}-boost mingw-w64-${arch}-aspell mingw-w64-${arch}-aspell-en mingw-w64-${arch}-libgit2" + sh -c "pacman -S --noconfirm git mingw-w64-${arch}-pygobject-devel mingw-w64-${arch}-cmake make mingw-w64-${arch}-toolchain mingw-w64-${arch}-clang mingw-w64-${arch}-gtkmm3 mingw-w64-${arch}-gtksourceviewmm3 mingw-w64-${arch}-boost mingw-w64-${arch}-aspell mingw-w64-${arch}-aspell-en mingw-w64-${arch}-libgit2" } if [ "$TRAVIS_OS_NAME" == "" ]; then TRAVIS_OS_NAME=windows fi -$TRAVIS_OS_NAME \ No newline at end of file +$TRAVIS_OS_NAME diff --git a/docs/install.md b/docs/install.md index 357ac587..ad90fa35 100644 --- a/docs/install.md +++ b/docs/install.md @@ -142,7 +142,7 @@ make install Install dependencies (replace `x86_64` with `i686` for 32-bit MSYS2 installs): ```sh -pacman -S git mingw-w64-x86_64-cmake make mingw-w64-x86_64-toolchain mingw-w64-x86_64-clang mingw-w64-x86_64-gtkmm3 mingw-w64-x86_64-gtksourceviewmm3 mingw-w64-x86_64-boost mingw-w64-x86_64-aspell mingw-w64-x86_64-aspell-en mingw-w64-x86_64-libgit2 mingw-w64-x86_64-universal-ctags-git +pacman -S git mingw-w64-x86_64-cmake make mingw-w64-x86_64-toolchain mingw-w64-x86_64-clang mingw-w64-x86_64-gtkmm3 mingw-w64-x86_64-gtksourceviewmm3 mingw-w64-x86_64-boost mingw-w64-x86_64-aspell mingw-w64-x86_64-aspell-en mingw-w64-x86_64-gobject-introspection mingw-w64-x86_64-pygobject-devel mingw-w64-x86_64-python3-gobject mingw-w64-x86_64-libgit2 mingw-w64-x86_64-universal-ctags-git ``` Note that juCi++ must be built and run in a MinGW Shell (for instance MinGW-w64 Win64 Shell). diff --git a/pybind11 b/pybind11 new file mode 160000 index 00000000..ed07e492 --- /dev/null +++ b/pybind11 @@ -0,0 +1 @@ +Subproject commit ed07e49236d937fd4ae98d0b0cb58a962cd269a6 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9c32003c..254a4cd4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,6 +33,9 @@ target_link_libraries(juci_shared ${LIBGIT2_LIBRARIES} clangmm tiny-process-library + pybind11 + ${PYTHON_LIBRARIES} + ${PYGOBJECT_LIBRARIES} ) add_executable(juci @@ -46,6 +49,7 @@ add_executable(juci notebook.cc project.cc selection_dialog.cc + python_interpreter.cc tooltips.cc window.cc ) diff --git a/src/config.cc b/src/config.cc index 934b4388..f12cafd8 100644 --- a/src/config.cc +++ b/src/config.cc @@ -38,6 +38,7 @@ void Config::find_or_create_config_files() { auto config_json = config_dir/"config.json"; boost::filesystem::create_directories(config_dir); // io exp captured by calling method + boost::filesystem::create_directories(home_juci_path/"plugins"); if (!boost::filesystem::exists(config_json)) filesystem::write(config_json, default_config_file); @@ -101,6 +102,8 @@ void Config::make_version_dependent_corrections(boost::property_tree::ptree &cfg catch(const std::exception &e) { std::cerr << "Error correcting preferences: " << e.what() << std::endl; } + python.plugin_directory=cfg.get("python.plugin_directory",(home_juci_path/"plugins").string()); + python.site_packages=cfg.get("python.site_packages",""); } bool Config::add_missing_nodes(boost::property_tree::ptree &cfg, const boost::property_tree::ptree &default_cfg, std::string parent_path) { diff --git a/src/config.h b/src/config.h index 1c1000be..7ff87331 100644 --- a/src/config.h +++ b/src/config.h @@ -93,6 +93,12 @@ class Config { std::unordered_map documentation_searches; }; + + class Python { + public: + std::string site_packages; + std::string plugin_directory; + }; private: Config(); public: @@ -108,6 +114,7 @@ class Config { Terminal terminal; Project project; Source source; + Python python; boost::filesystem::path home_path; boost::filesystem::path home_juci_path; diff --git a/src/juci.cc b/src/juci.cc index a0ad6773..952c82bf 100644 --- a/src/juci.cc +++ b/src/juci.cc @@ -8,6 +8,7 @@ #ifndef _WIN32 #include #endif +#include "python_interpreter.h" int Application::on_command_line(const Glib::RefPtr &cmd) { Glib::set_prgname("juci"); @@ -120,6 +121,7 @@ void Application::on_startup() { set_app_menu(Menu::get().juci_menu); set_menubar(Menu::get().window_menu); } + Python::Interpreter::get(); } Application::Application() : Gtk::Application("no.sout.juci", Gio::APPLICATION_NON_UNIQUE | Gio::APPLICATION_HANDLES_COMMAND_LINE) { diff --git a/src/menu.h b/src/menu.h index f1afec45..a2a59673 100644 --- a/src/menu.h +++ b/src/menu.h @@ -23,6 +23,7 @@ class Menu { std::unique_ptr right_click_line_menu; std::unique_ptr right_click_selected_menu; std::function toggle_menu_items = []{}; + Glib::RefPtr plugin_menu; private: Glib::RefPtr builder; }; diff --git a/src/python_interpreter.cc b/src/python_interpreter.cc new file mode 100644 index 00000000..d9aecd69 --- /dev/null +++ b/src/python_interpreter.cc @@ -0,0 +1,155 @@ +#include "python_interpreter.h" +#include "notebook.h" +#include "config.h" +#include +#include +#include "menu.h" +#include "directories.h" +#include "terminal.h" + +static wchar_t* DecodeLocale(const char* arg, size_t *size) +{ +#ifndef PY_VERSION_HEX +#error Python not included +#elif PY_VERSION_HEX < 0x03050000 + return _Py_char2wchar(arg, size); +#else + return Py_DecodeLocale(arg, size); +#endif +} + +inline pybind11::module pyobject_from_gobj(gpointer ptr){ + auto obj=G_OBJECT(ptr); + if(obj) + return pybind11::reinterpret_steal(pygobject_new(obj)); + return pybind11::reinterpret_steal(Py_None); +} + +Python::Interpreter::Interpreter(){ + + auto init_juci_api=[](){ + auto module = pybind11::reinterpret_steal(pygobject_init(-1,-1,-1)); + pybind11::module api("jucpp","Python bindings for juCi++"); + api + .def("get_juci_home",[](){return Config::get().home_juci_path.string();}) + .def("get_plugin_folder",[](){return Config::get().python.plugin_directory;}); + api + .def_submodule("editor") + .def("get_current_gtk_source_view",[](){ + auto view=Notebook::get().get_current_view(); + if(view) + return pyobject_from_gobj(view->gobj()); + return pybind11::reinterpret_steal(Py_None); + }) + .def("get_file_path",[](){ + auto view=Notebook::get().get_current_view(); + if(view) + return view->file_path.string(); + return std::string(); + }); + api + .def("get_gio_plugin_menu",[](){ + if(!Menu::get().plugin_menu){ + Menu::get().plugin_menu=Gio::Menu::create(); + Menu::get().plugin_menu->append(""); + Menu::get().window_menu->append_submenu("_Plugins",Menu::get().plugin_menu); + } + return pyobject_from_gobj(Menu::get().plugin_menu->gobj()); + }) + .def("get_gio_window_menu",[](){return pyobject_from_gobj(Menu::get().window_menu->gobj());}) + .def("get_gio_juci_menu",[](){return pyobject_from_gobj(Menu::get().juci_menu->gobj());}) + .def("get_gtk_notebook",[](){return pyobject_from_gobj(Notebook::get().gobj());}) + .def_submodule("terminal") + .def("get_gtk_text_view",[](){return pyobject_from_gobj(Terminal::get().gobj());}) + .def("println", [](const std::string &message){ Terminal::get().print(message +"\n"); }); + api.def_submodule("directories") + .def("get_gtk_treeview",[](){return pyobject_from_gobj(Directories::get().gobj());}) + .def("open",[](const std::string &directory){Directories::get().open(directory);}) + .def("update",[](){Directories::get().update();}); + return api.ptr(); + }; + PyImport_AppendInittab("jucipp", init_juci_api); + + Config::get().load(); + configure_path(); + Py_Initialize(); + #ifdef _WIN32 + long long unsigned size = 0L; + #else + long unsigned size = 0L; + #endif + argv=DecodeLocale("",&size); + PySys_SetArgv(0,&argv); + boost::filesystem::directory_iterator end_it; + for(boost::filesystem::directory_iterator it(Config::get().python.plugin_directory);it!=end_it;it++){ + auto module_name=it->path().stem().string(); + if(module_name.empty()) + continue; + auto is_directory=boost::filesystem::is_directory(it->path()); + auto has_py_extension=it->path().extension()==".py"; + auto is_pycache=module_name=="__pycache__"; + if((is_directory && !is_pycache)||has_py_extension){ + try { + pybind11::module::import(module_name.c_str()); + } catch (pybind11::error_already_set &error) { + Terminal::get().print("Error loading plugin `"+module_name+"`:\n"+error.what()+"\n"); + } + } + } + auto sys=find_module("sys"); + if(sys){ + auto exc_func=[](pybind11::object type,pybind11::object value,pybind11::object traceback){ + std::cerr << "ERROR FUNCTION"; + }; + sys.attr("excepthook")=pybind11::cpp_function(exc_func); + } else { + std::cerr << "Failed to set exception hook\n"; + } +} + +pybind11::module Python::Interpreter::find_module(const std::string &module_name){ + return pybind11::reinterpret_borrow(PyImport_AddModule(module_name.c_str())); +} + +pybind11::module Python::Interpreter::reload(pybind11::module &module){ + auto reload=pybind11::reinterpret_steal(PyImport_ReloadModule(module.ptr())); + if(!reload) + throw pybind11::error_already_set(); + return reload; +} + +void Python::Interpreter::configure_path(){ + const std::vector python_path = { + "/usr/lib/python3.6", + "/usr/lib/python3.6/lib-dynload", + "/usr/lib/python3.6/site-packages", + Config::get().python.site_packages, + Config::get().python.plugin_directory + }; + std::wstring sys_path; + for(auto &path:python_path){ + if(path.empty()) + continue; + if(!sys_path.empty()){ + #ifdef _WIN32 + sys_path += ';'; + #else + sys_path += ':'; + #endif + } + sys_path += path.generic_wstring(); + } + Py_SetPath(sys_path.c_str()); +} + +Python::Interpreter::~Interpreter(){ + if(Py_IsInitialized()) + Py_Finalize(); + if(error()) + std::cerr << pybind11::error_already_set().what() << std::endl; +} + +pybind11::object Python::Interpreter::error(){ + return pybind11::reinterpret_borrow(PyErr_Occurred()); +} + diff --git a/src/python_interpreter.h b/src/python_interpreter.h new file mode 100644 index 00000000..b30ef1ae --- /dev/null +++ b/src/python_interpreter.h @@ -0,0 +1,29 @@ +#ifndef JUCI_PYTHON_INTERPRETER_H_ +#define JUCI_PYTHON_INTERPRETER_H_ + +#include +#include + +#include +using namespace std; + +namespace Python { + class Interpreter { + public: + pybind11::module static find_module(const std::string &module_name); + pybind11::module static reload(pybind11::module &module); + pybind11::object static error(); + private: + Interpreter(); + ~Interpreter(); + wchar_t *argv; + void configure_path(); + public: + static Interpreter& get(){ + static Interpreter singleton; + return singleton; + } + }; +}; + +#endif // JUCI_PYTHON_INTERPRETER_H_ diff --git a/src/window.cc b/src/window.cc index 62a6dc83..dc7b2992 100644 --- a/src/window.cc +++ b/src/window.cc @@ -3,6 +3,7 @@ #include "menu.h" #include "notebook.h" #include "directories.h" +#include "python_interpreter.h" #include "dialogs.h" #include "filesystem.h" #include "project.h" @@ -342,6 +343,37 @@ void Window::set_menu_actions() { Notebook::get().configure(c); } } + const auto refresh_module = [](const std::string &stem){ + auto module = Python::Interpreter::find_module(stem); + if(module) { + try { + module = pybind11::reinterpret_steal(PyImport_ReloadModule(module.ptr())); + } catch (const pybind11::error_already_set &error) { + Terminal::get().print("Plugin `"+stem+"` didn't reload\n"+error.what()+"\n"); + } + } else { + { pybind11::error_already_set(); } + try { + module = pybind11::module::import(stem.c_str()); + } catch (const pybind11::error_already_set &error) { + Terminal::get().print("Plugin `"+stem+"` didn't reload\n"+error.what()+"\n"); + } + } + if(module) + Terminal::get().print("Plugin `"+stem+"` was reloaded\n"); + }; + if(view->file_path.extension().string()==".py"){ + auto file_path=view->file_path; + while(file_path.has_parent_path()){ + auto parent=file_path.parent_path(); + if(parent==Config::get().python.plugin_directory){ + auto stem=file_path.stem().string(); + refresh_module(stem); + break; + } + file_path=parent; + } + } } } }); diff --git a/tiny-process-library b/tiny-process-library index a0348126..0b989631 160000 --- a/tiny-process-library +++ b/tiny-process-library @@ -1 +1 @@ -Subproject commit a034812647a89e924c9114db9f2d4c89c70226d1 +Subproject commit 0b989631f11156aab1b7532345116f0ea5f2f51c