#!/usr/bin/env python import argparse import os import xml.etree.ElementTree as ET import zipfile from datetime import datetime from pathlib import Path import yaml def indent(elem: ET.Element, level: int = 0) -> None: """ Nicely formats output xml with newlines and spaces https://stackoverflow.com/a/33956544 """ i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def create_addon_xml(config: dict, source: str, py_version: str) -> None: """ Create addon.xml from template file """ # Load template file with open(f'{source}/.config/template.xml', 'r') as f: tree = ET.parse(f) root = tree.getroot() # Populate dependencies in template dependencies = config['dependencies'].get(py_version) for dep in dependencies: ET.SubElement(root.find('requires'), 'import', attrib=dep) # Populate version string addon_version = config.get('version') root.attrib['version'] = f'{addon_version}+{py_version}' # Populate Changelog date = datetime.today().strftime('%Y-%m-%d') changelog = config.get('changelog') for section in root.findall('extension'): news = section.findall('news') if news: news[0].text = f'v{addon_version} ({date}):\n{changelog}' # Format xml tree indent(root) # Write addon.xml tree.write(f'{source}/addon.xml', encoding='utf-8', xml_declaration=True) def zip_files(py_version: str, source: str, target: str, dev: bool) -> None: """ Create installable addon zip archive """ archive_name = f'plugin.video.jellycon+{py_version}.zip' with zipfile.ZipFile(f'{target}/{archive_name}', 'w') as z: for root, dirs, files in os.walk(args.source): for filename in filter(file_filter, files): file_path = os.path.join(root, filename) if dev or folder_filter(file_path): relative_path = os.path.join('plugin.video.jellycon', os.path.relpath(file_path, source)) z.write(file_path, relative_path) def file_filter(file_name: str) -> bool: """ True if file_name is meant to be included """ return ( not (file_name.startswith('plugin.video.jellycon') and file_name.endswith('.zip')) and not file_name.endswith('.pyo') and not file_name.endswith('.pyc') and not file_name.endswith('.pyd') ) def folder_filter(folder_name: str) -> bool: """ True if folder_name is meant to be included """ filters = [ '.ci', '.git', '.github', '.config', '.mypy_cache', '.pytest_cache', '__pycache__', 'venv', ] for f in filters: if f in folder_name.split(os.path.sep): return False return True if __name__ == '__main__': parser = argparse.ArgumentParser(description='Build flags:') parser.add_argument( '--version', type=str, choices=('py2', 'py3'), default='py3') parser.add_argument( '--source', type=Path, default=Path(__file__).absolute().parent) parser.add_argument( '--target', type=Path, default=Path(__file__).absolute().parent) parser.add_argument('--dev', dest='dev', action='store_true') parser.set_defaults(dev=False) args = parser.parse_args() # Load config file config_path = os.path.join(args.source, 'release.yaml') with open(config_path, 'r') as fh: release_config = yaml.safe_load(fh) create_addon_xml(release_config, args.source, args.version) zip_files(args.version, args.source, args.target, args.dev)