Skip to content

Fix plugin installation across development and frozen environments - #830

Open
Irozuku wants to merge 10 commits into
developfrom
fix/uv-plugin-installation
Open

Fix plugin installation across development and frozen environments#830
Irozuku wants to merge 10 commits into
developfrom
fix/uv-plugin-installation

Conversation

@Irozuku

@Irozuku Irozuku commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Plugin installation was broken in several environments: a uv managed .venv has no pip script, and frozen builds (PyInstaller/AppImage) ship a read only environment where plugins can't be installed at all.

This branch moves plugin distributions into a writable per user, per interpreter directory outside the environment and drives installation through sys.executable -m pip, so it works from uv run, a dev checkout, and a frozen bundle.


Type of Change

  • Backend change
  • Frontend change
  • CI / Workflow change
  • Build / Packaging change
  • Bug fix
  • Documentation

Changes (by file)

  • DashAI/back/plugins/environment.py (new): Resolves and activates a writable, interpreter scoped plugins directory (~/.DashAI/plugins/pyX.Y), appends it to sys.path/PYTHONPATH, and invalidates import caches.
  • DashAI/back/plugins/installer.py (new): Installs/uninstalls plugins via sys.executable -m pip. Uses a two phase install (--dry-run --report, then --target --no-deps) so already satisfied dependencies (e.g. torch) aren't re downloaded. A .dashai-plugins.json ledger tracks distribution ownership for clean uninstalls. Raises PluginInstallError with pip's real error.
  • DashAI/back/plugins/utils.py: Replaced the bare pip subprocess with the new installer. get_available_plugins() now activates the plugins directory, deduplicates plugins, and skips plugins that fail to import instead of crashing the registry.
  • DashAI/back/api/api_v1/endpoints/plugins.py: Surfaces PluginInstallError to the client as an HTTP 500 with pip's traceback instead of a generic error.
  • DashAI/back/app.py: Activates the plugins directory before the initial config/component collection.
  • DashAI/__main__.py: Activates the plugins directory before copying the environment to the Huey consumer.
  • pyproject.toml: Added pip>=22.2 as a runtime dependency.
  • dashai.spec: Bundles pip and dependency .dist-info metadata into the frozen build and registers the interpreter runtime hook.
  • hooks/rthook_python_surrogate.py (new): Makes the frozen launcher act as a Python interpreter (-m / .py script) and registers a distlib resource finder so pip can install wheels inside the bundle.
  • tests/back/plugins/*: Added coverage for the environment resolver, installer (resolve/install/uninstall/ledger), utils, and the PyInstaller surrogate hook.

Testing

  • uv run pytest tests/back/plugins/ covers the new installer and environment logic.
  • Verify installing/uninstalling a plugin from a uv run dev server and from a frozen build across different operative systems

Notes

  • Uninstall falls back to removing legacy plugins from the environment's site-packages when nothing is found in the plugins directory, preserving backwards compatibility.

Plugins are installed at runtime by shelling out to pip. A uv managed
.venv ships no pip at all, and a PyInstaller bundle has no interpreter to
bootstrap one with ensurepip, so pip has to be declared explicitly.

The floor is 22.2, the first release with "pip install --report".
Plugins cannot live in the interpreter's own environment. The packaged
distributions are read only (a PyInstaller bundle, the squashfs image
inside an AppImage), and in a uv managed checkout every uv run re-syncs
.venv against uv.lock and uninstalls whatever is not locked, plugins
included.

Add <DASHAI_LOCAL_PATH>/plugins/py<major>.<minor> instead, activated on
sys.path and exported through PYTHONPATH so child processes such as the
Huey consumer see it too. The directory is scoped by interpreter version
because plugins may ship compiled extension modules.
pip's --target mode hardcodes ignore_installed, so installing a plugin
straight into the plugins directory re-downloads its whole dependency
tree, torch included. Split the installation in two phases instead:

1. resolve the requirement against the running environment with
   pip install --dry-run --report, which lists only what is missing;
2. install exactly those, pinned to the resolved artifact URLs, with
   --target <plugins dir> --no-deps.

pip is invoked as "sys.executable -m pip" rather than as a bare pip
executable, since a bare pip found on PATH belongs to some unrelated
interpreter and would put the plugin where dashAI can never import it.

pip uninstall refuses to touch a --target directory, so removal walks the
RECORD file of every distribution the plugin owns and that no other
installed plugin still needs, tracked in a ledger next to them.
execute_pip_command ran "pip install <name>" through a PATH lookup and
installed into the interpreter's own environment. Neither half survives
the move to uv: a uv managed .venv has no pip script, so the call either
fails outright or reaches an unrelated global pip whose entry points
dashAI never sees, and anything that did land in .venv was deleted by the
next uv run.

Route install_plugin and uninstall_plugin through the plugins installer
instead. get_available_plugins now activates the plugins directory and
invalidates the import caches first, so a plugin installed while the app
is running is discovered without a restart, and an entry point that fails
to import is skipped rather than taking the whole registry down.
create_app has to make the directory importable before build_config_dict
runs, because collecting the initial components already enumerates the
installed plugin entry points.

The CLI activates it before copying the environment for the Huey
consumer, which imports plugin components in its own process and
therefore needs the directory on its PYTHONPATH.
A failed install only raised out of the endpoint, so the frontend got a
bare Internal Server Error with nothing to act on. Catch
PluginInstallError in the install and upgrade paths and return the pip
error text as the response detail.
A PyInstaller bundle ships no python executable, so "sys.executable -m
pip" re-enters the launcher. Add a runtime hook that answers the two
interpreter invocations that path needs, "-m <module>" and a script path,
and falls through to the normal CLI for anything else. pip uses the same
form to spawn its own build isolation, so both are required.

The spec now carries pip itself, plus the dist-info metadata of every
dependency collected recursively. Without the metadata the resolution
phase considers torch and friends missing and downloads the whole tree
into the user's plugins directory.
pip reports an unhandled crash as an "ERROR: Exception:" line followed by
the traceback that explains it. The error formatter kept only the lines
containing "ERROR", so a real failure surfaced as the single useless
string "ERROR: Exception:" and the cause was unrecoverable.

Report everything from the first error line onwards, capped, and log the
complete pip output whenever pip exits non zero.
Installing a plugin from the Windows executable died with "Unable to
locate finder for 'pip._vendor.distlib'". pip imports
pip._vendor.distlib.scripts before installing a wheel, and that module
builds its script wrapper table at import time through
distlib.resources.finder(), which dispatches on the type of the package's
loader against a registry holding only the stdlib loaders and
zipimporter. PyInstaller's loader is in neither, so the import raised.

Resolution was unaffected because pip imports that module only when it is
about to install something, which is why the failure looked like a
network or metadata problem rather than an import error.

Register the bundle's loader before handing control to pip. distlib's
register_finder applies type() to its argument, so it takes the loader
instance and not the loader class.
@Irozuku Irozuku added bug Something isn't working help wanted Extra attention is needed QA needed Needs heavy QA, breaking change labels Aug 19, 2026
@Irozuku
Irozuku marked this pull request as ready for review August 19, 2026 18:54
@Irozuku
Irozuku marked this pull request as draft August 19, 2026 18:58
Installing a plugin from the AppImage failed with "pip exited with code 2"
and no output at all. sys.executable inside a bundled launcher is dashAI
itself, not a bare interpreter, so "sys.executable -m pip install ..."
re-entered the app; its CLI rejected the arguments and click exits with
code 2 for a usage error, while the relaunch logic in the AppImage entry
point kept the message off our pipes. __main__ already documents the same
caveat for the Huey consumer.

Resolve the interpreter instead of assuming it: try sys.executable first,
the interpreters next to sys.prefix first when running inside an AppImage,
and verify each candidate actually executes Python before using it. PATH
is deliberately not consulted, since an interpreter found there belongs to
a different environment.

A silent pip failure now names the command that produced it, which is what
this report was missing.
@Irozuku Irozuku added the back Backend work label Aug 19, 2026
@Irozuku
Irozuku marked this pull request as ready for review August 19, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

back Backend work bug Something isn't working help wanted Extra attention is needed QA needed Needs heavy QA, breaking change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant