The I/O surface for MobDev.Release.* modules — every external command,
every env-var read, every filesystem inspection that crosses out of
Elixir's BEAM happens through a function declared here.
Why this exists
Release-script logic is mostly orchestration: "for each source file,
call clang with these flags; then call ar; then run xcrun nm to verify
the symbol exists." The actual correctness of the orchestration —
did we pass the right flags? did we name the output file correctly? —
is independent of whether clang itself runs. By routing every external
invocation through a behaviour, tests can substitute a Mox mock that
asserts on the exact argv we'd have invoked, without paying the
wall-clock cost of running the real tool or depending on the host
environment.
Production code uses MobDev.Release.Shell.System (the default impl).
Tests use MobDev.Release.ShellMock (defined in test/support/).
Modules under MobDev.Release.* get the impl via application env:
impl = Application.get_env(:mob_dev, :release_shell, MobDev.Release.Shell.System)
impl.cmd(["clang", "-c", "x.c"], cd: "/tmp")Tests flip the env via setup to install the Mox.
Contract
Every callback returns either {:ok, value} or a tagged-tuple error
from MobDev.Release.Errors. No callback raises on user-facing error
paths — they all return tagged tuples so the caller chains them with
with.
Exception: programmer errors (bad argument types, etc.) may raise.
Summary
Callbacks
Run an external command. Returns {:ok, output} on exit-0,
{:error, {:cmd_failed, %{cmd, exit, output}}} otherwise.
Returns true if the path exists and is a directory.
Read an environment variable. Returns {:ok, value} if set,
:error if unset. Mirrors System.fetch_env/1 but routed through
the behaviour so tests don't have to poke the global env.
Returns true if the path exists and is a regular file.
Create a directory and any missing parents. Returns :ok or fs_failed.
Remove a file (best-effort). Returns :ok whether it existed or not,
errors only on permission issues. Mirrors rm -f.
Callbacks
@callback cmd( [String.t()], keyword() ) :: {:ok, String.t()} | MobDev.Release.Errors.t()
Run an external command. Returns {:ok, output} on exit-0,
{:error, {:cmd_failed, %{cmd, exit, output}}} otherwise.
opts accepts:
:cd— working directory (default: cwd):env— list of{name, value}env overrides (default: []):into— passthrough toSystem.cmd/3(default: nil, output is captured)
Returns true if the path exists and is a directory.
Read an environment variable. Returns {:ok, value} if set,
:error if unset. Mirrors System.fetch_env/1 but routed through
the behaviour so tests don't have to poke the global env.
Returns true if the path exists and is a regular file.
@callback mkdir_p(Path.t()) :: :ok | MobDev.Release.Errors.t()
Create a directory and any missing parents. Returns :ok or fs_failed.
@callback rm_f(Path.t()) :: :ok | MobDev.Release.Errors.t()
Remove a file (best-effort). Returns :ok whether it existed or not,
errors only on permission issues. Mirrors rm -f.