Packages

Declare the external command-line tools your app needs, check they are installed with version constraints, cache the result, and tell users how to install what is missing.

Current section

Files

Jump to
cli_deps README.md
Raw

README.md

# CliDeps
[![Hex.pm](https://img.shields.io/hexpm/v/cli_deps.svg)](https://hex.pm/packages/cli_deps)
[![Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/cli_deps)
Declare the external command-line tools your app shells out to, check they are
actually installed, and tell people how to install the ones that are missing.
```elixir
def deps do
[{:cli_deps, "~> 0.1"}]
end
```
## Why
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
someone was expecting something to work.
This turns "it broke" into "install ffmpeg", and makes it checkable in CI
before it reaches anyone.
## Usage
```elixir
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"]
end
```
```elixir
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: "..."}, ...]
```
### Fail the build instead of the request
```bash
mix cli_deps.check MyApp.Deps
```
Exits non-zero when anything is missing or too old, so it works as a CI step
or a container-build gate.
### Options
| Option | Purpose |
|---|---|
| `:version` | A requirement such as `">= 6.0.0"`. Omit to check only that the tool exists. |
| `:args` | Arguments that make it print its version. Defaults to `["--version"]`; many tools want `["-version"]`. |
| `:pattern` | A regex with one capture group locating the version. The default finds the first dotted number. |
| `:executable` | The binary name on `PATH`. Defaults to the tool's name. |
| `:label` | A human name for messages. |
| `:install` | Per-OS install commands, keyed `:macos`, `:linux`, `:windows`, `:default`. |
## Things worth knowing
**Version output is a mess, and that's the hard part.** `ffmpeg -version`
prints `ffmpeg version 7.1.1`, ImageMagick prints `Version: ImageMagick
7.1.1-47 Q16-HDRI`, Postgres prints `pg_dump (PostgreSQL) 17.6`. The default
pattern takes the first dotted number, which handles all of those; `:pattern`
exists for when it doesn't.
Two normalisations happen because `Version` is stricter than reality: a
two-part version like `17.6` is padded to `17.6.0`, and leading zeros are
stripped — poppler prints `25.04.0`, and SemVer forbids a leading zero, so
`Version.parse/1` rejects it outright.
**Exit status is ignored when a version was found.** A surprising number of
tools exit non-zero for `--version`, or print it to stderr. If a version can
be read, the tool is there.
**Nothing is executed unless the executable exists.** `System.find_executable/1`
is consulted first, so a missing tool costs a `PATH` scan rather than a
process spawn.
**Results are cached for an hour.** Whether `ffmpeg` is installed doesn't
change between requests, but it does change between deploys, so entries expire
rather than persisting for the life of the node. Use
`use CliDeps, ttl: :timer.minutes(5)`, or `ttl: :infinity` to check once per
boot. `invalidate/0,1` clears entries after installing something.
**Declarations are validated at compile time.** A malformed version
requirement, a bad regex, or a duplicate tool name fails the build rather than
the first check.
## What this doesn't do
It doesn't run your tools, and it doesn't install anything. Running a
subprocess well — streaming, timeouts, not orphaning it when the VM dies — is
its own problem, and [MuonTrap](https://hex.pm/packages/muontrap),
[Rambo](https://hex.pm/packages/rambo), and `System.cmd/3` already solve it.
This answers only "is it there, and is it new enough".
### Formatting
The package exports a formatter config, so declarations keep reading as
declarations in your project too:
```elixir
# .formatter.exs
[import_deps: [:cli_deps]]
```
## Development
```bash
mix test
mix precommit # compile --warnings-as-errors, deps check, hex.audit,
# format, credo --strict, dialyzer, test
```
## License
MIT