๐Ÿงฉ MishkaInstaller

A runtime plugin / event engine and pre-built library installer for Elixir. โœจ

Hex.pmHex DownloadsCILicenseGitHub SponsorsBuy Me a Coffee


Warning

๐Ÿ”ง Low maintenance. The author currently only responds to pull requests. Don't use master; the current release line is 0.1.6.


๐Ÿ’ญ Why?

Build apps whose features are activated as plugins at runtime โ€” registration, SMS, social login โ€” without touching the core source. Each feature becomes an event; anyone can attach plugins to it from outside your app.


โœจ Features


๐Ÿ”Œ Events & Hooks

Define a plugin for an event โ€” it auto-registers and runs whenever the event is called:

defmodule RegisterEmailSender do
use MishkaInstaller.Event.Hook, event: "after_success_login"
@impl true
def call(entries), do: {:reply, entries}
end
# tweak defaults
use MishkaInstaller.Event.Hook,
event: "after_success_login",
initial: %{depends: [SomeEvent], priority: 20}

Call every plugin registered for an event:

alias MishkaInstaller.Event.Hook
Hook.call("after_success_login", params)
Hook.call("after_success_login", params, private: keep_this) # extra data, untouched by plugins
Hook.call("after_success_login", params, return: true) # return the original input

To start a plugin automatically, add its module to your supervision tree:

children = [RegisterEmailSender, ...]

Note

A plugin's depends always run before it (cycles are rejected at registration), and a plugin can return {:reply, :halt, state} to stop the rest of the chain. See MishkaInstaller.Event.Hook.

Run in Livebook


๐Ÿ“ฆ Installer

Load an already-compiled ebin at runtime โ€” no source compilation, so it's release-safe:

alias MishkaInstaller.Installer.Installer
Installer.install(%{app: "demo", version: "0.1.0", type: :path, path: "/ext/demo-0.1.0"})
Installer.install(%{app: "demo", version: "0.1.0", type: :url, path: "https://.../demo-ebin.tar.gz"})
Installer.install(%{app: "demo", version: "0.1.0", type: :github_tag, path: "owner/repo", tag: "0.1.0"})
Installer.install(%{app: "demo", version: "0.1.0", type: :github_latest_release, path: "owner/repo"})
Installer.uninstall(%{app: "demo", version: "0.1.0"})

Note

Remote installs (:url/:github_*) are fail-closed: they require the source host/repo in config :mishka_installer, :allowlist, url_hosts:/github_repos:. Optional checksum: (sha256) pins the artifact; :protected_apps guards apps from being overwritten/removed. See MishkaInstaller.Installer.Installer.


๐Ÿญ Production deployment

An installed library is a pre-built ebin on disk plus a Mnesia record; on every boot it is replayed (put back on the code path and started). So both the ebins and the Mnesia data must live on a persistent (mounted) volume โ€” otherwise installs do not survive a restart/redeploy. Point both at your volume (here /data):

# config/runtime.exs โ€” /data is your mounted volume
config :mishka_installer,
project_path: "/data",
extensions_path: "/data/extensions"
config :mishka_installer, MishkaInstaller.MnesiaRepo,
mnesia_dir: "/data/mnesia",
essential: [MishkaInstaller.Event.Event, MishkaInstaller.Installer.Installer]

:extensions_path is where ebins are written; mnesia_dir is where the records live; :essential are the tables created on boot (the plugin and install stores โ€” keep both). Just depend on :mishka_installer; its supervision tree starts automatically outside :test.

A real release restart proof lives in test/integration/production_release/ โ€” run it with mix test --only production_release.


๐Ÿš€ Installation

def deps do
[{:mishka_installer, "~> 0.1.6"}]
end

Docs: hexdocs.pm/mishka_installer


๐Ÿ’– Funding & sponsorship

MishkaInstaller is open-source software developed by Mishka Group. If your team or company benefits from it, please consider supporting continued development:

GitHub Sponsors ย ย ย  Buy Me a Coffee

โ˜• Donate / sponsor:github.com/sponsors/mishka-group ยท buymeacoffee.com/mishkagroup

Thank you. ๐Ÿ’š


๐Ÿ“œ License

Apache License 2.0 โ€” see LICENSE.

Copyright ยฉ Mishka Group and contributors.