Hex.pm Docs

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.

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

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

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

OptionPurpose
:versionA requirement such as ">= 6.0.0". Omit to check only that the tool exists.
:argsArguments that make it print its version. Defaults to ["--version"]; many tools want ["-version"].
:patternA regex with one capture group locating the version. The default finds the first dotted number.
:executableThe binary name on PATH. Defaults to the tool's name.
:labelA human name for messages.
:installPer-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, 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:

# .formatter.exs
[import_deps: [:cli_deps]]

Development

mix test
mix precommit   # compile --warnings-as-errors, deps check, hex.audit,
                # format, credo --strict, dialyzer, test

License

MIT