diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce2bbba4f6a..409c19438395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added `RobotModel.remove_link`, `RobotModel.remove_joint`, `RobotModel.to_urdf_string`, and `RobotModel.ensure_geometry`. * Added Blender Python-example to the documentation section: Tutorials -> Robots * Added `compas_blender.unload_modules`. +* Added `after_rhino_install` and `after_rhino_uninstall` pluggable interfaces to extend the install/uninstall with arbitrary steps. ### Changed diff --git a/docs/plugins.rst b/docs/plugins.rst index 1ff657590bce..b586f645373e 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -24,9 +24,13 @@ Category: ``booleans`` Category: ``install`` ^^^^^^^^^^^^^^^^^^^^^ -.. currentmodule:: compas_rhino.install +.. currentmodule:: * :func:`installable_rhino_packages` +* :func:`after_rhino_install` + +.. currentmodule:: compas_rhino.uninstall +* :func:`after_rhino_uninstall` Category: ``intersections`` diff --git a/src/compas/plugins.py b/src/compas/plugins.py index b18e6c3c01fb..1c1f10db1ee1 100644 --- a/src/compas/plugins.py +++ b/src/compas/plugins.py @@ -288,7 +288,11 @@ def wrapper(*args, **kwargs): results = [] for plugin_impl in _collect_plugins(extension_point_url): - results.append(plugin_impl.method(*args, **kwargs)) + try: + result = plugin_impl.method(*args, **kwargs) + results.append(result) + except Exception as e: + results.append(e) return results else: diff --git a/src/compas_ghpython/__init__.py b/src/compas_ghpython/__init__.py index 186491e8a7c4..3aca24e1fd9c 100644 --- a/src/compas_ghpython/__init__.py +++ b/src/compas_ghpython/__init__.py @@ -12,6 +12,7 @@ compas_ghpython.utilities """ +import os import compas if compas.RHINO: @@ -21,5 +22,16 @@ __version__ = '1.0.0' +def get_grasshopper_library_path(version): + if compas.WINDOWS: + grasshopper_library_path = os.path.join(os.getenv('APPDATA'), 'Grasshopper', 'Libraries') + elif compas.OSX: + grasshopper_library_path = os.path.join(os.getenv('HOME'), 'Library', 'Application Support', 'McNeel', 'Rhinoceros', '{}'.format(version), + 'Plug-ins', 'Grasshopper (b45a29b1-4343-4035-989e-044e8580d9cf)', 'Libraries') + else: + raise Exception('Unsupported platform') + return grasshopper_library_path + + __all_plugins__ = ['compas_ghpython.install'] __all__ = [name for name in dir() if not name.startswith('_')] diff --git a/src/compas_rhino/__init__.py b/src/compas_rhino/__init__.py index f66097d77a48..44a4d41eb33b 100644 --- a/src/compas_rhino/__init__.py +++ b/src/compas_rhino/__init__.py @@ -211,4 +211,5 @@ def _try_remove_bootstrapper(path): __all_plugins__ = [ 'compas_rhino.geometry.booleans', 'compas_rhino.install', + 'compas_rhino.uninstall', ] diff --git a/src/compas_rhino/install.py b/src/compas_rhino/install.py index 670c29675720..330372583e36 100644 --- a/src/compas_rhino/install.py +++ b/src/compas_rhino/install.py @@ -12,7 +12,11 @@ import compas._os import compas.plugins -__all__ = ['install'] +__all__ = [ + 'install', + 'installable_rhino_packages', + 'after_rhino_install', +] def install(version=None, packages=None): @@ -84,8 +88,13 @@ def install(version=None, packages=None): symlinks = [(link['source_path'], link['link']) for link in symlinks_to_install] install_results = compas._os.create_symlinks(symlinks) + installed_packages = [] for install_data, success in zip(symlinks_to_install, install_results): - result = 'OK' if success else 'ERROR: Cannot create symlink, try to run as administrator.' + if success: + installed_packages.append(install_data['name']) + result = 'OK' + else: + result = 'ERROR: Cannot create symlink, try to run as administrator.' results.append((install_data['name'], result)) if not all(install_results): @@ -106,11 +115,49 @@ def install(version=None, packages=None): if status != 'OK': exit_code = -1 + if exit_code == 0 and len(installed_packages): + print() + print('Running post-installation steps...') + print() + if not _run_post_execution_steps(after_rhino_install(installed_packages)): + exit_code = -1 + print('\nCompleted.') if exit_code != 0: sys.exit(exit_code) +def _run_post_execution_steps(steps_generator): + all_steps_succeeded = True + post_execution_errors = [] + + for result in steps_generator: + if isinstance(result, Exception): + post_execution_errors.append(result) + continue + + for item in result: + try: + package, message, success = item + status = 'OK' if success else 'ERROR' + if not success: + all_steps_succeeded = False + print(' {} {}: {}'.format(package.ljust(20), status, message)) + except ValueError: + post_execution_errors.append(ValueError('Step ran without errors but result is wrongly formatted: {}'.format(str(item)))) + + if post_execution_errors: + print() + print('One or more errors occurred:') + print() + for error in post_execution_errors: + print(' - {}'.format(repr(error))) + + all_steps_succeeded = False + + return all_steps_succeeded + + @compas.plugins.plugin(category='install', pluggable_name='installable_rhino_packages', tryfirst=True) def default_installable_rhino_packages(): # While this list could obviously be hard-coded, I think @@ -143,6 +190,36 @@ def installable_rhino_packages(): pass +@compas.plugins.pluggable(category='install', selector='collect_all') +def after_rhino_install(installed_packages): + """Allows extensions to execute actions after install to Rhino is done. + + Extensions providing Rhino or Grasshopper features + can implement this pluggable interface to perform + additional steps after an installation to Rhino has + been completed. + + Parameters + ---------- + installed_packages : :obj:`list` of :obj:`str` + List of packages that have been installed successfully. + + Examples + -------- + >>> import compas.plugins + >>> @compas.plugins.plugin(category='install') + ... def after_rhino_install(installed_packages): + ... # Do something after package is installed to Rhino, eg, copy components, etc + ... return [('compas_ghpython', 'GH Components installed', True)] + + Returns + ------- + :obj:`list` of 3-tuple (str, str, bool) + List containing a 3-tuple with component name, message and ``True``/``False`` success flag. + """ + pass + + def _update_bootstrapper(install_path, packages): # Take either the CONDA environment directory or the current Python executable's directory python_directory = os.environ.get('CONDA_PREFIX', None) or os.path.dirname(sys.executable) diff --git a/src/compas_rhino/uninstall.py b/src/compas_rhino/uninstall.py index 227256e5bdb3..9b544e9eb34d 100644 --- a/src/compas_rhino/uninstall.py +++ b/src/compas_rhino/uninstall.py @@ -6,12 +6,16 @@ import os import sys +import compas._os +import compas.plugins import compas_rhino +from compas_rhino.install import _run_post_execution_steps from compas_rhino.install import installable_rhino_packages -import compas._os - -__all__ = ['uninstall'] +__all__ = [ + 'uninstall', + 'after_rhino_uninstall', +] def uninstall(version=None, packages=None): @@ -68,8 +72,13 @@ def uninstall(version=None, packages=None): symlinks = [link['link'] for link in symlinks_to_uninstall] uninstall_results = compas._os.remove_symlinks(symlinks) + uninstalled_packages = [] for uninstall_data, success in zip(symlinks_to_uninstall, uninstall_results): - result = 'OK' if success else 'ERROR: Cannot remove symlink, try to run as administrator.' + if success: + uninstalled_packages.append(uninstall_data['name']) + result = 'OK' + else: + result = 'ERROR: Cannot remove symlink, try to run as administrator.' results.append((uninstall_data['name'], result)) if not all(uninstall_results): @@ -92,6 +101,13 @@ def uninstall(version=None, packages=None): if status != 'OK': exit_code = -1 + if exit_code == 0 and len(uninstalled_packages): + print() + print('Running post-uninstallation steps...') + print() + if not _run_post_execution_steps(after_rhino_uninstall(uninstalled_packages)): + exit_code = -1 + print('\nUninstall completed.') if exit_code != 0: sys.exit(exit_code) @@ -126,6 +142,36 @@ def _filter_installed_packages(version, packages): return packages +@compas.plugins.pluggable(category='install', selector='collect_all') +def after_rhino_uninstall(uninstalled_packages): + """Allows extensions to execute actions after uninstall from Rhino is done. + + Extensions providing Rhino or Grasshopper features + can implement this pluggable interface to perform + additional steps after the uninstall from Rhino has + been completed. + + Parameters + ---------- + uninstalled_packages : :obj:`list` of :obj:`str` + List of packages that have been uninstalled. + + Examples + -------- + >>> import compas.plugins + >>> @compas.plugins.plugin(category='install') + ... def after_rhino_uninstall(uninstalled_packages): + ... # Do something cleanup, eg remove copied files. + ... return [('compas_ghpython', 'GH Components uninstalled', True)] + + Returns + ------- + :obj:`list` of 3-tuple (str, str, bool) + List containing a 3-tuple with component name, message and ``True``/``False`` success flag. + """ + pass + + # ============================================================================== # Main # ==============================================================================