MobDev.Release.Shell behaviour (mob_dev v0.5.3)

Copy Markdown View Source

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.

Functions

Return the configured implementation module. Production: the real System impl. Tests: whatever Mox/test setup installs.

Callbacks

cmd(list, keyword)

@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 to System.cmd/3 (default: nil, output is captured)

dir?(t)

@callback dir?(Path.t()) :: boolean()

Returns true if the path exists and is a directory.

fetch_env(t)

@callback fetch_env(String.t()) :: {:ok, String.t()} | :error

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.

file?(t)

@callback file?(Path.t()) :: boolean()

Returns true if the path exists and is a regular file.

mkdir_p(t)

@callback mkdir_p(Path.t()) :: :ok | MobDev.Release.Errors.t()

Create a directory and any missing parents. Returns :ok or fs_failed.

rm_f(t)

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

Functions

impl()

@spec impl() :: module()

Return the configured implementation module. Production: the real System impl. Tests: whatever Mox/test setup installs.