oscillode/scripts/pack.py

235 lines
7.5 KiB
Python
Raw Normal View History

2024-09-26 10:23:17 +02:00
# 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.
2024-04-07 18:39:27 +02:00
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,
2024-04-07 18:39:27 +02:00
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)