From 1e7736ccadedebbeeba3095543c1cb77cbb94d4b Mon Sep 17 00:00:00 2001 From: agsantos <aline.gondimsantos@savoirfairelinux.com> Date: Wed, 28 Oct 2020 14:39:13 -0400 Subject: [PATCH] SDK: Scripts to generate plugin's skeleton code done: - create plugin folder structure - manifest.json + icon.png - copyright header - main.cpp - choose number of functionalities and the api to each one of them - create src files for each functionality (APIs skeleton .h and .cpp) - create preferences.json - put colors into prints and clear outputs when needed - modify src files to set preferences code - create pakage.json - add helper files - use library Cmd - reorganize functions into classes define inherits stack - add GNU GPL to python files - jpl merge function - pre and post assembles - default options plugin build - windows build with build-plugin.py - add build option for windows build - generate base CMakeLists.txt and build.sh Change-Id: Id8eb5a97fa7a51e99a0f9215835c3d5ffea630ad GitLab: #2 --- .gitignore | 6 + GreenScreen/CMakeLists.txt | 14 +- GreenScreen/build.sh | 79 ++-- GreenScreen/main.cpp | 3 +- GreenScreen/manifest.json | 2 +- GreenScreen/package.json | 12 +- GreenScreen/pluginMediaHandler.cpp | 7 +- GreenScreen/pluginMediaHandler.h | 2 +- GreenScreen/pluginProcessor.cpp | 1 - GreenScreen/videoSubscriber.cpp | 2 +- README_ASSEMBLE.md | 2 +- SDK/Docs/buildHelper.txt | 30 ++ SDK/Docs/functionalityHelper.txt | 20 + SDK/Docs/jplAssembleHelper.txt | 26 ++ SDK/Docs/jplMergeHelper.txt | 15 + SDK/Docs/mainHelper.txt | 16 + SDK/Docs/manifestHelper.txt | 14 + SDK/Docs/packageHelper.txt | 29 ++ SDK/Docs/pipelineHelper.txt | 31 ++ SDK/Docs/preferenceHelper.txt | 44 ++ SDK/Docs/preferenceList.txt | 26 ++ SDK/Docs/preferencePath.txt | 20 + SDK/Templates/CMakeLists.txt | 81 ++++ SDK/Templates/build.sh | 205 ++++++++ SDK/Templates/copyright.txt | 19 + SDK/Templates/defaultDependenciesStrings.json | 10 + SDK/Templates/genericConversationHandler.h | 23 + SDK/Templates/genericMediaHandler.cpp | 84 ++++ SDK/Templates/genericMediaHandler.h | 37 ++ SDK/Templates/genericVideoSubscriber.cpp | 119 +++++ SDK/Templates/genericVideoSubscriber.h | 39 ++ SDK/Templates/icon.png | Bin 0 -> 21340 bytes SDK/Templates/main.cpp | 48 ++ SDK/Templates/manifest.json | 5 + SDK/Templates/package.json | 16 + SDK/Templates/preferences.json | 23 + SDK/generateProject.py | 76 +++ SDK/jplManipulation.py | 239 ++++++++++ SDK/manifestProfile.py | 104 +++++ SDK/pluginMainSDK.py | 200 ++++++++ SDK/pluginStructureProfile.py | 124 +++++ SDK/preferencesProfile.py | 251 ++++++++++ SDK/requirements.txt | 2 + SDK/sdkConstants.py | 62 +++ SDK/skeletonSrcProfile.py | 438 ++++++++++++++++++ SDK/userProfile.py | 65 +++ SDK/utils.py | 46 ++ assemble-plugin.py | 49 -- build-plugin.py | 65 ++- docker/Dockerfile_android | 84 ++-- lib/accel.cpp | 67 +++ lib/accel.h | 39 +- lib/framescaler.h | 6 +- 53 files changed, 2824 insertions(+), 203 deletions(-) create mode 100644 SDK/Docs/buildHelper.txt create mode 100644 SDK/Docs/functionalityHelper.txt create mode 100644 SDK/Docs/jplAssembleHelper.txt create mode 100644 SDK/Docs/jplMergeHelper.txt create mode 100644 SDK/Docs/mainHelper.txt create mode 100644 SDK/Docs/manifestHelper.txt create mode 100644 SDK/Docs/packageHelper.txt create mode 100644 SDK/Docs/pipelineHelper.txt create mode 100644 SDK/Docs/preferenceHelper.txt create mode 100644 SDK/Docs/preferenceList.txt create mode 100644 SDK/Docs/preferencePath.txt create mode 100644 SDK/Templates/CMakeLists.txt create mode 100644 SDK/Templates/build.sh create mode 100644 SDK/Templates/copyright.txt create mode 100644 SDK/Templates/defaultDependenciesStrings.json create mode 100644 SDK/Templates/genericConversationHandler.h create mode 100644 SDK/Templates/genericMediaHandler.cpp create mode 100644 SDK/Templates/genericMediaHandler.h create mode 100644 SDK/Templates/genericVideoSubscriber.cpp create mode 100644 SDK/Templates/genericVideoSubscriber.h create mode 100644 SDK/Templates/icon.png create mode 100644 SDK/Templates/main.cpp create mode 100644 SDK/Templates/manifest.json create mode 100644 SDK/Templates/package.json create mode 100644 SDK/Templates/preferences.json create mode 100644 SDK/generateProject.py create mode 100644 SDK/jplManipulation.py create mode 100644 SDK/manifestProfile.py create mode 100644 SDK/pluginMainSDK.py create mode 100644 SDK/pluginStructureProfile.py create mode 100644 SDK/preferencesProfile.py create mode 100644 SDK/requirements.txt create mode 100644 SDK/sdkConstants.py create mode 100644 SDK/skeletonSrcProfile.py create mode 100644 SDK/userProfile.py create mode 100644 SDK/utils.py delete mode 100644 assemble-plugin.py create mode 100644 lib/accel.cpp diff --git a/.gitignore b/.gitignore index 022c659..548a5c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ /build/ +*.jpl *msvc* +*build-local* *android-toolchain-* config.mak *Libs* +*__pycache__* +/foo/ +/.vscode/ +/HelloWorld/ diff --git a/GreenScreen/CMakeLists.txt b/GreenScreen/CMakeLists.txt index 61f9d40..9254bd9 100644 --- a/GreenScreen/CMakeLists.txt +++ b/GreenScreen/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.10) # set the project name set (ProjectName GreenScreen) -set (Version 1.0) +set (Version 1.0.1) project(${ProjectName} VERSION ${Version}) @@ -77,6 +77,7 @@ set(plugin_SRC main.cpp pluginProcessor.cpp TFInference.cpp videoSubscriber.cpp + ./../lib/accel.cpp ) set(plugin_HDR pluginInference.h @@ -142,14 +143,11 @@ endif() add_custom_command( TARGET ${ProjectName} PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E remove_directory -r ${JPL_DIRECTORY} - COMMAND ${CMAKE_COMMAND} -E remove_directory -r ${JPL_DIRECTORY}/../../../build/${ProjectName} - COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/data ${JPL_DIRECTORY}/data + COMMAND python ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --preassemble --plugin=GreenScreen COMMAND ${CMAKE_COMMAND} -E copy_directory ${LIBS_BIN_DIR}/${TENSORFLOW}/lib/ ${JPL_DIRECTORY}/lib COMMAND ${CMAKE_COMMAND} -E make_directory ${JPL_DIRECTORY}/data/models COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/modelsSRC/${model} ${JPL_DIRECTORY}/data/models COMMAND ${CMAKE_COMMAND} -E rename ${JPL_DIRECTORY}/data/models/${model} ${JPL_DIRECTORY}/data/models/mModel${modelType} - COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/manifest.json ${JPL_DIRECTORY} COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/${preferencesFile} ${JPL_DIRECTORY}/data COMMAND ${CMAKE_COMMAND} -E rename ${JPL_DIRECTORY}/data/${preferencesFile} ${JPL_DIRECTORY}/data/preferences.json COMMENT "Assembling Plugin files" @@ -162,7 +160,7 @@ if (WIN32) COMMAND ${CMAKE_COMMAND} -E make_directory ${JPL_DIRECTORY}/../../../build/x64-windows/${TENSORFLOW} COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/Release/${ProjectName}.lib ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM} COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/Release/${LIBRARY_FILE_NAME} ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM} - COMMAND python ${PROJECT_SOURCE_DIR}/../assemble-plugin.py --plugins=GreenScreen --extraPath=${TENSORFLOW} + COMMAND python ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --assemble --plugin=GreenScreen --extraPath=${TENSORFLOW} COMMENT "Generating JPL archive" ) else() @@ -170,8 +168,8 @@ else() TARGET ${ProjectName} POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${JPL_DIRECTORY}/../../../build/x86_64-linux-gnu/${TENSORFLOW} - COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/Release/${LIBRARY_FILE_NAME} ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM} - COMMAND python ${PROJECT_SOURCE_DIR}/../assemble-plugin.py --plugins=GreenScreen --extraPath=${TENSORFLOW} + COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/${LIBRARY_FILE_NAME} ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM} + COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --assemble --plugin=GreenScreen --extraPath=${TENSORFLOW} COMMENT "Generating JPL archive" ) diff --git a/GreenScreen/build.sh b/GreenScreen/build.sh index ad60269..b046871 100755 --- a/GreenScreen/build.sh +++ b/GreenScreen/build.sh @@ -2,7 +2,7 @@ # Build the plugin for the project export OSTYPE ARCH=$(arch) - +EXTRAPATH='' # Flags: # -p: number of processors to use @@ -59,30 +59,25 @@ if [ -z "${TF}" ]; then TF="_tensorflow_cc" fi echo "Building with ${TF}" -mkdir ./data/models - if [[ "${TF}" = "_tensorflow_cc" ]] && [[ "${PLATFORM}" = "linux-gnu" ]] then if [ -z "$CUDALIBS" ]; then - rm -r ./data/models echo "CUDALIBS must point to CUDA 10.1!" exit fi if [ -z "$CUDNN" ]; then - rm -r ./data/models echo "CUDNN must point to libcudnn.so 7!" exit fi echo "Building for ${PROCESSOR}" + python3 ./../SDK/jplManipulation.py --preassemble --plugin=${PLUGIN_NAME} + CONTRIB_PLATFORM_CURT=${ARCH} CONTRIB_PLATFORM=${CONTRIB_PLATFORM_CURT}-${PLATFORM} - DESTINATION_PATH="./../build/${CONTRIB_PLATFORM}/${TF}" - mkdir -p "lib/${CONTRIB_PLATFORM}" - mkdir -p "${DESTINATION_PATH}" - + EXTRAPATH=${TF} # Compile clang++ -std=c++17 -shared -fPIC \ @@ -99,6 +94,7 @@ then -I"${LIBS_DIR}/${TF}/include" \ -I"${LIBS_DIR}/${TF}/include/third_party/eigen3" \ -I"${PLUGINS_LIB}" \ + ./../lib/accel.cpp \ main.cpp \ videoSubscriber.cpp \ pluginProcessor.cpp \ @@ -117,28 +113,30 @@ then -llibpng \ -lva \ -ltensorflow_cc \ - -o "lib/${CONTRIB_PLATFORM}/${SO_FILE_NAME}" - - cp "${TF_LIBS_DIR}/${TF}/lib/${CONTRIB_PLATFORM}-gpu61/libtensorflow_cc.so" "lib/$CONTRIB_PLATFORM/libtensorflow_cc.so.2" - cp "${CUDALIBS}/libcudart.so" "lib/$CONTRIB_PLATFORM/libcudart.so.10.0" - cp "${CUDNN}/libcublas.so.10" "lib/$CONTRIB_PLATFORM/libcublas.so.10.0" - cp "${CUDALIBS}/libcufft.so.10" "lib/$CONTRIB_PLATFORM/libcufft.so.10.0" - cp "${CUDALIBS}/libcurand.so.10" "lib/$CONTRIB_PLATFORM/libcurand.so.10.0" - cp "${CUDALIBS}/libcusolver.so.10" "lib/$CONTRIB_PLATFORM/libcusolver.so.10.0" - cp "${CUDALIBS}/libcusparse.so.10" "lib/$CONTRIB_PLATFORM/libcusparse.so.10.0" - cp "${CUDNN}/libcudnn.so.7" "lib/$CONTRIB_PLATFORM" - - cp ./modelsSRC/mModel-resnet50float.pb ./data/models/mModel.pb - cp ./preferences-tfcc.json ./data/preferences.json + -o "build-local/jpl/lib/${CONTRIB_PLATFORM}/${SO_FILE_NAME}" + + cp "${TF_LIBS_DIR}/${TF}/lib/${CONTRIB_PLATFORM}-gpu61/libtensorflow_cc.so" "build-local/jpl/lib/$CONTRIB_PLATFORM/libtensorflow_cc.so.2" + cp "${CUDALIBS}/libcudart.so" "build-local/jpl/lib/$CONTRIB_PLATFORM/libcudart.so.10.0" + cp "${CUDNN}/libcublas.so.10" "build-local/jpl/lib/$CONTRIB_PLATFORM/libcublas.so.10.0" + cp "${CUDALIBS}/libcufft.so.10" "build-local/jpl/lib/$CONTRIB_PLATFORM/libcufft.so.10.0" + cp "${CUDALIBS}/libcurand.so.10" "build-local/jpl/lib/$CONTRIB_PLATFORM/libcurand.so.10.0" + cp "${CUDALIBS}/libcusolver.so.10" "build-local/jpl/lib/$CONTRIB_PLATFORM/libcusolver.so.10.0" + cp "${CUDALIBS}/libcusparse.so.10" "build-local/jpl/lib/$CONTRIB_PLATFORM/libcusparse.so.10.0" + cp "${CUDNN}/libcudnn.so.7" "build-local/jpl/lib/$CONTRIB_PLATFORM" + + pwd + mkdir ./build-local/jpl/data/models + cp ./modelsSRC/mModel-resnet50float.pb ./build-local/jpl/data/models/mModel.pb + cp ./preferences-tfcc.json ./build-local/jpl/data/preferences.json elif [ "${TF}" = "_tensorflowLite" ] then if [ "${PLATFORM}" = "linux-gnu" ] then + python3 ./../SDK/jplManipulation.py --preassemble --plugin=${PLUGIN_NAME} + CONTRIB_PLATFORM_CURT=${ARCH} CONTRIB_PLATFORM=${CONTRIB_PLATFORM_CURT}-${PLATFORM} - DESTINATION_PATH="./../build/${CONTRIB_PLATFORM}/${TF}" - mkdir -p "lib/${CONTRIB_PLATFORM}" - mkdir -p "${DESTINATION_PATH}" + EXTRAPATH="${TF}" # Compile clang++ -std=c++17 -shared -fPIC \ @@ -155,6 +153,7 @@ then -I"${LIBS_DIR}/${TF}/include/flatbuffers" \ -I"${LIBS_DIR}/${TF}/include" \ -I"${PLUGINS_LIB}" \ + ./../lib/accel.cpp \ main.cpp \ videoSubscriber.cpp \ pluginProcessor.cpp \ @@ -173,14 +172,13 @@ then -ltensorflowlite \ -llibpng \ -lva \ - -o "lib/${CONTRIB_PLATFORM}/${SO_FILE_NAME}" + -o "build-local/jpl/lib/${CONTRIB_PLATFORM}/${SO_FILE_NAME}" - cp "${TF_LIBS_DIR}/${TF}/lib/${CONTRIB_PLATFORM}/libtensorflowlite.so" "lib/$CONTRIB_PLATFORM" + cp "${TF_LIBS_DIR}/${TF}/lib/${CONTRIB_PLATFORM}/libtensorflowlite.so" "build-local/jpl/lib/$CONTRIB_PLATFORM" elif [ "${PLATFORM}" = "android" ] then - DESTINATION_PATH="./../build/android" - mkdir -p "${DESTINATION_PATH}" + python3 ./../SDK/jplManipulation.py --preassemble --plugin=${PLUGIN_NAME} --distribution=${PLATFORM} if [ -z "$ANDROID_NDK" ]; then ANDROID_NDK="/home/${USER}/Android/Sdk/ndk/21.1.6352462" @@ -198,7 +196,6 @@ then buildlib() { echo "$CURRENT_ABI" - mkdir -p "lib/$CURRENT_ABI" #========================================================= # ANDROID TOOLS @@ -241,8 +238,6 @@ then else echo "ABI NOT OK" >&2 - rm -r lib/ - rm -r ./data/models exit 1 fi @@ -304,6 +299,7 @@ then -I"${LIBS_DIR}/${TF}/include/flatbuffers" \ -I"${LIBS_DIR}/${TF}/include" \ -I"${PLUGINS_LIB}" \ + ./../lib/accel.cpp \ main.cpp \ videoSubscriber.cpp \ pluginProcessor.cpp \ @@ -323,9 +319,9 @@ then -ltensorflowlite \ -llog -lz \ --sysroot=$ANDROID_SYSROOT \ - -o "lib/$CURRENT_ABI/${SO_FILE_NAME}" + -o "build-local/jpl/lib/$CURRENT_ABI/${SO_FILE_NAME}" - cp "${TF_LIBS_DIR}/${TF}/lib/${CURRENT_ABI}/libtensorflowlite.so" "lib/$CURRENT_ABI" + cp "${TF_LIBS_DIR}/${TF}/lib/${CURRENT_ABI}/libtensorflowlite.so" "build-local/jpl/lib/$CURRENT_ABI" rm cpu-features.o } @@ -336,16 +332,9 @@ then done fi - cp ./modelsSRC/mobilenet_v2_deeplab_v3_256_myquant.tflite ./data/models/mModel.tflite - cp ./preferences-tflite.json ./data/preferences.json + mkdir ./build-local/jpl/data/models + cp ./modelsSRC/mobilenet_v2_deeplab_v3_256_myquant.tflite ./build-local/jpl/data/models/mModel.tflite + cp ./preferences-tflite.json ./build-local/jpl/data/preferences.json fi -zip -r ${JPL_FILE_NAME} data manifest.json lib -mv ${JPL_FILE_NAME} ${DESTINATION_PATH}/ - -# Cleanup -# Remove lib after compilation -rm -rf lib -rm -r ./data/models -rm ./data/preferences.json - +python3 ./../SDK/jplManipulation.py --assemble --plugin=${PLUGIN_NAME} --distribution=${PLATFORM} --extraPath=${EXTRAPATH} diff --git a/GreenScreen/main.cpp b/GreenScreen/main.cpp index b14e289..cdf3602 100644 --- a/GreenScreen/main.cpp +++ b/GreenScreen/main.cpp @@ -33,6 +33,7 @@ #define GreenScreen_VERSION_MAJOR 1 #define GreenScreen_VERSION_MINOR 0 +#define GreenScreen_VERSION_PATCH 1 extern "C" { void @@ -46,7 +47,7 @@ JAMI_dynPluginInit(const JAMI_PluginAPI* api) std::cout << "** GREENSCREEN PLUGIN **" << std::endl; std::cout << "**************************" << std::endl << std::endl; std::cout << " Version " << GreenScreen_VERSION_MAJOR << "." << GreenScreen_VERSION_MINOR - << std::endl; + << "." << GreenScreen_VERSION_PATCH << std::endl; // If invokeService doesn't return an error if (api) { diff --git a/GreenScreen/manifest.json b/GreenScreen/manifest.json index 49d8a79..0b7d078 100644 --- a/GreenScreen/manifest.json +++ b/GreenScreen/manifest.json @@ -1,5 +1,5 @@ { "name": "GreenScreen", "description" : "GreenScreen Plugin with Tensorflow 2.1.1", - "version" : "1.0" + "version" : "1.0.1" } diff --git a/GreenScreen/package.json b/GreenScreen/package.json index b5f0c31..295f2eb 100644 --- a/GreenScreen/package.json +++ b/GreenScreen/package.json @@ -1,20 +1,22 @@ { "name": "GreenScreen", - "version": "1.0", + "version": "1.0.1", "extractLibs": true, "deps": [ "ffmpeg", - "opencv"], + "opencv" + ], "defines": [ "TFLITE=False", - "CPU=False"], + "CPU=False" + ], "custom_scripts": { "pre_build": [ "mkdir msvc" ], "build": [ "cmake --build ./msvc --config Release" - ], + ], "post_build": [] } -} +} \ No newline at end of file diff --git a/GreenScreen/pluginMediaHandler.cpp b/GreenScreen/pluginMediaHandler.cpp index a9e4fe0..9aae38b 100644 --- a/GreenScreen/pluginMediaHandler.cpp +++ b/GreenScreen/pluginMediaHandler.cpp @@ -67,12 +67,7 @@ PluginMediaHandler::notifyAVFrameSubject(const StreamData& data, jami::avSubject std::map<std::string, std::string> PluginMediaHandler::getCallMediaHandlerDetails() { - std::map<std::string, std::string> mediaHandlerDetails = {}; - mediaHandlerDetails["name"] = NAME; - mediaHandlerDetails["iconPath"] = datapath_ + sep + "icon.png"; - mediaHandlerDetails["pluginId"] = id(); - - return mediaHandlerDetails; + return {{"name", NAME}, {"iconPath", datapath_ + sep + "icon.png"}, {"pluginId", id()}}; } void diff --git a/GreenScreen/pluginMediaHandler.h b/GreenScreen/pluginMediaHandler.h index 4748609..0e1d011 100644 --- a/GreenScreen/pluginMediaHandler.h +++ b/GreenScreen/pluginMediaHandler.h @@ -47,7 +47,7 @@ public: std::shared_ptr<VideoSubscriber> mVS; - std::string dataPath() const { return datapath_; } + const std::string& dataPath() const { return datapath_; } private: const std::string datapath_; diff --git a/GreenScreen/pluginProcessor.cpp b/GreenScreen/pluginProcessor.cpp index 6815f08..3b38830 100644 --- a/GreenScreen/pluginProcessor.cpp +++ b/GreenScreen/pluginProcessor.cpp @@ -168,7 +168,6 @@ copyByLine(uchar* frameData, uchar* applyMaskData, const int lineSize, cv::Size { if (3 * size.width == lineSize) { std::memcpy(frameData, applyMaskData, size.height * size.width * 3); - ; } else { int rows = size.height; int offset = 0; diff --git a/GreenScreen/videoSubscriber.cpp b/GreenScreen/videoSubscriber.cpp index dc14a40..b7ec336 100644 --- a/GreenScreen/videoSubscriber.cpp +++ b/GreenScreen/videoSubscriber.cpp @@ -22,9 +22,9 @@ #include "videoSubscriber.h" // Use for display rotation matrix extern "C" { -#include <accel.h> #include <libavutil/display.h> } +#include <accel.h> // Opencv processing #include <opencv2/imgcodecs.hpp> diff --git a/README_ASSEMBLE.md b/README_ASSEMBLE.md index 04bf28e..39b590f 100644 --- a/README_ASSEMBLE.md +++ b/README_ASSEMBLE.md @@ -43,7 +43,7 @@ Dependencies: $ git clone https://github.com/tensorflow/tensorflow.git $ cd tensorflow -$ git checkout -b v2.1.0 +$ git checkout v2.1.0 For Android: (Tensorflow Lite) diff --git a/SDK/Docs/buildHelper.txt b/SDK/Docs/buildHelper.txt new file mode 100644 index 0000000..4121eb6 --- /dev/null +++ b/SDK/Docs/buildHelper.txt @@ -0,0 +1,30 @@ + +To compile (or create build files) with default configurations: BUILD (-def) + +This option allows the default plugin compilation or the creation of the +CMakeLists.txt and build.sh files. + +As compilation tool: + This option does not support cross-compilation neither optional build. + If you want to compile for another platform, or if you have build variables + that may vary, please directly use the <jami-plugins>/build-plugin.py script. + + -> For GNU/Linux host, remember to set all environment variables + before calling the build script. + -> For Windows host, remember to properly set cmake configuration + options in plugin's package.json. + +As build files creation tool: + This option creates base files used for plugins build. For GNU/Linux and Android + compilations, the current plugin environment expects a build.sh file capable of + pre-assembling, building and assembling the final jpl. For Windows, the environment + expects a CMakeLists.txt and package.json with custom build rules for cmake. + + For pratical examples and further developpment of those files, you may peek at our + available plugins at https://git.jami.net/savoirfairelinux/jami-plugins . + +Alternatively, you can ignore this SDK and build your plugin using the build-plugin.py +script. + +For more technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . diff --git a/SDK/Docs/functionalityHelper.txt b/SDK/Docs/functionalityHelper.txt new file mode 100644 index 0000000..b238fdc --- /dev/null +++ b/SDK/Docs/functionalityHelper.txt @@ -0,0 +1,20 @@ + +Create a functionality's skeleton files: FUNCTIONALITY + +A functionality is a capability and a single Jami Plugin can implement +multiple functionalities. + +Each functionality may be applied to different data types that requires +diferent APIs. Currently, our plugin system supports the following data: +(1) video during a call (Media Handler API). + + +Be aware that when creating functionalities outside a full plugin pipeline +creation, some modifications to match the preferences usablities may have +to be done by hand. If you don't know how to do it, consider creating a +functionality from inside a full plugin creation pipeline and then you can +use it as an example. Alternatively, you can peek at our available plugins +at https://git.jami.net/savoirfairelinux/jami-plugins . + +For more technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . diff --git a/SDK/Docs/jplAssembleHelper.txt b/SDK/Docs/jplAssembleHelper.txt new file mode 100644 index 0000000..0dd95c6 --- /dev/null +++ b/SDK/Docs/jplAssembleHelper.txt @@ -0,0 +1,26 @@ + +To assemble a jpl: ASSEMBLE (-pre) + +The `assemble -pre` option gives the user the oportunity to create a folder +as bellow: + - linux host: <jami-plugins>/<PLUGINNAME>/build-local/jpl/ + - windows host: <jami-plugins>/<PLUGINNAME>/msvc/jpl/ +containing all data that will be inside `PLUGINNAME.jpl` archive. +The exception is the plugin library itself that only will be added after +compilation. + +The default usage will take all files inside the folder created by the `-pre` +variation and compress them to your PLUGINNAME.jpl archive. This file can be +find under <jami-plugins>/build/. + +Both process must be called from inside the CMakeLists.txt as POST_BUILD and +PRE_BUILD commands, respectively. Also, the build.sh script must call them. +Our default CMakeLists.txt and build.sh script already applies them. Also, you +can check other usage examples from our available plugins at +https://git.jami.net/savoirfairelinux/jami-plugins . + +For more information about our default CMakeLists.txt and build.sh, please refer to: + -> help build + +For more technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . diff --git a/SDK/Docs/jplMergeHelper.txt b/SDK/Docs/jplMergeHelper.txt new file mode 100644 index 0000000..d315a3f --- /dev/null +++ b/SDK/Docs/jplMergeHelper.txt @@ -0,0 +1,15 @@ + +To merge two different jpl: MERGE foo.jpl bar.jpl + +Be aware tha merging two or more jpls may incur orverwriting some of the files +inside your plugins archives if they are not equal for all plugins. The only files +that may not present conflicting contents are the ones that do not repeate themselves. +If conflicts occur, files from the first jpl, in our case the foo.jpl, will prevail +over the others. + +This tool is useful when you build a plugin for different platforms and want to compile +a single PLUGINNAME.jpl archive to be publish and used along cross platforms, ie: same jpl +can be used for Windows, GNU/Linux, and Android. + +For more technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . diff --git a/SDK/Docs/mainHelper.txt b/SDK/Docs/mainHelper.txt new file mode 100644 index 0000000..836001a --- /dev/null +++ b/SDK/Docs/mainHelper.txt @@ -0,0 +1,16 @@ + +Create a main.cpp skeleton files: MAIN + +This option creates plugin's 'main.cpp' file. If this file already exists, +it will be overwritten. +The main file implement plugin's external loading function that Jami plugin +system will call to initialize and register all functionalities for latter +use. + +Since every functionality must be covered by the initialization function, +every time a new functionality is added to the plugin, it also must be added +to the initialization function. For that reason, this SDK is set to rewrite +the main.cpp file every time you create a new functionality. + +For more technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . diff --git a/SDK/Docs/manifestHelper.txt b/SDK/Docs/manifestHelper.txt new file mode 100644 index 0000000..0c36a2f --- /dev/null +++ b/SDK/Docs/manifestHelper.txt @@ -0,0 +1,14 @@ + +Create/Modify (or Delete) a manifest file: MANIFEST (-del) + +A manifest.json is a json file containing the plugin name, desciption and version. + +manifest.json skeleton: +{ + "name": "foo", -> plugin name + "description: "This plugins does this and that", -> plugin functionalities description + "version": "0.0.0" -> plugin version, must be of the form X.Y.Z +} + +For more technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . diff --git a/SDK/Docs/packageHelper.txt b/SDK/Docs/packageHelper.txt new file mode 100644 index 0000000..60422f7 --- /dev/null +++ b/SDK/Docs/packageHelper.txt @@ -0,0 +1,29 @@ + +Create a package.json: PACKAGE + +A package.json is a json file containing important build information needed for the +windows build pipeline used by the daemon project. + +package.json skeleton: +{ + "name": "PLUGINNAME", -> plugin name; + "version": "X.Y.Z", -> plugin version, must be of the form X.Y.Z; + "extractLibs": false, -> use tensorflow header files inside + contrib/libs.tar.gz; + "deps": [], -> dependencies that can be build by ring-daemon + project. ie: ffmpeg and opencv; + "defines": [], -> cmake definitions to configure plugin project; + "custom_scripts": { + "pre_build": [ + "mkdir msvc" -> creates directory to generate and build plugin + project; + ], + "build": [ + "cmake --build ./msvc --config Release" -> cmake build command. + ], + "post_build": [] + } +} + +For more technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . diff --git a/SDK/Docs/pipelineHelper.txt b/SDK/Docs/pipelineHelper.txt new file mode 100644 index 0000000..bbe3f26 --- /dev/null +++ b/SDK/Docs/pipelineHelper.txt @@ -0,0 +1,31 @@ + +Create a plugin skeleton: PLUGIN + +A plugin creation will pass through several steps: + 1 - authorship information; + 2 - name; + 3 - manifest; + 4 - main file; + 5 - functionalities; + 6 - preferences; + 7 - package; + 8 - build related files, + +And will output a basic plugin capable of being build, installed, loaded and used! +This basic plugin, although will perform no modification to the data to which it +is linked to. The data process implementation is on your hands! + +Feel free to create awesome plugins! But respect these constraints: +- use ffmpeg and opencv from ring-daemon project. +- if using tensorflow, choose version 2.1.0. Why? + -> We have all needed header files of tensorflow in <jami-plugins>/contrib/libs.tar.gz; + -> We provide docker images with libraries for Tensorflow C++ API and Tensorflow + Lite, for both Android and Linux development. +- if you need other libraries, check if we support it's build with ring-daemon project, + otherwise, you will have to build it and ensure correctly link to the plugin libraries. + +For detailed information about manifest, main file, functionalities, preferences, package, +and build related files please refer to the specific help command. + +For DockerHub links and more technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . diff --git a/SDK/Docs/preferenceHelper.txt b/SDK/Docs/preferenceHelper.txt new file mode 100644 index 0000000..078c1a5 --- /dev/null +++ b/SDK/Docs/preferenceHelper.txt @@ -0,0 +1,44 @@ + +Create (or Delete) a preference: PREFERENCE (-del) + +A preference is a internal variable that can be used upon loading or while +running the plugin. It should be correctly formed and listed in a preferences.json +file. Also, a preference value only can be modified during runtime if the code that +applies the new value is within your functionality implementation. For more +technical information, please refer to: +https://git.jami.net/savoirfairelinux/ring-project/wikis/technical/7.-Jami-plugins . + +Prior continuing, and to easy the process of constructing your preferences, take your +time to think about the list of preferences you will be needing. +But do not worry! If you forget something you will have the oportunity to change it +later. + +Example of preferences: + -> LIST: + { + "category" : "Streams", + "type": "List", + "key": "videostreams", + "title": "Streams to transform", + "summary": "Select video to transform", + "defaultValue": "0", + "entries": ["sent", "received"], + "entryValues": ["0", "1"], + "scope": "plugin" + } + + -> PATH: + { + "category" : "Backgrounds", + "type": "Path", + "mimeType": "image/png", + "key": "background", + "title": "Background image", + "summary": "Select the image background to use", + "defaultValue": "data/backgrounds/background2.png", + "scope": "plugin, foo" + } + +For more detailed information for each of the preferences types, please specify the preference type: + -> preference -h list + -> preference -h path diff --git a/SDK/Docs/preferenceList.txt b/SDK/Docs/preferenceList.txt new file mode 100644 index 0000000..253e64e --- /dev/null +++ b/SDK/Docs/preferenceList.txt @@ -0,0 +1,26 @@ + +Create (or Delete) a preference: PREFERENCE (-del) + +List preference formation example: +{ + "category" : "Streams", -> user choice; + "type": "List", -> fixed as List; + "key": "videostreams", -> user choice, must not repeate other key inside + same preferences.json file; + "title": "Streams to transform", -> user choice; + "summary": "Select video to transform", -> user choice; + "defaultValue": "0", -> must be a value listed in 'entryValues'; + "entries": ["sent", "received"], -> names to the possible values listed in 'entryValues', + must have the same size as 'entryValues'; + "entryValues": ["0", "1"], -> list of possible values this preference can take; + "scope": "plugin" -> if a preference value may be changed while the + functionality is being used, this functionality name + must be listed in the scope. +} + +A List preference must have a list of possible values to be used. +and a list of 'names' for these values. For example: + If you have two values: '0' and '1', these values may not be understandable + by the user. + The names 'sent' and 'received' set at 'entries' will be shown in the UI and + are more intuitive! diff --git a/SDK/Docs/preferencePath.txt b/SDK/Docs/preferencePath.txt new file mode 100644 index 0000000..f9f78c4 --- /dev/null +++ b/SDK/Docs/preferencePath.txt @@ -0,0 +1,20 @@ + +Create (or Delete) a preference: PREFERENCE (-del) + +Path preference formation example: +{ + "category" : "Backgrounds", -> user choice; + "type": "Path", -> fixed as Path; + "mimeType": "image/png", -> user choice; + "key": "background", -> user choice, cannot repeate other keys inside + same preferences.json file; + "title": "Background image", -> user choice; + "summary": "Select the image background to use", -> user choice; + "defaultValue": "data/backgrounds/background2.png", -> must be a relative path within the plugin + developpment directory. More specifically any + file introduced by the developper must be inside + <jami-plugins>/<PLUGINNAME>/data directory; + "scope": "plugin, foo" -> if a preference value may be changed while the + functionality is being used, this functionality + name must be listed in the scope; +} diff --git a/SDK/Templates/CMakeLists.txt b/SDK/Templates/CMakeLists.txt new file mode 100644 index 0000000..882dafc --- /dev/null +++ b/SDK/Templates/CMakeLists.txt @@ -0,0 +1,81 @@ +cmake_minimum_required(VERSION 3.10) + +# set the project name +set (ProjectName PLUGINNAME) +set (Version MANIFESTVERSION) + +project(${ProjectName} VERSION ${Version}) + +set (DAEMON ${PROJECT_SOURCE_DIR}/../../daemon) +set (JPL_FILE_NAME ${ProjectName}.jpl) +set (DAEMON_SRC ${DAEMON}/src) +set (CONTRIB_PATH ${DAEMON}/contrib) +set (PLUGINS_LIB ${PROJECT_SOURCE_DIR}/../lib) +set (JPL_DIRECTORY ${PROJECT_BINARY_DIR}/jpl) +set (LIBS_DIR ${PROJECT_SOURCE_DIR}/../contrib/Libs) + +if(WIN32) + message(OS:\ WINDOWS\ ${CMAKE_SYSTEM_PROCESSOR}) + if (NOT ${CMAKE_CL_64}) + message( FATAL_ERROR "\nUse CMake only for x64 Windows" ) + endif() + set (CONTRIB_PLATFORM_CURT x64) + set (CONTRIB_PLATFORM ${CONTRIB_PLATFORM_CURT}-windows) + set (LIBRARY_FILE_NAME ${ProjectName}.dll) + ---set (FFMPEG ${CONTRIB_PATH}/build/ffmpeg/Build/win32/x64) +---else() + message( FATAL_ERROR "\nUse CMake only for Windows! For linux or Android (linux host), use our bash scripts." ) +endif() + +message(Building:\ ${ProjectName}\ ${Version}) +message(Build\ path:\ ${PROJECT_BINARY_DIR}) +message(JPL\ assembling\ path:\ ${JPL_DIRECTORY}) +message(JPL\ path:\ ${JPL_DIRECTORY}/../../../build/${ProjectName}/${JPL_FILE_NAME}) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd") + +set(plugin_SRC ---CPPFILENAME + ---../lib/accel.cppFFMPEGCPP + ---) + +set(plugin_HDR ---HFILENAME + ---../lib/accel.hFFMPEGH + ../lib/framescaler.h + ---../lib/pluglog.h + ) + +add_library(${ProjectName} SHARED ${plugin_SRC} + ${plugin_HDR} + ) + +target_include_directories(${ProjectName} PUBLIC ${PROJECT_BINARY_DIR} + ${PROJECT_SOURCE_DIR} + ${PLUGINS_LIB} + ${DAEMON_SRC} + ${CONTRIB_PATH} + ---${FFMPEG}/include--- + ) +target_link_directories(${ProjectName} PUBLIC ${CONTRIB_PATH} + ---${FFMPEG}/bin--- + ) + +target_link_libraries(${ProjectName} PUBLIC ---swscale avutil---) + +add_custom_command( + TARGET ${ProjectName} + PRE_BUILD + COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --preassemble --plugin=${ProjectName} + COMMENT "Assembling Plugin files" +) + +add_custom_command( + TARGET ${ProjectName} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/Release/${ProjectName}.lib ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM} + COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_BINARY_DIR}/Release/${LIBRARY_FILE_NAME} ${JPL_DIRECTORY}/lib/${CONTRIB_PLATFORM} + COMMAND python3 ${PROJECT_SOURCE_DIR}/../SDK/jplManipulation.py --assemble --plugin=${ProjectName} + COMMENT "Generating JPL archive" +) diff --git a/SDK/Templates/build.sh b/SDK/Templates/build.sh new file mode 100644 index 0000000..2452de4 --- /dev/null +++ b/SDK/Templates/build.sh @@ -0,0 +1,205 @@ +#! /bin/bash +# Build the plugin for the project +export OSTYPE +ARCH=$(arch) +EXTRAPATH='' +# Flags: + +# -p: number of processors to use +# -c: Runtime plugin cpu/gpu setting. +# -t: target platform. + + +if [ -z "${DAEMON}" ]; then + DAEMON="./../../daemon" + echo "DAEMON not provided, building with ${DAEMON}" +fi + +PLUGIN_NAME="PLUGINNAME" +JPL_FILE_NAME="${PLUGIN_NAME}.jpl" +SO_FILE_NAME="lib${PLUGIN_NAME}.so" +DAEMON_SRC="${DAEMON}/src" +CONTRIB_PATH="${DAEMON}/contrib" +PLUGINS_LIB="../lib" +LIBS_DIR="./../contrib/Libs" + +if [ -z "${PLATFORM}" ]; then + PLATFORM="linux-gnu" + echo "PLATFORM not provided, building with ${PLATFORM}" + echo "Other options: redhat-linux" +fi + +while getopts t:c:p OPT; do + case "$OPT" in + t) + PLATFORM="${OPTARG}" + ;; + c) + PROCESSOR="${OPTARG}" + ;; + p) + ;; + \?) + exit 1 + ;; + esac +done + +if [ "${PLATFORM}" = "linux-gnu" ] || [ "${PLATFORM}" = "redhat-linux" ] +then + python3 ./../SDK/jplManipulation.py --preassemble --plugin=${PLUGIN_NAME} + + CONTRIB_PLATFORM_CURT=${ARCH} + CONTRIB_PLATFORM=${CONTRIB_PLATFORM_CURT}-${PLATFORM} + + # Compile + clang++ -std=c++17 -shared -fPIC \ + -Wl,-Bsymbolic,-rpath,"\${ORIGIN}" \ + -Wall -Wextra \ + -Wno-unused-variable \ + -Wno-unused-function \ + -Wno-unused-parameter \ + -I"." \ + -I"${DAEMON_SRC}" \ + -I"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/include" \ + -I"${PLUGINS_LIB}" \ + ---FFMPEGCPP./../lib/accel.cpp \ + ---CPPFILENAME \ + ----L"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/lib/" \ + ----l:libswscale.a \ + -l:libavutil.a \ + -lva \ + ----o "build-local/jpl/lib/${CONTRIB_PLATFORM_CURT}-linux-gnu/${SO_FILE_NAME}" + +elif [ "${PLATFORM}" = "android" ] +then + python3 ./../SDK/jplManipulation.py --preassemble --plugin=${PLUGIN_NAME} --distribution=${PLATFORM} + + if [ -z "$ANDROID_NDK" ]; then + ANDROID_NDK="/home/${USER}/Android/Sdk/ndk/21.1.6352462" + echo "ANDROID_NDK not provided, building with ${ANDROID_NDK}" + fi + + #========================================================= + # Check if the ANDROID_ABI was provided + # if not, set default + #========================================================= + if [ -z "$ANDROID_ABI" ]; then + ANDROID_ABI="armeabi-v7a arm64-v8a x86_64" + echo "ANDROID_ABI not provided, building for ${ANDROID_ABI}" + fi + + buildlib() { + echo "$CURRENT_ABI" + + #========================================================= + # ANDROID TOOLS + #========================================================= + export HOST_TAG=linux-x86_64 + export TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG + + if [ "$CURRENT_ABI" = armeabi-v7a ] + then + export AR=$TOOLCHAIN/bin/arm-linux-android-ar + export AS=$TOOLCHAIN/bin/arm-linux-android-as + export CC=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang + export CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang++ + export LD=$TOOLCHAIN/bin/arm-linux-android-ld + export RANLIB=$TOOLCHAIN/bin/arm-linux-android-ranlib + export STRIP=$TOOLCHAIN/bin/arm-linux-androideabi-strip + export ANDROID_SYSROOT=${DAEMON}/../client-android/android-toolchain-21-arm/sysroot + + elif [ "$CURRENT_ABI" = arm64-v8a ] + then + export AR=$TOOLCHAIN/bin/aarch64-linux-android-ar + export AS=$TOOLCHAIN/bin/aarch64-linux-android-as + export CC=$TOOLCHAIN/bin/aarch64-linux-android21-clang + export CXX=$TOOLCHAIN/bin/aarch64-linux-android21-clang++ + export LD=$TOOLCHAIN/bin/aarch64-linux-android-ld + export RANLIB=$TOOLCHAIN/bin/aarch64-linux-android-ranlib + export STRIP=$TOOLCHAIN/bin/aarch64-linux-android-strip + export ANDROID_SYSROOT=${DAEMON}/../client-android/android-toolchain-21-arm64/sysroot + + elif [ "$CURRENT_ABI" = x86_64 ] + then + export AR=$TOOLCHAIN/bin/x86_64-linux-android-ar + export AS=$TOOLCHAIN/bin/x86_64-linux-android-as + export CC=$TOOLCHAIN/bin/x86_64-linux-android21-clang + export CXX=$TOOLCHAIN/bin/x86_64-linux-android21-clang++ + export LD=$TOOLCHAIN/bin/x86_64-linux-android-ld + export RANLIB=$TOOLCHAIN/bin/x86_64-linux-android-ranlib + export STRIP=$TOOLCHAIN/bin/x86_64-linux-android-strip + export ANDROID_SYSROOT=${DAEMON}/../client-android/android-toolchain-21-x86_64/sysroot + + else + echo "ABI NOT OK" >&2 + exit 1 + fi + + #========================================================= + # CONTRIBS + #========================================================= + if [ "$CURRENT_ABI" = armeabi-v7a ] + then + CONTRIB_PLATFORM=arm-linux-androideabi + + elif [ "$CURRENT_ABI" = arm64-v8a ] + then + CONTRIB_PLATFORM=aarch64-linux-android + + elif [ "$CURRENT_ABI" = x86_64 ] + then + CONTRIB_PLATFORM=x86_64-linux-android + fi + + #NDK SOURCES FOR cpufeatures + NDK_SOURCES=${ANDROID_NDK}/sources/android + + #========================================================= + # LD_FLAGS + #========================================================= + if [ "$CURRENT_ABI" = armeabi-v7a ] + then + export EXTRA_LDFLAGS="${EXTRA_LDFLAGS} -L${ANDROID_SYSROOT}/usr/lib/arm-linux-androideabi -L${ANDROID_SYSROOT}/usr/lib/arm-linux-androideabi/21" + elif [ "$CURRENT_ABI" = arm64-v8a ] + then + export EXTRA_LDFLAGS="${EXTRA_LDFLAGS} -L${ANDROID_SYSROOT}/usr/lib/aarch64-linux-android -L${ANDROID_SYSROOT}/usr/lib/aarch64-linux-android/21" + elif [ "$CURRENT_ABI" = x86_64 ] + then + export EXTRA_LDFLAGS="${EXTRA_LDFLAGS} -L${ANDROID_SYSROOT}/usr/lib/x86_64-linux-android -L${ANDROID_SYSROOT}/usr/lib/x86_64-linux-android/21" + fi + + #========================================================= + # Compile the plugin + #========================================================= + + # Create so destination folder + $CXX --std=c++14 -O3 -g -fPIC \ + -Wl,-Bsymbolic,-rpath,"\${ORIGIN}" \ + -shared \ + -Wall -Wextra \ + -Wno-unused-variable \ + -Wno-unused-function \ + -Wno-unused-parameter \ + -I"." \ + -I"${DAEMON_SRC}" \ + -I"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/include" \ + -I"${PLUGINS_LIB}" \ + ---FFMPEGCPP./../lib/accel.cpp \ + ---CPPFILENAME \ + ----L"${CONTRIB_PATH}/${CONTRIB_PLATFORM}/lib/" \ + ----lswscale \ + -lavutil \ + ----llog -lz \ + --sysroot=$ANDROID_SYSROOT \ + -o "build-local/jpl/lib/$CURRENT_ABI/${SO_FILE_NAME}" + } + + # Build the so + for i in ${ANDROID_ABI}; do + CURRENT_ABI=$i + buildlib + done +fi + +python3 ./../SDK/jplManipulation.py --assemble --plugin=${PLUGIN_NAME} --distribution=${PLATFORM} --extraPath=${EXTRAPATH} diff --git a/SDK/Templates/copyright.txt b/SDK/Templates/copyright.txt new file mode 100644 index 0000000..cf00a19 --- /dev/null +++ b/SDK/Templates/copyright.txt @@ -0,0 +1,19 @@ +/** + * YEAR + * + * Author: AUTHORNAME <AUTHORMAIL> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ \ No newline at end of file diff --git a/SDK/Templates/defaultDependenciesStrings.json b/SDK/Templates/defaultDependenciesStrings.json new file mode 100644 index 0000000..3488cb8 --- /dev/null +++ b/SDK/Templates/defaultDependenciesStrings.json @@ -0,0 +1,10 @@ +{ + "package.json": + { + "MediaHandler": { + "deps": [ + "ffmpeg" + ] + } + } +} \ No newline at end of file diff --git a/SDK/Templates/genericConversationHandler.h b/SDK/Templates/genericConversationHandler.h new file mode 100644 index 0000000..1906ce6 --- /dev/null +++ b/SDK/Templates/genericConversationHandler.h @@ -0,0 +1,23 @@ +HEADER + +#pragma once +//Project +#include "chatsubscriber.h" +//Jami plugin +#include "plugin/jamiplugin.h" +#include "plugin/conversationhandler.h" + +class GENERICConversationHandler : public jami::ConversationHandler +{ +public: + GENERICConversationHandler(const JAMI_PluginAPI * api, std::string &&dataPath); + ~GENERICConversationHandler(); + void detach(); + virtual void notifyStrMapSubject(const bool direction, + jami::strMapSubjectPtr subject) override; + const std::string& dataPath() const { return dataPath_; } + +private: + std::string dataPath_; + std::shared_ptr<ChatSubscriber> css; +}; diff --git a/SDK/Templates/genericMediaHandler.cpp b/SDK/Templates/genericMediaHandler.cpp new file mode 100644 index 0000000..f3bbb39 --- /dev/null +++ b/SDK/Templates/genericMediaHandler.cpp @@ -0,0 +1,84 @@ +HEADER + +#include "GENERICMediaHandler.h" +// Logger +#include "pluglog.h" +const char sep = separator(); +const std::string TAG = "GENERIC"; + +#define NAME "GENERIC" + +namespace jami { + +GENERICMediaHandler::GENERICMediaHandler(std::map<std::string, std::string>&& ppm, + std::string&& datapath) + : datapath_ {datapath} + , ppm_ {ppm} +{ + setId(datapath_); + mVS = std::make_shared<GENERICVideoSubscriber>(datapath_); +} + +void +GENERICMediaHandler::notifyAVFrameSubject(const StreamData& data, jami::avSubjectPtr subject) +{ + Plog::log(Plog::LogPriority::INFO, TAG, "IN AVFRAMESUBJECT"); + std::ostringstream oss; + std::string direction = data.direction ? "Receive" : "Preview"; + oss << "NEW SUBJECT: [" << data.id << "," << direction << "]" << std::endl; + + bool preferredStreamDirection = false; // false for output; true for input + oss << "preferredStreamDirection " << preferredStreamDirection << std::endl; + if (data.type == StreamType::video && !data.direction + && data.direction == preferredStreamDirection) { + subject->attach(mVS.get()); // your image + oss << "got my sent image attached" << std::endl; + } else if (data.type == StreamType::video && data.direction + && data.direction == preferredStreamDirection) { + subject->attach(mVS.get()); // the image you receive from others on the call + oss << "got received image attached" << std::endl; + } + + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +std::map<std::string, std::string> +GENERICMediaHandler::getCallMediaHandlerDetails() +{ + return {{"name", NAME}, {"iconPath", datapath_ + sep + "icon.png"}, {"pluginId", id()}}; +} + +void +GENERICMediaHandler::setPreferenceAttribute(const std::string& key, const std::string& value) +{ + auto it = ppm_.find(key); + if (it != ppm_.end() && it->second != value) { + it->second = value;---------------- + if (key == "PREFERENCE1") { + // use preference + return; + }---------------- + } +} + +bool +GENERICMediaHandler::preferenceMapHasKey(const std::string& key) +{---------------- + if (key == "PREFERENCE2") { return true; }---------------- + return false; +} + +void +GENERICMediaHandler::detach() +{ + mVS->detach(); +} + +GENERICMediaHandler::~GENERICMediaHandler() +{ + std::ostringstream oss; + oss << " ~GENERICMediaHandler from PLUGINNAME Plugin" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); + detach(); +} +} // namespace jami diff --git a/SDK/Templates/genericMediaHandler.h b/SDK/Templates/genericMediaHandler.h new file mode 100644 index 0000000..6b3a70d --- /dev/null +++ b/SDK/Templates/genericMediaHandler.h @@ -0,0 +1,37 @@ +HEADER + +#pragma once + +// Project +#include "GENERICVideoSubscriber.h" + +// Plugin +#include "plugin/jamiplugin.h" +#include "plugin/mediahandler.h" + +using avSubjectPtr = std::weak_ptr<jami::Observable<AVFrame*>>; + +namespace jami { + +class GENERICMediaHandler : public jami::CallMediaHandler +{ +public: + GENERICMediaHandler(std::map<std::string, std::string>&& ppm, std::string&& dataPath); + ~GENERICMediaHandler(); + + virtual void notifyAVFrameSubject(const StreamData& data, avSubjectPtr subject) override; + virtual std::map<std::string, std::string> getCallMediaHandlerDetails() override; + + virtual void detach() override; + virtual void setPreferenceAttribute(const std::string& key, const std::string& value) override; + virtual bool preferenceMapHasKey(const std::string& key) override; + + std::shared_ptr<GENERICVideoSubscriber> mVS; + + const std::string& dataPath() const { return datapath_; } + +private: + const std::string datapath_; + std::map<std::string, std::string> ppm_; +}; +} // namespace jami diff --git a/SDK/Templates/genericVideoSubscriber.cpp b/SDK/Templates/genericVideoSubscriber.cpp new file mode 100644 index 0000000..13398cb --- /dev/null +++ b/SDK/Templates/genericVideoSubscriber.cpp @@ -0,0 +1,119 @@ +HEADER + +#include "GENERICVideoSubscriber.h" + +extern "C" { +#include <libavutil/display.h> +} +#include <accel.h> + +// LOGGING +#include <pluglog.h> + +const std::string TAG = "GENERIC"; +const char sep = separator(); + +namespace jami { + +GENERICVideoSubscriber::GENERICVideoSubscriber(const std::string& dataPath) + : path_ {dataPath} +{} + +GENERICVideoSubscriber::~GENERICVideoSubscriber() +{ + std::ostringstream oss; + oss << "~GENERICMediaProcessor" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +void +GENERICVideoSubscriber::update(jami::Observable<AVFrame*>*, AVFrame* const& iFrame) +{ + if (!iFrame) + return; + AVFrame* pluginFrame = const_cast<AVFrame*>(iFrame); + + //====================================================================================== + // GET FRAME ROTATION + AVFrameSideData* side_data = av_frame_get_side_data(iFrame, AV_FRAME_DATA_DISPLAYMATRIX); + + int angle {0}; + if (side_data) { + auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data); + angle = static_cast<int>(av_display_rotation_get(matrix_rotation)); + } + + //====================================================================================== + // GET RAW FRAME + // Use a non-const Frame + // Convert input frame to RGB + int inputHeight = pluginFrame->height; + int inputWidth = pluginFrame->width; + FrameUniquePtr bgrFrame = scaler.convertFormat(transferToMainMemory(pluginFrame, + AV_PIX_FMT_NV12), + AV_PIX_FMT_RGB24); + + // transferToMainMemory USED TO COPY FRAME TO MAIN MEMORY IF HW ACCEL IS ENABLED + // NOT NEEDED TO BE USED IF ALL YOUR PROCESS WILL BE DONE WITHIN A + // HW ACCEL PLATFORM + + if (firstRun) { + // IMPLEMENT CODE TO CONFIGURE + // VARIABLES UPON FIRST RUN + firstRun = false; + } + + // IMPLEMENT PROCESS + + //====================================================================================== + // REPLACE AVFRAME DATA WITH FRAME DATA + if (bgrFrame && bgrFrame->data[0]) { + uint8_t* frameData = bgrFrame->data[0]; + if (angle == 90 || angle == -90) { + std::memmove(frameData, + frameData, // PUT HERE YOUR PROCESSED FRAME VARIABLE DATA! + static_cast<size_t>(pluginFrame->width * pluginFrame->height * 3) + * sizeof(uint8_t)); + } + } + // Copy Frame meta data + if (bgrFrame && pluginFrame) { + av_frame_copy_props(bgrFrame.get(), pluginFrame); + scaler.moveFrom(pluginFrame, bgrFrame.get()); + } + + // Remove the pointer + pluginFrame = nullptr; +} + +void +GENERICVideoSubscriber::attached(jami::Observable<AVFrame*>* observable) +{ + std::ostringstream oss; + oss << "::Attached ! " << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); + observable_ = observable; +} + +void +GENERICVideoSubscriber::detached(jami::Observable<AVFrame*>*) +{ + firstRun = true; + observable_ = nullptr; + std::ostringstream oss; + oss << "::Detached()" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); +} + +void +GENERICVideoSubscriber::detach() +{ + if (observable_) { + firstRun = true; + std::ostringstream oss; + oss << "::Calling detach()" << std::endl; + Plog::log(Plog::LogPriority::INFO, TAG, oss.str()); + observable_->detach(this); + } +} +} // namespace jami diff --git a/SDK/Templates/genericVideoSubscriber.h b/SDK/Templates/genericVideoSubscriber.h new file mode 100644 index 0000000..eaa6649 --- /dev/null +++ b/SDK/Templates/genericVideoSubscriber.h @@ -0,0 +1,39 @@ +HEADER + +#pragma once + +// AvFrame +extern "C" { +#include <libavutil/frame.h> +} +#include <observer.h> + +// Frame Scaler +#include <framescaler.h> + +namespace jami { + +class GENERICVideoSubscriber : public jami::Observer<AVFrame*> +{ +public: + GENERICVideoSubscriber(const std::string& dataPath); + ~GENERICVideoSubscriber(); + + virtual void update(jami::Observable<AVFrame*>*, AVFrame* const&) override; + virtual void attached(jami::Observable<AVFrame*>*) override; + virtual void detached(jami::Observable<AVFrame*>*) override; + + void detach(); + +private: + // Observer pattern + Observable<AVFrame*>* observable_ = nullptr; + + // Data + std::string path_; + FrameScaler scaler; + + // Status variables of the processing + bool firstRun {true}; +}; +} // namespace jami diff --git a/SDK/Templates/icon.png b/SDK/Templates/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2ce0aab9f71679f71f7b3d53d8448f7e9852b322 GIT binary patch literal 21340 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 literal 0 HcmV?d00001 diff --git a/SDK/Templates/main.cpp b/SDK/Templates/main.cpp new file mode 100644 index 0000000..e97416a --- /dev/null +++ b/SDK/Templates/main.cpp @@ -0,0 +1,48 @@ +HEADER +#include <iostream> +#include <string.h> +#include <thread> +#include <memory> +#include <plugin/jamiplugin.h> +---------------- +#include "INCLUDESAPI.h"---------------- + +#ifdef WIN32 +#define EXPORT_PLUGIN __declspec(dllexport) +#else +#define EXPORT_PLUGIN +#endif +#define PLUGINNAME_VERSION_MAJOR PLUGINVERSIONMAJOR +#define PLUGINNAME_VERSION_MINOR PLUGINVERSIONMINOR +#define PLUGINNAME_VERSION_PATCH PLUGINVERSIONPATCH +extern "C" { + +void +pluginExit(void) +{} + +EXPORT_PLUGIN JAMI_PluginExitFunc +JAMI_dynPluginInit(const JAMI_PluginAPI* api) +{ + std::cout << "**************************" << std::endl << std::endl; + std::cout << "** PLUGINNAME **" << std::endl; + std::cout << "**************************" << std::endl << std::endl; + std::cout << " Version " << PLUGINNAME_VERSION_MAJOR << "." << PLUGINNAME_VERSION_MINOR << "." + << PLUGINNAME_VERSION_PATCH << std::endl; + + // If invokeService doesn't return an error + if (api) { + std::map<std::string, std::string> ppm; + api->invokeService(api, "getPluginPreferences", &ppm); + std::string dataPath; + api->invokeService(api, "getPluginDataPath", &dataPath); +---------------- + auto fmpPLUGINAPI = std::make_unique<jami::PLUGINAPI>(std::move(ppm), std::move(dataPath)); + if (api->manageComponent(api, "APIMANAGER", fmpPLUGINAPI.release())) { + return nullptr; + } +---------------- + } + return pluginExit; +} +} diff --git a/SDK/Templates/manifest.json b/SDK/Templates/manifest.json new file mode 100644 index 0000000..d5c8df6 --- /dev/null +++ b/SDK/Templates/manifest.json @@ -0,0 +1,5 @@ +{ + "name": "pluginName", + "description": "plugin description", + "version": "version" +} \ No newline at end of file diff --git a/SDK/Templates/package.json b/SDK/Templates/package.json new file mode 100644 index 0000000..0bb6d39 --- /dev/null +++ b/SDK/Templates/package.json @@ -0,0 +1,16 @@ +{ + "name": "", + "version": "", + "extractLibs": false, + "deps": [], + "defines": [], + "custom_scripts": { + "pre_build": [ + "mkdir msvc" + ], + "build": [ + "cmake --build ./msvc --config Release" + ], + "post_build": [] + } +} diff --git a/SDK/Templates/preferences.json b/SDK/Templates/preferences.json new file mode 100644 index 0000000..4caacdc --- /dev/null +++ b/SDK/Templates/preferences.json @@ -0,0 +1,23 @@ +[ + { + "category" : "genericList", + "type": "List", + "key": "keyName", + "title": "preference title", + "summary": "preference summary", + "defaultValue": "default value", + "scope": "plugin, Name of API implementation", + "entryValues": ["List of", "variables", "true", "values"], + "entries": ["List of", "UI", "variables", "names"] + }, + { + "category" : "genericPath", + "type": "Path", + "key": "keyName", + "title": "preference title", + "summary": "preference summary", + "defaultValue": "default values", + "scope": "plugin, Name of API implementation", + "mimeType": "*/*" + } +] diff --git a/SDK/generateProject.py b/SDK/generateProject.py new file mode 100644 index 0000000..588f62d --- /dev/null +++ b/SDK/generateProject.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +from utils import * + + +def runoption(action): + if(initializeClasses(action)): + if (action == ACTIONS["CREATE"]): + runFullCreation() + elif (action == ACTIONS["CREATE_FUNCTIONALITY"]): + modifyFunctionality(action) + elif (action == ACTIONS["CREATE_MAIN"]): + getSkeletonSourceFiles().createMain(addAllowed='n') + elif (action == ACTIONS["CREATE_PREFERENCES"] or action == ACTIONS["DELETE_PREFERENCES"]): + modifyPreferences(action) + elif (action == ACTIONS["CREATE/MODIFY_MANIFEST"] or action == ACTIONS["DELETE_MANIFEST"]): + getSkeletonSourceFiles().modifyManifest(action) + elif (action == ACTIONS["CREATE_PACKAGE_JSON"]): + getSkeletonSourceFiles().createPackageJson() + elif (action == ACTIONS["DELETE"]): + delete() + elif (action == ACTIONS["PREASSEMBLE"]): + from jplManipulation import preAssemble + preAssemble(getSkeletonSourceFiles().pluginName) + elif (action == ACTIONS["BUILD"]): + from jplManipulation import build + build(getSkeletonSourceFiles().pluginName) + elif (action == ACTIONS["ASSEMBLE"]): + from jplManipulation import assemble + assemble(getSkeletonSourceFiles().pluginName) + elif (action == ACTIONS["CREATE_BUILD_FILES"]): + getSkeletonSourceFiles().createBuildFiles() + + +def delete(): + getSkeletonSourceFiles().delete() + + +def modifyPreferences(action): + if (action == ACTIONS["DELETE_PREFERENCES"]): + getSkeletonSourceFiles().deletePreference() + elif (action == ACTIONS["CREATE_PREFERENCES"]): + getSkeletonSourceFiles().createPreferences(getSkeletonSourceFiles().names) + + +def runFullCreation(): + getSkeletonSourceFiles().createManifest() + getSkeletonSourceFiles().createMain() + getSkeletonSourceFiles().createPackageJson() + getSkeletonSourceFiles().createBuildFiles() + + +def modifyFunctionality(action): + if (action == ACTIONS["CREATE_FUNCTIONALITY"]): + getSkeletonSourceFiles().createFunctionality() diff --git a/SDK/jplManipulation.py b/SDK/jplManipulation.py new file mode 100644 index 0000000..558760a --- /dev/null +++ b/SDK/jplManipulation.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +import os +import sys +import shutil +import argparse +import platform +import subprocess +from zipfile import ZipFile + +from sdkConstants import OS_IDS + + +def getSystem(): + system = platform.system().lower() + if system == "linux" or system == "linux2": + return OS_IDS["Linux"] + elif system == "darwin": + sys.exit("Plugins not supported on MacOS and IOS") + elif system == "windows": + return OS_IDS["Windows"] + sys.exit("Plugins SDK not supported on this system") + + +def getJpls(): + filePaths = input("\nInput jpls path you want to merge:\n") + filePaths = filePaths.replace(' ', ',').replace(",,", ',') + filePaths = filePaths.split(',') + if (len(filePaths) > 0): + print("\nJpl files to merge:") + for filePath in filePaths: + print(filePath) + return filePaths + + +def checkValidityJpls(filePaths): + for filePath in filePaths: + if (not os.path.exists(filePath) or not filePath.endswith(".jpl")): + return False + return True + + +class JPLStructure: + def __init__(self, paths): + self.paths = paths + self.outputName = '' + self.pluginNames = [] + self.ZipObjcts = {} + self.getOutName() + self.discoverJPLs() + self.mergeJPLs() + + def getOutName(self): + while (not self.outputName or not self.outputName.endswith(".jpl")): + self.outputName = input("\nWhere save the resulting JPL? ") + self.OutObj = ZipFile(self.outputName, 'w') + + def discoverJPLs(self): + for path in self.paths: + name = os.path.split(path)[0].split('.')[0] + self.pluginNames.append(name) + self.ZipObjcts[path] = [] + self.ZipObjcts[path].append(ZipFile(path, 'r')) + return + + def mergeJPLs(self): + self.fileNames = [] + for path in self.paths: + [obj] = self.ZipObjcts[path] + for item in obj.filelist: + if item.filename not in self.fileNames: + self.OutObj.writestr(item, obj.open(item).read()) + self.fileNames.append(item.filename) + self.OutObj.close() + + +def onerror(func, path, exc_info): + """ + Error handler for ``shutil.rmtree``. + + If the error is due to an access error (read only file) + it attempts to add write permission and then retries. + + If the error is for another reason it re-raises the error. + + Usage : ``shutil.rmtree(path, onerror=onerror)`` + """ + import stat + if not os.access(path, os.W_OK): + # Is the error an access error ? + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + + +def preAssemble(pluginName, distribution=''): + localSystem = getSystem() + + osBuildPath = "build-local" + if localSystem == OS_IDS["Linux"]: + if (distribution != 'android'): + distribution = "x86_64-linux-gnu" + elif localSystem == OS_IDS["Darwin"]: + sys.exit("Plugins not supported on MacOS and IOS") + elif localSystem == OS_IDS["Windows"]: + distribution = "x64-windows" + osBuildPath = "msvc" + + chDir = False + if (osBuildPath in os.getcwd()): + chDir = True + os.chdir("./../") + if (os.path.exists(f"./../{pluginName}/{osBuildPath}/")): + if (os.path.exists(f"./../{pluginName}/{osBuildPath}/jpl")): + shutil.rmtree(f"./../{pluginName}/{osBuildPath}/jpl", onerror=onerror) + else: + os.mkdir(f"./../{pluginName}/{osBuildPath}") + os.mkdir(f"./../{pluginName}/{osBuildPath}/jpl") + os.mkdir(f"./../{pluginName}/{osBuildPath}/jpl/lib") + if (distribution != 'android'): + os.mkdir(f"./../{pluginName}/{osBuildPath}/jpl/lib/{distribution}") + else: + if ("ANDROID_ABI" in os.environ.keys()): + for abi in os.environ["ANDROID_ABI"].split(' '): + os.mkdir(f"./../{pluginName}/{osBuildPath}/jpl/lib/{abi}") + + shutil.copytree(f"./../{pluginName}/data/", + f"./../{pluginName}/{osBuildPath}/jpl/data/") + shutil.copyfile(f"./../{pluginName}/manifest.json", + f"./../{pluginName}/{osBuildPath}/jpl/manifest.json") + if (os.path.exists(f"./../{pluginName}/data/preferences.json")): + shutil.copyfile( + f"./../{pluginName}/data/preferences.json", + f"./../{pluginName}/{osBuildPath}/jpl/data/preferences.json") + if (chDir): + os.chdir(f"./{osBuildPath}") + + +def assemble(pluginName, extraPath='', distribution=''): + extraPath = '/' + extraPath + localSystem = getSystem() + root = os.path.dirname(os.path.abspath(__file__)) + "/.." + osBuildPath = "build-local" + + if localSystem == OS_IDS["Linux"]: + if (distribution != 'android'): + distribution = "x86_64-linux-gnu" + elif localSystem == OS_IDS["Darwin"]: + sys.exit("Plugins not supported on MacOS and IOS") + elif localSystem == OS_IDS["Windows"]: + distribution = "x64-windows" + osBuildPath = 'msvc' + if (not os.path.exists(f"{root}/build")): + os.mkdir(f"{root}/build") + if (not os.path.exists(f"{root}/build/{distribution}")): + os.mkdir(f"{root}/build/{distribution}") + if (not os.path.exists(f"{root}/build/{distribution}{extraPath}")): + os.mkdir(f"{root}/build/{distribution}{extraPath}") + if (os.path.exists(f"./../build/{pluginName}.jpl")): + os.remove(f"./../build/{pluginName}.jpl") + + outputJPL = f"{root}/build/{distribution}{extraPath}/{pluginName}.jpl" + outputBuild = f"{root}/{pluginName}/{osBuildPath}/jpl" + + with ZipFile(outputJPL, 'w') as zipObj: + for folderName, subfolders, filenames in os.walk(outputBuild): + for filename in filenames: + filePath = os.path.join(folderName, filename) + zipObj.write( + filePath, f"{folderName.split('/jpl')[-1]}/{filename}") + zipObj.close() + + +def build(pluginName): + currentDir = os.getcwd() + os.chdir('./../') + subprocess.run([ + sys.executable, os.path.join( + os.getcwd(), "build-plugin.py"), + "--projects", pluginName + ], check=True) + os.chdir(currentDir) + + +def parser(): + parser = argparse.ArgumentParser(description='Build some plugins.') + parser.add_argument('--plugin', type=str, + help='Name of plugin to be build') + parser.add_argument('--extraPath', type=str, default="", + help='output intermediate Path') + parser.add_argument('--distribution', type=str, default='', + help="GNU/Linux or Windows, leave empty. Android, type android") + # to build or not to build + parser.add_argument('--build', action='store_true') + parser.add_argument( + '--preassemble', + action='store_true') # to preassemble or not + # to assemble jpl or not + parser.add_argument('--assemble', action='store_true') + + args = parser.parse_args() + return args + + +def main(): + args = parser() + + if args.preassemble: + preAssemble(args.plugin, args.distribution) + if (args.build): + build(args.plugin) + if (args.assemble): + assemble(args.plugin, args.extraPath, args.distribution) + + +if __name__ == '__main__': + main() diff --git a/SDK/manifestProfile.py b/SDK/manifestProfile.py new file mode 100644 index 0000000..ba59d7c --- /dev/null +++ b/SDK/manifestProfile.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +import os +import json +from termcolor import colored, cprint + +from sdkConstants import * +from pluginStructureProfile import PluginStructure + + +class Manifest(PluginStructure): + def __init__(self, action): + PluginStructure.__init__(self, action) + self.description = '' + self.version = '0' + self.manifest = {} + with open("./Templates/manifest.json") as f: + self.manifestSkeleton = f.read() + f.close() + if (self.isManifest()): + with open(self.manifestFile) as f: + self.manifest = json.load(f) + f.close() + self.pluginName = self.manifest["name"] + self.description = self.manifest["description"] + self.version = self.manifest["version"] + + def checkVersionFormat(self, version): + return False if (len(version.split('.')) != 3) else True + + def createManifest(self): + print( + f"\nDefining a manifest for \"{self.pluginName}\" plugin, we gonna need more information..") + print(colored("\nPress 'q' to quit this option.", "yellow")) + + pluginDescription = input("\nTell us a description: ") + if (pluginDescription == 'q'): + return False + self.description = pluginDescription + pluginVersion = '0' + while (not self.checkVersionFormat( + pluginVersion) and pluginVersion != ""): + print(colored("\nVersion must be of the form X.Y.Z", "yellow")) + pluginVersion = input("Set plugin version (default 0.0.0): ") + if (pluginVersion == 'q'): + return False + if (not pluginVersion): + pluginVersion = '0.0.0' + self.version = pluginVersion + self.manifest["name"] = self.pluginName + self.manifest["description"] = self.description + self.manifest["version"] = self.version + + self.saveManifest(self.manifest) + return True + + def modifyManifestInternal(self, action): + self.callUpdateVersion = False + if (action == ACTIONS["DELETE_MANIFEST"]): + self.deleteManifest() + elif (self.isManifest()): + self.manifest["name"] = self.pluginName + pluginDescription = input( + "New description for your plugin (ignore to keep previous description): ") + pluginVersion = input( + "New plugin version (ignore to keep previous version): ") + while (not self.checkVersionFormat( + pluginVersion) and pluginVersion != ""): + print("Version must be of the form X.Y.Z") + pluginVersion = input( + "New plugin version (ignore to keep previous version): ") + if (pluginDescription != ""): + self.description = pluginDescription + self.manifest["description"] = self.description + if (pluginVersion != + "" and self.manifest["version"] != pluginVersion): + self.version = pluginVersion + self.manifest["version"] = self.version + self.callUpdateVersion = True + self.saveManifest(self.manifest) + else: + self.createManifest() + return self.callUpdateVersion diff --git a/SDK/pluginMainSDK.py b/SDK/pluginMainSDK.py new file mode 100644 index 0000000..1e73c98 --- /dev/null +++ b/SDK/pluginMainSDK.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +import cmd +import sys +from termcolor import colored, cprint + +from generateProject import * +from jplManipulation import * + +os.chdir("./SDK") + + +def mainRun(action): + runoption(action) + + +class JamiPluginsSDKShell(cmd.Cmd): + intro = colored( + "\nWelcome to the Jami Plugins SDK shell. Type help or ? to list commands.\n", + "green") + prompt = colored('(Jami Plugins SDK) ', "yellow") + file = None + + # ----- basic Jami Plugins SDK commands ----- + # ---- BUILD ---- + def help_build(self): + helperFile = './Docs/buildHelper.txt' + with open(helperFile) as f: + helperText = f.read() + f.close() + print(helperText) + + def do_build(self, arg): + if (arg == '-def'): + runoption(ACTIONS["CREATE_BUILD_FILES"]) + else: + runoption(ACTIONS["BUILD"]) + + # ---- JPL MANIPULATION ---- + def help_merge(self): + helperFile = './Docs/jplMergeHelper.txt' + with open(helperFile) as f: + helperText = f.read() + f.close() + print(helperText) + + def do_merge(self, arg): + filePaths = getJpls() + if (len(filePaths) < 2 or not checkValidityJpls(filePaths)): + print( + colored( + "Files are not valid.\nYou must provide two or more valid jpls to merge\n.", + "red")) + return + JPLStructure(filePaths) + return + + def help_assemble(self): + helperFile = './Docs/jplAssembleHelper.txt' + with open(helperFile) as f: + helperText = f.read() + f.close() + print(helperText) + + def do_assemble(self, arg): + if (arg == '-pre'): + runoption(ACTIONS["PREASSEMBLE"]) + else: + runoption(ACTIONS["ASSEMBLE"]) + return + + # ---- FULL PIPELINE ---- + def help_plugin(self): + helperFile = './Docs/pipelineHelper.txt' + with open(helperFile) as f: + helperText = f.read() + f.close() + print(helperText) + + def do_plugin(self, arg): + mainRun(ACTIONS["CREATE"]) + + # ---- MAIN ---- + def help_main(self): + helperFile = './Docs/mainHelper.txt' + with open(helperFile) as f: + helperText = f.read() + f.close() + print(helperText) + + def do_main(self, args): + mainRun(ACTIONS["CREATE_MAIN"]) + + # ---- FUNCTIONALITY ---- + def help_functionality(self): + helperFile = './Docs/functionalityHelper.txt' + with open(helperFile) as f: + helperText = f.read() + f.close() + print(helperText) + + def do_functionality(self, args): + mainRun(ACTIONS["CREATE_FUNCTIONALITY"]) + + # ---- PACKAGE ---- + def help_package(self): + helperFile = './Docs/packageHelper.txt' + with open(helperFile) as f: + helperText = f.read() + f.close() + print(helperText) + + def do_package(self, args): + mainRun(ACTIONS["CREATE_PACKAGE_JSON"]) + + # ---- PREFERENCES ---- + def help_preference(self, args=''): + helperFile = "./Docs/preferenceHelper.txt" + if (PREFERENCES_TYPES[0].lower() in args): # List - 0 + helperFile = "./Docs/preferenceList.txt" + elif (PREFERENCES_TYPES[1].lower() in args): # Path - 1 + helperFile = "./Docs/preferencePath.txt" + + with open(helperFile) as f: + helpText = f.read() + f.close() + print(helpText) + + def do_preference(self, arg): + if ("-h" in arg): + self.help_preference(arg.replace("-h", "")) + elif ("-del" in arg): + mainRun(ACTIONS["DELETE_PREFERENCES"]) + else: + mainRun(ACTIONS["CREATE_PREFERENCES"]) + + # ---- MANIFEST ---- + def help_manifest(self): + helperFile = './Docs/manifestHelper.txt' + with open(helperFile) as f: + helperText = f.read() + f.close() + print(helperText) + + def do_manifest(self, arg): + if (arg == "-del"): + mainRun(ACTIONS["DELETE_MANIFEST"]) + else: + mainRun(ACTIONS["CREATE/MODIFY_MANIFEST"]) + + # ---- PLUGIN DELETION ---- + def do_delete(self, arg): + 'Choose a plugin to delete: DELETE' + print("\nYou can not delete these plugins/folders: ") + print(colored(FORBIDDEN_NAMES[:-1], "yellow")) + mainRun(ACTIONS["DELETE"]) + + # ---- EXIT ---- + def do_exit(self, arg): + 'Stop Jami Plugins SDK, and exit: EXIT' + print('Thank you for using Jami Plugins SDK') + return True + + # ----- clear shell ----- + def do_clear(self, arg): + 'Clear your prompt: CLEAR' + os.system("clear") + print(self.intro) + + def precmd(self, line): + line = line.lower() + if line == 'eof': + return ('exit') + return line + + +if __name__ == '__main__': + os.system('clear') + JamiPluginsSDKShell().cmdloop() diff --git a/SDK/pluginStructureProfile.py b/SDK/pluginStructureProfile.py new file mode 100644 index 0000000..d4b5c43 --- /dev/null +++ b/SDK/pluginStructureProfile.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +from userProfile import UserInfos +from sdkConstants import * +import os +import json +import shutil +from termcolor import colored, cprint + + +class PluginStructure(UserInfos): + def __init__(self, action): + UserInfos.__init__(self, action) + self.create(action) + self.createFolderStructure() + + def delete(self): + if (os.path.exists(self.pluginDirectory) + and os.path.isdir(self.pluginDirectory)): + shutil.rmtree(self.pluginDirectory) + + def reInitialize(self, pluginName): + self.pluginName = pluginName + self.pluginDirectory = f"./../{self.pluginName}" + self.manifestFile = f"{self.pluginDirectory}/manifest.json" + self.pluginDataDirectory = f"{self.pluginDirectory}/data" + self.pluginIcon = f"{self.pluginDataDirectory}/icon.png" + self.preferencesFile = f"{self.pluginDataDirectory}/preferences.json" + self.packageFile = f"{self.pluginDirectory}/package.json" + self.cmakelistsFile = f"{self.pluginDirectory}/CMakeLists.txt" + self.buildFile = f"{self.pluginDirectory}/build.sh" + + def create(self, action): + plugins = os.listdir("../") + print(colored("\nLeave Empty or Press 'q' to quit this option.", "yellow")) + if (action == ACTIONS["CREATE"]): + print("\nNow, you need to tell how you want yout plugin to be called.") + pluginName = "SDK" + while (pluginName in plugins): + pluginName = input("\nChoose a cool name for your plugin: ") + if (pluginName == 'q' or not pluginName): + self.reInitialize('') + break + if (pluginName in plugins): + print(colored("This name is invalid!", "red")) + else: + self.reInitialize(pattern.sub('', pluginName)) + if (self.pluginName): + coloredTitle = colored(self.pluginName, "green") + print(f"\nNice! Your {coloredTitle} will be awesome!") + + elif (action == ACTIONS["DELETE"] or action == ACTIONS["DELETE_PREFERENCES"] \ + or action == ACTIONS["DELETE_MANIFEST"]): + pluginName = "" + while (pluginName not in plugins or pluginName in FORBIDDEN_NAMES): + pluginName = input("\nPlugin to be modified: ") + if (pluginName == 'q' or not pluginName): + self.reInitialize('') + break + if (pluginName not in plugins or pluginName in FORBIDDEN_NAMES): + print(colored("This name is invalid!", "red")) + else: + self.reInitialize(pattern.sub('', pluginName)) + + elif (action == ACTIONS["CREATE/MODIFY_MANIFEST"] or action == ACTIONS["CREATE_PREFERENCES"] \ + or action == ACTIONS["CREATE_FUNCTIONALITY"] or action == ACTIONS["CREATE_PACKAGE_JSON"] \ + or action == ACTIONS["CREATE_MAIN"] or action == ACTIONS["CREATE_BUILD_FILES"] \ + or action == ACTIONS["ASSEMBLE"] or action == ACTIONS["PREASSEMBLE"] \ + or action == ACTIONS["BUILD"]): + pluginName = "" + while (pluginName in FORBIDDEN_NAMES): + pluginName = input("\nPlugin to be modified or created: ") + if (pluginName == 'q' or not pluginName): + self.reInitialize('') + break + if (pluginName in FORBIDDEN_NAMES): + print(colored("This name is invalid!", "red")) + else: + self.reInitialize(pattern.sub('', pluginName)) + + def createFolderStructure(self): + if (self.pluginName): + if (not os.path.exists(self.pluginDirectory)): + os.mkdir(self.pluginDirectory) + if (not os.path.exists(self.pluginDataDirectory)): + os.mkdir(self.pluginDataDirectory) + if (not os.path.exists(self.pluginIcon)): + shutil.copyfile("./Templates/icon.png", self.pluginIcon) + + def saveManifest(self, manifestTxt): + with open(self.manifestFile, 'w') as f: + json.dump(manifestTxt, f, indent=4) + f.close() + + def deleteManifest(self): + if (os.path.exists(self.manifestFile)): + os.remove(self.manifestFile) + + def isManifest(self): + return os.path.exists(self.manifestFile) + + def isMainDirectory(self): + return os.path.exists(self.pluginDirectory) diff --git a/SDK/preferencesProfile.py b/SDK/preferencesProfile.py new file mode 100644 index 0000000..43a9a9c --- /dev/null +++ b/SDK/preferencesProfile.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +import os +import json +from termcolor import colored, cprint + +from manifestProfile import Manifest + + +class Preferences(Manifest): + def __init__(self, action): + Manifest.__init__(self, action) + self.preferences = [] + self.preferencesTitles = [] + self.preferencesKeys = [""] + self.preferences_options = {} + self.preferences_options_keys = [] + self.getPreferencesOptions() + self.globPreferencesList() + + def getPreferencesOptions(self): + with open("./Templates/preferences.json") as f: + preferences = json.load(f) + for preference in preferences: + self.preferences_options_keys.append(preference["type"]) + self.preferences_options[preference["type"]] = preference + f.close() + + def globPreferencesList(self): + if os.path.exists(self.preferencesFile): + with open(self.preferencesFile) as f: + self.preferences = json.load(f) + for preference in self.preferences: + self.preferencesTitles.append(preference["title"]) + self.preferencesKeys.append(preference["key"]) + f.close() + + def completeListValues(self, item, entryValues=[]): + if (item == "entryValues"): + addNew = '' + entryValues = [] + while (addNew != 'n'): + addNew = input( + "\nDo you want to add a new entry Value? [y/n] ") + if (addNew == 'y'): + newValue = input("Type one new entry: ") + if (newValue): + entryValues.append(newValue) + return entryValues + elif (item == 'entries'): + entries = [] + addNew = '' + for i in range(len(entryValues)): + newValue = input( + f"Type an entry name for '{entryValues[i]}': ") + entries.append(newValue) + return entries + + def completePathValues(self): + allExts = '' + while(allExts not in ['y', 'n']): + allExts = input("If you want to access all kinds of files? [y/n] ") + if (allExts not in ['y', 'n']): + print( + colored( + "Invalid answer, please type 'y' or 'n' and then press enter", + "red")) + if (allExts == 'y'): + return "*/*" + else: + print("Now, tell us the accepted files extensions, one by one!") + newExt = '' + ext = [] + while (newExt != 'n'): + temp = input( + "Type a file extension (if empty, all files extensions will be considered valid): ") + if (temp): + ext.append("*/." + temp.split('.')[-1]) + newExt = '' + while (newExt not in ['y', 'n']): + newExt = input( + "Do you want to add another extension? [y/n] ") + if (newExt not in ['y', 'n']): + print( + colored( + "Invalid answer, please type 'y' or 'n' and then press enter", + "red")) + else: + return "*/*" + return ','.join(ext) + + def createPreferencesInternal(self, names, loop=True): + self.newPreferences = [] + addAllowed = '' + while (addAllowed != "n" and addAllowed != "N"): + addAllowed = input("\nWould you like to add a preference? [y/n] ") + if (addAllowed == "y"): + print("\nYour preferences options available are: ") + options = [] + for i, key in enumerate(self.preferences_options_keys): + options.append(str(i)) + print(f"({i}) {key};") + newType = '' + while (newType not in options): + newType = input("\nWhich preference type do you choose: ") + preferenceSkeleton = self.preferences_options[self.preferences_options_keys[int( + newType)]].copy() + for item in preferenceSkeleton: + if (item != "type"): + if (item == "scope"): + scopeStr = ["plugin"] + forbbidenScope = [""] + scopeOptions = [] + addScope = 'n' + if (len(names) > 0): + addScope = '' + for j, name in enumerate(names): + scopeOptions.append(str(j)) + while (addScope != 'n' and not set( + scopeOptions).issubset(set(forbbidenScope))): + if (not set(scopeOptions).issubset( + set(forbbidenScope))): + addScope = input( + "\nWould you like to add a scope? [y/n] ") + if (addScope == "y"): + newScope = '' + print( + "\nPossible values for scope:") + for j, name in enumerate(names): + if (str(j) not in forbbidenScope): + print(f"({j}) {name};") + while((newScope not in scopeOptions or newScope in forbbidenScope)): + newScope = input( + "\nWhich scope do you choose: ") + forbbidenScope.append(newScope) + scopeStr.append( + f"{names[int(newScope)]}") + else: + print( + colored( + "\nAll available scopes have been added to this preference!\ncontinuing ...", + "yellow")) + addScope = "n" + preferenceSkeleton[item] = ", ".join(scopeStr) + elif (preferenceSkeleton["type"] == "List" and item in [ + "entries", "entryValues"]): + if (item == 'entryValues'): + preferenceSkeleton[item] = self.completeListValues( + item) + if (item == 'entries'): + preferenceSkeleton[item] = self.completeListValues( + item, preferenceSkeleton['entryValues'].copy()) + elif (preferenceSkeleton["type"] == "Path" and item in ["mimeType"]): + preferenceSkeleton[item] = self.completePathValues( + ) + elif (item == "key"): + itemValue = "" + while (itemValue in self.preferencesKeys): + itemValue = input( + f"Type a value for {item}: ") + if (itemValue in self.preferencesKeys): + print( + colored( + "\nValue not permited", + "red")) + self.preferencesKeys.append(itemValue) + preferenceSkeleton[item] = itemValue + elif (item == "defaultValue"): + itemValue = '' + while (not itemValue): + itemValue = input( + f"Type a value for {item}: ") + if (not itemValue): + print( + colored( + "\nValue not permited", + "red")) + preferenceSkeleton[item] = itemValue + else: + preferenceSkeleton[item] = input( + f"Type a value for {item}: ") + self.newPreferences.append(preferenceSkeleton) + if (not loop): + return + + def createPreferences(self, names): + self.createPreferencesInternal(names) + + for p in self.newPreferences: + self.preferences.append(p) + + if (len(self.newPreferences) > 0): + with open(self.preferencesFile, 'w') as f: + json.dump(self.preferences, f, indent=4) + f.close() + elif (len(self.preferences) == 0): + with open(self.preferencesFile, 'w') as f: + json.dump(self.preferences, f, indent=4) + f.close() + + def deletePreference(self): + options = [] + print("\nCurrently, these are you preferences: ") + for i in range(len(self.preferencesTitles)): + print(colored(f"({i}) {self.preferencesTitles[i]}", "yellow")) + options.append(str(i)) + prefDel = '' + if (len(options) > 0): + while (prefDel not in options): + prefDel = input( + "\nWich preference do you want to delete? (choose 'q' to quit) ") + if (prefDel not in options and prefDel != 'q'): + print(colored("Invalid options!", "red")) + if (prefDel == 'q'): + break + if (prefDel != 'q'): + for preference in self.preferences: + if self.preferencesTitles[int( + prefDel)] == preference["title"]: + self.preferences.remove(preference) + prefDel = preference + break + with open(self.preferencesFile, 'w') as f: + json.dump(self.preferences, f, indent=4) + f.close + else: + input( + colored( + "\nThere is no preference to be deleted.\nPress enter to continue...", + "yellow")) diff --git a/SDK/requirements.txt b/SDK/requirements.txt new file mode 100644 index 0000000..ee28905 --- /dev/null +++ b/SDK/requirements.txt @@ -0,0 +1,2 @@ +termcolor==1.1.0 +numpy \ No newline at end of file diff --git a/SDK/sdkConstants.py b/SDK/sdkConstants.py new file mode 100644 index 0000000..5c62f2c --- /dev/null +++ b/SDK/sdkConstants.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +import re +import string +pattern = re.compile(r'[\W_]+') + +PLUGINS_APIS = {"MEDIA_HANDLER": '1', + "CONVERSATION_HANDLER": '2'} +APIS_NAMES = {"MEDIA_HANDLER": "MediaHandler", + "CONVERSATION_HANDLER": "ConversationHandler"} + +ACTIONS = {"CREATE": '1', + "CREATE_MAIN": '2', + "CREATE/MODIFY_MANIFEST": '3', + "DELETE_MANIFEST": '4', + "CREATE_PREFERENCES": '5', + "DELETE_PREFERENCES": '6', + "CREATE_FUNCTIONALITY": '7', + "CREATE_PACKAGE_JSON": '8', + "DELETE": '9', + "ASSEMBLE": '10', + "BUILD": '11', + "PREASSEMBLE": '12', + "CREATE_BUILD_FILES": '13'} + +FORBIDDEN_NAMES = [ + "SDK", + "lib", + "contrib", + "build", + "docker", + ''] + +PREFERENCES_TYPES = ["List", "Path"] + +OS_IDS = {"Linux": 1, + "Windows": 2, + "Darwin": 3} + +NAME = '' +EMAIL = '' \ No newline at end of file diff --git a/SDK/skeletonSrcProfile.py b/SDK/skeletonSrcProfile.py new file mode 100644 index 0000000..6e8e7d2 --- /dev/null +++ b/SDK/skeletonSrcProfile.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +import os +import sys +import json +from zipfile import ZipFile +from termcolor import colored, cprint + +from sdkConstants import * +from preferencesProfile import Preferences + + +class SkeletonSourceFiles(Preferences): + def __init__(self, action): + Preferences.__init__(self, action) + self.names = [] + self.apis = [] + self.mainFile = self.pluginDirectory + "/main.cpp" + self.newNames = [] + self.globNames() + self.initPackage() + + def initPackage(self): + if os.path.exists(self.packageFile): + with open(self.packageFile, 'r') as f: + self.package = json.load(f) + + def updateMainFileVersion(self): + if os.path.exists(self.mainFile): + with open(self.mainFile, 'r') as f: + lines = f.readlines() + splits = self.version.split('.') + if (not self.version): + splits = ['', '', ''] + splitStr = [ + "#define " + + self.pluginName + + "_VERSION_MAJOR ", + "#define " + + self.pluginName + + "_VERSION_MINOR ", + "#define " + + self.pluginName + + "_VERSION_PATCH "] + outStr = [] + idx = 0 + for line in lines: + if (idx < 3 and splitStr[idx] in line): + parts = line.split(splitStr[idx]) + line = line.replace(parts[-1], splits[idx] + "\n") + idx += 1 + outStr.append(line) + f.close() + with open(self.mainFile, 'w') as f: + f.write(''.join(outStr)) + + def updatePackageFileVersion(self): + if os.path.exists(self.packageFile): + if(not self.package): + with open(self.packageFile, 'w') as f: + self.package = json.load(f) + f.close() + self.package["version"] = self.version + with open(self.packageFile, 'w') as f: + json.dump(self.package, f, indent=4) + f.close() + + def updateCMakeListsFileVersion(self): + if os.path.exists(self.cmakelistsFile): + with open(self.cmakelistsFile, 'r') as f: + outStr = f.read() + lines = outStr.split("\n") + for i, line in enumerate(lines): + if ("set (Version " in line): + lines[i] = f"set (Version {self.version})" + break + f.close() + + with open(self.cmakelistsFile, 'w') as f: + f.write('\n'.join(lines)) + f.close() + + def updateVersion(self): + self.updateMainFileVersion() + self.updatePackageFileVersion() + self.updateCMakeListsFileVersion() + + def createMain(self, addAllowed='y'): + if (not self.isManifest()): + self.createManifest() + with open('./Templates/main.cpp') as f: + mainStr = f.read() + mainStr = mainStr.replace('HEADER', self.header) + mainStr = mainStr.replace('PLUGINNAME', self.pluginName) + if (not self.checkVersionFormat(self.version)): + self.version = '0.0.0' + version = self.version.split(".") + pluginVersionMajor = version[0] + pluginVersionMinor = version[1] + pluginVersionPatch = version[2] + + mainStr = mainStr.replace('PLUGINVERSIONMAJOR', pluginVersionMajor) + mainStr = mainStr.replace('PLUGINVERSIONMINOR', pluginVersionMinor) + mainStr = mainStr.replace('PLUGINVERSIONPATCH', pluginVersionPatch) + splits = mainStr.split("----------------") + includesAPIStr = None + pluginAPIStr = None + for i, split in enumerate(splits): + if ("PLUGINAPI" in split and "APIMANAGER" in split): + if (not pluginAPIStr): + pluginAPIStr = self.createHandlers(split) + splits[i] = splits[i].replace(split, pluginAPIStr) + if ("INCLUDESAPI" in split): # must always be called first + if (not includesAPIStr): + includesAPIStr = self.getIncludeStr(split, addAllowed) + splits[i] = splits[i].replace(split, includesAPIStr) + + mainStr = ''.join(splits) + f.close() + with open(self.mainFile, 'w') as f: + f.write(mainStr) + f.close() + return True + + def globNames(self): + if (self.isMainDirectory()): + files = os.listdir(self.pluginDirectory) + for file in files: + if (".h" in file): + if (("MediaHandler" in file or "ConversationHandler" in file)): + name = file.replace(".h", "") + name = name.replace("MediaHandler", "") + name = name.replace("ConversationHandler", "") + self.names.append(name) + if ("MediaHandler" in file): + self.apis.append(PLUGINS_APIS["MEDIA_HANDLER"]) + if ("ConversationHandler" in file): + self.apis.append(PLUGINS_APIS["CONVERSATION_HANDLER"]) + + def createHandlers(self, split): + if (len(self.names) == 0): + return "" + createStr = "" + for i, name in enumerate(self.names): + if (self.apis[i] == PLUGINS_APIS["MEDIA_HANDLER"]): + temp = split.replace("PLUGINAPI", f"{name}MediaHandler") + temp = temp.replace("APIMANAGER", "CallMediaHandlerManager") + elif (self.apis[i] == PLUGINS_APIS["CONVERSATION_HANDLER"]): + temp = split.replace("PLUGINAPI", f"{name}ConversationHandler") + temp = temp.replace("APIMANAGER", "ConversationHandlerManager") + createStr += temp + return createStr + + def getIncludeStr(self, split='', addAllowed='y'): + includesStr = "" + self.newNames = [] + while (addAllowed == "y" or addAllowed == "Y"): + addAllowed = '' + functionName = "" + while(functionName == "" or functionName in self.names): + functionName = input("\nChose a functionality name: ") + functionName = pattern.sub('', functionName) + apiType = '' + while (apiType not in PLUGINS_APIS.values()): + print(f"\nChoose a API for functionality \"{functionName}\".") + print("\nAvailable APIs: ") + print("(1) video during a call (Media Handler API)") + print( + colored( + "For more information about the API, call help preferences.", + "yellow")) + # or (2) to chat messages: ") + apiType = input("\nEnter a data type number: ") + if (apiType not in PLUGINS_APIS.values()): + print(colored(f"Data type '{apiType}' not valid!", "red")) + self.names.append(functionName) + self.newNames.append(functionName) + self.apis.append(apiType) + while (addAllowed not in ['y', 'N', 'Y', 'n']): + addAllowed = input("\nAdd another functionaliy? [y/N] ") + if not addAllowed: + addAllowed = 'N' + break + for j, item in enumerate(self.apis): + temp = '' + localNames = self.names.copy() + localApis = self.apis.copy() + if (item == PLUGINS_APIS["MEDIA_HANDLER"]): + localNames[j] = self.names[j] + "MediaHandler" + if (split): + temp = split.replace("INCLUDESAPI", localNames[j]) + elif (item == PLUGINS_APIS["CONVERSATION_HANDLER"]): + localNames[j] = self.names[j] + "ConversationHandler" + if (split): + temp = split.replace("INCLUDESAPI", localNames[j]) + includesStr += temp + if (self.newNames != []): + self.createAPIHeaderFiles() + self.createAPIImplFiles() + return includesStr + + def createAPIHeaderFiles(self): + for j, item in enumerate(self.apis): + if (self.names[j] in self.newNames): + if (item == PLUGINS_APIS["MEDIA_HANDLER"]): + with open('./Templates/genericMediaHandler.h', 'r') as f: + data = f.read() + data = data.replace("HEADER", self.header) + data = data.replace("GENERIC", self.names[j]) + f.close() + with open(f"{self.pluginDirectory}/{self.names[j]}MediaHandler.h", 'w') as f: + f.write(data) + f.close() + with open('./Templates/genericVideoSubscriber.h', 'r') as f: + data = f.read() + data = data.replace("HEADER", self.header) + data = data.replace("GENERIC", self.names[j]) + f.close() + with open(f"{self.pluginDirectory}/{self.names[j]}VideoSubscriber.h", 'w') as f: + f.write(data) + f.close() + elif (item == PLUGINS_APIS["CONVERSATION_HANDLER"]): + with open('./Templates/genericConversationHandler.h', 'r') as f: + data = f.read() + data = data.replace("HEADER", self.header) + data = data.replace('GENERIC', self.names[j]) + f.close() + with open(f"{self.pluginDirectory}/{self.names[j]}ConversationHandler.h", 'w') as f: + f.write(data) + f.close() + + def createAPIImplFiles(self): + for j, item in enumerate(self.apis): + if (self.names[j] in self.newNames): + if (item == PLUGINS_APIS["MEDIA_HANDLER"]): + with open('./Templates/genericMediaHandler.cpp', 'r') as f: + data = f.read() + data = data.replace("HEADER", self.header) + data = data.replace("PLUGINNAME", self.pluginName) + data = data.replace("GENERIC", self.names[j]) + f.close() + with open(f"{self.pluginDirectory}/{self.names[j]}MediaHandler.cpp", 'w') as f: + f.write(data) + f.close() + with open('./Templates/genericVideoSubscriber.cpp', 'r') as f: + data = f.read() + data = data.replace("HEADER", self.header) + data = data.replace("GENERIC", self.names[j]) + f.close() + with open(f"{self.pluginDirectory}/{self.names[j]}VideoSubscriber.cpp", 'w') as f: + f.write(data) + f.close() + elif (item == PLUGINS_APIS["CONVERSATION_HANDLER"]): + with open('./Templates/genericConversationHandler.cpp', 'r') as f: + data = f.read() + data = data.replace("HEADER", self.header) + data = data.replace("GENERIC", self.names[j]) + f.close() + with open(f"{self.pluginDirectory}/{self.names[j]}ConversationHandler.cpp", 'w') as f: + f.write(data) + f.close() + self.createPreferences(self.names) + self.setRuntimePreferences() + + def setRuntimePreferences(self): + for j, item in enumerate(self.apis): + baseStr1 = '' + baseStr2 = '' + if (len(self.newNames) > 0 and self.names[j] in self.newNames): + if (item == PLUGINS_APIS["MEDIA_HANDLER"]): + with open(f"{self.pluginDirectory}/{self.names[j]}MediaHandler.cpp", 'r') as f: + data = f.read() + parts = data.split("----------------") + for part in parts: + if "PREFERENCE1" in part: + baseStr1 = part + if "PREFERENCE2" in part: + baseStr2 = part + newStr1 = '' + newStr2 = '' + for preference in self.preferences: + if (self.names[j] in preference['scope']): + editable = '' + while(editable not in ['y', 'n']): + editable = input( + f"\nThe preference {preference['title']} will be changeable during running time\nfor {self.names[j]} functionality? [y/n] ") + if (editable == 'y'): + newStr1 += baseStr1.replace( + "PREFERENCE1", preference['key']) + newStr2 += baseStr2.replace( + "PREFERENCE2", preference['key']) + data = data.replace(baseStr1, newStr1) + data = data.replace(baseStr2, newStr2) + f.close() + with open(f"{self.pluginDirectory}/{self.names[j]}MediaHandler.cpp", 'w') as f: + parts = data.split("----------------") + f.write(''.join(parts)) + f.close() + elif (item == PLUGINS_APIS["CONVERSATION_HANDLER"]): + with open(f"{self.pluginDirectory}/{self.names[j]}ConversationHandler.cpp", 'r') as f: + data = f.read() + parts = data.split("----------------") + for part in parts: + if "PREFERENCE1" in part: + baseStr1 = part + if "PREFERENCE2" in part: + baseStr2 = part + newStr1 = '' + newStr2 = '' + for preference in self.preferences: + if (self.names[j] in preference['scope']): + editable = '' + while(editable not in ['y', 'n']): + editable = input( + f"\nThe preference {preference['title']} will be changeable during running time\nfor {self.names[j]} functionality? [y/n] ") + if (editable == 'y'): + newStr1 += baseStr1.replace( + "PREFERENCE1", preference['key']) + newStr2 += baseStr2.replace( + "PREFERENCE1", preference['key']) + data = data.replace(baseStr1, newStr1) + data = data.replace(baseStr2, newStr2) + f.close() + with open(f"{self.pluginDirectory}/{self.names[j]}ConversationHandler.cpp", 'w') as f: + parts = data.split("----------------") + f.write(''.join(parts)) + f.close() + + def createFunctionality(self): + if (not self.isManifest()): + print("\nBefore creating a functionality, you must define your manifest.") + self.createManifest() + print("\nManifest ok, continuing functionality creation.") + self.getIncludeStr() + self.createMain(addAllowed='n') + self.createBuildFiles() + + def modifyManifest(self, action): + if (self.modifyManifestInternal(action)): + self.updateVersion() + + def createPackageJson(self): + if (not self.isManifest()): + print("\nBefore creating a package, you must define your manifest.") + self.createManifest() + print("\nManifest ok, continuing package creation.") + with open("./Templates/package.json") as f: + self.package = json.load(f) + self.package["name"] = self.pluginName + self.package["version"] = self.version + if (not self.checkVersionFormat(self.version)): + self.package["version"] = '0.0.0' + f.close() + + with open('./Templates/defaultDependenciesStrings.json') as f: + defaultStrings = json.load(f) + f.close() + + for api in self.apis: + if api == PLUGINS_APIS["MEDIA_HANDLER"]: + for dep in defaultStrings["package.json"]["MediaHandler"]["deps"]: + if dep not in self.package["deps"]: + self.package["deps"].append(dep) + with open(f"{self.pluginDirectory}/package.json", 'w') as f: + json.dump(self.package, f, indent=4) + f.close() + + print("\nPackage ok.") + + def globFiles(self, baseStr, key, ext): + import glob + files = glob.glob(f"{self.pluginDirectory}/**.{ext}", recursive=True) + outputStr = "" + for item in files: + name = os.path.split(item)[1] + outputStr += baseStr.replace(key, name) + return outputStr + + def fillBuildFile(self, inputStr): + inputStr = inputStr.replace('PLUGINNAME', self.pluginName) + inputStr = inputStr.replace('MANIFESTVERSION', self.version) + splits = inputStr.split('---') + tempPart = [] + for i, split in enumerate(splits): + if (("FFMPEG" in split or "avutil" in split) + and PLUGINS_APIS['MEDIA_HANDLER'] not in self.apis): + splits[i] = '' + elif ("CPPFILENAME" in split): + splits[i] = self.globFiles(split, "CPPFILENAME", "cpp") + elif ("HFILENAME" in split): + splits[i] = self.globFiles(split, "HFILENAME", "h") + elif ("FFMPEGCPP" in split): + splits[i] = split.replace("FFMPEGCPP", '') + elif ("FFMPEGH" in split): + splits[i] = split.replace("FFMPEGH", '') + inputStr = ''.join(splits) + return inputStr + + def createBuildFiles(self): + with open("./Templates/CMakeLists.txt", 'r') as f: + cmakelists = f.read() + f.close() + cmakelists = self.fillBuildFile(cmakelists) + with open(self.cmakelistsFile, 'w') as f: + f.write(cmakelists) + f.close() + + with open("./Templates/build.sh", 'r') as f: + build = f.read() + f.close() + build = self.fillBuildFile(build) + with open(self.buildFile, 'w') as f: + f.write(build) + f.close() + + print("\nCMakeLists.txt and build.sh ok.") + return diff --git a/SDK/userProfile.py b/SDK/userProfile.py new file mode 100644 index 0000000..387f81a --- /dev/null +++ b/SDK/userProfile.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +from datetime import datetime + +from sdkConstants import * + + +class UserInfos: + def __init__(self, action): + self.name = "AUTHORNAME" + self.mail = "AUTHORMAIL" + self.header = '' + self.initHeaderSkeleton() + if (action == ACTIONS["CREATE"] or action == + ACTIONS["CREATE_FUNCTIONALITY"] or action == ACTIONS["CREATE_MAIN"]): + self.createCopyRight() + + def initHeaderSkeleton(self): + with open('./Templates/copyright.txt') as f: + self.header = f.read() + f.close() + + def createCopyRight(self): + global NAME + global EMAIL + with open('./Templates/copyright.txt') as f: + data = f.read() + today = datetime.today() + data = data.replace("YEAR", str(today.year)) + authorName = NAME + if (authorName == ""): + authorName = input("\nWhat's your name? ") + if (authorName != ""): + self.name = authorName + NAME = self.name + data = data.replace("AUTHORNAME", self.name) + authorMail = EMAIL + if (authorMail == ""): + authorMail = input("\nWhat's your e-mail? ") + if (authorMail != ""): + self.mail = authorMail + EMAIL = self.mail + data = data.replace("AUTHORMAIL", self.mail) + self.header = data diff --git a/SDK/utils.py b/SDK/utils.py new file mode 100644 index 0000000..926e232 --- /dev/null +++ b/SDK/utils.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + +import os +import glob +import platform + +from sdkConstants import * +from skeletonSrcProfile import SkeletonSourceFiles + + +def initializeClasses(action): + setSkeletonSourceFiles(action) + return bool(globalSkeletonSourceFiles.pluginName) + + +globalSkeletonSourceFiles = '' + + +def setSkeletonSourceFiles(action): + global globalSkeletonSourceFiles + globalSkeletonSourceFiles = SkeletonSourceFiles(action) + + +def getSkeletonSourceFiles(): + return globalSkeletonSourceFiles diff --git a/assemble-plugin.py b/assemble-plugin.py deleted file mode 100644 index 96e307c..0000000 --- a/assemble-plugin.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -import argparse -import sys -import subprocess -from zipfile import ZipFile -import platform - - -def parser(): - parser = argparse.ArgumentParser(description='Build some plugins.') - parser.add_argument('--plugins', type=str, - help='Names of plugins to be build') - parser.add_argument('--extraPath', type=str, default="", - help='output intermediate Path') - - args = parser.parse_args() - return args - - -def main(): - args = parser() - - system = platform.system().lower() - if system == "linux" or system == "linux2": - osBuildPath = "/build-local" - distribution = "x86_64-linux-gnu" - elif system == "darwin": - sys.exit("Plugins not supported on MacOS and IOS") - elif system == "windows": - osBuildPath = "/msvc" - distribution = "x64-windows" - - plugins = args.plugins.split(',') - for plugin in plugins: - root = os.path.dirname(os.path.abspath(__file__)) - outputJPL = root + "/build/" + distribution + \ - "/" + args.extraPath + "/" + plugin + ".jpl" - outputBuild = root + "/" + plugin + osBuildPath + "/jpl" - - with ZipFile(outputJPL, 'w') as zipObj: - for folderName, subfolders, filenames in os.walk(outputBuild): - for filename in filenames: - filePath = os.path.join(folderName, filename) - zipObj.write(filePath, folderName.split("/") - [-1][4:] + "/" + filename) - - -if __name__ == '__main__': - main() diff --git a/build-plugin.py b/build-plugin.py index d96e575..6190afd 100644 --- a/build-plugin.py +++ b/build-plugin.py @@ -1,10 +1,32 @@ #!/usr/bin/env python3 # +# Copyright (C) 2020 Savoir-faire Linux Inc. +# +# Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Creates packaging targets for a distribution and architecture. +# This helps reduce the length of the top Makefile. +# + # This script must unify the Plugins build for # every project and Operational System import os import sys +import json import platform import argparse import subprocess @@ -28,6 +50,8 @@ def parse(): parser.add_argument('--distribution') parser.add_argument('--processor', type=str, default="GPU", help='Runtime plugin CPU/GPU setting.') + parser.add_argument('--buildOptions', default='', type=str, + help="Type all build optionsto pass to package.json 'defines' property.\nThis argument consider that you're using cmake.") dist = choose_distribution() @@ -50,6 +74,10 @@ def parse(): args.processor *= len(args.projects) validate_args(args) + + if dist != WIN32_DISTRIBUTION_NAME: + args.toolset = '' + args.sdk = '' return args @@ -75,6 +103,9 @@ def validate_args(parsed_args): if (processor not in ['GPU', 'CPU']): sys.exit('Processor can only be GPU or CPU.') + if (parsed_args.buildOptions): + parsed_args.buildOptions = parsed_args.buildOptions.split(',') + def choose_distribution(): system = platform.system().lower() @@ -95,14 +126,32 @@ def choose_distribution(): return 'Unknown' -def buildPlugin(pluginPath, processor, distribution): +def buildPlugin(pluginPath, processor, distribution, toolset='', sdk='', buildOptions=''): if distribution == WIN32_DISTRIBUTION_NAME: - return subprocess.run([ + if (buildOptions): + with open(f"{pluginPath}/package.json") as f: + defaultPackage = json.load(f) + package = defaultPackage.copy() + package['defines'] = [] + for option in buildOptions: + package['defines'].append(option) + f.close() + with open(f"{pluginPath}/package.json", 'w') as f: + json.dump(package, f, indent=4) + f.close() + subprocess.run([ sys.executable, os.path.join( - os.getcwd(), pluginPath + "/build-windows.py"), - "--toolset", args.toolset, - "--sdk", args.sdk + os.getcwd(), "../../daemon/compat/msvc/winmake.py"), + "-P", + "--toolset", toolset, + "--sdk", sdk, + "-fb", pluginPath.split('/')[-1] ], check=True) + if (buildOptions): + with open(f"{pluginPath}/package.json", "w") as f: + json.dump(defaultPackage, f, indent=4) + f.close() + return environ = os.environ.copy() @@ -117,6 +166,7 @@ def buildPlugin(pluginPath, processor, distribution): install_args.append('-p') install_args.append(str(multiprocessing.cpu_count())) + subprocess.check_call(['chmod', '+x', pluginPath + "/build.sh"]) return subprocess.run([pluginPath + "/build.sh"] + install_args, env=environ, check=True) @@ -130,7 +180,10 @@ def main(): buildPlugin( currentDir + "/" + plugin, args.processor[i], - args.distribution) + args.distribution, + args.toolset, + args.sdk, + args.buildOptions) if __name__ == "__main__": diff --git a/docker/Dockerfile_android b/docker/Dockerfile_android index 4723c09..e9684ac 100644 --- a/docker/Dockerfile_android +++ b/docker/Dockerfile_android @@ -6,50 +6,50 @@ ENV HOME /home/gradle ENV SSH_AUTH_SOCK /home/gradle/.sockets/ssh RUN apt-get update && apt-get install -y --no-install-recommends \ - asciidoc \ - autogen \ - automake \ - autoconf \ - autopoint \ - gettext \ - ca-certificates \ - cmake \ - bc \ - bison \ - build-essential \ - bzip2 \ - doxygen \ - git \ - lib32stdc++6 \ - lib32z1 \ - libtool \ - locales \ - m4 \ - pkg-config \ - software-properties-common \ - ssh \ - unzip \ - wget \ - curl \ - yasm \ - nasm \ - zip \ - libpcre3 \ - libpcre3-dev \ - && locale-gen $LANG $LC_ALL && update-locale $LANG $LC_ALL + asciidoc \ + autogen \ + automake \ + autoconf \ + autopoint \ + gettext \ + ca-certificates \ + cmake \ + bc \ + bison \ + build-essential \ + bzip2 \ + doxygen \ + git \ + lib32stdc++6 \ + lib32z1 \ + libtool \ + locales \ + m4 \ + pkg-config \ + software-properties-common \ + ssh \ + unzip \ + wget \ + curl \ + yasm \ + nasm \ + zip \ + libpcre3 \ + libpcre3-dev \ + && locale-gen $LANG $LC_ALL && update-locale $LANG $LC_ALL # Android SDK tools RUN wget -O /tmp/android-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-6200805_latest.zip && \ - mkdir -p /opt/android-sdk && \ - unzip -q -d /opt/android-sdk /tmp/android-tools.zip && \ - rm -f /tmp/android-tools.zip && \ - chown -R root:root /opt/android-sdk + mkdir -p /opt/android-sdk && \ + unzip -q -d /opt/android-sdk /tmp/android-tools.zip && \ + rm -f /tmp/android-tools.zip && \ + chown -R root:root /opt/android-sdk # Swig 4.0.1 RUN wget -O /tmp/swig.tar.gz https://github.com/swig/swig/archive/rel-4.0.1.tar.gz && \ - tar xzf /tmp/swig.tar.gz -C /opt && \ - cd /opt/swig-rel-4.0.1/ && ./autogen.sh && ./configure && make && make install && \ - cd .. && rm -rf /opt/swig-rel-4.0.1 /tmp/swig.tar.gz + tar xzf /tmp/swig.tar.gz -C /opt && \ + cd /opt/swig-rel-4.0.1/ && ./autogen.sh && ./configure && make && make install && \ + cd .. && rm -rf /opt/swig-rel-4.0.1 /tmp/swig.tar.gz ENV ANDROID_HOME /opt/android-sdk ENV PATH ${PATH}:${ANDROID_HOME}/tools/bin @@ -57,10 +57,10 @@ ENV PATH ${PATH}:${ANDROID_HOME}/tools/bin # Android SDK libraries, NDK RUN sdkmanager --sdk_root=${ANDROID_HOME} --update RUN (while sleep 1; do echo "y"; done) | sdkmanager --sdk_root=${ANDROID_HOME} 'build-tools;30.0.2' \ - 'platforms;android-29'\ - 'extras;android;m2repository'\ - 'extras;google;m2repository'\ - 'ndk;21.3.6528147' + 'platforms;android-29'\ + 'extras;android;m2repository'\ + 'extras;google;m2repository'\ + 'ndk;21.3.6528147' ENV ANDROID_SDK ${ANDROID_HOME} ENV ANDROID_NDK ${ANDROID_HOME}/ndk/21.3.6528147 diff --git a/lib/accel.cpp b/lib/accel.cpp new file mode 100644 index 0000000..d5d92aa --- /dev/null +++ b/lib/accel.cpp @@ -0,0 +1,67 @@ +/** + * Copyright (C) 2020 Savoir-faire Linux Inc. + * + * Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef ACCEL_H +#define ACCEL_H + +#include "accel.h" + +extern "C" { +#if LIBAVUTIL_VERSION_MAJOR < 56 +AVFrameSideData* +av_frame_new_side_data_from_buf(AVFrame* frame, enum AVFrameSideDataType type, AVBufferRef* buf) +{ + auto side_data = av_frame_new_side_data(frame, type, 0); + av_buffer_unref(&side_data->buf); + side_data->buf = buf; + side_data->data = side_data->buf->data; + side_data->size = side_data->buf->size; + return side_data; +} +#endif +} + +AVFrame* +transferToMainMemory(AVFrame* framePtr, AVPixelFormat desiredFormat) +{ + AVFrame* out = av_frame_alloc(); + auto desc = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(framePtr->format)); + + if (desc && !(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) { + out = framePtr; + return out; + } + + out->format = desiredFormat; + if (av_hwframe_transfer_data(out, framePtr, 0) < 0) { + out = framePtr; + return out; + } + + out->pts = framePtr->pts; + if (AVFrameSideData* side_data = av_frame_get_side_data(framePtr, AV_FRAME_DATA_DISPLAYMATRIX)) { + av_frame_new_side_data_from_buf(out, + AV_FRAME_DATA_DISPLAYMATRIX, + av_buffer_ref(side_data->buf)); + } + return out; +} + +#endif diff --git a/lib/accel.h b/lib/accel.h index 0f3ceec..4d78abe 100644 --- a/lib/accel.h +++ b/lib/accel.h @@ -37,41 +37,10 @@ using VideoFrame = DRing::VideoFrame; extern "C" { #if LIBAVUTIL_VERSION_MAJOR < 56 -AVFrameSideData* -av_frame_new_side_data_from_buf(AVFrame* frame, enum AVFrameSideDataType type, AVBufferRef* buf) -{ - auto side_data = av_frame_new_side_data(frame, type, 0); - av_buffer_unref(&side_data->buf); - side_data->buf = buf; - side_data->data = side_data->buf->data; - side_data->size = side_data->buf->size; - return side_data; -} +AVFrameSideData* av_frame_new_side_data_from_buf(AVFrame* frame, + enum AVFrameSideDataType type, + AVBufferRef* buf); #endif } -AVFrame* -transferToMainMemory(AVFrame* framePtr, AVPixelFormat desiredFormat) -{ - AVFrame* out = av_frame_alloc(); - auto desc = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(framePtr->format)); - - if (desc && not(desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) { - out = framePtr; - return out; - } - - out->format = desiredFormat; - if (av_hwframe_transfer_data(out, framePtr, 0) < 0) { - out = framePtr; - return out; - } - - out->pts = framePtr->pts; - if (AVFrameSideData* side_data = av_frame_get_side_data(framePtr, AV_FRAME_DATA_DISPLAYMATRIX)) { - av_frame_new_side_data_from_buf(out, - AV_FRAME_DATA_DISPLAYMATRIX, - av_buffer_ref(side_data->buf)); - } - return out; -} +AVFrame* transferToMainMemory(AVFrame* framePtr, AVPixelFormat desiredFormat); diff --git a/lib/framescaler.h b/lib/framescaler.h index 7ee271c..285b3ec 100644 --- a/lib/framescaler.h +++ b/lib/framescaler.h @@ -18,7 +18,9 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#pragma once +#ifndef FRAMESCALER_H +#define FRAMESCALER_H + extern "C" { #include <libavutil/avutil.h> #include <libavutil/frame.h> @@ -131,3 +133,5 @@ protected: SwsContext* ctx_; int mode_; }; + +#endif -- GitLab