diff --git a/CMakeLists.txt b/CMakeLists.txt index b516de6b63..4954ac46dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,7 +49,7 @@ endif() if(NOT ${URANIUM_DIR} STREQUAL "") - set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake") + set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake") endif() if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "") list(APPEND CMAKE_MODULE_PATH ${URANIUM_DIR}/cmake) @@ -63,8 +63,8 @@ endif() install(DIRECTORY resources DESTINATION ${CMAKE_INSTALL_DATADIR}/cura) -install(DIRECTORY plugins - DESTINATION lib${LIB_SUFFIX}/cura) + +include(CuraPluginInstall) if(NOT APPLE AND NOT WIN32) install(FILES cura_app.py diff --git a/cmake/CuraPluginInstall.cmake b/cmake/CuraPluginInstall.cmake new file mode 100644 index 0000000000..d35e74acb8 --- /dev/null +++ b/cmake/CuraPluginInstall.cmake @@ -0,0 +1,99 @@ +# Copyright (c) 2019 Ultimaker B.V. +# CuraPluginInstall.cmake is released under the terms of the LGPLv3 or higher. + +# +# This module detects all plugins that need to be installed and adds them using the CMake install() command. +# It detects all plugin folder in the path "plugins/*" where there's a "plugin.json" in it. +# +# Plugins can be configured to NOT BE INSTALLED via the variable "CURA_NO_INSTALL_PLUGINS" as a list of string in the +# form of "a;b;c" or "a,b,c". By default all plugins will be installed. +# + +# FIXME: Remove the code for CMake <3.12 once we have switched over completely. +# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3 +# module is copied from the CMake repository here so in CMake <3.12 we can still use it. +if(${CMAKE_VERSION} VERSION_LESS 3.12) + # Use FindPythonInterp and FindPythonLibs for CMake <3.12 + find_package(PythonInterp 3 REQUIRED) + + set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE}) +else() + # Use FindPython3 for CMake >=3.12 + find_package(Python3 REQUIRED COMPONENTS Interpreter) +endif() + +# Options or configuration variables +set(CURA_NO_INSTALL_PLUGINS "" CACHE STRING "A list of plugins that should not be installed, separated with ';' or ','.") + +file(GLOB_RECURSE _plugin_json_list ${CMAKE_SOURCE_DIR}/plugins/*/plugin.json) +list(LENGTH _plugin_json_list _plugin_json_list_len) + +# Sort the lists alphabetically so we can handle cases like this: +# - plugins/my_plugin/plugin.json +# - plugins/my_plugin/my_module/plugin.json +# In this case, only "plugins/my_plugin" should be added via install(). +set(_no_install_plugin_list ${CURA_NO_INSTALL_PLUGINS}) +# Sanitize the string so the comparison will be case-insensitive. +string(STRIP "${_no_install_plugin_list}" _no_install_plugin_list) +string(TOLOWER "${_no_install_plugin_list}" _no_install_plugin_list) + +# WORKAROUND counterpart of what's in cura-build. +string(REPLACE "," ";" _no_install_plugin_list "${_no_install_plugin_list}") + +list(LENGTH _no_install_plugin_list _no_install_plugin_list_len) + +if(_no_install_plugin_list_len GREATER 0) + list(SORT _no_install_plugin_list) +endif() +if(_plugin_json_list_len GREATER 0) + list(SORT _plugin_json_list) +endif() + +# Check all plugin directories and add them via install() if needed. +set(_install_plugin_list "") +foreach(_plugin_json_path ${_plugin_json_list}) + get_filename_component(_plugin_dir ${_plugin_json_path} DIRECTORY) + file(RELATIVE_PATH _rel_plugin_dir ${CMAKE_CURRENT_SOURCE_DIR} ${_plugin_dir}) + get_filename_component(_plugin_dir_name ${_plugin_dir} NAME) + + # Make plugin name comparison case-insensitive + string(TOLOWER "${_plugin_dir_name}" _plugin_dir_name_lowercase) + + # Check if this plugin needs to be skipped for installation + set(_add_plugin ON) # Indicates if this plugin should be added to the build or not. + set(_is_no_install_plugin OFF) # If this plugin will not be added, this indicates if it's because the plugin is + # specified in the NO_INSTALL_PLUGINS list. + if(_no_install_plugin_list) + if("${_plugin_dir_name_lowercase}" IN_LIST _no_install_plugin_list) + set(_add_plugin OFF) + set(_is_no_install_plugin ON) + endif() + endif() + + # Make sure this is not a subdirectory in a plugin that's already in the install list + if(_add_plugin) + foreach(_known_install_plugin_dir ${_install_plugin_list}) + if(_plugin_dir MATCHES "${_known_install_plugin_dir}.+") + set(_add_plugin OFF) + break() + endif() + endforeach() + endif() + + if(_add_plugin) + message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}") + get_filename_component(_rel_plugin_parent_dir ${_rel_plugin_dir} DIRECTORY) + install(DIRECTORY ${_rel_plugin_dir} + DESTINATION lib${LIB_SUFFIX}/cura/${_rel_plugin_parent_dir} + PATTERN "__pycache__" EXCLUDE + PATTERN "*.qmlc" EXCLUDE + ) + list(APPEND _install_plugin_list ${_plugin_dir}) + elseif(_is_no_install_plugin) + message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}") + execute_process(COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mod_bundled_packages_json.py + -d ${CMAKE_CURRENT_SOURCE_DIR}/resources/bundled_packages + ${_plugin_dir_name} + RESULT_VARIABLE _mod_json_result) + endif() +endforeach() diff --git a/cmake/mod_bundled_packages_json.py b/cmake/mod_bundled_packages_json.py new file mode 100755 index 0000000000..6423591f57 --- /dev/null +++ b/cmake/mod_bundled_packages_json.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# This script removes the given package entries in the bundled_packages JSON files. This is used by the PluginInstall +# CMake module. +# + +import argparse +import collections +import json +import os +import sys + + +## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths. +# +# \param work_dir The directory to look for JSON files recursively. +# \return A list of JSON files in absolute paths that are found in the given directory. +def find_json_files(work_dir: str) -> list: + json_file_list = [] + for root, dir_names, file_names in os.walk(work_dir): + for file_name in file_names: + abs_path = os.path.abspath(os.path.join(root, file_name)) + json_file_list.append(abs_path) + return json_file_list + + +## Removes the given entries from the given JSON file. The file will modified in-place. +# +# \param file_path The JSON file to modify. +# \param entries A list of strings as entries to remove. +# \return None +def remove_entries_from_json_file(file_path: str, entries: list) -> None: + try: + with open(file_path, "r", encoding = "utf-8") as f: + package_dict = json.load(f, object_hook = collections.OrderedDict) + except Exception as e: + msg = "Failed to load '{file_path}' as a JSON file. This file will be ignored Exception: {e}"\ + .format(file_path = file_path, e = e) + sys.stderr.write(msg + os.linesep) + return + + for entry in entries: + if entry in package_dict: + del package_dict[entry] + print("[INFO] Remove entry [{entry}] from [{file_path}]".format(file_path = file_path, entry = entry)) + + try: + with open(file_path, "w", encoding = "utf-8", newline = "\n") as f: + json.dump(package_dict, f, indent = 4) + except Exception as e: + msg = "Failed to write '{file_path}' as a JSON file. Exception: {e}".format(file_path = file_path, e = e) + raise IOError(msg) + + +def main() -> None: + parser = argparse.ArgumentParser("mod_bundled_packages_json") + parser.add_argument("-d", "--dir", dest = "work_dir", + help = "The directory to look for bundled packages JSON files, recursively.") + parser.add_argument("entries", metavar = "ENTRIES", type = str, nargs = "+") + + args = parser.parse_args() + + json_file_list = find_json_files(args.work_dir) + for json_file_path in json_file_list: + remove_entries_from_json_file(json_file_path, args.entries) + + +if __name__ == "__main__": + main()