mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
311 lines
8.7 KiB
Python
311 lines
8.7 KiB
Python
import os
|
|
import sys
|
|
import re
|
|
import shutil
|
|
import argparse
|
|
import zipfile
|
|
import platform
|
|
import collections
|
|
from pathlib import Path
|
|
from typing import Any, Optional, Iterable, Pattern, List, Tuple
|
|
|
|
# Patterns of directories to be skipped for server part of addon
|
|
IGNORE_DIR_PATTERNS: List[Pattern] = [
|
|
re.compile(pattern)
|
|
for pattern in {
|
|
# Skip directories starting with '.'
|
|
r"^\.",
|
|
# Skip any pycache folders
|
|
"^__pycache__$"
|
|
}
|
|
]
|
|
|
|
# Patterns of files to be skipped for server part of addon
|
|
IGNORE_FILE_PATTERNS: List[Pattern] = [
|
|
re.compile(pattern)
|
|
for pattern in {
|
|
# Skip files starting with '.'
|
|
# NOTE this could be an issue in some cases
|
|
r"^\.",
|
|
# Skip '.pyc' files
|
|
r"\.pyc$"
|
|
}
|
|
]
|
|
|
|
IGNORED_HOSTS = [
|
|
"flame",
|
|
"harmony",
|
|
]
|
|
|
|
IGNORED_MODULES = []
|
|
|
|
PACKAGE_PY_TEMPLATE = """name = "{addon_name}"
|
|
version = "{addon_version}"
|
|
plugin_for = ["ayon_server"]
|
|
"""
|
|
|
|
|
|
class ZipFileLongPaths(zipfile.ZipFile):
|
|
"""Allows longer paths in zip files.
|
|
|
|
Regular DOS paths are limited to MAX_PATH (260) characters, including
|
|
the string's terminating NUL character.
|
|
That limit can be exceeded by using an extended-length path that
|
|
starts with the '\\?\' prefix.
|
|
"""
|
|
_is_windows = platform.system().lower() == "windows"
|
|
|
|
def _extract_member(self, member, tpath, pwd):
|
|
if self._is_windows:
|
|
tpath = os.path.abspath(tpath)
|
|
if tpath.startswith("\\\\"):
|
|
tpath = "\\\\?\\UNC\\" + tpath[2:]
|
|
else:
|
|
tpath = "\\\\?\\" + tpath
|
|
|
|
return super(ZipFileLongPaths, self)._extract_member(
|
|
member, tpath, pwd
|
|
)
|
|
|
|
|
|
def _value_match_regexes(value: str, regexes: Iterable[Pattern]) -> bool:
|
|
return any(
|
|
regex.search(value)
|
|
for regex in regexes
|
|
)
|
|
|
|
|
|
def find_files_in_subdir(
|
|
src_path: str,
|
|
ignore_file_patterns: Optional[List[Pattern]] = None,
|
|
ignore_dir_patterns: Optional[List[Pattern]] = None,
|
|
ignore_subdirs: Optional[Iterable[Tuple[str]]] = None
|
|
):
|
|
"""Find all files to copy in subdirectories of given path.
|
|
|
|
All files that match any of the patterns in 'ignore_file_patterns' will
|
|
be skipped and any directories that match any of the patterns in
|
|
'ignore_dir_patterns' will be skipped with all subfiles.
|
|
|
|
Args:
|
|
src_path (str): Path to directory to search in.
|
|
ignore_file_patterns (Optional[List[Pattern]]): List of regexes
|
|
to match files to ignore.
|
|
ignore_dir_patterns (Optional[List[Pattern]]): List of regexes
|
|
to match directories to ignore.
|
|
ignore_subdirs (Optional[Iterable[Tuple[str]]]): List of
|
|
subdirectories to ignore.
|
|
|
|
Returns:
|
|
List[Tuple[str, str]]: List of tuples with path to file and parent
|
|
directories relative to 'src_path'.
|
|
"""
|
|
|
|
if ignore_file_patterns is None:
|
|
ignore_file_patterns = IGNORE_FILE_PATTERNS
|
|
|
|
if ignore_dir_patterns is None:
|
|
ignore_dir_patterns = IGNORE_DIR_PATTERNS
|
|
output: list[tuple[str, str]] = []
|
|
|
|
hierarchy_queue = collections.deque()
|
|
hierarchy_queue.append((src_path, []))
|
|
while hierarchy_queue:
|
|
item: tuple[str, str] = hierarchy_queue.popleft()
|
|
dirpath, parents = item
|
|
if ignore_subdirs and parents in ignore_subdirs:
|
|
continue
|
|
for name in os.listdir(dirpath):
|
|
path = os.path.join(dirpath, name)
|
|
if os.path.isfile(path):
|
|
if not _value_match_regexes(name, ignore_file_patterns):
|
|
items = list(parents)
|
|
items.append(name)
|
|
output.append((path, os.path.sep.join(items)))
|
|
continue
|
|
|
|
if not _value_match_regexes(name, ignore_dir_patterns):
|
|
items = list(parents)
|
|
items.append(name)
|
|
hierarchy_queue.append((path, items))
|
|
|
|
return output
|
|
|
|
|
|
def read_addon_version(version_path: Path) -> str:
|
|
# Read version
|
|
version_content: dict[str, Any] = {}
|
|
with open(str(version_path), "r") as stream:
|
|
exec(stream.read(), version_content)
|
|
return version_content["__version__"]
|
|
|
|
|
|
def get_addon_version(addon_dir: Path) -> str:
|
|
return read_addon_version(addon_dir / "server" / "version.py")
|
|
|
|
|
|
def create_addon_zip(
|
|
output_dir: Path,
|
|
addon_name: str,
|
|
addon_version: str,
|
|
keep_source: bool,
|
|
):
|
|
zip_filepath = output_dir / f"{addon_name}-{addon_version}.zip"
|
|
|
|
addon_output_dir = output_dir / addon_name / addon_version
|
|
with ZipFileLongPaths(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
# Add client code content to zip
|
|
src_root = os.path.normpath(str(addon_output_dir.absolute()))
|
|
src_root_offset = len(src_root) + 1
|
|
for root, _, filenames in os.walk(str(addon_output_dir)):
|
|
rel_root = ""
|
|
if root != src_root:
|
|
rel_root = root[src_root_offset:]
|
|
|
|
for filename in filenames:
|
|
src_path = os.path.join(root, filename)
|
|
if rel_root:
|
|
dst_path = os.path.join(rel_root, filename)
|
|
else:
|
|
dst_path = filename
|
|
|
|
zipf.write(src_path, dst_path)
|
|
|
|
if not keep_source:
|
|
shutil.rmtree(str(output_dir / addon_name))
|
|
|
|
|
|
def create_addon_package(
|
|
addon_dir: Path,
|
|
output_dir: Path,
|
|
create_zip: bool,
|
|
keep_source: bool,
|
|
):
|
|
addon_version = get_addon_version(addon_dir)
|
|
|
|
addon_output_dir = output_dir / addon_dir.name / addon_version
|
|
if addon_output_dir.exists():
|
|
shutil.rmtree(str(addon_output_dir))
|
|
addon_output_dir.mkdir(parents=True)
|
|
|
|
# Copy server content
|
|
package_py = addon_output_dir / "package.py"
|
|
addon_name = addon_dir.name
|
|
if addon_name == "royal_render":
|
|
addon_name = "royalrender"
|
|
package_py_content = PACKAGE_PY_TEMPLATE.format(
|
|
addon_name=addon_name, addon_version=addon_version
|
|
)
|
|
|
|
with open(package_py, "w+") as pkg_py:
|
|
pkg_py.write(package_py_content)
|
|
|
|
server_dir = addon_dir / "server"
|
|
shutil.copytree(
|
|
server_dir, addon_output_dir / "server", dirs_exist_ok=True
|
|
)
|
|
|
|
if create_zip:
|
|
create_addon_zip(
|
|
output_dir, addon_dir.name, addon_version, keep_source
|
|
)
|
|
|
|
|
|
def main(
|
|
output_dir=None,
|
|
skip_zip=True,
|
|
keep_source=False,
|
|
clear_output_dir=False,
|
|
addons=None,
|
|
):
|
|
current_dir = Path(os.path.dirname(os.path.abspath(__file__)))
|
|
create_zip = not skip_zip
|
|
|
|
if output_dir:
|
|
output_dir = Path(output_dir)
|
|
else:
|
|
output_dir = current_dir / "packages"
|
|
|
|
if output_dir.exists() and clear_output_dir:
|
|
shutil.rmtree(str(output_dir))
|
|
|
|
print("Package creation started...")
|
|
print(f"Output directory: {output_dir}")
|
|
|
|
# Make sure output dir is created
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
ignored_addons = set(IGNORED_HOSTS) | set(IGNORED_MODULES)
|
|
for addon_dir in current_dir.iterdir():
|
|
if not addon_dir.is_dir():
|
|
continue
|
|
|
|
if addons and addon_dir.name not in addons:
|
|
continue
|
|
|
|
if addon_dir.name in ignored_addons:
|
|
continue
|
|
|
|
server_dir = addon_dir / "server"
|
|
if not server_dir.exists():
|
|
continue
|
|
|
|
create_addon_package(
|
|
addon_dir, output_dir, create_zip, keep_source
|
|
)
|
|
|
|
print(f"- package '{addon_dir.name}' created")
|
|
print(f"Package creation finished. Output directory: {output_dir}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"--skip-zip",
|
|
dest="skip_zip",
|
|
action="store_true",
|
|
help=(
|
|
"Skip zipping server package and create only"
|
|
" server folder structure."
|
|
)
|
|
)
|
|
parser.add_argument(
|
|
"--keep-sources",
|
|
dest="keep_sources",
|
|
action="store_true",
|
|
help=(
|
|
"Keep folder structure when server package is created."
|
|
)
|
|
)
|
|
parser.add_argument(
|
|
"-o", "--output",
|
|
dest="output_dir",
|
|
default=None,
|
|
help=(
|
|
"Directory path where package will be created"
|
|
" (Will be purged if already exists!)"
|
|
)
|
|
)
|
|
parser.add_argument(
|
|
"-c", "--clear-output-dir",
|
|
dest="clear_output_dir",
|
|
action="store_true",
|
|
help=(
|
|
"Clear output directory before package creation."
|
|
)
|
|
)
|
|
parser.add_argument(
|
|
"-a",
|
|
"--addon",
|
|
dest="addons",
|
|
action="append",
|
|
help="Limit addon creation to given addon name",
|
|
)
|
|
|
|
args = parser.parse_args(sys.argv[1:])
|
|
main(
|
|
args.output_dir,
|
|
args.skip_zip,
|
|
args.keep_sources,
|
|
args.clear_output_dir,
|
|
args.addons,
|
|
)
|