Packages
mob_dev
0.5.5
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/release/shell.ex
defmodule MobDev.Release.Shell do
@moduledoc """
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.
"""
alias MobDev.Release.Errors
@doc """
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)
"""
@callback cmd([String.t()], keyword()) :: {:ok, String.t()} | Errors.t()
@doc "Returns `true` if the path exists and is a directory."
@callback dir?(Path.t()) :: boolean()
@doc "Returns `true` if the path exists and is a regular file."
@callback file?(Path.t()) :: boolean()
@doc """
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.
"""
@callback fetch_env(String.t()) :: {:ok, String.t()} | :error
@doc "Create a directory and any missing parents. Returns `:ok` or fs_failed."
@callback mkdir_p(Path.t()) :: :ok | Errors.t()
@doc """
Remove a file (best-effort). Returns `:ok` whether it existed or not,
errors only on permission issues. Mirrors `rm -f`.
"""
@callback rm_f(Path.t()) :: :ok | Errors.t()
# ── Default impl resolver ─────────────────────────────────────────────
@doc """
Return the configured implementation module. Production: the real
`System` impl. Tests: whatever Mox/test setup installs.
"""
@spec impl() :: module()
def impl, do: Application.get_env(:mob_dev, :release_shell, MobDev.Release.Shell.System)
end
defmodule MobDev.Release.Shell.System do
@moduledoc """
Production implementation of `MobDev.Release.Shell` — uses the real
`System`, `File`, and OS to do work. Pure passthrough; the
intelligence is in the callers.
"""
@behaviour MobDev.Release.Shell
alias MobDev.Release.Errors
@impl true
def cmd(argv, opts \\ []) when is_list(argv) and length(argv) >= 1 do
[exe | args] = argv
cmd_opts = Keyword.merge([stderr_to_stdout: true], Keyword.take(opts, [:cd, :env, :into]))
case System.cmd(exe, args, cmd_opts) do
{output, 0} -> {:ok, output}
{output, exit_code} -> Errors.cmd_failed(argv, exit_code, output)
end
rescue
e in ErlangError ->
# Most common cause: executable not on $PATH. Translate to
# precondition_failed since it's almost always an env issue.
Errors.precondition("could not execute #{hd(argv)}: #{Exception.message(e)}")
end
@impl true
def dir?(path), do: File.dir?(path)
@impl true
def file?(path), do: File.regular?(path)
@impl true
def fetch_env(name), do: System.fetch_env(name)
@impl true
def mkdir_p(path) do
case File.mkdir_p(path) do
:ok -> :ok
{:error, reason} -> Errors.fs_failed(path, reason)
end
end
@impl true
def rm_f(path) do
case File.rm(path) do
:ok -> :ok
{:error, :enoent} -> :ok
{:error, reason} -> Errors.fs_failed(path, reason)
end
end
end