Installer reference

v0.1.16

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.

FieldTypeDefaultMeaning
idstringgeneratedA version 4 UUID.
appstringnoneRequired. The OTP application name.
versionstringnoneRequired. 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.
typeatom:pathOne of :path, :url, :github_tag, :github_latest_release.
pathstringnoneRequired. For :path, the package folder. For :url, the artifact address. For :github_*, "owner/repo".
tagstringnoneThe release tag for :github_tag.
assetstringnoneThe release asset file name for :github_*. Without it, the first asset.
checksumstringnoneThe SHA-256 of the downloaded archive, as hex. Checked for downloads only. A remote install without one logs a warning, and require_checksum: true in the allowlist refuses it.
dependslist[]Stored with the record. The installer does not act on it.
prepend_pathslist[]The {app, ebin_path} pairs added to the code path. Filled by an install and stored. Boot does not use it: it builds the path again from the extensions directory, the app and the version.
inserted_at, updated_atintegernowUnix 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

FunctionReturnsWhat it does
install(app){:ok, %{extension: record, dir: path}} or an errorInstalls 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 come from the new install, except inserted_at, which keeps the time of the first install.
async_install(app):ok or an errorValidates 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 errorChecks 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 recordsEvery install record.
get(id)record or nilOne record by id.
get(:app, name)record or nilOne record by application name.
write(map){:ok, record} or an errorValidates and stores a record. Does not install anything.
write(field, value, changes){:ok, record} or an errorMerges changes into the record found by :app or :id.
delete(field, value){:ok, record} or an errorDeletes a record by :id or :app. Does not touch the files.
ids()list of stringsEvery record id.
drop(){:ok, :atomic}Empties the table.
unique(:app, name), unique?(:app, name):ok or an error, a booleanWhether 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

  1. Validate the map with builder/1. This refuses a version that is not a semantic version.
  2. Check the application name against \A[a-z][a-z0-9_]*\z, at most 255 characters.
  3. Check that the application is not on the protected_apps list.
  4. Get the package into <extensions_path>/<app>-<version>. For :path, check the folder is below the extensions directory (the directory itself does not count) and move it if needed. For other types, check the allowlist, which includes https for a URL and the require_checksum rule, 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.
  5. Check that ebin holds a .app file, and log a warning when priv holds a .so, .dll or .dylib file.
  6. Read ebin/<app>.app and check it describes the application you named.
  7. 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.
  8. Add ebin to the code path, stop the application if it is running and unload it, then load the new .app file 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.
  9. Write the record, or update the one the application already has, and announce the install. If the write fails, step 8 is not undone.

install/1 does not change the working directory of the node.

Errors

Every error is {:error, [%{action: action, field: field, message: message}]}, sometimes with a source key.

actionfieldMessage
:semver:versionInvalid semantic version format in the version field (expected MAJOR.MINOR.PATCH).
:valid_name:appThe application name is invalid. Use only lowercase letters, digits and underscores.
:allowlist:appBlocked by <kind> policy: <value>. The kind is url_hosts, url_scheme, github_repos, protected_apps or require_checksum.
:package_dir:versionInvalid 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:pathYour library path is not allowed. It must be inside the extensions directory. The error also has an allowed key with the directory.
:rename_dir:pathThere was a problem moving the file.
:download:pathThere 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! The action is also returned for an address that is not https, a redirect to a host that is not allowed, more than 3 redirects, a GitHub path, tag or asset with characters it may not hold, an asset that is not an https address on github.com, and a connection that cannot be made.
:verify_checksum:checksumThe downloaded artifact does not match the expected sha256 checksum.
:extract:pathThe archive contains unsafe member paths, cannot be read or extracted, has no compiled ebin, or cannot be moved. Also: The name of the extracted package must be a single folder name.
:ebin_exist:pathThere is no compiled ebin directory (with a .app file) in the specified path.
:read_app:ebin_path, :erl_scanThe file cannot be read or parsed, or the application in it is not the one you named.
:compare_version_with_installed_app:appThere 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:pathSome routes cannot be prepended.
:application_ensure:appThe application, or one in its dependencies, could not be loaded or started.
:unload:appAn error occurred in deactivating the app.
:unique:globalThis library already exists in the database. unique/2 returns it when a record has the application name. Before 0.1.15 the text said event.
:delete:globalThe ID of the record you want to delete is incorrect or has already been deleted. delete/2 returns it when no record matches.
:write:globalThe ID of the record you want to update is incorrect or has already been deleted. write/3 returns it when no record matches.
:database:globalUnfortunately, 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

FunctionReturnsWhat it does
extensions_path()pathThe :extensions_path setting, or <project_path>/deployment/<env>/extensions.
prepend_compiled_apps(pairs):ok or an errorAdds each {app, ebin_path} to the code path with Code.prepend_path/1.
read_app(app, app_file){:ok, properties} or an errorParses an Erlang .app file and checks it names app.
extract(:tar, binary, name):ok or an errorExtracts a tar.gz into the extensions directory as name, which must be one folder 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 errorLoads 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 errorUnloads 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.

TypePackage mapRequest
:url%{path: url}A GET of url, which must be https. The status must be 200. The body is the raw bytes, with no decompression or decoding. The installer follows redirects, at most 3 times, and only to an https host that is the host of url, a host in url_hosts or, for GitHub, its release CDN.
:github_tag%{path: "owner/repo", tag: tag}, and asset if wantedThe 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, which must be an https address on github.com. The path, tag and asset are checked before the request: path is "owner/repo", and the other two hold only letters, digits and . _ + -.
:github_latest_release%{path: "owner/repo"}, and asset if wantedThe same at /releases/latest.

Requests use Req. Any status other than 200 is a :download error. A connection that cannot be made is a :download error too, and the library logs a warning with the host. 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.