CliDeps behaviour (CliDeps v0.1.0)

Copy Markdown View Source

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"]
end

Using

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.

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

Types

A per-tool entry in report/0.

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.

Types

report_entry()

@type report_entry() :: %{
  name: atom(),
  label: String.t(),
  ok?: boolean(),
  result: CliDeps.Check.result(),
  message: String.t()
}

A per-tool entry in report/0.

Callbacks

all_available?()

@callback all_available?() :: boolean()

Whether every declared tool is satisfied.

available?(atom)

@callback available?(atom()) :: boolean()

Whether a tool is present and satisfies its version requirement.

check(atom)

@callback check(atom()) :: CliDeps.Check.result()

Checks a tool, using the cache.

check!(atom)

@callback check!(atom()) :: CliDeps.Check.result()

Checks a tool, ignoring any cached result.

explain(atom)

@callback explain(atom()) :: String.t()

A human-readable explanation of a tool's current status.

invalidate(arg1)

@callback invalidate(atom() | :all) :: :ok

Clears cached results for one tool, or all of them.

report()

@callback report() :: [report_entry()]

Checks every declared tool.

tool(atom)

@callback tool(atom()) :: {:ok, CliDeps.Tool.t()} | :error

Looks up one declared tool by name.

tools()

@callback tools() :: [CliDeps.Tool.t()]

Every tool declared in this module.

Functions

tool(name, opts \\ [])

(macro)

Declares a tool.

Options

  • :executable — the binary name on PATH. 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.