How the runtime installer works
v0.1.14
The second half of Mishka Installer loads libraries into a running node. It compiles nothing. This page explains why it accepts only compiled packages, what an install changes in the running system, what the library stores, how a node loads the libraries again after a restart, and what the library leaves to you.
Why only compiled packages
A production mix release ships no Mix, no Hex, no project source and no _build directory. Runtime compilation, such as mix deps.get and mix compile, has nothing to run on there. So the installer accepts a package that is already compiled: an ebin directory with the .beam files and the <app>.app resource file. Build the package with the same Erlang/OTP and Elixir versions as the node that loads it. The changes documentation for 0.1.6 lists what moved when the installer became this.
What an install does
The code path and the list of started applications live in memory. An install changes both, in this order:
- It checks the fields of the map, including that the version is a semantic version. It then checks the application name, and that the application is not on the protected list.
- It obtains the package. A local package is moved to
<extensions_path>/<app>-<version>if it is not there already. A remote package is checked against the allowlist, downloaded, checked against the optional checksum, and extracted into that folder. A folder that already has that name is deleted first, before the version check of step 4. - It confirms that the package has an
ebinfolder with a.appfile, and logs a warning ifprivholds native files (.so,.dllor.dylib), which must match the node's operating system, architecture and Erlang runtime. - It reads the
.appfile and refuses the install when the application is already running at the same or a higher version. A running copy whose own version is not a semantic version cannot be compared, so that refuses the install too. - It adds the
ebinfolder to the code path, stops the application if it is running and unloads it, then loads the new.appfile and every module that is not loaded yet, and starts the application with its dependencies. A module that is already loaded keeps its old code until the node restarts. - It writes the stored record, or updates the one the application already has, and announces the install.
If step 5 fails, the installer stops and unloads the application and removes the folder from the code path. It does not undo the package folder of step 2, and it does not undo step 6: when the record cannot be stored, the application keeps running with no record and is not loaded again after a restart. Whatever the result, install/1 then changes the working directory back to :project_path. Recovery is in the documentation on installing a library at runtime.
What the library stores
An install leaves two things behind. The files stay in <extensions_path>/<app>-<version>. A record in the install table holds the application name, the version, how it was obtained, and prepend_paths, the list of {app, ebin_path} pairs to add to the code path again. There is one record for each application: an upgrade updates it and an uninstall deletes it. Releases before 0.1.14 wrote another record on each upgrade, as the changes documentation for 0.1.14 explains. Both must survive a restart. See Deploy to production.
What happens at boot
The code path does not survive a restart, but the files and the record do. When the Mnesia store is ready, the install handler replays every stored record: it adds the paths, unloads the application, and loads and starts it again. Each application is handled on its own. A failure is logged as skipped re-loading and the next application is tried. Only then does the handler mark the installed code as ready, so that plugin modules shipped in an installed library exist before the events start.
What the library checks
- The application name must match
^[a-z][a-z0-9_]*$, so a name cannot spell a path. - The version must be a semantic version, such as
0.1.0or1.0.0-rc.1, so it cannot spell a path. - The package folder must be inside the extensions directory. An uninstall also checks that the app and version cannot place it outside.
- An archive with an absolute path or a
..segment in any member is refused before anything is extracted. - A remote source must be on the allowlist. An empty list blocks every remote install. Local installs do not use the allowlist.
- An optional SHA-256 checksum must match the downloaded archive.
- The protected list, by default
mishka_installer, cannot be installed over or uninstalled.
What the library leaves to you
- Trust. Loading a
.beamruns arbitrary code with full node privileges, and the BEAM has no sandbox. Install only artifacts you trust, from code that only trusted people can reach. - Dependencies. The installer adds only the new application's
ebin. Its dependencies must already be on the node, or be installed first. - Folders of other versions. An upgrade keeps the folder of the earlier version, and uninstall deletes only the folder of the version you name.
- Where a download comes from. The allowlist checks the host or repository you name. It does not check where a redirect leads, it does not require https, and it does not check the asset address of a GitHub release. Pin a checksum.
- Plugins the library registers. Uninstall and upgrade do not touch the plugin registry, and an upgrade does not reload a module that is already loaded. See the documentation on plugins in installed libraries.
Example: install and replay
After a local install of demo version 0.1.0 from /data/extensions/demo-0.1.0, the stored record holds the path to add again:
alias MishkaInstaller.Installer.Installer
{:ok, output} =
Installer.install(%{app: "demo", version: "0.1.0", path: "/data/extensions/demo-0.1.0"})
output.extension.prepend_paths
# [demo: "/data/extensions/demo-0.1.0/ebin"]When the node restarts, the install handler reads that record, adds the folder to the code path and starts demo again. The library's tests simulate this by stopping and unloading the application, removing it from the code path, and sending the handler its synchronized message.