Packages
mob_dev
0.5.4
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/mix/tasks/mob.cache.ex
defmodule Mix.Tasks.Mob.Cache do
use Mix.Task
@shortdoc "Show or clear the machine-wide caches Mob writes to"
@moduledoc """
Inspects every cache `mix mob.*` writes to outside the project tree, and
(with `--clear`) deletes them. Distinct from `mix clean` (build artifacts
in `_build/`) and `mix deps.clean` (deps in `deps/`) — this targets caches
in your home directory that survive across projects.
By default the command is read-only — it prints a summary of what's on
disk and exits. Pass `--clear` to wipe Mob's own cache, and add
`--include-transitive` to also wipe caches owned by transitive deps
(currently `elixir_make`, used by `exqlite` for its prebuilt NIF tarball).
Caches we *do not* touch — even with `--include-transitive` — because
they're shared with the rest of your Elixir/Android/iOS work:
* `~/.hex/`, `~/.mix/` — Hex/Mix global state
* `~/.gradle/` — Gradle wrapper + caches
* `~/Library/Developer/Xcode/` — Xcode DerivedData, Index
* `~/Library/Caches/com.apple.dt.*` — Xcode-related caches
Clean those manually if you want a true scorched-earth reset.
## Usage
mix mob.cache # show what's on disk (default)
mix mob.cache --include-transitive # also list elixir_make cache
mix mob.cache --clear # delete Mob's own cache
mix mob.cache --clear --include-transitive # delete Mob's + elixir_make's
mix mob.cache --clear --yes # skip the confirmation prompt
mix mob.cache --dry-run # explicit "list only" (alias for default)
## Where the caches live
**Mob's own cache** — pre-built OTP runtimes (iOS sim, iOS device, Android
arm64, Android arm32). One per platform/ABI; ~200–400 MB each. Reused
across every Mob project on this machine.
$MOB_CACHE_DIR (if set)
~/.mob/cache/ (default)
Override with `MOB_CACHE_DIR` in your shell or `mob.exs` if you want it
somewhere project-local or sandbox-friendly (Nix users: this is the
switch you want).
**`elixir_make` cache** — pre-built NIF tarballs that `exqlite` and other
NIF-using deps download instead of recompiling from source. The same
tarball is reused across every Elixir project on this machine.
~/Library/Caches/elixir_make/ (macOS)
~/.cache/elixir_make/ (Linux)
This belongs to `elixir_make`, not Mob — but `mix mob.deploy` is what
populated it, so we offer to clear it here.
"""
@switches [
clear: :boolean,
include_transitive: :boolean,
dry_run: :boolean,
yes: :boolean
]
@impl Mix.Task
def run(args) do
{opts, _, _} = OptionParser.parse(args, switches: @switches)
targets = resolve_targets(opts)
print_header()
print_targets(targets)
cond do
opts[:clear] != true ->
IO.puts("")
IO.puts("(read-only — pass --clear to delete; add --include-transitive to widen)")
opts[:dry_run] == true ->
IO.puts("")
IO.puts("(--dry-run: nothing was deleted)")
opts[:yes] == true or confirm_delete?(targets) ->
delete_targets(targets)
true ->
IO.puts("")
IO.puts("Aborted.")
end
end
# ── Target resolution ──────────────────────────────────────────────────────
defp resolve_targets(opts) do
list = [our_cache()] ++ sim_runtime_targets()
list = if opts[:include_transitive], do: list ++ [elixir_make_cache()], else: list
list
end
@doc false
@spec our_cache() :: %{name: String.t(), path: String.t(), kind: :ours}
def our_cache do
base =
System.get_env("MOB_CACHE_DIR") ||
Path.join([System.user_home!(), ".mob", "cache"])
%{
name: "Mob OTP runtime cache",
path: base,
kind: :ours,
hint: "set MOB_CACHE_DIR to relocate; otherwise lives at ~/.mob/cache"
}
end
# Return both the new (~/.mob/runtime/ios-sim) and legacy (/tmp/otp-ios-sim)
# iOS simulator runtime locations, plus any MOB_SIM_RUNTIME_DIR override —
# deduplicated. Users can have stale data in either place if they've used
# multiple projects, so list both unconditionally regardless of which one
# the current project would resolve to.
@doc false
@spec sim_runtime_targets() :: [map()]
def sim_runtime_targets do
new_default = MobDev.Paths.default_runtime_dir()
legacy = MobDev.Paths.legacy_tmp_path()
override = System.get_env("MOB_SIM_RUNTIME_DIR")
[new_default, legacy, override]
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
|> Enum.map(&sim_runtime_entry/1)
end
defp sim_runtime_entry(path) do
%{
name: "iOS simulator runtime (#{label_for_runtime(path)})",
path: path,
kind: :ours,
hint: hint_for_runtime(path)
}
end
defp label_for_runtime(path) do
cond do
path == MobDev.Paths.default_runtime_dir() -> "current default"
path == MobDev.Paths.legacy_tmp_path() -> "legacy /tmp location"
true -> "MOB_SIM_RUNTIME_DIR override"
end
end
defp hint_for_runtime(path) do
cond do
path == MobDev.Paths.default_runtime_dir() ->
"writable OTP root for new projects; mob_new ≥ 0.1.20"
path == MobDev.Paths.legacy_tmp_path() ->
"used by projects whose ios/build.sh predates MOB_SIM_RUNTIME_DIR"
true ->
"set MOB_SIM_RUNTIME_DIR to override the default"
end
end
@doc false
@spec elixir_make_cache() :: %{name: String.t(), path: String.t(), kind: :transitive}
def elixir_make_cache do
%{
name: "elixir_make precompiled-NIF cache",
path: elixir_make_cache_path(),
kind: :transitive,
hint: "owned by elixir_make (used by exqlite); reused across all Elixir projects"
}
end
@doc false
@spec elixir_make_cache_path() :: String.t()
def elixir_make_cache_path do
case :os.type() do
{:unix, :darwin} ->
Path.join([System.user_home!(), "Library", "Caches", "elixir_make"])
_ ->
Path.join([System.user_home!(), ".cache", "elixir_make"])
end
end
# ── Reporting ──────────────────────────────────────────────────────────────
defp print_header do
IO.puts("")
IO.puts("Mob caches on this machine:")
IO.puts("")
end
defp print_targets(targets) do
Enum.each(targets, fn t ->
{exists?, size_str} = path_status(t.path)
status_icon =
case {exists?, t.kind} do
{true, :ours} -> IO.ANSI.cyan() <> "●" <> IO.ANSI.reset()
{true, :transitive} -> IO.ANSI.yellow() <> "●" <> IO.ANSI.reset()
{false, _} -> IO.ANSI.faint() <> "○" <> IO.ANSI.reset()
end
IO.puts(" #{status_icon} #{t.name}")
IO.puts(" path: #{t.path}")
IO.puts(" size: #{size_str}")
IO.puts(" note: #{t.hint}")
IO.puts("")
end)
end
@doc false
@spec path_status(String.t()) :: {boolean(), String.t()}
def path_status(path) do
cond do
not File.exists?(path) ->
{false, "(not present)"}
File.dir?(path) ->
{true, format_size(dir_size(path))}
true ->
case File.stat(path) do
{:ok, %File.Stat{size: s}} -> {true, format_size(s)}
_ -> {true, "(unknown)"}
end
end
end
defp dir_size(dir) do
Path.wildcard(Path.join(dir, "**/*"), match_dot: true)
|> Enum.reduce(0, fn p, acc ->
case File.stat(p) do
{:ok, %File.Stat{type: :regular, size: s}} -> acc + s
_ -> acc
end
end)
end
@doc false
@spec format_size(non_neg_integer()) :: String.t()
def format_size(bytes) when bytes < 1024, do: "#{bytes} B"
def format_size(bytes) when bytes < 1024 * 1024 do
:io_lib.format("~.1f KB", [bytes / 1024]) |> IO.iodata_to_binary()
end
def format_size(bytes) when bytes < 1024 * 1024 * 1024 do
:io_lib.format("~.1f MB", [bytes / (1024 * 1024)]) |> IO.iodata_to_binary()
end
def format_size(bytes) do
:io_lib.format("~.2f GB", [bytes / (1024 * 1024 * 1024)]) |> IO.iodata_to_binary()
end
# ── Deletion ───────────────────────────────────────────────────────────────
defp confirm_delete?(targets) do
paths_to_delete = Enum.filter(targets, &File.exists?(&1.path))
if paths_to_delete == [] do
IO.puts("Nothing to delete — all listed caches are already absent.")
false
else
IO.puts("")
IO.write("Delete the paths above? [y/N] ")
case IO.gets("") do
:eof -> false
input -> String.trim(input) |> String.downcase() == "y"
end
end
end
defp delete_targets(targets) do
Enum.each(targets, fn t ->
if File.exists?(t.path) do
case File.rm_rf(t.path) do
{:ok, _} ->
IO.puts(" #{IO.ANSI.green()}✓ deleted#{IO.ANSI.reset()} #{t.path}")
{:error, reason, file} ->
IO.puts(
" #{IO.ANSI.red()}✗ failed#{IO.ANSI.reset()} #{t.path} — #{inspect(reason)} (#{file})"
)
end
end
end)
IO.puts("")
end
end