# `MobDev.MLXDownloader`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/mlx_downloader.ex#L1)

Downloads and caches pre-built Apple MLX + EMLX NIF static archives so
iOS Mob apps can ship `EMLX.Backend` as an Nx backend without
cross-compiling MLX themselves.

Mirrors the `MobDev.OtpDownloader` / `MobDev.PythonAppleSupport` pattern:
hashed URL + cached download at `~/.mob/cache/mlx-<version>-<target>/`,
validated against the expected layout. Reused across projects.

Used by `MobDev.NativeBuild` when the project's deps include `:emlx` or
`mob.exs` declares `mlx_enabled: true`. The build template sources
`MLX_DIR` from `dir/1` and links `libmlx.a` + `libemlx.a` from there.

## Scope (v1)

CPU-only MLX. Metal-on-iOS is gated behind a separate tarball variant
(`libmlx-<ver>-ios-device-metal.tar.gz`) that requires the iOS-Metal
CMakeLists patch and the Xcode Metal Toolchain installed at build
time. v1 ships CPU + Accelerate framework — already
~10-50x faster than `Nx.BinaryBackend` for typical Nx workloads.

## Local-build override

Set `MOB_MLX_LOCAL_TARBALL_DIR=/path/to/dir` to bypass the GitHub
download and use locally-built tarballs (named exactly as
`tarball_name/1` returns). Useful when iterating on the cross-compile
scripts in `mob_dev/scripts/release/mlx/`.

# `target`

```elixir
@type target() :: :ios_device | :ios_sim
```

Target slice this downloader supports.

# `dir`

```elixir
@spec dir(target()) :: String.t()
```

Cached MLX root directory for `target`. May not exist if `ensure/1`
hasn't been called.

# `download_url`

```elixir
@spec download_url(target()) :: String.t()
```

Download URL for the `target` tarball.

# `ensure`

```elixir
@spec ensure(target()) :: {:ok, String.t()} | {:error, term()}
```

Ensure the MLX bundle for `target` is cached and extracted.
Returns `{:ok, path}` where `path` is the unpacked root containing
`lib/libmlx.a`, `lib/libemlx.a`, `include/mlx/...`, `VERSION`.

# `ensure_ios_device`

```elixir
@spec ensure_ios_device() :: {:ok, String.t()} | {:error, term()}
```

Convenience for iOS device.

# `ensure_ios_sim`

```elixir
@spec ensure_ios_sim() :: {:ok, String.t()} | {:error, term()}
```

Convenience for iOS simulator.

# `metallib_path`

```elixir
@spec metallib_path(String.t()) :: nil | String.t()
```

Path to `mlx.metallib` if this bundle ships Metal GPU kernels, or
`nil` if it's a CPU-only bundle.

The CPU build (`scripts/release/mlx/ios_device.sh`) doesn't produce
a metallib. The Metal build (`ios_device_metal.sh`) puts one at
`lib/mlx.metallib` alongside the static archives. Callers use this
to decide whether to copy the metallib into the iOS .app bundle so
EMLX's runtime `device: :gpu` path can find it (via MLX's
`load_colocated_library`).

# `mlx_version`

```elixir
@spec mlx_version() :: String.t()
```

Pinned MLX upstream version (e.g. `0.25.1`).

# `name`

```elixir
@spec name(target()) :: String.t()
```

Bundle name (no extension, no path) for `target` — `libmlx-0.25.1-ios-device` etc.

# `release_tag`

```elixir
@spec release_tag() :: String.t()
```

GitHub release tag this downloader targets.

# `tarball_name`

```elixir
@spec tarball_name(target()) :: String.t()
```

Tarball file name for `target` (e.g. `libmlx-0.25.1-ios-device.tar.gz`).

# `valid_dir?`

```elixir
@spec valid_dir?(String.t()) :: boolean()
```

Returns true if the cache directory has the expected layout
(`lib/libmlx.a`, `lib/libemlx.a`, `include/mlx/`, `VERSION`).

Public for testing and so `NativeBuild` can cheaply probe for a partial
cache without parsing the VERSION file.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
