load plugins from system-wide directory

https://www.mobileread.com/forums/showthread.php?t=333893
This commit is contained in:
Thomas Weißschuh 2020-11-14 15:13:00 +01:00
parent 03ef47c8a5
commit 3981f79a34
6 changed files with 94 additions and 24 deletions

View File

@ -166,6 +166,8 @@ def cache_dir():
# plugins {{{ # plugins {{{
plugins_loc = sys.extensions_location plugins_loc = sys.extensions_location
system_plugins_loc = getattr(sys, 'system_plugins_location', None)
from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, ExtensionFileLoader from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, ExtensionFileLoader
from importlib.util import find_spec from importlib.util import find_spec
from importlib import import_module from importlib import import_module

View File

@ -2,7 +2,7 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, sys, zipfile, importlib import os, sys, zipfile, importlib, enum
from calibre.constants import numeric_version, iswindows, ismacos from calibre.constants import numeric_version, iswindows, ismacos
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
@ -24,10 +24,17 @@ class InvalidPlugin(ValueError):
pass pass
class PluginInstallationType(enum.Enum):
BUILTIN = enum.auto()
SYSTEM = enum.auto()
EXTERNAL = enum.auto()
class Plugin(object): # {{{ class Plugin(object): # {{{
''' '''
A calibre plugin. Useful members include: A calibre plugin. Useful members include:
* ``self.installation_type``: Stores how the plugin was installed.
* ``self.plugin_path``: Stores path to the ZIP file that contains * ``self.plugin_path``: Stores path to the ZIP file that contains
this plugin or None if it is a builtin this plugin or None if it is a builtin
plugin plugin
@ -73,6 +80,9 @@ class Plugin(object): # {{{
#: The earliest version of calibre this plugin requires #: The earliest version of calibre this plugin requires
minimum_calibre_version = (0, 4, 118) minimum_calibre_version = (0, 4, 118)
#: The way this plugin is installed
installation_type = None
#: If False, the user will not be able to disable this plugin. Use with #: If False, the user will not be able to disable this plugin. Use with
#: care. #: care.
can_be_disabled = True can_be_disabled = True

View File

