Precompiled (precompiled v0.2.1)

Copy Markdown View Source

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

OptionDefaultMeaning
:sourceconfig :precompiled, :device, source:, else the official APIThe 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_secondsas on the hostSame meaning as the host config keys.
:cacerts_pathnoneA PEM CA bundle to verify an https source with (a path).
:dir/data/precompiledThe 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.
:starttrueStart the installed apps (:temporary) after loading them.
:closuretrueInstall absent runtime dependencies. false refuses with {:needs, [...]} instead.
:max_closure, :max_bytes25, 64 MBBounds on the apps of one call and on the store's size.
:dry_runfalseSelect 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.
:reportnoneWith dry_run: true, write the device facts and the plan as JSON to this path (for mix precompiled.bundle --device-report).
:quietfalsePrint nothing.
:lock_retries60For 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}.

Summary

Functions

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.

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

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.

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.

Types

request()

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

Functions

clean(opts \\ [])

@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(request, opts \\ [])

@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(opts \\ [])

@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(app, opts \\ [])

@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.