Give repo generation tool clearer name
This commit is contained in:
228
bpkg-repogen
Executable file
228
bpkg-repogen
Executable file
@@ -0,0 +1,228 @@
|
||||
#!/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):
|
||||
"""
|
||||
Generator, yields (path, bl_info) of blender addons in `path`.
|
||||
"""
|
||||
for item in path.iterdir():
|
||||
try:
|
||||
yield (item, extract_blinfo(item))
|
||||
except BadAddon as err:
|
||||
log.debug("Skipping '{}': {}".format(item.name, err))
|
||||
|
||||
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 blinfo_from_zip(item: Path) -> dict:
|
||||
try:
|
||||
with zipfile.ZipFile(str(item), 'r') as z:
|
||||
if len(z.namelist()) == 1:
|
||||
# zipfile with one item: just read that item
|
||||
return 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)
|
||||
# for now we just break after the first one
|
||||
if fname.endswith('__init__.py'):
|
||||
try:
|
||||
return parse_blinfo(z.read(fname))
|
||||
except BadAddon:
|
||||
continue
|
||||
|
||||
raise BadAddon("Zipfile '%s' doesn't contain a readable bl_info dict" % item)
|
||||
|
||||
except zipfile.BadZipFile as err:
|
||||
raise BadAddon("Bad zipfile '%s'" % item) from err
|
||||
|
||||
|
||||
def blinfo_from_py(item: Path) -> dict:
|
||||
with item.open() as f:
|
||||
return parse_blinfo(f.read())
|
||||
|
||||
def blinfo_from_dir(item: Path) -> dict:
|
||||
try:
|
||||
f = (item / '__init__.py').open("r")
|
||||
except FileNotFoundError as err:
|
||||
raise BadAddon("Directory '%s' doesn't contain __init__.py; not a python package" % item) from err
|
||||
with f:
|
||||
return parse_blinfo(f.read())
|
||||
|
||||
def extract_blinfo(item: Path) -> dict:
|
||||
"""
|
||||
Extract bl_info dict from addon at path (can be single file, python package, or zip)
|
||||
"""
|
||||
|
||||
if not item.exists():
|
||||
raise FileNotFoundError("Cannot extract blinfo from '%s'; no such file or directory" % item)
|
||||
|
||||
if item.is_dir():
|
||||
return blinfo_from_dir(item)
|
||||
|
||||
if item.is_file():
|
||||
ext = item.suffix.lower()
|
||||
|
||||
if ext == '.zip':
|
||||
return blinfo_from_zip(item)
|
||||
elif ext == '.py':
|
||||
return blinfo_from_py(item)
|
||||
else:
|
||||
raise BadAddon("File '%s' doesn't have a .zip or .py extension; not an addon" % item)
|
||||
|
||||
# This should not happen
|
||||
raise RuntimeError("Could not read addon '%s'" % item.name)
|
||||
|
||||
class Package:
|
||||
"""
|
||||
Stores package path and metadata
|
||||
"""
|
||||
|
||||
def __init__(self, path: Path, bl_info: dict):
|
||||
self.bl_info = bl_info
|
||||
self.path = path
|
||||
self.url = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Return a dict representation of the package
|
||||
"""
|
||||
return {
|
||||
'bl_info': self.bl_info,
|
||||
'url': self.url,
|
||||
}
|
||||
|
||||
class Repository:
|
||||
"""
|
||||
Stores repository metadata (including a list of packages)
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.url = None
|
||||
self.packages = []
|
||||
|
||||
def add_package(self, pkg: Package):
|
||||
"""
|
||||
Add a package to the repository
|
||||
"""
|
||||
# if pkg.url is None:
|
||||
# pkg.url =
|
||||
self.packages.append(pkg)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
Return a dict representation of the repository
|
||||
"""
|
||||
return {
|
||||
'name': self.name,
|
||||
'packages': [p.to_dict() for p in self.packages],
|
||||
'url': self.url,
|
||||
}
|
||||
|
||||
def dump(self, path: Path):
|
||||
"""
|
||||
Dump repository as a repo.json file in 'path'
|
||||
"""
|
||||
with (path / 'repo.json').open('w', encoding='utf-8') as repo_file:
|
||||
json.dump(self.to_dict(), repo_file, indent=4, sort_keys=True)
|
||||
log.info("repo.json written to %s" % path)
|
||||
|
||||
|
||||
def make_repo(path: Path, name: str, baseurl: str) -> Repository:
|
||||
"""Make repo.json for files in directory 'path'"""
|
||||
|
||||
repo = Repository(name)
|
||||
|
||||
if not path.is_dir():
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
for addon, bl_info in iter_addons(path):
|
||||
|
||||
# Check if we have all bl_info fields we want
|
||||
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 = Package(addon, bl_info)
|
||||
package.url = baseurl + package.path.name
|
||||
repo.add_package(package)
|
||||
|
||||
log.info("Repository generation successful")
|
||||
return repo
|
||||
|
||||
|
||||
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('-u', '--baseurl',
|
||||
#TODO: support relative URLs
|
||||
help="Component of URL leading up to the package filename.")
|
||||
parser.add_argument('-n', '--name',
|
||||
help="Name of repo (defaults to basename of 'path')")
|
||||
parser.add_argument('-o', '--output',
|
||||
help="Directory in which to write repo.json file",
|
||||
type=Path,
|
||||
default=Path.cwd())
|
||||
parser.add_argument('path',
|
||||
type=Path,
|
||||
help="Path to addon directory")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(format='%(levelname)8s: %(message)s', level=logging.INFO)
|
||||
log.level += args.verbose
|
||||
|
||||
if args.name is None:
|
||||
args.name = args.path.name
|
||||
|
||||
if args.baseurl is None:
|
||||
log.warning("No baseurl given, package URLs will just be filenames")
|
||||
args.baseurl = ""
|
||||
log.debug("Repository name: '%s'" % args.name)
|
||||
|
||||
repo = make_repo(args.path, args.name, args.baseurl)
|
||||
repo.dump(args.output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Reference in New Issue
Block a user