@ -4,14 +4,14 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, shutil, traceback, functools, sys import os, shutil, traceback, functools, sys
from collections import defaultdict from collections import defaultdict
from itertools import chain from itertools import chain, repeat
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound, from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
MetadataReaderPlugin, MetadataWriterPlugin, MetadataReaderPlugin, MetadataWriterPlugin,
InterfaceActionBase as InterfaceAction, InterfaceActionBase as InterfaceAction,
PreferencesPlugin, platform, InvalidPlugin, PreferencesPlugin, platform, InvalidPlugin,
StoreBase as Store, EditBookToolPlugin, StoreBase as Store, EditBookToolPlugin,
LibraryClosedPlugin) LibraryClosedPlugin, PluginInstallationType)
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
from calibre.customize.zipplugin import loader from calibre.customize.zipplugin import loader
from calibre.customize.profiles import InputProfile, OutputProfile from calibre.customize.profiles import InputProfile, OutputProfile
@ -21,13 +21,17 @@ from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import (make_config_dir, Config, ConfigProxy, from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
plugin_dir, OptionParser) plugin_dir, OptionParser)
from calibre.ebooks.metadata.sources.base import Source from calibre.ebooks.metadata.sources.base import Source
from calibre.constants import DEBUG, numeric_version from calibre.constants import DEBUG, numeric_version, system_plugins_loc
from polyglot.builtins import iteritems, itervalues, unicode_type from polyglot.builtins import iteritems, itervalues, unicode_type
builtin_names = frozenset(p.name for p in builtin_plugins) builtin_names = frozenset(p.name for p in builtin_plugins)
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'}) BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
def zip_value(iterable, value):
return zip(iterable, repeat(value))
class NameConflict(ValueError): class NameConflict(ValueError):
pass pass
@ -341,10 +345,19 @@ def reread_metadata_plugins():
for ft in plugin.file_types: for ft in plugin.file_types:
_metadata_writers[ft].append(plugin) _metadata_writers[ft].append(plugin)
# Ensure custom metadata plugins are used in preference to builtin # Ensure the following metadata plugin preference is used:
# ones for a given filetype # external > system > builtin
def key(plugin): def key(plugin):
return (1 if plugin.plugin_path is None else 0), plugin.name installation_type = plugin.installation_type
if installation_type is PluginInstallationType.BUILTIN:
order = 2
elif installation_type is PluginInstallationType.SYSTEM:
order = 1
elif installation_type is PluginInstallationType.EXTERNAL:
order = 0
else:
raise ValueError(installation_type)
return order, plugin.name
for group in (_metadata_readers, _metadata_writers): for group in (_metadata_readers, _metadata_writers):
for plugins in itervalues(group): for plugins in itervalues(group):
@ -473,7 +486,10 @@ def add_plugin(path_to_zip_file):
if plugin.name in builtin_names: if plugin.name in builtin_names:
raise NameConflict( raise NameConflict(
'A builtin plugin with the name %r already exists' % plugin.name) 'A builtin plugin with the name %r already exists' % plugin.name)
plugin = initialize_plugin(plugin, path_to_zip_file) if plugin.name in _list_system_plugins():
raise NameConflict(
'A system plugin with the name %r already exists' % plugin.name)
plugin = initialize_plugin(plugin, path_to_zip_file, PluginInstallationType.EXTERNAL)
plugins = config['plugins'] plugins = config['plugins']
zfp = os.path.join(plugin_dir, plugin.name+'.zip') zfp = os.path.join(plugin_dir, plugin.name+'.zip')
if os.path.exists(zfp): if os.path.exists(zfp):
@ -659,9 +675,10 @@ def all_edit_book_tool_plugins():
_initialized_plugins = [] _initialized_plugins = []
def initialize_plugin(plugin, path_to_zip_file): def initialize_plugin(plugin, path_to_zip_file, installation_type):
try: try:
p = plugin(path_to_zip_file) p = plugin(path_to_zip_file)
p.installation_type = installation_type
p.initialize() p.initialize()
return p return p
except Exception: except Exception:
@ -676,36 +693,67 @@ def has_external_plugins():
return bool(config['plugins']) return bool(config['plugins'])
@functools.lru_cache(maxsize=None)
def _list_system_plugins():
if not system_plugins_loc:
return
try:
plugin_file_names = os.listdir(system_plugins_loc)
except OSError:
return
for plugin_file_name in plugin_file_names:
plugin_path = os.path.join(system_plugins_loc, plugin_file_name)
if os.path.isfile(plugin_path) and plugin_file_name.endswith('.zip'):
yield (os.path.splitext(plugin_file_name)[0], plugin_path)
def initialize_plugins(perf=False): def initialize_plugins(perf=False):
global _initialized_plugins global _initialized_plugins, _system_plugins
_initialized_plugins = [] _initialized_plugins = []
_system_plugins = []
system_plugins = dict(_list_system_plugins())
conflicts = [name for name in config['plugins'] if name in conflicts = [name for name in config['plugins'] if name in
builtin_names] builtin_names or name in system_plugins]
for p in conflicts: for p in conflicts:
remove_plugin(p) remove_plugin(p)
system_conflicts = [name for name in system_plugins if name in
builtin_names]
for p in system_conflicts:
system_plugins.pop(p, None)
external_plugins = config['plugins'].copy() external_plugins = config['plugins'].copy()
for name in BLACKLISTED_PLUGINS: for name in BLACKLISTED_PLUGINS:
external_plugins.pop(name, None) external_plugins.pop(name, None)
system_plugins.pop(name, None)
ostdout, ostderr = sys.stdout, sys.stderr ostdout, ostderr = sys.stdout, sys.stderr
if perf: if perf:
from collections import defaultdict from collections import defaultdict
import time import time
times = defaultdict(lambda:0) times = defaultdict(lambda:0)
for zfp in list(external_plugins) + builtin_plugins:
for zfp, installation_type in chain(
zip_value(external_plugins.items(), PluginInstallationType.EXTERNAL),
zip_value(system_plugins.items(), PluginInstallationType.SYSTEM),
zip_value(builtin_plugins, PluginInstallationType.BUILTIN),
):
try: try:
if not isinstance(zfp, type): if not isinstance(zfp, type):
# We have a plugin name # We have a plugin name
pname = zfp pname, path = zfp
zfp = os.path.join(plugin_dir, zfp+'.zip') zfp = os.path.join(plugin_dir, pname+'.zip')
if not os.path.exists(zfp): if not os.path.exists(zfp):
zfp = external_plugins[pname] zfp = path
try: try:
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
except PluginNotFound: except PluginNotFound:
continue continue
if perf: if perf:
st = time.time() st = time.time()
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp) plugin = initialize_plugin(
plugin,
None if isinstance(zfp, type) else zfp, installation_type,
)
if perf: if perf:
times[plugin.name] = time.time() - st times[plugin.name] = time.time() - st
_initialized_plugins.append(plugin) _initialized_plugins.append(plugin)

