diff --git a/cmake/UraniumPluginInstall.cmake b/cmake/UraniumPluginInstall.cmake new file mode 100644 index 000000000..8ba93c60b --- /dev/null +++ b/cmake/UraniumPluginInstall.cmake @@ -0,0 +1,86 @@ +# Copyright (c) 2022 Ultimaker B.V. +# UraniumPluginInstall.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 "UM_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. +# + +# Options or configuration variables +set(UM_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 ${UM_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}/uranium/${_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 ${Python_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/UraniumTranslationTools.cmake b/cmake/UraniumTranslationTools.cmake new file mode 100644 index 000000000..91e65f5a7 --- /dev/null +++ b/cmake/UraniumTranslationTools.cmake @@ -0,0 +1,114 @@ +## 3rd party code: + +# Macro needed to list all sub-directory of a directory. +# There is no function in cmake as far as I know. +# Found at: http://stackoverflow.com/a/7788165 +MACRO(SUBDIRLIST result curdir) + FILE(GLOB children RELATIVE ${curdir} ${curdir}/*) + SET(dirlist "") + FOREACH(child ${children}) + IF(IS_DIRECTORY ${curdir}/${child}) + STRING(REPLACE "/" "" child ${child}) + LIST(APPEND dirlist ${child}) + ENDIF() + ENDFOREACH() + SET(${result} ${dirlist}) +ENDMACRO() + + +## Translation tools: + +SET(CURA_BINARY_DATA_DIRECTORY CACHE PATH "Directory to the cura-binary-data repository") + +if(NOT CURA_BINARY_DATA_DIRECTORY AND NOT DEFINED $ENV{CURA_BINARY_DATA_DIRECTORY}) + message(STATUS "Using CURA_BINARY_DATA_DIRECTORY from set of environment variables...") + SET(CURA_BINARY_DATA_DIRECTORY $ENV{CURA_BINARY_DATA_DIRECTORY}) +endif() + +# Dynamically creates targets for each language to create a *.po-file +MACRO(TARGETS_FOR_PO_FILES language) + if(DEFINED GETTEXT_MSGINIT_EXECUTABLE) + message(STATUS "Creating target i18n-create-po-${language}") + add_custom_target(i18n-create-po-${language}) + add_dependencies(i18n-create-po i18n-create-po-${language}) + endif() + message(STATUS "Creating target i18n-update-po-${language}") + add_custom_target(i18n-update-po-${language}) + add_dependencies(i18n-update-po i18n-update-po-${language}) + foreach(pot_file ${pot_files}) + string(REGEX REPLACE ".*/(.*).pot" "${CMAKE_SOURCE_DIR}/resources/i18n/${language}/\\1.po" po_file ${pot_file}) + if(DEFINED GETTEXT_MSGINIT_EXECUTABLE) + add_custom_command(TARGET i18n-create-po-${language} POST_BUILD + COMMAND ${GETTEXT_MSGINIT_EXECUTABLE} ARGS --no-wrap --no-translator -l ${language} -i ${pot_file} -o ${po_file}) + endif() + add_custom_command(TARGET i18n-update-po-${language} POST_BUILD + COMMAND ${GETTEXT_MSGMERGE_EXECUTABLE} ARGS --no-wrap --no-fuzzy-matching -o ${po_file} ${po_file} ${pot_file}) + endforeach() +ENDMACRO() + +# Dynamically creates targets for each language to create a *.mo-file +MACRO(TARGETS_FOR_MO_FILES language) + message(STATUS "Creating target i18n-create-mo-${language}") + add_custom_target(i18n-create-mo-${language}) + add_dependencies(i18n-create-mo i18n-create-mo-${language}) + if(TARGET i18n-copy-mo) + message(STATUS "Creating target i18n-copy-mo-${language}") + add_custom_target(i18n-copy-mo-${language}) + add_dependencies(i18n-copy-mo i18n-copy-mo-${language}) + endif() + file(GLOB po_files ${CMAKE_SOURCE_DIR}/resources/i18n/${language}/*.po) + foreach(po_file ${po_files}) + string(REGEX REPLACE ".*/(.*).po" "${CMAKE_BINARY_DIR}/resources/i18n/${language}/LC_MESSAGES/\\1.mo" mo_file ${po_file}) + add_custom_command(TARGET i18n-create-mo-${language} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/resources/i18n/${language}/LC_MESSAGES/ + COMMAND ${GETTEXT_MSGFMT_EXECUTABLE} ARGS ${po_file} -o ${mo_file} -f) + if(TARGET i18n-copy-mo-${language}) + string(REGEX REPLACE ".*/(.*).po" "${CURA_BINARY_DATA_DIRECTORY}/${PROJECT_NAME}/resources/i18n/${language}/LC_MESSAGES/\\1.mo" mo_file_binary_copy ${po_file}) + add_custom_command(TARGET i18n-copy-mo-${language} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${CURA_BINARY_DATA_DIRECTORY}/resources/i18n/${language}/LC_MESSAGES/ + COMMAND ${CMAKE_COMMAND} -E copy ${mo_file} ${mo_file_binary_copy}) + add_dependencies(i18n-copy-mo-${language} i18n-create-mo-${language}) + endif() + endforeach() +ENDMACRO() + +# Checks for availability of gettext and when found creates all targets +# TODO: Adding option to set the PROJECT_NAME externally! +MACRO(CREATE_TRANSLATION_TARGETS) +find_package(Gettext) + +if(GETTEXT_FOUND) + # translations target will convert .po files into .mo and .qm as needed. + # The files are checked for a _qt suffix and if it is found, converted to + # qm, otherwise they are converted to .po. + if(DEFINED GETTEXT_MSGINIT_EXECUTABLE) + message(STATUS "Creating target i18n-create-po") + add_custom_target(i18n-create-po) + else() + message(WARNING "GETTEXT_MSGINIT_EXECUTABLE is undefined!\nSkipping to create i18n-create-po* targets...") + endif() + if(CURA_BINARY_DATA_DIRECTORY) + if(EXISTS ${CURA_BINARY_DATA_DIRECTORY}) + message(STATUS "CURA_BINARY_DATA_DIRECTORY: ${CURA_BINARY_DATA_DIRECTORY}") + message(STATUS "Creating target i18n-copy-mo") + add_custom_target(i18n-copy-mo) + else() + message(WARNING "CURA_BINARY_DATA_DIRECTORY does not exist! (${CURA_BINARY_DATA_DIRECTORY})") + endif() + else() + message(WARNING "CURA_BINARY_DATA_DIRECTORY is not set!") + endif() + message(STATUS "Creating target i18n-update-po") + add_custom_target(i18n-update-po) + message(STATUS "Creating target i18n-create-mo") + add_custom_target(i18n-create-mo ALL) + + SUBDIRLIST(languages ${CMAKE_SOURCE_DIR}/resources/i18n/) + file(GLOB pot_files ${CMAKE_SOURCE_DIR}/resources/i18n/*.pot) + foreach(language ${languages}) + TARGETS_FOR_PO_FILES(${language}) + TARGETS_FOR_MO_FILES(${language}) + endforeach() + install(DIRECTORY ${CMAKE_BINARY_DIR}/resources DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/) +endif() +ENDMACRO() diff --git a/cmake/mod_bundled_packages_json.py b/cmake/mod_bundled_packages_json.py new file mode 100644 index 000000000..6423591f5 --- /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()