2017-07-02 15:15:48 -07:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
import argparse
|
|
|
|
import zipfile
|
|
|
|
import ast
|
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
from utils import *
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
REQUIRED_KEYS = set(['name', 'blender', 'version'])
|
|
|
|
SCHEMA_VERSION = 1
|
|
|
|
|
|
|
|
class BadAddon(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def iter_addons(path: Path) -> (Path, dict):
|
|
|
|
"""
|
2017-07-02 16:55:17 -07:00
|
|
|
Generator, yields (path, bl_info) of blender addons in `path`.
|
2017-07-02 15:15:48 -07:00
|
|
|
"""
|
|
|
|
pass
|
2017-07-02 16:55:17 -07:00
|
|
|
for item in path.iterdir():
|
|
|
|
try:
|
|
|
|
yield(item, extract_blinfo(item))
|
|
|
|
except BadAddon as err:
|
|
|
|
log.debug("Skipping '{}': {}".format(item.name, err))
|
2017-07-02 15:15:48 -07:00
|
|
|
|
|
|
|
def parse_blinfo(source: str) -> dict:
|
|
|
|
"""Parse a python file and return its bl_info dict, if there is one (else return None)"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
tree = ast.parse(source)
|
|
|
|
except SyntaxError as e:
|
|
|
|
raise BadAddon("Syntax error") from e
|
|
|
|
|
|
|
|
for body in tree.body:
|
|
|
|
if body.__class__ != ast.Assign:
|
|
|
|
continue
|
|
|
|
if len(body.targets) != 1:
|
|
|
|
continue
|
|
|
|
if getattr(body.targets[0], 'id', '') != 'bl_info':
|
|
|
|
continue
|
|
|
|
|
|
|
|
return ast.literal_eval(body.value)
|
|
|
|
|
|
|
|
raise BadAddon('No bl_info found')
|
|
|
|
|
|
|
|
|
|
|
|
def extract_blinfo(item: Path) -> dict:
|
|
|
|
"""
|
|
|
|
Extract bl_info dict from addon at path (can be single file, python package, or zip)
|
|
|
|
"""
|
|
|
|
|
|
|
|
blinfo = None
|
|
|
|
addon_name = item.name
|
|
|
|
|
|
|
|
if not item.exists():
|
|
|
|
raise FileNotFoundError("Cannot extract blinfo from '%s'; no such file or directory" % item)
|
|
|
|
|
|
|
|
if item.is_dir():
|
|
|
|
fname = item / '__init__.py'
|
|
|
|
try:
|
|
|
|
with fname.open("r") as f:
|
|
|
|
blinfo = parse_blinfo(f.read())
|
|
|
|
except FileNotFoundError as err:
|
|
|
|
# directory with no __init__.py: not an addon
|
|
|
|
raise BadAddon("Directory '%s' doesn't contain __init__.py; not a python package" % item) from err
|
|
|
|
|
|
|
|
elif item.is_file():
|
|
|
|
try:
|
|
|
|
with zipfile.ZipFile(str(item), 'r') as z:
|
|
|
|
if len(z.namelist()) == 1:
|
|
|
|
# zipfile with one item: just read that item
|
|
|
|
blinfo = parse_blinfo(z.read(z.namelist()[0]))
|
|
|
|
else:
|
|
|
|
# zipfile with multiple items: try all __init__.py files
|
|
|
|
for fname in z.namelist():
|
|
|
|
# TODO: zips with multiple bl_infos might be a problem,
|
|
|
|
# not sure how such cases should be handled (if at all)
|
2017-07-02 16:55:17 -07:00
|
|
|
# for now we just break after the first one
|
2017-07-02 15:15:48 -07:00
|
|
|
if fname.endswith('__init__.py'):
|
|
|
|
try:
|
|
|
|
blinfo = parse_blinfo(z.read(fname))
|
|
|
|
break
|
|
|
|
except BadAddon:
|
|
|
|
continue
|
2017-07-02 16:55:17 -07:00
|
|
|
if blinfo is None:
|
|
|
|
raise BadAddon("Zipfile '%s' doesn't contain a readable bl_info dict" % item)
|
2017-07-02 15:15:48 -07:00
|
|
|
|
|
|
|
except zipfile.BadZipFile:
|
2017-07-02 16:55:17 -07:00
|
|
|
# If it's not a valid zip, assume file is just a normal file
|
2017-07-02 18:39:58 -07:00
|
|
|
try:
|
|
|
|
with item.open() as f:
|
|
|
|
blinfo = parse_blinfo(f.read())
|
|
|
|
except: #HACK
|
|
|
|
# If it's not a zip and its not parse-able python, then it's not an addon
|
|
|
|
raise BadAddon("File '%s' doesn't appear to be an addon" % item)
|
2017-07-02 15:15:48 -07:00
|
|
|
|
|
|
|
# This should not happen
|
|
|
|
if blinfo == None:
|
|
|
|
raise RuntimeError("Could not read addon '%s'" % addon_name)
|
|
|
|
|
|
|
|
return blinfo
|
|
|
|
|
|
|
|
|
|
|
|
|
2017-07-02 16:55:17 -07:00
|
|
|
def make_repo(path: Path):
|
|
|
|
"""Make repo.json for files in directory 'path'"""
|
2017-07-02 15:15:48 -07:00
|
|
|
|
|
|
|
repo_data = {}
|
|
|
|
package_data = []
|
|
|
|
|
2017-07-02 16:55:17 -07:00
|
|
|
if not path.is_dir():
|
|
|
|
raise FileNotFoundError(path)
|
2017-07-02 15:15:48 -07:00
|
|
|
|
2017-07-02 16:55:17 -07:00
|
|
|
for addon, bl_info in iter_addons(path):
|
2017-07-02 15:15:48 -07:00
|
|
|
package_datum = {}
|
|
|
|
|
|
|
|
if not REQUIRED_KEYS.issubset(set(bl_info)):
|
|
|
|
log.warning(
|
|
|
|
"Required key(s) '{}' not found in bl_info of '{}'".format(
|
|
|
|
"', '".join(REQUIRED_KEYS.difference(set(bl_info))), addon)
|
|
|
|
)
|
|
|
|
|
|
|
|
package_datum['bl_info'] = bl_info
|
|
|
|
package_datum['type'] = 'addon'
|
|
|
|
package_data.append(package_datum)
|
|
|
|
|
|
|
|
repo_data['packages'] = package_data
|
|
|
|
|
|
|
|
log.info("Repository generation successful")
|
|
|
|
cwd = Path.cwd()
|
|
|
|
dump_repo(cwd, repo_data)
|
|
|
|
log.info("repo.json written to %s" % cwd)
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
parser = argparse.ArgumentParser(description='Generate a blender package repository from a directory of addons')
|
|
|
|
|
|
|
|
parser.add_argument('-v', '--verbose',
|
|
|
|
help="Increase verbosity (can be used multiple times)",
|
|
|
|
action="count",
|
|
|
|
default=0)
|
|
|
|
parser.add_argument('path',
|
|
|
|
type=Path,
|
|
|
|
nargs='?',
|
|
|
|
default=Path.cwd(),
|
|
|
|
help="Path to addon directory")
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
log.level = args.verbose
|
|
|
|
logging.basicConfig(level=logging.INFO,
|
|
|
|
format='%(levelname)8s: %(message)s')
|
|
|
|
|
|
|
|
make_repo(args.path)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|