Skip to content

Commit

Permalink
fix(core20): use importlib for local plugins
Browse files Browse the repository at this point in the history
Avoid random importing from sys.path by using importlib.

This will only ever work with python modules, and not python
packages, we we theoretically do not support per our docs at
https://snapcraft.io/docs/writing-local-plugins

Fixes #5217
Signed-off-by: Sergio Schvezov <[email protected]>
  • Loading branch information
sergiusens committed Feb 26, 2025
1 parent aa11a37 commit 64cce0b
Showing 1 changed file with 29 additions and 40 deletions.
69 changes: 29 additions & 40 deletions snapcraft_legacy/internal/pluginhandler/_plugin_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ def load_plugin(
definitions_schema,
) -> plugins.v2.PluginV2:
local_plugins_dir = project._get_local_plugins_dir()
if local_plugins_dir is not None:
plugin_class = _get_local_plugin_class(
plugin_name=plugin_name, local_plugins_dir=local_plugins_dir
)

plugin_class = _get_local_plugin_class(
plugin_name=plugin_name, local_plugins_dir=Path(local_plugins_dir)
)
if plugin_class is None:
plugin_class = plugins.get_plugin_for_base(
plugin_name, build_base=project._get_build_base()
Expand All @@ -57,57 +57,46 @@ def load_plugin(
return plugin


def _load_compat_x_prefix(plugin_name: str, module_name: str, local_plugin_dir: str):
compat_path = Path(local_plugin_dir, f"x-{plugin_name}.py")
if not compat_path.exists():
return None

preferred_name = f"{module_name}.py"
logger.warning(
f"Legacy plugin name detected, please rename the plugin's file name {compat_path.name!r} to {preferred_name!r}."
def _load_local(plugin_name: str, local_plugin_dir: Path):
# The legacy module path is for the case when we allowed the plugin
# file name to have '-', this is the first entry
module_names = (
Path(f"x-{plugin_name}.py"),
Path(f"{plugin_name.replace('-', '_')}.py"),
)
module_paths = (local_plugin_dir / m for m in module_names)
valid_paths = [m for m in module_paths if m.exists()]
# No valid paths means no local plugin to load
if not valid_paths:
return None

spec = importlib.util.spec_from_file_location(plugin_name, compat_path)
module_path = valid_paths[0]
spec = importlib.util.spec_from_file_location(plugin_name, module_path)
if spec.loader is None:
return None
raise errors.PluginError(f"unknown plugin: {plugin_name!r}")

# Prevent mypy type complaints by asserting type.
assert isinstance(spec.loader, importlib.abc.Loader)

module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def _load_local(plugin_name: str, local_plugin_dir: str):
module_name = plugin_name.replace("-", "_")

module = _load_compat_x_prefix(plugin_name, module_name, local_plugin_dir)
if module is None:
sys.path = [local_plugin_dir] + sys.path
logger.debug(
f"Loading plugin module {module_name!r} with sys.path {sys.path!r}"
)
try:
module = importlib.import_module(module_name)
finally:
sys.path.pop(0)

return module


def _get_local_plugin_class(*, plugin_name: str, local_plugins_dir: str):
with contextlib.suppress(ImportError):
module = _load_local(plugin_name, local_plugins_dir)
logger.info(f"Loaded local plugin for {plugin_name}")
def _get_local_plugin_class(*, plugin_name: str, local_plugins_dir: Path):
module = _load_local(plugin_name, local_plugins_dir)
if not module:
return
logger.info(f"Loaded local plugin for {plugin_name}")

# v2 requires plugin implementation to be named "PluginImpl".
if hasattr(module, "PluginImpl") and issubclass(
module.PluginImpl, plugins.v2.PluginV2
):
return module.PluginImpl
# v2 requires plugin implementation to be named "PluginImpl".
if hasattr(module, "PluginImpl") and issubclass(
module.PluginImpl, plugins.v2.PluginV2
):
return module.PluginImpl

raise errors.PluginError(f"unknown plugin: {plugin_name!r}")
raise errors.PluginError(f"unknown plugin: {plugin_name!r}")


def _validate_pull_and_build_properties(
Expand Down

0 comments on commit 64cce0b

Please sign in to comment.