View File

@ -20,6 +20,7 @@ from calibre import prints
from calibre.constants import ( from calibre.constants import (
DEBUG, __appname__, __version__, ismacos, iswindows, numeric_version DEBUG, __appname__, __version__, ismacos, iswindows, numeric_version
) )
from calibre.customize import PluginInstallationType
from calibre.customize.ui import ( from calibre.customize.ui import (
NameConflict, add_plugin, disable_plugin, enable_plugin, has_external_plugins, NameConflict, add_plugin, disable_plugin, enable_plugin, has_external_plugins,
initialized_plugins, is_disabled, remove_plugin initialized_plugins, is_disabled, remove_plugin
@ -56,7 +57,8 @@ def get_plugin_updates_available(raise_error=False):
def filter_upgradeable_plugins(display_plugin): def filter_upgradeable_plugins(display_plugin):
return display_plugin.is_upgrade_available() return display_plugin.installation_type is PluginInstallationType.EXTERNAL \
and display_plugin.is_upgrade_available()
def filter_not_installed_plugins(display_plugin): def filter_not_installed_plugins(display_plugin):
@ -96,7 +98,8 @@ def get_installed_plugin_status(display_plugin):
display_plugin.installed_version = None display_plugin.installed_version = None
display_plugin.plugin = None display_plugin.plugin = None
for plugin in initialized_plugins(): for plugin in initialized_plugins():
if plugin.name == display_plugin.qname and plugin.plugin_path is not None: if plugin.name == display_plugin.qname \
and plugin.installation_type is not PluginInstallationType.BUILTIN:
display_plugin.plugin = plugin display_plugin.plugin = plugin
display_plugin.installed_version = plugin.version display_plugin.installed_version = plugin.version
break break
@ -195,6 +198,7 @@ class DisplayPlugin(object):
self.uninstall_plugins = plugin['uninstall'] or [] self.uninstall_plugins = plugin['uninstall'] or []
self.has_changelog = plugin['history'] self.has_changelog = plugin['history']
self.is_deprecated = plugin['deprecated'] self.is_deprecated = plugin['deprecated']
self.installation_type = plugin['installation_type']
def is_disabled(self): def is_disabled(self):
if self.plugin is None: if self.plugin is None:

View File

@ -14,6 +14,7 @@ from qt.core import (Qt, QModelIndex, QAbstractItemModel, QIcon,
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.plugins_ui import Ui_Form from calibre.gui2.preferences.plugins_ui import Ui_Form
from calibre.customize import PluginInstallationType
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin, from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
disable_plugin, plugin_customization, add_plugin, disable_plugin, plugin_customization, add_plugin,
remove_plugin, NameConflict) remove_plugin, NameConflict)
@ -211,6 +212,8 @@ class PluginModel(QAbstractItemModel, AdaptSQP): # {{{
ans += _('\nCustomization: ')+c ans += _('\nCustomization: ')+c
if disabled: if disabled:
ans += _('\n\nThis plugin has been disabled') ans += _('\n\nThis plugin has been disabled')
if plugin.installation_type is PluginInstallationType.SYSTEM:
ans += _('\n\nThis plugin is installed system-wide and can not be managed from within calibre')
return (ans) return (ans)
if role == Qt.ItemDataRole.DecorationRole: if role == Qt.ItemDataRole.DecorationRole:
return self.disabled_icon if disabled else self.icon return self.disabled_icon if disabled else self.icon

View File

@ -28,6 +28,7 @@ from calibre import detect_ncpus, force_unicode, prints
from calibre.constants import ( from calibre.constants import (
DEBUG, __appname__, config_dir, filesystem_encoding, ismacos, iswindows DEBUG, __appname__, config_dir, filesystem_encoding, ismacos, iswindows
) )
from calibre.customize import PluginInstallationType
from calibre.customize.ui import available_store_plugins, interface_actions from calibre.customize.ui import available_store_plugins, interface_actions
from calibre.db.legacy import LibraryDatabase from calibre.db.legacy import LibraryDatabase
from calibre.gui2 import ( from calibre.gui2 import (
@ -127,7 +128,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.iactions = OrderedDict() self.iactions = OrderedDict()
# Actions # Actions
for action in interface_actions(): for action in interface_actions():
if opts.ignore_plugins and action.plugin_path is not None: if opts.ignore_plugins \
and action.installation_type is not PluginInstallationType.BUILTIN:
continue continue
try: try:
ac = self.init_iaction(action) ac = self.init_iaction(action)
@ -139,7 +141,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except Exception: except Exception:
if action.plugin_path: if action.plugin_path:
print('Failed to load Interface Action plugin:', action.plugin_path, file=sys.stderr) print('Failed to load Interface Action plugin:', action.plugin_path, file=sys.stderr)
if action.plugin_path is None: if action.installation_type is PluginInstallationType.BUILTIN:
raise raise
continue continue
ac.plugin_path = action.plugin_path ac.plugin_path = action.plugin_path
@ -166,7 +168,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
from calibre.gui2.store.loader import Stores from calibre.gui2.store.loader import Stores
self.istores = Stores() self.istores = Stores()
for store in available_store_plugins(): for store in available_store_plugins():
if self.opts.ignore_plugins and store.plugin_path is not None: if self.opts.ignore_plugins \
and store.installation_type is not PluginInstallationType.BUILTIN:
continue continue
try: try:
st = self.init_istore(store) st = self.init_istore(store)
@ -175,7 +178,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
# Ignore errors in loading user supplied plugins # Ignore errors in loading user supplied plugins
import traceback import traceback
traceback.print_exc() traceback.print_exc()
if store.plugin_path is None: if store.installation_type is PluginInstallationType.BUILTIN:
raise raise
continue continue
self.istores.builtins_loaded() self.istores.builtins_loaded()
@ -369,7 +372,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
if ac.plugin_path is None: if ac.installation_type is PluginInstallationType.BUILTIN:
raise raise
if config['autolaunch_server']: if config['autolaunch_server']:
@ -389,7 +392,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except: except:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
if ac.plugin_path is None: if ac.installation_type is PluginInstallationType.BUILTIN:
raise raise
self.set_current_library_information(current_library_name(), db.library_id, self.set_current_library_information(current_library_name(), db.library_id,
db.field_metadata) db.field_metadata)