Commit: 0ff4627729cbcaaaf9c0545792ace21e35736cb5 Author: gandalf3 Date: Sat Aug 26 02:07:18 2017 -0700 Branches: soc-2017-package_manager https://developer.blender.org/rB0ff4627729cbcaaaf9c0545792ace21e35736cb5
Move package manager code out of addon Code from addon repository: https://developer.blender.org/diffusion/BPMA/ =================================================================== A release/scripts/modules/bpkg/__init__.py A release/scripts/modules/bpkg/exceptions.py A release/scripts/modules/bpkg/messages.py A release/scripts/modules/bpkg/subproc.py A release/scripts/modules/bpkg/types.py A release/scripts/modules/bpkg/utils.py M release/scripts/startup/bl_operators/__init__.py A release/scripts/startup/bl_operators/package.py M release/scripts/startup/bl_ui/__init__.py A release/scripts/startup/bl_ui/properties_package.py M release/scripts/startup/bl_ui/space_userpref.py =================================================================== diff --git a/release/scripts/modules/bpkg/__init__.py b/release/scripts/modules/bpkg/__init__.py new file mode 100644 index 00000000000..01fe1a422a2 --- /dev/null +++ b/release/scripts/modules/bpkg/__init__.py @@ -0,0 +1,71 @@ +# __all__ = ( +# "exceptions", +# "types", +# ) + +from . import utils +from .types import ( + Package, + ConsolidatedPackage, + Repository, + ) +from pathlib import Path +from collections import OrderedDict +import logging +import bpy + +packages = {} + +def get_installed_packages(refresh=False) -> list: + """Get list of packages installed on disk""" + import addon_utils + installed_pkgs = [] + for mod in addon_utils.modules(refresh=refresh): + pkg = Package.from_module(mod) + pkg.installed = True + installed_pkgs.append(pkg) + return installed_pkgs + +def get_repo_storage_path() -> Path: + return Path(bpy.utils.user_resource('CONFIG', 'repositories')) + +def get_repositories() -> list: + """ + Get list of downloaded repositories and update wm.package_repositories + """ + log = logging.getLogger(__name__ + ".get_repositories") + storage_path = get_repo_storage_path() + repos = utils.load_repositories(storage_path) + log.debug("repos: %s", repos) + + return repos + + +def list_packages() -> OrderedDict: # {{{ + """Make an OrderedDict of ConsolidatedPackages from known repositories + + installed packages, keyed by package name""" + + # log = logging.getLogger(__name__ + ".build_composite_packagelist") + masterlist = {} + installed_packages = get_installed_packages(refresh=True) + known_repositories = get_repositories() + + for repo in known_repositories: + for pkg in repo.packages: + pkg.repositories.add(repo) + if pkg.name is None: + return OrderedDict() + if pkg.name in masterlist: + masterlist[pkg.name].add_version(pkg) + else: + masterlist[pkg.name] = ConsolidatedPackage(pkg) + + for pkg in installed_packages: + if pkg.name in masterlist: + masterlist[pkg.name].add_version(pkg) + else: + masterlist[pkg.name] = ConsolidatedPackage(pkg) + + # log.debug(masterlist[None].__dict__) + return OrderedDict(sorted(masterlist.items())) +# }}} diff --git a/release/scripts/modules/bpkg/exceptions.py b/release/scripts/modules/bpkg/exceptions.py new file mode 100644 index 00000000000..0e9d3395ed1 --- /dev/null +++ b/release/scripts/modules/bpkg/exceptions.py @@ -0,0 +1,14 @@ +class BpkgException(Exception): + """Superclass for all package manager exceptions""" + +class InstallException(BpkgException): + """Raised when there is an error during installation""" + +class DownloadException(BpkgException): + """Raised when there is an error downloading something""" + +class BadRepositoryException(BpkgException): + """Raised when there is an error while reading or manipulating a repository""" + +class PackageException(BpkgException): + """Raised when there is an error while manipulating a package""" diff --git a/release/scripts/modules/bpkg/messages.py b/release/scripts/modules/bpkg/messages.py new file mode 100644 index 00000000000..ce754d49811 --- /dev/null +++ b/release/scripts/modules/bpkg/messages.py @@ -0,0 +1,73 @@ +from .types import Repository + +class Message: + """Superclass for all message sent over pipes.""" + + +# Blender messages + +class BlenderMessage(Message): + """Superclass for all messages sent from Blender to the subprocess.""" + +class Abort(BlenderMessage): + """Sent when the user requests abortion of a task.""" + + +# Subproc messages + +class SubprocMessage(Message): + """Superclass for all messages sent from the subprocess to Blender.""" + +class Progress(SubprocMessage): + """Send from subprocess to Blender to report progress. + + :ivar progress: the progress percentage, from 0-1. + """ + + def __init__(self, progress: float): + self.progress = progress + +class Success(SubprocMessage): + """Sent when an operation finished sucessfully.""" + +class RepositoryResult(SubprocMessage): + """Sent when an operation returns a repository to be used on the parent process.""" + + def __init__(self, repository_name: str): + self.repository = repository + +class Aborted(SubprocMessage): + """Sent as response to Abort message.""" + +# subproc warnings + +class SubprocWarning(SubprocMessage): + """Superclass for all non-fatal warning messages sent from the subprocess.""" + + def __init__(self, message: str): + self.message = message + +# subproc errors + +class SubprocError(SubprocMessage): + """Superclass for all fatal error messages sent from the subprocess.""" + + def __init__(self, message: str): + self.message = message + +class InstallError(SubprocError): + """Sent when there was an error installing something.""" + +class UninstallError(SubprocError): + """Sent when there was an error uninstalling something.""" + +class BadRepositoryError(SubprocError): + """Sent when a repository can't be used for some reason""" + +class DownloadError(SubprocMessage): + """Sent when there was an error downloading something.""" + + def __init__(self, message: str, status_code: int = None): + self.status_code = status_code + self.message = message + diff --git a/release/scripts/modules/bpkg/subproc.py b/release/scripts/modules/bpkg/subproc.py new file mode 100644 index 00000000000..aa249517d61 --- /dev/null +++ b/release/scripts/modules/bpkg/subproc.py @@ -0,0 +1,85 @@ +""" +All the stuff that needs to run in a subprocess. +""" + +from pathlib import Path +from . import ( + messages, + exceptions, + utils, +) +from .types import ( + Package, + Repository, +) +import logging + +def download_and_install_package(pipe_to_blender, package: Package, install_path: Path): + """Downloads and installs the given package.""" + + log = logging.getLogger(__name__ + '.download_and_install') + + from . import cache + cache_dir = cache.cache_directory('downloads') + + try: + package.install(install_path, cache_dir) + except exceptions.DownloadException as err: + pipe_to_blender.send(messages.DownloadError(err)) + log.exception(err) + except exceptions.InstallException as err: + pipe_to_blender.send(messages.InstallError(err)) + log.exception(err) + + pipe_to_blender.send(messages.Success()) + + +def uninstall_package(pipe_to_blender, package: Package, install_path: Path): + """Deletes the given package's files from the install directory""" + #TODO: move package to cache and present an "undo" button to user, to give nicer UX on misclicks + + for pkgfile in [install_path / Path(p) for p in package.files]: + if not pkgfile.exists(): + pipe_to_blender.send(messages.UninstallError("Could not find file owned by package: '%s'. Refusing to uninstall." % pkgfile)) + return None + + for pkgfile in [install_path / Path(p) for p in package.files]: + utils.rm(pkgfile) + + pipe_to_blender.send(messages.Success()) + + +def refresh_repositories(pipe_to_blender, repo_storage_path: Path, repository_urls: str, progress_callback=None): + """Downloads and stores the given repository""" + + log = logging.getLogger(__name__ + '.refresh_repository') + + if progress_callback is None: + progress_callback = lambda x: None + progress_callback(0.0) + + repos = utils.load_repositories(repo_storage_path) + + def prog(progress: float): + progress_callback(progress/len(repos)) + + known_repo_urls = [repo.url for repo in repos] + for repo_url in repository_urls: + if repo_url not in known_repo_urls: + repos.append(Repository(repo_url)) + + for repo in repos: + log.debug("repo name: %s, url: %s", repo.name, repo.url) + for repo in repos: + try: + repo.refresh(repo_storage_path, progress_callback=prog) + except exceptions.DownloadException as err: + pipe_to_blender.send(messages.DownloadError(err)) + log.exception("Download error") + except exceptions.BadRepositoryException as err: + pipe_to_blender.send(messages.BadRepositoryError(err)) + log.exception("Bad repository") + + progress_callback(1.0) + pipe_to_blender.send(messages.Success()) + diff --git a/release/scripts/modules/bpkg/types.py b/release/scripts/modules/bpkg/types.py new file mode 100644 index 00000000000..7ed99b2d997 --- /dev/null +++ b/release/scripts/modules/bpkg/types.py @@ -0,0 +1,501 @@ +import logging +import json +from pathlib import Path +from . import exceptions +from . import utils + +class Package: + """ + Stores package methods and metadata + """ + + log = logging.getLogger(__name__ + ".Package") + + def __init__(self, package_dict:dict = None): + self.bl_info = {} + self.url = "" + self.files = [] + + self.repositories = set() + self.installed_location = None + self.module_name = None + + self.installed = False + self.is_user = False + self.enabled = False + + self.set_from_dict(package_dict) + + def test_is_user(self) -> bool: + """Return true if package's install location is in user or preferences scripts path""" + import bpy + user_script_path = bpy.utils.script_path_user() + prefs_script_path = bpy.utils.script_path_pref() + + if user_script_path is not None: + in_user = Path(user_script_path) in Path(self.installed_location).parents + else: + in_user = False + + if prefs_script_path is not None: + in_prefs = Path(prefs_script_path) in Path(self.installed_location).parents + else: + in_prefs = False + + return in_user or in_prefs + + def test_enabled(self) -> bool: + """Return true if package is enabled""" + import bpy + if self.module_name is not None: + return (self.module_name in bpy.context.user_preferences.addons) + else: + r @@ Diff output truncated at 10240 characters. @@ _______________________________________________ Bf-blender-cvs mailing list [email protected] https://lists.blender.org/mailman/listinfo/bf-blender-cvs
