Runs a tool's availability and version check.
Three things here are deliberate and easy to get wrong:
A non-zero exit fails the check, even when the output contains a
version. The first version of this took any parsed version as proof of
presence, on the theory that some tools exit non-zero for --version — and
that theory inverted the check: failure output routinely contains a
dotted number. A broken install dying in the dynamic loader was reported as
version "265.215.0", read out of libx265.215.dylib in the error message.
Tools that genuinely report their version with a non-zero exit (ssh -V
exits 255) opt in with allow_nonzero: true.
The version subprocess has a timeout, ten seconds by default. A tool that ignores its version flag and waits on stdin would otherwise hang the check forever — and the moduledoc places checks on a request path behind an expiring cache, which is exactly where "forever" is unaffordable. On timeout the process is killed, not abandoned.
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.
Summary
Functions
A human-readable explanation of a result, including the install hint.
Extracts a version string from command output.
Checks one tool, without consulting or populating any cache.
Types
@type result() :: {:ok, String.t() | :unknown} | {:error, :not_found | {:version_mismatch, String.t(), String.t()} | {:unparseable_version, String.t()} | {:exec_failed, term()} | {:check_failed, non_neg_integer(), String.t()} | {:timeout, pos_integer()}}
The outcome of checking one tool.
{:ok, version}— present, and satisfying the requirement if one was declared{:ok, :unknown}— present, but no version could be parsed and none was required{:error, :not_found}— not onPATH{:error, {:version_mismatch, found, requirement}}— present but too old{:error, {:unparseable_version, output}}— present, a version was required, none found{:error, {:exec_failed, reason}}— the executable exists but could not be run{:error, {:check_failed, status, output}}— the version command exited non-zero{:error, {:timeout, ms}}— the version command did not finish in time
Functions
@spec explain(CliDeps.Tool.t(), result()) :: String.t()
A human-readable explanation of a result, including the install hint.
Written for whoever has to fix it, which is usually not the person who wrote the declaration.
@spec extract_version(CliDeps.Tool.t(), String.t()) :: String.t() | nil
Extracts a version string from command output.
Returns a value Version.parse/1 accepts, padding a two-part version such
as 7.1 to 7.1.0 — tools print both forms and Version requires three.
@spec run(CliDeps.Tool.t()) :: result()
Checks one tool, without consulting or populating any cache.