# oscillode # Copyright (C) 2024 oscillode Project Contributors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # blender_maxwell # Copyright (C) 2024 blender_maxwell Project Contributors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import sys import contextlib import logging import tempfile import typing as typ import zipfile from pathlib import Path import itertools import subprocess import tomli_w import info LogLevel: typ.TypeAlias = int BL_EXT__MANIFEST_FILENAME = 'blender_manifest.toml' BL_EXT__SCHEMA_VERSION = '1.0.0' BL_EXT__TYPE = 'add-on' #################### # - Generate Manifest #################### # See https://docs.blender.org/manual/en/4.2/extensions/getting_started.html # See https://packaging.python.org/en/latest/guides/writing-pyproject-toml ## TODO: More validation and such. _FIRST_MAINTAINER = info.PROJ_SPEC['project']['maintainers'][0] _SPDX_LICENSE_NAME = info.PROJ_SPEC['project']['license']['text'] BL_EXT_MANIFEST = { 'schema_version': BL_EXT__SCHEMA_VERSION, # Basics 'id': info.PROJ_SPEC['project']['name'], 'name': info.PROJ_SPEC['tool']['bl_ext']['pretty_name'], 'version': info.PROJ_SPEC['project']['version'], 'tagline': info.PROJ_SPEC['project']['description'], 'maintainer': f'{_FIRST_MAINTAINER["name"]} <{_FIRST_MAINTAINER["email"]}>', # Blender Compatibility 'type': BL_EXT__TYPE, 'blender_version_min': info.PROJ_SPEC['tool']['bl_ext']['blender_version_min'], 'blender_version_max': info.PROJ_SPEC['tool']['bl_ext']['blender_version_max'], 'platforms': list(info.PROJ_SPEC['tool']['bl_ext']['platforms'].keys()), # OS/Arch Compatibility ## See https://docs.blender.org/manual/en/dev/extensions/python_wheels.html 'wheels': [ f'./wheels/{wheel_path.name}' for wheel_path in info.PATH_WHEELS.iterdir() ], # Permissions ## * "files" (for access of any filesystem operations) ## * "network" (for internet access) ## * "clipboard" (to read and/or write the system clipboard) ## * "camera" (to capture photos and videos) ## * "microphone" (to capture audio) 'permissions': info.PROJ_SPEC['tool']['bl_ext']['permissions'], # Addon Tags 'tags': info.PROJ_SPEC['tool']['bl_ext']['bl_tags'], 'license': [f'SPDX:{_SPDX_LICENSE_NAME}'], 'copyright': info.PROJ_SPEC['tool']['bl_ext']['copyright'], 'website': info.PROJ_SPEC['project']['urls']['Homepage'], } #################### # - Generate Init Settings #################### def generate_init_settings_dict(profile: str) -> dict: profile_settings = info.PROJ_SPEC['tool']['bl_ext']['profiles'][profile] if profile_settings['use_path_local']: base_path = info.PATH_LOCAL else: base_path = Path('{USER}') log_levels = { None: logging.NOTSET, 'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, 'critical': logging.CRITICAL, } return { # File Logging 'use_log_file': profile_settings['use_log_file'], 'log_file_path': str(base_path / profile_settings['log_file_path']), 'log_file_level': log_levels[profile_settings['log_file_level']], # Console Logging 'use_log_console': profile_settings['use_log_console'], 'log_console_level': log_levels[profile_settings['log_console_level']], } #################### # - Wheel Downloader #################### def download_wheels() -> dict: with tempfile.NamedTemporaryFile(delete=False) as f_reqlock: reqlock_str = subprocess.check_output(['uv', 'export', '--no-dev']) f_reqlock.write(reqlock_str) reqlock_path = Path(f_reqlock.name) for platform, pypi_platform_tags in info.PROJ_SPEC['tool']['bl_ext'][ 'platforms' ].items(): print(f'[{platform}] Downloading Wheels...') print() print() platform_constraints = list( itertools.chain.from_iterable( [ ['--platform', pypi_platform_tag] for pypi_platform_tag in pypi_platform_tags ] ) ) subprocess.check_call( [ sys.executable, '-m', 'pip', 'download', '--requirement', str(reqlock_path), '--dest', str(info.PATH_WHEELS), '--require-hashes', '--only-binary', ':all:', '--python-version', info.REQ_PYTHON_VERSION, ] + platform_constraints ) print() #################### # - Pack Extension to ZIP #################### def pack_bl_extension( # noqa: PLR0913 profile: str, replace_if_exists: bool = False, ) -> typ.Iterator[Path]: """Context manager exposing a folder as a (temporary) zip file. Parameters: path_addon_pkg: Path to the folder containing __init__.py of the Blender addon. path_addon_zip: Path to the Addon ZIP to generate. path_pyproject_toml: Path to the `pyproject.toml` of the project. This is made available to the addon, to de-duplicate definition of name, The .zip file is deleted afterwards, unless `remove_after_close` is specified. """ # Delete Existing ZIP (maybe) if info.PATH_ZIP.is_file(): if replace_if_exists: msg = 'File already exists where extension ZIP would be generated ({info.PATH_ZIP})' raise ValueError(msg) info.PATH_ZIP.unlink() init_settings: dict = generate_init_settings_dict(profile) # Create New ZIP file of the addon directory with zipfile.ZipFile(info.PATH_ZIP, 'w', zipfile.ZIP_DEFLATED) as f_zip: # Write Blender Extension Manifest print('Writing Blender Extension Manifest...') f_zip.writestr(BL_EXT__MANIFEST_FILENAME, tomli_w.dumps(BL_EXT_MANIFEST)) # Write Init Settings print('Writing Init Settings...') f_zip.writestr( info.PROJ_SPEC['tool']['bl_ext']['packaging']['init_settings_filename'], tomli_w.dumps(init_settings), ) # Install Addon Files @ /* print('Writing Addon Files Settings...') for file_to_zip in info.PATH_PKG.rglob('*'): f_zip.write(file_to_zip, file_to_zip.relative_to(info.PATH_PKG.parent)) # Install Wheels @ /wheels/* print('Writing Wheels...') for wheel_to_zip in info.PATH_WHEELS.rglob('*'): f_zip.write(wheel_to_zip, Path('wheels') / wheel_to_zip.name) # Delete the ZIP print('Packed Blender Extension!') #################### # - Run Blender w/Clean Addon Reinstall #################### if __name__ == '__main__': if not list(info.PATH_WHEELS.iterdir()) or '--download-wheels' in sys.argv: download_wheels() profile = sys.argv[1] if sys.argv[1] in ['dev', 'release', 'release-debug']: pack_bl_extension(profile) else: msg = f'Packaging profile "{profile}" is invalid. Refer to source of pack.py for more information' raise ValueError(msg)