Installer reference
v0.1.14
Reference for the runtime installer: MishkaInstaller.Installer.Installer, MishkaInstaller.Installer.LibraryHandler, MishkaInstaller.Installer.Downloader and MishkaInstaller.Installer.CompileHandler. It lists the fields of an install record, every function, the result of an install, and the errors with the checks that raise them. For the steps, see Install a library at runtime.
The install record
The argument of install/1, uninstall/1 and async_install/1 is a map or a %MishkaInstaller.Installer.Installer{} with these fields. The same struct is stored in a Mnesia table indexed by app.
| Field | Type | Default | Meaning |
|---|---|---|---|
id | string | generated | A version 4 UUID. |
app | string | none | Required. The OTP application name. |
version | string | none | Required. A semantic version: MAJOR.MINOR.PATCH, with an optional pre-release and build, such as 0.1.0 or 1.0.0-rc.1. It is compared with the version already running. |
type | atom | :path | One of :path, :url, :github_tag, :github_latest_release. |
path | string | none | Required. For :path, the package folder. For :url, the artifact address. For :github_*, "owner/repo". |
tag | string | none | The release tag for :github_tag. |
asset | string | none | The release asset file name for :github_*. Without it, the first asset. |
checksum | string | none | The SHA-256 of the downloaded archive, as hex. Checked for downloads only. |
depends | list | [] | Stored with the record. The installer does not act on it. |
prepend_paths | list | [] | The {app, ebin_path} pairs added to the code path. Filled by an install and replayed on boot. |
inserted_at, updated_at | integer | now | Unix time in seconds. |
A string field that is present must not be empty. Installer.builder/1 validates a map and returns {:ok, struct} or an error whose field names the field. install/1 and async_install/1 call it first.
A version that Version.parse/1 does not accept, such as "1.0" or "v1.0.0", is refused by builder/1, write/1, install/1 and async_install/1 with {:error, [%{action: :semver, field: :version, message: message}]}. Nothing is installed or stored. uninstall/1 does not check the version this way. Before 0.1.14 the builder accepted any non-empty string for the version. The changes documentation says what to do when you upgrade.
Installer functions
| Function | Returns | What it does |
|---|---|---|
install(app) | {:ok, %{extension: record, dir: path}} or an error | Installs and starts a pre-built package. See the order below. An application that already has a record keeps its id, and the record is updated. Its other fields, inserted_at included, come from the new install. |
async_install(app) | :ok or an error | Validates the map and queues the install in CompileHandler. Installs run one at a time. The result is not returned. Only the validation of the fields runs before it returns. A later failure shows in the log and in the telemetry event. |
uninstall(app) | :ok or an error | Checks the name, the protected list and the package folder, stops and unloads the application, removes its ebin from the code path, deletes the <app>-<version> folder and deletes the record. Needs the app and version keys. Refuses a version that would place the folder outside the extensions directory. Returns :ok also when the application is not running or has no record. Does not purge the application's modules or touch the plugin registry. |
get() | list of records | Every install record. |
get(id) | record or nil | One record by id. |
get(:app, name) | record or nil | One record by application name. |
write(map) | {:ok, record} or an error | Validates and stores a record. Does not install anything. |
write(field, value, changes) | {:ok, record} or an error | Merges changes into the record found by :app or :id. |
delete(field, value) | {:ok, record} or an error | Deletes a record by :id or :app. Does not touch the files. |
ids() | list of strings | Every record id. |
drop() | {:ok, :atomic} | Empties the table. |
unique(:app, name), unique?(:app, name) | :ok or an error, a boolean | Whether no record has that application name. |
When the read itself aborts, get/0 returns [] and get/1 and get/2 return nil. No error is returned.
Order of an install
- Validate the map with
builder/1. This refuses aversionthat is not a semantic version. - Check the application name against
^[a-z][a-z0-9_]*$, at most 255 characters. - Check that the application is not on the
protected_appslist. - Get the package into
<extensions_path>/<app>-<version>. For:path, check the folder is inside the extensions directory and move it if needed. For other types, check the allowlist, download, check the checksum, and extract. Either way, a folder that already has that name is deleted first, before the version check of step 7. - Check that
ebinholds a.appfile, and log a warning whenprivholds a.so,.dllor.dylibfile. - Read
ebin/<app>.appand check it describes the application you named. - Require the version to be higher than the one loaded, if any. Both are compared as semantic versions, so a loaded application whose own version is not one is refused.
- Add
ebinto the code path, stop the application if it is running and unload it, then load the new.appfile and the modules that are not loaded yet, and start the application with its dependencies. A module that is already loaded keeps its old code. On failure, undo this step. - Write the record, or update the one the application already has, and announce the install. If the write fails, step 8 is not undone.
Whatever the result, install/1 ends by changing the working directory back to :project_path or PROJECT_PATH. Without one it raises.
Errors
Every error is {:error, [%{action: action, field: field, message: message}]}, sometimes with a source key.
action | field | Message |
|---|---|---|
:semver | :version | Invalid semantic version format in the version field (expected MAJOR.MINOR.PATCH). |
:valid_name | :app | The application name is invalid. Use only lowercase letters, digits and underscores. |
:allowlist | :app | Blocked by <kind> policy: <value>. The kind is url_hosts, github_repos or protected_apps. |
:package_dir | :version | Invalid app/version: the package path escapes the extensions directory. Only uninstall/1 returns it, because install/1 refuses such a version as :semver. |
:allowed_extract_path | :path | Your library path is not allowed. It must be inside the extensions directory. The error also has an allowed key with the directory. |
:rename_dir | :path | There was a problem moving the file. |
:download | :path | There is a problem downloading the pre-built artifact. Or: No matching release asset was found. Or: The information sent to download the desired library is wrong! |
:verify_checksum | :checksum | The downloaded artifact does not match the expected sha256 checksum. |
:extract | :path | The archive contains unsafe member paths, cannot be read or extracted, has no compiled ebin, or cannot be moved. |
:ebin_exist | :path | There is no compiled ebin directory (with a .app file) in the specified path. |
:read_app | :ebin_path, :erl_scan | The file cannot be read or parsed, or the application in it is not the one you named. |
:compare_version_with_installed_app | :app | There is an app of the same name with the same or higher version. Or: The version of the installed app of the same name is not a semantic version, so it cannot be compared. |
:prepend_compiled_apps | :path | Some routes cannot be prepended. |
:application_ensure | :app | The application, or one in its dependencies, could not be loaded or started. |
:unload | :app | An error occurred in deactivating the app. |
:unique | :global | This event already exists in the database. unique/2 returns it when a record has the application name. The text says event and means the library record. |
:delete | :global | The ID of the record you want to delete is incorrect or has already been deleted. delete/2 returns it when no record matches. |
:write | :global | The ID of the record you want to update is incorrect or has already been deleted. write/3 returns it when no record matches. |
:database | :global | Unfortunately, there is a problem in <storing or deleting> data in the database, followed by the reason. write/1, write/3, delete/2 and the record write of install/1 return it when the Mnesia transaction aborts. The source key holds the {:aborted, reason} tuple. |
LibraryHandler functions
| Function | Returns | What it does |
|---|---|---|
extensions_path() | path | The :extensions_path setting, or <project_path>/deployment/<env>/extensions. |
prepend_compiled_apps(pairs) | :ok or an error | Adds each {app, ebin_path} to the code path with Code.prepend_path/1. |
read_app(app, app_file) | {:ok, properties} or an error | Parses an Erlang .app file and checks it names app. |
extract(:tar, binary, name) | :ok or an error | Extracts a tar.gz into the extensions directory as name. ebin may be at the top or one directory down. Refuses an archive with an absolute or .. member path. A folder that already has that name is deleted first. |
application_ensure(app) | :ok or an error | Loads the application, loads the modules that are not loaded yet, and starts it with its dependencies. An application that is already loaded counts as loaded, and a module that is already loaded keeps its code. |
unload(app) | :ok or an error | Unloads the application. An application that is not loaded returns :ok. Its modules stay loaded. |
Downloader
Downloader.download(type, package) returns {:ok, binary} or an error with the action :download.
| Type | Package map | Request |
|---|---|---|
:url | %{path: url} | A GET of url. The status must be 200. The body is the raw bytes, with no decompression or decoding. Redirects are followed. |
:github_tag | %{path: "owner/repo", tag: tag}, and asset if wanted | The GitHub API at https://api.github.com/repos/<owner/repo>/releases/tags/<tag>, sent with no authorization header, then a download of the chosen asset from its browser_download_url. |
:github_latest_release | %{path: "owner/repo"}, and asset if wanted | The same at /releases/latest. |
Requests use Req. Any status other than 200 is a :download error. A connection that cannot be made raises a Req.TransportError instead. The :downloader_req_options and :proxy settings are lists of Req options. Both are merged into every request, :proxy second, so a key that both set takes its value from :proxy. For examples, see the documentation on installing a library at runtime.