235 lines
7.5 KiB
Python
235 lines
7.5 KiB
Python
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
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)
|