diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f946cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/modules.json +/venv/** +/.idea/** + +/cache/** +/out/** + diff --git a/build.py b/build.py new file mode 100755 index 0000000..e3d7822 --- /dev/null +++ b/build.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +### +# File: build.py +# Project: Kodi-Module-Generator +# File Created: Monday, 10th May 2021 8:52:42 pm +# Author: Josh.5 (jsunnex@gmail.com) +# ----- +# Last Modified: Monday, 10th May 2021 11:34:57 pm +# Modified By: Josh.5 (jsunnex@gmail.com) +### + +# Requirements = `python3 -m pip install pipgrip` + +import json +import os +import shutil +import subprocess + +template_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'template') +out_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'out') +cache_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'cache') + + +def install_module(module_dir, module_name, version): + module_lib_dir = os.path.abspath(os.path.join(module_dir, 'lib')) + if not module_lib_dir: + os.makedirs(module_lib_dir) + + # First install local + cmd = 'python3 -m pip install --user {}=={}'.format(module_name, version) + subprocess.call(cmd, shell=True) + + # Then install to module directory + cmd = 'python3 -m pip install --ignore-installed --no-dependencies --target={} {}=={}'.format(module_lib_dir, + module_name, + version) + subprocess.call(cmd, shell=True) + + +def update_module_data(module_dir, module_name, module_deps_list): + # Read data from module + res = subprocess.check_output('python3 -m pip show {}'.format(module_name), shell=True) + module_version = '' + module_author = '' + module_author_email = '' + module_license = '' + module_summary = '' + module_website = 'https://pypi.org/project/{}/'.format(module_name) + for line in res.splitlines(): + x = str(line.decode("utf-8")) + print(x) + if x.startswith('Version:'): + module_version = x.split(': ')[1].strip() + elif x.startswith('Author:'): + module_author = x.split(': ')[1].strip() + elif x.startswith('Author-email:'): + module_author_email = x.split(': ')[1].strip() + elif x.startswith('License:'): + module_license = x.split(': ')[1].strip() + elif x.startswith('Summary:'): + module_summary = x.split(': ')[1].strip() + + module_xml = os.path.join(module_dir, 'addon.xml') + # Read in the file + with open(module_xml, 'r') as file: + file_data = file.read() + + # Replace the target string with data from pip show + if module_author_email: + module_author = '{} ({})'.format(module_author, module_author_email) + file_data = file_data.replace('PYTHON_MODULE_NAME', module_name) + file_data = file_data.replace('PYTHON_MODULE_VERSION', module_version) + file_data = file_data.replace('PYTHON_MODULE_AUTHOR', module_author) + file_data = file_data.replace('PYTHON_MODULE_LICENSE', module_license) + file_data = file_data.replace('PYTHON_MODULE_SUMMARY', module_summary) + file_data = file_data.replace('PYTHON_MODULE_DESCRIPTION', module_summary) + file_data = file_data.replace('PYTHON_MODULE_WEBSITE', module_website) + + # Append dependencies list + template = ' ' + deps_to_add = "" + for dep in module_deps_list: + line = template.format(dep.get('name'), dep.get('version')) + deps_to_add = "{}\n{}".format(deps_to_add, line) + file_data = file_data.replace('PYTHON_MODULE_DEPENDENCIES', deps_to_add) + + # Write the file out again + with open(module_xml, 'w') as file: + file.write(file_data) + + +def sanitize_module(module_name): + module_dir = os.path.join(out_dir, 'script.module.{}'.format(module_name)) + sanitize_script = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'sanitize.sh') + cmd = '{} {}'.format(sanitize_script, module_dir) + subprocess.call(cmd, shell=True) + + +def build_script_module(module_name, version, module_deps): + module_dir = os.path.join(out_dir, 'script.module.{}'.format(module_name)) + if not os.path.exists(module_dir): + os.makedirs(module_dir) + + if not os.path.exists(os.path.join(module_dir, 'addon.xml')): + shutil.copy(os.path.join(template_dir, 'addon.xml'), os.path.join(module_dir, 'addon.xml')) + + if not os.path.exists(os.path.join(module_dir, 'icon.png')): + shutil.copy(os.path.join(template_dir, 'icon.png'), os.path.join(module_dir, 'icon.png')) + + # Install modules + install_module(module_dir, module_name, version) + + # Sanitize module + sanitize_module(module_name) + + # Write addon.xml + update_module_data(module_dir, module_name, module_deps) + + +def process_module_deps_list(deps_list): + for dep in deps_list: + module_name = dep.get('name') + module_version = dep.get('version') + child_deps = dep.get('dependencies', []) + + # Build module for each item found in depends list + process_module_deps_list(child_deps) + + # Build module + print("Building Kodi Python module for {} v{}".format(module_name, module_version)) + build_script_module(module_name, module_version, child_deps) + + +def create_dep_list(module, version): + deps_cache = os.path.join(cache_dir, 'deps-{}-{}.json'.format(module, version)) + + if not os.path.exists(deps_cache): + print("Fetching Dependencies list for {} v{}".format(module, version)) + cmd = 'pipgrip --tree --json unmanic'.format(module, version) + res = subprocess.check_output(cmd, shell=True) + decoded = str(res.decode("utf-8")) + data = json.loads(decoded) + + with open(deps_cache, 'w') as file: + json.dump(data, file, indent=2) + + with open(deps_cache, 'r') as file: + deps_list = json.load(file) + + return deps_list + + +def run(): + if not os.path.exists(template_dir): + os.makedirs(template_dir) + if not os.path.exists(out_dir): + os.makedirs(out_dir) + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + try: + with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'modules.json'), 'r') as file: + modules = json.load(file) + except Exception as e: + print("No modules configured... - {}".format(str(e))) + modules = [] + + for module_data in modules: + original_module = module_data.get('module') + version = module_data.get('version') + deps_list = create_dep_list(original_module, version) + process_module_deps_list(deps_list) + + +if __name__ == '__main__': + run() diff --git a/sanitize.sh b/sanitize.sh new file mode 100755 index 0000000..ac9ac8d --- /dev/null +++ b/sanitize.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +### +# File: sanitize.sh +# Project: Kodi-Module-Generator +# File Created: Monday, 10th May 2021 11:02:32 pm +# Author: Josh.5 (jsunnex@gmail.com) +# ----- +# Last Modified: Monday, 10th May 2021 11:10:23 pm +# Modified By: Josh.5 (jsunnex@gmail.com) +### + + + +addon_root=${@} +lib_dir="${addon_root}/lib" + + +print_step(){ + ten=" " + spaces="${ten}${ten}${ten}${ten}${ten}${ten}${ten}${ten}${ten}" + message=" - ${@}" + message="${message:0:70}${spaces:0:$((70 - ${#message}))}" + echo -ne "${message}" +} +print_sub_step(){ + ten=" " + spaces="${ten}${ten}${ten}${ten}${ten}${ten}${ten}${ten}${ten}" + message=" - ${@}" + message="${message:0:70}${spaces:0:$((70 - ${#message}))}" + echo -ne "${message}" +} +mark_step(){ + RED="\e[31m" + GREEN="\e[32m" + ENDCOLOR="\e[0m" + status_message="" + [[ ${1} == 'failed' ]] && status_message="${RED}[FAILED]${ENDCOLOR}" + [[ ${1} == 'success' ]] && status_message="${GREEN}[SUCCESS]${ENDCOLOR}" + echo -e "${status_message}" +} + + +# Remove everything except the Python source +remove_extensions=( + "ans" + "bz2" + "db" + "dll" + "exe" + "gz" + "mo" + "pyc" + "pyo" + "so" + "xbt" + "xpr" +) +print_step "Cleaning out unnecessary binary files:" +mark_step +for ext in "${remove_extensions[@]}"; do + print_sub_step "Delete all '*.${ext}' files..." + find "${lib_dir}" -type f -iname "*.${ext}" -delete + [[ $? > 0 ]] && mark_step failed && exit 1 + mark_step success +done +print_step "Cleaning out Python cache directories" +find "${lib_dir}" -type d -name "__pycache__" -exec rm -rf {} + +[[ $? > 0 ]] && mark_step failed && exit 1 +mark_step success +print_step "Cleaning out Python 'dist-info' directories" +find "${lib_dir}" -type d -name "*.dist-info" -exec rm -rf {} + +[[ $? > 0 ]] && mark_step failed && exit 1 +mark_step success + +# Remove Python bin +print_step "Removing 'bin' Python directory" +rm -rf "${lib_dir}/bin" +[[ $? > 0 ]] && mark_step failed && exit 1 +mark_step success + +# Ensure all files are not executable +print_step "Ensure all items in the lib directory are not executable" +find "${lib_dir}" -type f -exec chmod a-x {} + +[[ $? > 0 ]] && mark_step failed && exit 1 +mark_step success + +exit 0 diff --git a/template/addon.xml b/template/addon.xml new file mode 100644 index 0000000..8fafb3b --- /dev/null +++ b/template/addon.xml @@ -0,0 +1,23 @@ + + + + PYTHON_MODULE_DEPENDENCIES + + + + PYTHON_MODULE_SUMMARY + PYTHON_MODULE_DESCRIPTION + PYTHON_MODULE_LICENSE + all + PYTHON_MODULE_WEBSITE + PYTHON_MODULE_WEBSITE + + icon.png + + + \ No newline at end of file diff --git a/template/icon.png b/template/icon.png new file mode 100644 index 0000000..893dda0 Binary files /dev/null and b/template/icon.png differ