# `Precompiled`
[🔗](https://github.com/TomHoenderdos/elixir_precompiled/blob/v0.2.1/lib/precompiled.ex#L5)

Installs signed precompiled libraries into a running Nerves device, with
no Mix, no Hex and no compiler.

    Precompiled.install(:recon)
    Precompiled.install({:circuits_gpio, "2.3.1"})
    Precompiled.install([:recon, {:circuits_gpio, "~> 2.3"}])
    Precompiled.install(:recon, dry_run: true)
    Precompiled.install(:recon, source: "file:///media/usb/precompiled")
    Precompiled.list()
    Precompiled.uninstall(:recon)

A call selects the build that matches the device (its Nerves system and
version, OTP, Elixir, CPU and the apps it already has, by name and exact
version), verifies every byte through the signed root, batch, manifest
and file hashes, stages the files under the store directory, and loads
and starts them. Absent runtime dependencies are installed with it, as
one unit for the running VM: anything that fails leaves the code path
and the loaded apps as they were. The store is a cache and may change
before a later step fails: least recently used entries can be evicted
for room, and builds that verified and were committed stay in it,
unloaded, for a later call. Nothing is compiled, nothing unsigned is
loaded, and nothing already present is replaced.

It is available by default. Nothing runs on its own: only someone with a
shell on the device calls it. The files stay in the store across reboots
as a cache, but nothing is loaded at boot.

## Cache

Installed builds stay in `dir` across reboots, but nothing is loaded at
boot. Installing the same build again loads it from there without
downloading, after checking every cached file against the signed hashes
of the current batch. The cache is bounded by `max_bytes` and evicts the
least recently used builds that are not loaded. `Precompiled.clean/0`
empties it.

## Options

| Option | Default | Meaning |
| --- | --- | --- |
| `:source` | `config :precompiled, :device, source:`, else the official API | The API root (`https://...`, or `http://` on loopback only), or `file://DIR` for a bundle. The default is `https://compatibility.nerves-project.org/api/precompiled`, checked against the root this library ships. Any other source must be configured or passed. |
| `:content`, `:deadline_seconds` | as on the host | Same meaning as the host config keys. |
| `:cacerts_path` | none | A PEM CA bundle to verify an `https` source with (a path). |
| `:dir` | `/data/precompiled` | The store. Its trust state lives in `DIR/.trust`: a call with another `:dir` gets its own trust state (root chain and sequence floors), starting again from the shipped root. |
| `:start` | `true` | Start the installed apps (`:temporary`) after loading them. |
| `:closure` | `true` | Install absent runtime dependencies. `false` refuses with `{:needs, [...]}` instead. |
| `:max_closure`, `:max_bytes` | 25, 64 MB | Bounds on the apps of one call and on the store's size. |
| `:dry_run` | `false` | Select and plan, verify the trust chain, fetch the manifests and `.app` files only. Returns the plan. It writes no entry, staging dir or eviction: only the trust state in `DIR/.trust`, and the store dir and its marker when they do not exist yet. |
| `:report` | none | With `dry_run: true`, write the device facts and the plan as JSON to this path (for `mix precompiled.bundle --device-report`). |
| `:quiet` | `false` | Print nothing. |
| `:lock_retries` | 60 | For tests: how often to retry a store another call holds. |

Another trust anchor than the shipped `priv/precompiled/root.json` is
set in the firmware's config only (`config :precompiled,
:device, root: path`), never per call.

The device facts, the clock check and the trust anchor have test seams
(`:env`, `:clock_synchronized?`, `:root`). A call may pass them only while this library's
application env has `test_seams: true`, as its own test suite sets;
otherwise they are refused as unknown options, and the firmware's
`:device` config never sets them.

`config :precompiled, :device, enabled: false` in the firmware
disables `install/2` (`{:error, :disabled}`); `list/1`, `uninstall/2`
and `clean/1` keep working, so what is installed can still be removed.
`allow: [app]` limits installs to those apps, closure members included.
Both are read from the firmware's config only: a call can neither pass,
widen nor clear them. The same config may set the defaults of `source`,
`content`, `root`, `cacerts_path`, `deadline_seconds`, `dir`,
`max_bytes`, `max_closure`, `start`, `closure` and `quiet`; any other key
there is ignored.

An online call needs a synchronized clock: on an unsynchronized one it
refuses before any request. A `file://` bundle with a signed
`offline.json` needs no clock and no network at all; one without it is
bounded by its batch's own expiry, judged against the device clock (no
NTP needed, but a clock before this library's build is refused).

## Return values

  * `{:ok, [%Precompiled.Installed{}]}`, the apps this call installed
    (empty when everything was already there);
  * with `dry_run: true`, `{:ok, %Precompiled.Plan{}}`;
  * `{:error, reason}`, where `reason` is one of:
    * `{:bad_request, term}`, `{:bad_option, key}`, `{:bad_source, s}`;
    * `{:clock_unsynchronized, now}`;
    * `{:missing_app, app}`, a network app the firmware leaves out;
    * `{:batch, why}`, the trust chain refused (`why` is the client's
      batch refusal text);
    * `{:no_build, pkg, why}`, the first failed rule of the build that
      got furthest;
    * `{:needs, [{app, vsn}]}`, with `closure: false`;
    * `{:not_allowed, app}`;
    * `{:already_present, app, vsn}`;
    * `{:module_loaded | :module_conflict, mod, path}`;
    * `{:noexec, mount}`;
    * `{:conflict, app, [vsn]}`, two versions of one app in one call;
    * `{:too_big, needed, free}`, `{:too_many, n, max}`;
    * `{:enospc, path}`, a full disk while staging or committing;
    * `{:unsafe_store, why}`;
    * `{:rename, posix}`, `{:bad_staging, path}`, or a posix atom such
      as `:eacces`, when the store refuses a commit;
    * `{:fetch, pkg, why}`, a file that did not verify;
    * `{:elf, why}`, a native library built for another CPU;
    * `{:on_load_failed, mod, reason}`;
    * `{:start_failed, app, reason}`;
    * `{:in_use, mod, pids}`, `{:in_use_by, [app]}`, `{:not_installed, app}`;
    * from `uninstall/2`: `{:stop_failed, app, why}`, the app would not
      stop or unload, and `{:in_use, app}`, with `remove: true`, when
      its files are still in use;
    * `{:report, path, why}`, the dry run's report could not be written;
    * `:busy`, another call holds the store;
    * `:disabled`;
    * `{:internal, message}`.

`install/2` never raises on the network or on bad input.

The store's lock serializes calls on this node. An installed app whose
`start/2` itself calls `install/2` waits out the retries and gets
`{:error, :busy}`.

# `request`

```elixir
@type request() :: atom() | {atom(), String.t()}
```

# `clean`

```elixir
@spec clean(keyword()) ::
  {:ok, %{removed: [atom()], bytes: non_neg_integer(), kept: [atom()]}}
  | {:error, term()}
```

Empties the cache: removes every store entry that is not loaded in this
VM. Keeps `.trust` and whatever this VM is running from. Runs under the
store's lock. Prints one summary line unless `quiet: true`.

# `install`

```elixir
@spec install(request() | [request()], keyword()) ::
  {:ok, [Precompiled.Installed.t()]}
  | {:ok, Precompiled.Plan.t()}
  | {:error, term()}
```

Installs, loads and starts `request` and its absent runtime
dependencies. See the module documentation for the options and reasons.

# `list`

```elixir
@spec list(keyword()) :: [Precompiled.Installed.t()] | {:error, term()}
```

The apps in the store, each with whether it is loaded in this VM (`[]`
when there is no store yet), or `{:error, reason}`: a bad option, an
unsafe store, `{:internal, message}`. Prints one line per app unless
`quiet: true`.

# `uninstall`

```elixir
@spec uninstall(atom(), keyword()) :: :ok | {:error, term()}
```

Unloads an installed app: stops it, unloads it, purges its modules and
takes its directory off the code path. Its files stay in the store as a
cache unless `remove: true`.

Refuses `{:in_use_by, apps}` while other installed apps that are loaded
need it, unless `cascade: true` (which unloads them first). Refuses
`{:in_use, mod, pids}` while other processes run its code, unless
`force: true`.

"In use" means code: processes running the app's modules, as
`:code.soft_purge/1` sees them. Data is not detected: an open NIF
resource or a reference into the app (a GPIO handle from
`Circuits.GPIO.open/2`, say) held by a shell or another process does not
stop the uninstall. Close such handles (`Circuits.GPIO.close/1`) before
uninstalling.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
