v0.2.1 - 2026-11-10
- Docs only. The README's guide links open the HexDocs pages again (on
hex.pm they opened the raw Markdown),
Precompiled.Host.run/1documents its ownreport()type, and the docs build without warnings. mix hex.publishruns in the docs env, where ex_doc lives.
v0.2.0 - 2026-11-10
Renamed to
precompiledbefore the first Hex release: the app and package:precompiled, modulesPrecompiled.*(the host API,run/1and theMix.install/2wrapperinstall/2, is nowPrecompiled.Host;Precompileditself is the device API), configconfig :precompiled, environment variablesPRECOMPILED_*, the default cache dir:filename.basedir(:user_cache, "precompiled"), the provenance file.mix/precompiled.jsonand the archiveprecompiled-<vsn>.ez. The Igniter installer ismix igniter.install precompiled(mix precompiled.install), and thedeps.loadpathsstep it adds is nowmix precompiled.deps. A clean break: none of the interimelixir_precompilednames is read, and a cache under the old dir is simply not used. The repository keeps its name,elixir_precompiled.Precompiled.install/1,2installs signed precompiled builds into a running Nerves device from an IEx shell, with no Mix, Hex or compiler on the device: it selects the build for the device's system, OTP, Elixir, CPU and present apps (by name and exact version), verifies every byte, loads and starts it with its absent runtime dependencies, and rolls the whole call back on any failure. Nothing present is ever replaced.Precompiled.list/1,Precompiled.uninstall/2anddry_run: true(withreport:for a bundle) come with it. See the "On a device" guide.On by default, with the official signed source (
https://compatibility.nerves-project.org/api/precompiled) as the default source; it only acts when called.config :precompiled, :devicesetsenabled: false(no installs),allow: [...](an allowlist), anothersource,dir,max_bytesandcacerts_path;enabledandallowcannot be overridden by a call.Installs are kept as a cache on
/dataacross reboots, never loaded at boot; installing again loads from the cache after re-verifying every file. The cache is bounded bymax_byteswith least recently used eviction, andPrecompiled.clean/0empties it.file://sources: a bundle directory with the API's layout, read from local storage only, with no network and no network apps.Offline bundles: a bundle's
offline.json, signed by the root'sofflinerole, has no expiry and needs no clock; a device honours it only from afile://source, under a root with anofflinerole, and never below the highest offline sequence it has seen. A device adopts a newer root from a bundle when it chains. The shipped root has noofflinerole yet, so a bundle is valid for its batch's 24 hours.mix precompiled.bundle PKG[@REQ]... --out DIRwrites a bundle on a host with network, planned from the device's facts (flags, or a dry run's--device-report), with every byte checked before it is written.mix igniter.install precompiled --devicekeeps the dependency at runtime so it ships in the firmware, and addscastoreunless the project already depends on it.:inets,:ssland:public_keyare optional applications;:cryptostays required. A firmware without them installs from bundles only (an online call returns{:error, {:missing_app, app}}).cacerts_path:(an option ofPrecompiled.install/2and a:deviceconfig key) names a PEM CA bundle for a device install. Without one (and withoutPRECOMPILED_CACERTS_PATH/HEX_CACERTS_PATH), an empty or missing OS certificate store now falls back to thecastorepackage's bundle when that is loaded. With no certificates at all the error now readsno CA certificates(before:no CA certificate store on this system), and an empty store is that error too, never a TLS attempt with no CAs.The host's check that an installed build is still intact is stricter about
priv: aprivthat is a copy rather than Mix's link to the dependency's sources (as on Windows, where links may be refused) no longer counts as intact, so such a build is installed once more.enabled: falsedisablesinstall/2only:list/1,uninstall/2andclean/1keep working, so what is installed can still be removed.
v0.1.0 - unreleased
- Any Mix project can use it:
mix precompiled.deps, reached through adeps.loadpathsalias (mix igniter.install precompiledadds it), installs builds before Mix compiles dependencies in plain projects, umbrellas and Phoenix apps, and for Nerves with released nerves too, where onlypure_elixirbuilds match: system (target) builds need the Nerves fork'sNerves.build_plan/0to name the system.Precompiled.Host.run/1for the nerves hook is unchanged. The Igniter installer sets the dependencyruntime: false,:sourceinconfig/config.exsand the alias, prepended to an existing one; Igniter is an optional dependency, declared only on Elixir 1.18+. - Never fails a build: any internal error is one warning and a source
compile; no
HOMEor an unwritable cache moves the cache under_build. - Warm runs make one conditional batch request (
If-None-Match) and download nothing once the source answersIf-None-Matchwith304(until then the batch itself is downloaded again; manifests and files still come from the cache); the root is checked at most once an hour; manifests are cached by SHA-256; files of a dependency download in parallel.PRECOMPILED_OFFLINE=1works from the cache; a run has a network deadline (:deadline_seconds, 300; root and batch 15 s). When the source cannot be reached, a cached batch that still passes every check is used; a refused response never falls back. A source that failed to connect is then not asked again in that run, so uncached manifests and files miss at once; a content origin that fails to connect is skipped for the run (the source still serves). - Content origins: the signed batch (or
:content) may name CDNs for files and manifests, with fallback to the source; every byte is still checked. - An installed build is kept while the batch still offers its exact files, so a new portal run of the same inputs no longer recompiles the project.
- Install refuses colliding paths (duplicate, case or Unicode variants,
path collision: ...), paths that are not plain relative paths inside the dependency's own build dir (unsafe path or mode (...)),ebin/entries other than beams,<dep>.appand<dep>.appup, such as another app's.app(foreign path: ...), and beams the.appdoes not list; linking happens before the install is renamed into place. A cache entry that is a symlink is never followed: that dependency compiles, and the line names the path to remove. - Dependencies that read
Mix.target()or environment variables at compile time are matched on them (README, rule 8). nerves' own dependencies are recognised from Mix's dependency data, so a git or pathnerveskeeps them out.PRECOMPILED=0, or no source configured, now removes installed builds (Mix compiles them in that run). Only the_buildof the run's own env and target is touched: under Nerves a run onhostremoves the host installs, never a device target's.mix precompiled.statusandmix precompiled.clean [--cache]; the cache is bounded (:cache_max_mb, 4096).- A batch the source did not deliver is reported as
batch unavailable: <why>(before: the bare reason); a delivered batch that fails a check is still reported by the check alone (batch stale,batch signature invalid, ...). - Lower-case proxy variables,
NO_PROXY, and a CA bundle (PRECOMPILED_CACERTS_PATHorHEX_CACERTS_PATH). The bundle replaces the OS store, as Hex's does: verification stays on and only its CAs are trusted. PRECOMPILED_SOURCEopts in without a config change; it wins over app config, an explicitsource:option wins over it. An empty value counts as unset.Precompiled.Host.install/2on Elixir 1.17 and 1.18 installs into the dirMix.install/2uses there (elixir-<vsn>-erts-<vsn>); before, its installs landed beside it and Mix compiled everything.- CI runs Elixir 1.17 to 1.20.
- A batch issued more than 24 hours ago, more than 5 minutes in the future, valid for more than 30 days, or read against a local clock earlier than this library's build time, is refused; the run falls back to compiling, same as an expired or rolled-back batch.
- Every run,
Precompiled.Host.install/2included, rechecks dependencies an earlier run installed (in a Mix project's_buildor theMix.install/2project) against the current batch: one the batch no longer names, or one newly listed in:except, is removed and compiled from source; one whose manifest changed is matched again. When no batch is available, installed builds are left as they are and one warning line says how many were kept, not rechecked (report.jsonstill lists each asfail,(installed build kept)). Builds installed by an earlier version carry no provenance file and are rechecked only aftermix deps.clean --buildor a clean_build. - Root rotation follows TUF: an intermediate root that expired after a newer one was adopted no longer ends the chain; only the root in use must be unexpired. Before, v1 -> v2 -> v3 fell back to v1 once v2 expired, which with the version floor failed every run.
- The adopted root version is kept and never goes back (the error names
the
root_versionfile to delete to accept an older root); a trust-state file that exists but will not parse is warned about and treated as 0. - On a device, the Nerves system is read from
/usr/lib/os-release; the loaded-application lookup never matched because systems areruntime: false, so devices only got pure-Elixir hits. - Extracted from nerves PR #1220:
Precompiled.Host.run/1is the build-time stepnervescalls;Config,Match,TrustandFetchmoved unchanged apart from their HTTP client and output. - New:
Precompiled.Host.install/2,Mix.install/2with the precompiled step in front of it, for use on a device. - Config moved to
config :precompiled, source: ...; theconfig :nerves, :precompiledform still works. - On a device, a dependency already compiled in the
Mix.install/2project is no candidate again: a second boot with a cached install re-places nothing and works offline. Builds this library installed there are rechecked against the batch, so such a boot fetches the root and batch. - Every HTTP request has an overall deadline (300 s by default) on top of the inactivity timer, so a server that keeps dripping data cannot hold a build or a boot open.
precompilednever installs a portal build of itself.- Renamed to
precompiled; old config keys andNERVES_PRECOMPILED*variables still work for one release. - Reads manifest format 2: a version lists its builds, each with a target
(
pure_elixiror a Nerves system) and the toolchain it was made with. Format 1 manifests still work. - Matching rules: the running Elixir must have the build's minor and at
least its patch (pre-releases compare as older), the OTP major must be
equal, and the dependency's compile env (
dep_env,produnless the project overrides it withenv:) must equal the build's. Among passing builds the closest Elixir patch wins, thenpure_elixir, then the newest run. report.jsoncarries"format": 1.- Ships as a Mix archive (
mix archive.install github TomHoenderdos/elixir_precompiled) so Livebook and scripts can callPrecompiled.Host.install/2in place ofMix.install/2; CI builds it. - The file cache moved from
~/.nerves/precompiledto the user cache dir (:filename.basedir(:user_cache, "precompiled"),~/Library/Caches/precompiledon macOS,~/.cache/precompiledon Linux). The old directory is not migrated, so the first run after upgrading downloads everything again; setPRECOMPILED_HOME=~/.nerves/precompiledto keep using the old one.