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_loc = sys.extensions_location
system_plugins_loc = getattr(sys, 'system_plugins_location', None)
from importlib.machinery import ModuleSpec, EXTENSION_SUFFIXES, ExtensionFileLoader
from importlib.util import find_spec
from importlib import import_module

View File

@ -2,7 +2,7 @@
__license__ = 'GPL v3'
__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.ptempfile import PersistentTemporaryFile
@ -24,10 +24,17 @@ class InvalidPlugin(ValueError):
pass
class PluginInstallationType(enum.Enum):
BUILTIN = enum.auto()
SYSTEM = enum.auto()
EXTERNAL = enum.auto()
class Plugin(object): # {{{
'''
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
this plugin or None if it is a builtin
plugin
@ -73,6 +80,9 @@ class Plugin(object): # {{{
#: The earliest version of calibre this plugin requires
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
#: care.
can_be_disabled = True

View File

@ -4,14 +4,14 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, shutil, traceback, functools, sys
from collections import defaultdict
from itertools import chain
from itertools import chain, repeat
from calibre.customize import (CatalogPlugin, FileTypePlugin, PluginNotFound,
MetadataReaderPlugin, MetadataWriterPlugin,
InterfaceActionBase as InterfaceAction,
PreferencesPlugin, platform, InvalidPlugin,
StoreBase as Store, EditBookToolPlugin,
LibraryClosedPlugin)
LibraryClosedPlugin, PluginInstallationType)
from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin
from calibre.customize.zipplugin import loader
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,
plugin_dir, OptionParser)
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
builtin_names = frozenset(p.name for p in builtin_plugins)
BLACKLISTED_PLUGINS = frozenset({'Marvin XD', 'iOS reader applications'})
def zip_value(iterable, value):
return zip(iterable, repeat(value))
class NameConflict(ValueError):
pass
@ -341,10 +345,19 @@ def reread_metadata_plugins():
for ft in plugin.file_types:
_metadata_writers[ft].append(plugin)
# Ensure custom metadata plugins are used in preference to builtin
# ones for a given filetype
# Ensure the following metadata plugin preference is used:
# external > system > builtin
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 plugins in itervalues(group):
@ -473,7 +486,10 @@ def add_plugin(path_to_zip_file):
if plugin.name in builtin_names:
raise NameConflict(
'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']
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
if os.path.exists(zfp):
@ -659,9 +675,10 @@ def all_edit_book_tool_plugins():
_initialized_plugins = []
def initialize_plugin(plugin, path_to_zip_file):
def initialize_plugin(plugin, path_to_zip_file, installation_type):
try:
p = plugin(path_to_zip_file)
p.installation_type = installation_type
p.initialize()
return p
except Exception:
@ -676,36 +693,67 @@ def has_external_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):
global _initialized_plugins
global _initialized_plugins, _system_plugins
_initialized_plugins = []
_system_plugins = []
system_plugins = dict(_list_system_plugins())
conflicts = [name for name in config['plugins'] if name in
builtin_names]
builtin_names or name in system_plugins]
for p in conflicts:
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()
for name in BLACKLISTED_PLUGINS:
external_plugins.pop(name, None)
system_plugins.pop(name, None)
ostdout, ostderr = sys.stdout, sys.stderr
if perf:
from collections import defaultdict
import time
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:
if not isinstance(zfp, type):
# We have a plugin name
pname = zfp
zfp = os.path.join(plugin_dir, zfp+'.zip')
pname, path = zfp
zfp = os.path.join(plugin_dir, pname+'.zip')
if not os.path.exists(zfp):
zfp = external_plugins[pname]
zfp = path
try:
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
except PluginNotFound:
continue
if perf:
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:
times[plugin.name] = time.time() - st
_initialized_plugins.append(plugin)

View File

@ -20,6 +20,7 @@ from calibre import prints
from calibre.constants import (
DEBUG, __appname__, __version__, ismacos, iswindows, numeric_version
)
from calibre.customize import PluginInstallationType
from calibre.customize.ui import (
NameConflict, add_plugin, disable_plugin, enable_plugin, has_external_plugins,
initialized_plugins, is_disabled, remove_plugin
@ -56,7 +57,8 @@ def get_plugin_updates_available(raise_error=False):
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):
@ -96,7 +98,8 @@ def get_installed_plugin_status(display_plugin):
display_plugin.installed_version = None
display_plugin.plugin = None
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.installed_version = plugin.version
break
@ -195,6 +198,7 @@ class DisplayPlugin(object):
self.uninstall_plugins = plugin['uninstall'] or []
self.has_changelog = plugin['history']
self.is_deprecated = plugin['deprecated']
self.installation_type = plugin['installation_type']
def is_disabled(self):
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.plugins_ui import Ui_Form
from calibre.customize import PluginInstallationType
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
disable_plugin, plugin_customization, add_plugin,
remove_plugin, NameConflict)
@ -211,6 +212,8 @@ class PluginModel(QAbstractItemModel, AdaptSQP): # {{{
ans += _('\nCustomization: ')+c
if 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)
if role == Qt.ItemDataRole.DecorationRole:
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 (
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.db.legacy import LibraryDatabase
from calibre.gui2 import (
@ -127,7 +128,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.iactions = OrderedDict()
# 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
try:
ac = self.init_iaction(action)
@ -139,7 +141,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except Exception:
if action.plugin_path:
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
continue
ac.plugin_path = action.plugin_path
@ -166,7 +168,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
from calibre.gui2.store.loader import Stores
self.istores = Stores()
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
try:
st = self.init_istore(store)
@ -175,7 +178,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
# Ignore errors in loading user supplied plugins
import traceback
traceback.print_exc()
if store.plugin_path is None:
if store.installation_type is PluginInstallationType.BUILTIN:
raise
continue
self.istores.builtins_loaded()
@ -369,7 +372,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except:
import traceback
traceback.print_exc()
if ac.plugin_path is None:
if ac.installation_type is PluginInstallationType.BUILTIN:
raise
if config['autolaunch_server']:
@ -389,7 +392,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
except:
import traceback
traceback.print_exc()
if ac.plugin_path is None:
if ac.installation_type is PluginInstallationType.BUILTIN:
raise
self.set_current_library_information(current_library_name(), db.library_id,
db.field_metadata)