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.

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.
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 replayed on boot.
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, inserted_at included, come from the new 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-z][a-z0-9_]*$, 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 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.
  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.

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.

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, github_repos or protected_apps.
: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!
: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.
: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 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: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. 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. 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 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.
: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 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.