Packages
mob_dev
0.3.10
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/otp_downloader.ex
defmodule MobDev.OtpDownloader do
@moduledoc """
Downloads and caches pre-built OTP releases from GitHub for Android and iOS simulator.
Artifacts are cached at `~/.mob/cache/` and reused across projects.
"""
@otp_hash "73ba6e0f"
@release_tag "otp-#{@otp_hash}"
@base_url "https://github.com/GenericJam/mob/releases/download/#{@release_tag}"
@android_name "otp-android-#{@otp_hash}"
@android_arm32_name "otp-android-arm32-#{@otp_hash}"
@ios_sim_name "otp-ios-sim-#{@otp_hash}"
@ios_device_name "otp-ios-device-#{@otp_hash}"
@doc "Ensures the Android OTP release is cached. Returns {:ok, path} or {:error, reason}."
@spec ensure_android(String.t()) :: {:ok, String.t()} | {:error, term()}
def ensure_android(abi \\ "arm64-v8a") do
case abi do
"armeabi-v7a" -> ensure(@android_arm32_name, "#{@android_arm32_name}.tar.gz")
_ -> ensure(@android_name, "#{@android_name}.tar.gz")
end
end
@doc "Ensures the iOS simulator OTP release is cached. Returns {:ok, path} or {:error, reason}."
@spec ensure_ios_sim() :: {:ok, String.t()} | {:error, term()}
def ensure_ios_sim do
ensure(@ios_sim_name, "#{@ios_sim_name}.tar.gz")
end
@doc "Ensures the iOS device OTP release is cached. Returns {:ok, path} or {:error, reason}."
@spec ensure_ios_device() :: {:ok, String.t()} | {:error, term()}
def ensure_ios_device do
ensure(@ios_device_name, "#{@ios_device_name}.tar.gz")
end
@doc "Returns the cached Android OTP directory path (may not exist yet)."
@spec android_otp_dir(String.t()) :: String.t()
def android_otp_dir(abi \\ "arm64-v8a") do
case abi do
"armeabi-v7a" -> cache_dir(@android_arm32_name)
_ -> cache_dir(@android_name)
end
end
@doc "Returns the cached iOS simulator OTP directory path (may not exist yet)."
@spec ios_sim_otp_dir() :: String.t()
def ios_sim_otp_dir, do: cache_dir(@ios_sim_name)
@doc "Returns the cached iOS device OTP directory path (may not exist yet)."
@spec ios_device_otp_dir() :: String.t()
def ios_device_otp_dir, do: cache_dir(@ios_device_name)
# ── Private ──────────────────────────────────────────────────────────────────
defp ensure(name, tarball) do
dir = cache_dir(name)
if valid_otp_dir?(dir) do
{:ok, dir}
else
# Remove stale/incomplete directory before re-downloading.
# This happens when a previous download attempt failed after mkdir
# but before (or during) extraction — e.g. on Nix where curl may use
# different CA certificates, or on a flaky network.
if File.dir?(dir), do: File.rm_rf!(dir)
download_and_extract(name, tarball, dir)
end
end
# A valid extracted OTP dir must contain at least one erts-* subdirectory.
defp valid_otp_dir?(dir) do
File.dir?(dir) and Path.wildcard(Path.join(dir, "erts-*")) != []
end
defp cache_dir(name) do
base =
System.get_env("MOB_CACHE_DIR") ||
Path.join([System.get_env("HOME"), ".mob", "cache"])
Path.join(base, name)
end
defp download_and_extract(name, tarball, dest_dir) do
url = "#{@base_url}/#{tarball}"
tmp_file = Path.join(System.tmp_dir!(), tarball)
IO.puts(" Downloading #{name} OTP release...")
IO.puts(" URL: #{url}")
File.mkdir_p!(Path.dirname(dest_dir))
with :ok <- download(url, tmp_file),
:ok <- extract(tmp_file, dest_dir),
:ok <- verify_erts(dest_dir) do
File.rm(tmp_file)
IO.puts(" Cached at #{dest_dir}")
{:ok, dest_dir}
else
{:error, reason} ->
File.rm(tmp_file)
File.rm_rf(dest_dir)
{:error, reason}
end
end
defp download(url, dest) do
case System.cmd("curl", ["-L", "--fail", "--progress-bar", "-o", dest, url],
stderr_to_stdout: false
) do
{_, 0} -> :ok
{out, rc} -> {:error, "curl failed (exit #{rc}): #{String.trim(out)}"}
end
end
defp extract(tarball, dest_dir) do
File.mkdir_p!(dest_dir)
# The tarball extracts into a single top-level directory; strip it with --strip-components=1.
case System.cmd("tar", ["xzf", tarball, "-C", dest_dir, "--strip-components=1"],
stderr_to_stdout: true
) do
{_, 0} -> :ok
{out, rc} -> {:error, "tar failed (exit #{rc}): #{String.trim(out)}"}
end
end
defp verify_erts(dir) do
case Path.wildcard(Path.join(dir, "erts-*")) do
[_ | _] ->
:ok
[] ->
{:error,
"OTP extraction produced no erts-* directory in #{dir}.\n" <>
" The tarball may have an unexpected layout.\n" <>
" Run `mix mob.doctor` for diagnosis, or report at https://github.com/GenericJam/mob/issues"}
end
end
end