Declare the external command-line tools your application needs.
Plenty of Elixir apps shell out — to ffmpeg, convert, pdftoppm,
pandoc, pg_dump. When one of those is missing the failure usually
surfaces as a System.cmd/3 crash deep inside a background job, at the
moment a user was expecting something to work.
This declares them up front, checks whether they are actually installed, and turns "it broke" into "install ffmpeg".
Declaring
defmodule MyApp.Deps do
use CliDeps
tool :ffmpeg,
version: ">= 6.0.0",
args: ["-version"],
install: [
macos: "brew install ffmpeg",
linux: "apt install ffmpeg",
default: "https://ffmpeg.org/download.html"
]
tool :convert,
label: "ImageMagick",
version: ">= 7.0.0",
install: [macos: "brew install imagemagick"]
endUsing
MyApp.Deps.check(:ffmpeg)
#=> {:ok, "7.1.1"}
MyApp.Deps.check(:convert)
#=> {:error, :not_found}
MyApp.Deps.available?(:ffmpeg)
#=> true
MyApp.Deps.explain(:convert)
#=> "ImageMagick was not found on PATH. Install it with: brew install imagemagick"
MyApp.Deps.report()
#=> [%{name: :ffmpeg, ok?: true, result: {:ok, "7.1.1"}, message: "..."}, ...]report/0 is meant for a status page, a health endpoint, or the
mix cli_deps.check task, which exits non-zero when anything is missing and
so works as a CI or container-build gate.
Awkward tools
Three kinds of tool need one extra line:
# Prints its version to stderr and exits 255 — opt in explicitly.
tool :ssh, args: ["-V"], allow_nonzero: true
# The OTP banner precedes "Elixir 1.18.4", so the default
# first-dotted-number pattern would read the erts version instead.
tool :elixir, version: "~> 1.18", pattern: ~r/Elixir (+.+.+)/
# First run is slow, or the machine is — the default kill is at 10s.
tool :corepack, timeout: :timer.seconds(30)A failing version command is an error even if its output happens to
contain a dotted number — failure output usually does, and trusting it once
reported a broken install as version 265.215.0, read out of a dylib name
in the loader error.
Caching
Results are cached for an hour, because whether ffmpeg is installed does
not change between requests — but it does change between deploys, so entries
expire rather than persisting for the life of the node. Override with
use CliDeps, ttl: :timer.minutes(5), or ttl: :infinity to check once per
boot. invalidate/0,1 clears entries after installing something.
What this does not do
It does not run your tools, and it does not install anything. Running a
subprocess well — streaming, timeouts, not orphaning it when the VM dies —
is its own problem, and MuonTrap, Rambo, and System.cmd/3 already
solve it. This answers only "is it there, and is it new enough".
Summary
Callbacks
Whether every declared tool is satisfied.
Whether a tool is present and satisfies its version requirement.
Checks a tool, using the cache.
Checks a tool, ignoring any cached result.
A human-readable explanation of a tool's current status.
Clears cached results for one tool, or all of them.
Checks every declared tool.
Looks up one declared tool by name.
Every tool declared in this module.
Functions
Declares a tool.
Types
Callbacks
@callback all_available?() :: boolean()
Whether every declared tool is satisfied.
Whether a tool is present and satisfies its version requirement.
@callback check(atom()) :: CliDeps.Check.result()
Checks a tool, using the cache.
@callback check!(atom()) :: CliDeps.Check.result()
Checks a tool, ignoring any cached result.
A human-readable explanation of a tool's current status.
@callback invalidate(atom() | :all) :: :ok
Clears cached results for one tool, or all of them.
@callback report() :: [report_entry()]
Checks every declared tool.
@callback tool(atom()) :: {:ok, CliDeps.Tool.t()} | :error
Looks up one declared tool by name.
@callback tools() :: [CliDeps.Tool.t()]
Every tool declared in this module.
Functions
Declares a tool.
Options
:executable— the binary name onPATH. Defaults to the tool's name.:version— a requirement string such as">= 6.0.0". Omit to check only that the tool exists.:args— arguments that make it print its version. Defaults to["--version"]; plenty of tools want["-version"]or["-v"].:pattern— a regex with one capture group locating the version in the output. The default takes the first dotted number, which is usually right and occasionally is not.:label— a human name for messages. Defaults to the executable.:install— per-OS install commands, keyed:macos,:linux,:windows, or:default.