Packages

Encapsulates Elixir releases alongside their ERTS into self-contained executable binaries. Downloads ERTS from the official mirror (Lorenzo-SF/Batamanta---ERTS-repository) with fallback to system ERTS if unavailable.

Current section

Files

Jump to
batamanta README.md
Raw

README.md

<p align="center">
  <img src="https://raw.githubusercontent.com/Lorenzo-SF/Batamanta/main/assets/batamantaman.png" width="400" alt="Batamanta Mascot" />
</p>

> Package your Elixir applications as 100% self-contained executables. No Erlang/Elixir installation required on the target machine.

---
  
<p align="center">
  <a href="https://hex.pm/packages/batamanta">
    <img src="https://img.shields.io/hexpm/v/batamanta.svg" alt="Hex Version">
  </a>
  <a href="https://hexdocs.pm/batamanta">
    <img src="https://img.shields.io/badge/docs-hexdocs-blue" alt="HexDocs">
  </a>
  <a href="https://github.com/Lorenzo-SF/Batamanta/actions/workflows/ci.yml">
    <img src="https://img.shields.io/github/actions/workflow/status/Lorenzo-SF/Batamanta/ci.yml?branch=main" alt="CI Status">
  </a>
  <img src="https://img.shields.io/badge/self--contained-binaries-success" alt="Self-contained">
  <img src="https://img.shields.io/badge/ERTS-embedded-critical" alt="ERTS Embedded">
</p>

---

## Features

- **Self-contained binaries**: Single executable with your app + ERTS embedded
- **Smart ERTS provisioning**: Auto-detects platform or force specific target
- **Cross-compilation**: Build for Linux (glibc/musl), macOS from any platform
- **Zstandard compression**: Optimal balance between size and speed
- **Multiple execution modes**: CLI, TUI, Daemon, and Escript support
- **BEAM-keeps-alive daemon mode** (Unix): wraps a persistent Erlang VM behind the binary so repeated invocations skip the cold-start cost (mix release + cargo build + Erlang boot — ~10-25s → ~5-15ms on warm calls). Unix-domain socket + length-prefixed JSON protocol; per-app/version/target namespaced; configurable TTL
- **Relativized releases**: Portable binaries with no absolute paths
- **Automatic build cleanup**: Intermediate artifacts are wiped after build, leaving the system pristine while preserving ERTS cache
- **Build Environment Isolation**: Automatically isolates the build from version managers (`asdf`, `mise`, `kerl`) to prevent ERTS mismatches
- **Robust downloads**: Automatic retry with exponential backoff on network failures
- **Concurrent-safe caching**: File-based locking prevents race conditions in multi-process builds
- **Clear error messages**: Specific error codes for disk full, permission denied, corrupted archives

---

## Requirements

- Erlang/OTP 25+
- Elixir 1.15+
- Rust (cargo)
- Zstandard (zstd)

### Banner Dependencies (Optional)

When `show_banner: true` (default), the build process displays a banner image in the terminal. To enable full image support across all terminal emulators, install these dependencies:

#### macOS

```bash
# For Sixel support (Alacritty, Ghostty, other terminals)
brew install libsixel

# Optional: for ASCII art fallback
# img2txt is included in libsixel
```

#### Linux

```bash
# Ubuntu/Debian
sudo apt install libsixel-tools

# Arch Linux
sudo pacman -S libsixel

# Fedora
sudo dnf install libsixel
```

#### Terminal Compatibility

| Terminal | Protocol | Requires |
|----------|----------|----------|
| iTerm2 | Inline Images | Built-in |
| Ghostty | Kitty protocol | Built-in |
| WezTerm | Kitty protocol | Built-in |
| Alacritty | Kitty protocol | Built-in |
| Kitty | Kitty protocol | Built-in |
| VS Code | Sixel | `libsixel` |
| foot | Sixel | `libsixel` |
| Other terminals | ASCII fallback | None |

If no image support is detected, the banner falls back to text-only mode.

---

## Quick Start

### 1. Add Dependency

```elixir
# mix.exs
def deps do
  [{:batamanta, "~> 3.0", runtime: false}]
end
```

### 2. Configure (Auto-detect)

```elixir
def project do
  [
    app: :my_app,
    version: "0.1.0",
    batamanta: [
      format: :escript,       # :escript | :release
      erts_target: :auto,        # Auto-detect host platform (RECOMMENDED)
      execution_mode: :cli,      # :cli | :tui | :daemon
      compression: 3,            # 1-19 (zstd level)
      binary_name: "my_app",     # Optional: custom binary name
      show_banner: true          # Optional: show build banner
    ]
  ]
end
```

### Banner image protocol

With `show_banner: true` (the default) the build draws a banner image, which
needs the terminal to speak an inline-image protocol. Detection reads the
environment and picks one of:

| Terminal | Protocol |
|----------|----------|
| kitty, ghostty, WezTerm, konsole, **WaveTerm** | kitty graphics |
| iTerm2 | iTerm2 inline images |
| Alacritty, foot, VS Code terminal | Sixel |
| anything else, or stdout not a TTY | text only |

If detection picks the wrong one — or your terminal supports a protocol that
isn't in the table — pin it in the project config instead of rebuilding to
test:

```elixir
batamanta: [show_banner: true, image_protocol: :kitty]
```

or for one build, without touching `mix.exs`:

```bash
BATAMANTA_IMAGE_PROTOCOL=iterm2 mix batamanta
```

The config value wins over the environment variable. An unrecognised value is
a hard error rather than a silent downgrade, because a typo that fell back to
text mode is indistinguishable from "this terminal can't do images" — the
banner just quietly stops appearing.

### Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `erts_target` | atom | `:auto` | Target platform (see below) |
| `otp_version` | string | `:auto` | OTP version (e.g., "28.1") |
| `format` | atom | `:release`¹ | `:escript` o `:release` |
| `execution_mode` | atom | `:cli` | `:cli`, `:tui`, or `:daemon` |
| `compression` | integer | `3` | Zstd compression level (1-19) |
| `binary_name` | string | app name | Custom binary name |
| `show_banner` | boolean | `true` | Show build banner (an image, when the terminal supports it) |
| `image_protocol` | atom | `:auto` | Force the banner image protocol: `:kitty`, `:iterm2`, `:sixel`, `:ascii` (see below) |
| `umbrella` | boolean | `false` | Enable umbrella mode (see below) |
| `force_os` | string | nil | Force OS: `"linux"`, `"macos"`, `"windows"` |
| `force_arch` | string | nil | Force arch: `"x86_64"`, `"aarch64"` |
| `force_libc` | string | nil | Force libc: `"gnu"`, `"musl"` (Linux only) |

> ¹ Auto-detected as `:escript` when the project defines `escript: [main_module: ...]` in `mix.exs`.

### 3. Build

```bash
mix batamanta
```

This generates: `my_app-0.1.0-x86_64-linux` (or appropriate target)

---

## ERTS Target System

Batamanta uses a unified **ERTS target** system for platform specification.

### Supported Targets

| Target Atom | OS | Arch | Libc | Use Case |
|-------------|-----|------|------|----------|
| `:auto` | - | - | - | Auto-detect host (default) |
| `:ubuntu_22_04_x86_64` | Linux | x86_64 | glibc | Debian, Ubuntu, Arch, CachyOS |
| `:ubuntu_22_04_arm64` | Linux | aarch64 | glibc | ARM servers, Raspberry Pi 4 |
| `:alpine_3_19_x86_64` | Linux | x86_64 | musl | Alpine Linux, containers |
| `:alpine_3_19_arm64` | Linux | aarch64 | musl | Alpine on ARM |
| `:macos_12_x86_64` | macOS | x86_64 | - | Intel Mac |
| `:macos_12_arm64` | macOS | aarch64 | - | Apple Silicon (M1/M2/M3) |
| `:windows_x86_64` | Windows | x86_64 | msvc | ✅ Supported |

### Manual Override

Force a specific target regardless of host:

```elixir
batamanta: [
  erts_target: :alpine_3_19_x86_64,  # Force Alpine musl
  execution_mode: :cli
]
```

Or use individual overrides:

```elixir
batamanta: [
  force_os: "linux",
  force_arch: "x86_64",
  force_libc: "musl"
]
```

### CLI Override

```bash
# Auto-detect (default)
mix batamanta

# Force specific target
mix batamanta --erts-target alpine_3_19_x86_64

# Force individual components
mix batamanta --force-os linux --force-arch aarch64 --force-libc musl
```

---

## OTP Version Control

**You specify, you own.** If you specify `otp_version`, that exact version is used. If not specified, a conservative fallback is used.

### Configuration

```elixir
# Use exact OTP version (recommended for production)
batamanta: [
  otp_version: "28.1"
]
```

### Behavior

| Mode | Description | When to Use |
|------|-------------|-------------|
| **Explicit** | Uses exact version specified. Fails if not available in repository. | Production builds, reproducibility |
| **Auto** | Uses conservative fallback (28.0 → 28.1 → ...). Uses system ERTS if not found. | Development, quick builds |

### CLI Override

```bash
# Specify exact OTP version
mix batamanta --otp-version 28.1

# Auto mode (default)
mix batamanta
```

### Version Resolution

In auto mode, if the exact version is not available:

1. Tries `OTP-28.0` first (most common)
2. Then `OTP-28.1`, `OTP-28.2`, etc.
3. Falls back to system ERTS if nothing found

---

## Execution Modes

| Mode | Description | Terminal Mode | Platform |
|------|-------------|---------------|----------|
| `:cli` | Standard CLI with inherited stdin/stdout/stderr | **Cooked** (canonical mode, line-buffered input) | All |
| `:tui` | Text UI with raw terminal mode, arrow key navigation | **Raw** (direct character input, no line buffering) | Unix only |
| `:daemon` | Runs in background, no terminal I/O | N/A (detached) | Unix only |

> **Note:** In `:cli` mode (cooked), the terminal processes input line-by-line - Enter sends the line, backspace works normally, and special keys like arrow keys are not directly captured. In `:tui` mode (raw), the app has direct control over the terminal and can capture individual keypresses including arrow keys, function keys, etc.

---

## BEAM Daemon Mode (Unix)

> 🦇 **New in 2.0**: every invocation of your `batamanta` binary used to pay the full `mix release` + `cargo build` + Erlang boot cost — about 10-25 seconds per call. Daemon mode wraps a **persistent Erlang VM** behind the binary so subsequent invocations hit a warm, already-booted BEAM in **~5-15 ms** via Unix-domain socket.

### What it does

The first time you run your binary, the Rust wrapper extracts the payload and
spawns a long-lived BEAM process that:

1. Binds a Unix-domain socket under `$XDG_RUNTIME_DIR/batamanta/` (or `/tmp`
   fallback) namespaced by `<app>-<version>-<target>`.
2. Boots your app and parks on a receive loop.
3. Writes a PID file alongside the socket.

Subsequent invocations:

- Connect to the existing socket
- Send a length-prefixed JSON request (`<u32 length><json payload>`) with
  the user's CLI args + environment
- Wait for the matching JSON response
- Exit

The BEAM keeps running between calls. After `default_ttl_ms` of inactivity
(default 30s) it shuts itself down. You can also send a `batamanta_daemon_kill`
arg to ask it to exit cleanly.

### Performance

| Path | Cold call | Warm call (1-1000+) |
|------|-----------|---------------------|
| **Legacy single-shot** (`:cli`/`:tui`/`:escript`) | 10-25s | 10-25s (no caching) |
| **Daemon mode** (`:daemon`) | 10-25s (first call) | **5-15 ms** (socket dispatch) |

This is a **~50-300× speedup** for the warm path. Most useful in CI
pipelines, batch jobs, and scripts that invoke the same binary many times
in a row.

### Configuration

Add to your `mix.exs`:

```elixir
def project do
  [
    # ...
    batamanta: [
      execution_mode: :cli,         # your CLI module is invoked per call
      daemon: [
        enabled: true,              # turn on BEAM-keeps-alive
        default_ttl_ms: 30_000,     # daemon self-shuts after 30s idle
        request_timeout_ms: 60_000  # per-request upper bound
      ]
    ]
  ]
end
```

The `execution_mode` field controls which entry point the daemon dispatches
to — typically `:cli` (it forwards the request to `YourApp.CLI.main/1`).
`:daemon` mode is also valid for long-running service binaries.

### Manual daemon control

| Action | What to run |
|--------|-------------|
| Force-shutdown the daemon for the current app/version/target | `<binary> batamanta_daemon_kill` |
| Override TTL for a single invocation | `<binary> --batamanta-daemon-ttl-ms 5000 …` (sends 5s TTL with this request) |
| Disable daemon for one call | `<binary> --batamanta-daemon-disable …` (falls back to legacy) |
| Bypass daemon entirely (e.g. fresh boot) | `<binary> --batamanta-daemon-bootstrap …` (always cold-start) |

### Platform support

| Platform | Status |
|----------|--------|
| Linux (glibc/musl) | ✅ Full daemon mode |
| macOS (aarch64, x86_64) | ✅ Full daemon mode |
| Windows | ⚠️ Compiles, falls back to legacy single-shot. The wrapper prints `daemon mode is Unix-only; falling back to legacy single-shot` and exits non-zero. A `uds`-backed Windows port is tracked in the post-2.0 backlog. |

### How dispatch works

```
                +------------------------+
$ ./mybin ...   |       Rust wrapper     |
   ──────────►  |  (compiled C, ~600kB)  |
                |                        |
                | 1. If BATAMANTA_BEAM_  |
                |    ALIVE=1 → connect   |
                |    to $XDG_RUNTIME_    |
                |    DIR/batamanta/      |
                |    <app>-<v>-<t>.sock  |
                |                        |
                | 2. Send JSON request   |
                |    over UDS:           |
                |    <u32 length>        |
                |    <utf-8 JSON body>   |
                |                        |
                | 3. Read framed reply   |
                |    & exit with rc.     |
                +──────────┬─────────────+
                           │
                           ▼
              +─────────────────────────────+
              │   BEAM daemon (long-lived)  │
              │                             │
              │   • YourApp.CLI.main(args)  │
              │   • YourApp.Application     │
              │   • Persistence (ets/dets)  │
              │   • Process registry        │
              │                             │
              │  Park on receive; shut      │
              │  down after TTL idle.       │
              └─────────────────────────────┘
```

### Reset & cleanup

The daemon's runtime files are under:

- `$XDG_RUNTIME_DIR/batamanta/<app>-<version>-<target>.sock`
- `$XDG_RUNTIME_DIR/batamanta/<app>-<version>-<target>.pid`

These are cleaned up automatically when the daemon exits (TTL or kill
arg). On hard kill, the next invocation of the same binary detects the
stale PID file and bootstraps a fresh daemon.

To wipe everything: `rm -rf "${XDG_RUNTIME_DIR:-/tmp}/batamanta"`.

### Source of truth

- Spec: `attachments/faf7f99e756c9e7b/batamanta-daemon-mode-spec.md`
- Protocol: 4-byte big-endian length prefix + JSON
- Build hash: SHA-256 of the payload, first 6 bytes hex (12 chars)
- TTL cap: 86_400_000 ms (24h), validated client-side

---

## Output Formats

| Format | Description | Notes |
|--------|-------------|-------|
| `:release` | Full OTP release with ERTS (default) | Larger (~60-70MB), self-contained |
| `:escript` | Lightweight escript bundle with minified ERTS | Smaller (~20MB), self-contained |

---

## Umbrella Projects

Batamanta supports Elixir umbrella projects. Set `umbrella: true` at the umbrella root to package only the sub-apps that have `batamanta:` configured in their individual `mix.exs`:

```elixir
# umbrella_root/mix.exs
def project do
  [
    apps_path: "apps",
    deps: deps(),
    batamanta: [
      umbrella: true,
      show_banner: true
    ]
  ]
end

# umbrella_root/apps/my_service/mix.exs
def project do
  [
    app: :my_service,
    version: "0.1.0",
    batamanta: [
      format: :release,
      binary_name: "my_service"
    ],
    deps: deps()
  ]
end

# umbrella_root/apps/my_cli/mix.exs
def project do
  [
    app: :my_cli,
    version: "0.1.0",
    batamanta: [
      format: :escript
    ],
    escript: [main_module: MyCli.CLI],
    deps: deps()
  ]
end
```

When you run `mix batamanta` at the umbrella root, batamanta:
1. Detects all sub-apps in `apps/` that have `batamanta:` config
2. Builds releases for all apps once (`mix release` is umbrella-aware)
3. Packages only the configured apps into standalone binaries
4. Each app uses its own `format`, `binary_name`, `compression`, and `execution_mode`

Sub-apps without `batamanta:` config are ignored. The umbrella root config provides shared settings (ERTS target, OTP version) while each sub-app overrides individual settings.

---

## Compatibility Matrix

### Operating Systems

| OS | Architectures | Modes | Status |
|----|---------------|-------|--------|
| **macOS 11+** | x86_64, aarch64 | CLI, TUI, Daemon | ✅ Full Support |
| **Linux (glibc)** | x86_64, aarch64 | CLI, TUI, Daemon | ✅ Full Support |
| **Linux (musl)** | x86_64, aarch64 | CLI, Daemon | ✅ Supported |
| **Windows 10+** | x86_64 | CLI | ✅ Supported |

> **Windows note:** binaries boot exclusively from the bundled ERTS —
> system Erlang is never consulted. At runtime they only need Git for
> Windows (bash) to interpret the `.run` launcher.

### OTP / Elixir Versions

| OTP | Elixir | Status |
|-----|--------|--------|
| 25 | 1.15 | ✅ Minimum Supported |
| 26 | 1.15, 1.16 | ✅ Supported |
| 27 | 1.15, 1.16, 1.17 | ✅ Supported |
| 28 | 1.16, 1.17, 1.18+ | ✅ Latest |

### Restrictions

- ❌ Windows + TUI mode (requires Unix terminal)
- ❌ Windows + Daemon mode (requires Unix process management)
- ❌ OTP < 25 (missing required BEAM features)
- ❌ Elixir < 1.15 (missing required language features)

---

## Troubleshooting: Linux musl/glibc

### Problem: "libc mismatch detected" Warning

If you see a warning like:
```
⚠️  libc mismatch detected!
  Expected: glibc (Debian/Ubuntu/Arch/Fedora)
  Detected: musl libc (Alpine)
```

This means your system's libc type doesn't match the expected ERTS target.

**Solution 1: Let Batamanta auto-detect (recommended)**
```elixir
batamanta: [
  erts_target: :auto  # Auto-detects musl vs glibc
]
```

**Solution 2: Force specific target**
```elixir
batamanta: [
  erts_target: :alpine_3_19_x86_64  # Force musl
]
```

**Solution 3: Use CLI override**
```bash
mix batamanta --erts-target alpine_3_19_x86_64
```

### Problem: ERTS download fails on Alpine/musl

If ERTS download fails with 404 error on musl systems, try one of these solutions:

**Solution 1: Use auto-detection (recommended)**
```elixir
batamanta: [
  erts_target: :auto  # Auto-detects musl vs glibc
]
```

**Solution 2: Use a specific OTP version**
```elixir
batamanta: [
  otp_version: "28.0"  # Try an older version that may have musl builds
]
```

**Solution 3: Build custom ERTS for musl** (advanced)
```bash
# On Alpine Linux
apk add erlang-dev
cd /tmp
git clone https://github.com/erlang/otp.git
cd otp
./otp_build autoconf
./configure --prefix=/usr/local
make
make install
tar -czf musl-erts.tar.gz /usr/local/lib/erlang
```

### Problem: Binary doesn't run on target system

If the binary works on build machine but fails on target:

**Check libc compatibility:**
```bash
# On build machine
ldd --version

# On target machine  
ldd --version

# They should match (both glibc or both musl)
```

**Solution: Build for oldest supported glibc version**
```elixir
# Use Ubuntu 22.04 target (most compatible glibc)
batamanta: [
  erts_target: :ubuntu_22_04_x86_64
]
```

### Problem: Cross-compilation from macOS to Linux

**Install Rust targets:**
```bash
rustup target add x86_64-unknown-linux-gnu
rustup target add aarch64-unknown-linux-gnu
```

**Build with explicit target:**
```bash
mix batamanta --erts-target ubuntu_22_04_x86_64
```

### How libc Detection Works

Batamanta uses multiple methods in order:

1. **`ldd --version`** - Most reliable, checks output for "musl" or "glibc"
2. **Dynamic loader files** - Checks `/lib/ld-musl-*.so` vs `/lib64/ld-linux-*.so`
3. **`/etc/os-release`** - Checks `ID=alpine`, `ID=void`, etc.
4. **`/proc/self/maps`** - Advanced, checks loaded libraries

Detection always falls back to glibc if uncertain (90%+ of systems use glibc).

### ERTS Download Fallback

Batamanta attempts to download pre-compiled ERTS from Hex.pm builds. If the download fails:

```
⚠️  Could not download ERTS, using system ERTS instead.
```

The build continues using the system ERTS (similar to Bakeware). This means:

- ✅ **Build succeeds** - Your application compiles
- ⚠️ **Binary requires ERTS** - Target machine needs compatible Erlang/Elixir
- ✅ **Portable within same OS** - Works on machines with same libc type

**For production self-contained binaries:**

1. Ensure network access during build
2. Use specific ERTS version: `batamanta: [otp_version: "26.2.5"]`
3. Ensure the target platform has pre-built ERTS available

---

## CLI Options

Override configuration via command line:

```bash
# Use auto-detection (default)
mix batamanta

# Force ERTS target
mix batamanta --erts-target alpine_3_19_x86_64

# Force individual components
mix batamanta --force-os linux --force-arch aarch64 --force-libc musl

# Force escript format
mix batamanta --format escript

# Adjust compression level
mix batamanta --compression 9

# Combine options
mix batamanta --erts-target ubuntu_22_04_arm64 --compression 5
```

### Available CLI Flags

| Flag | Description |
|------|-------------|
| `--format` | Output format: `release` or `escript` |
| `--erts-target` | Override ERTS target atom |
| `--otp-version` | Specify exact OTP version (e.g., "28.1") |
| `--force-os` | Force OS: `linux`, `macos`, `windows` |
| `--force-arch` | Force architecture: `x86_64`, `aarch64` |
| `--force-libc` | Force libc: `gnu`, `musl` (Linux only) |
| `--compression` | Zstd compression level (1-19) |

---

## For CLI Applications

Use Erlang's `:init` to read arguments:

```elixir
defmodule MyApp do
  use Application

  @impl true
  def start(_type, _args) do
    args =
      :init.get_plain_arguments()
      |> Enum.map(&to_string/1)
      |> Enum.reject(&(&1 == "--"))

    case args do
      ["hello", name] -> IO.puts("Hello, #{name}!")
      _ -> IO.puts("Usage: my_app hello <name>")
    end

    System.halt(0)
  end
end
```

Don't forget `System.halt/1` when your CLI finishes!

---

## How ERTS Provisioning Works

1. **Auto-detection**: Batamanta detects your host platform using:
   - `:os.type()` for OS identification
   - `:erlang.system_info(:system_architecture)` for architecture
   - `ldd --version` for libc detection on Linux (glibc vs musl)

2. **Download**: Fetches pre-compiled ERTS from [Hex.pm builds](https://builds.hex.pm/builds/otp/) or from the [Batamanta ERTS Repository](https://github.com/Lorenzo-SF/Batamanta---ERTS-repository)

3. **Cache**: Stores in `~/.cache/batamanta/` for reuse

4. **Package**: Bundles your release + ERTS into a single compressed tarball

5. **Compile**: Rust dispenser embeds the payload and handles extraction at runtime

---

## Build Environment Isolation

Batamanta version 1.4.0+ includes `Batamanta.EnvCleaner`, which automatically handles environment isolation during binary generation.

### Why this matters

When using version managers like `asdf`, `mise`, or `kerl`, your shell's `PATH` points to shimmed versions of Erlang and Elixir. If these versions differ from the ERTS being embedded, you may encounter:
- "Corrupt atom table" crashes
- Inconsistent behavior between build-time and runtime
- Compilation failures in CI environments

### How it works

When you run `mix batamanta`, the tool:
1. Detects and filters out version manager paths from the `PATH`.
2. Sanitizes the environment to include only essential system variables.
3. (In Escript mode) Prepends the downloaded ERTS bin directory to the `PATH` during compilation, ensuring 100% version parity.

This mechanism ensures that the binary you build is exactly matched to the runtime environment it will use.

---

## ERTS Repository

Batamanta uses a separate repository for pre-compiled ERTS binaries:

**[Batamanta ERTS Repository](https://github.com/Lorenzo-SF/Batamanta---ERTS-repository)**

This repository hosts pre-compiled Erlang Run-Time System (ERTS) binaries for:
- **macOS**: aarch64 (Apple Silicon)
- **Linux (glibc)**: x86_64 & aarch64
- **Linux (musl)**: x86_64 & aarch64

The binaries are compiled from official Erlang/OTP sources and are subject to the Apache License 2.0 (see the repository for details).

---

## Troubleshooting

### Linux: "ERTS not found" or wrong ERTS downloaded

Batamanta auto-detects using `ldd --version`. If this fails:

```bash
# Check what ldd reports
ldd --version

# Force specific target
mix batamanta --erts-target ubuntu_22_04_x86_64
```

### macOS: Binary doesn't run on older macOS versions

Ensure you're building with the correct deployment target:

```elixir
batamanta: [
  erts_target: :macos_12_x86_64  # or :macos_12_arm64
]
```

### Cross-compilation from macOS to Linux

Install Rust targets:

```bash
rustup target add x86_64-unknown-linux-gnu
rustup target add aarch64-unknown-linux-gnu
```

Then build:

```bash
mix batamanta --erts-target ubuntu_22_04_x86_64
```

### Alpine/musl: "Library not found"

Ensure musl development headers are installed:

```bash
# Alpine
apk add musl-dev

# Or use the Alpine Docker image
docker run --rm -v $(pwd):/app -w /app elixir:1.18-alpine ...
```

---

## Architecture

1. **Detect**: Auto-detect or resolve manual target configuration
2. **Fetch**: Download ERTS from Hex.pm builds
3. **Release**: Compile your Elixir code with `mix release`
4. **Package**: Bundle release + ERTS with Zstd compression
5. **Compile**: Build Rust dispenser that embeds the payload
6. **Run**: Dispenser extracts payload and spawns Erlang VM

---

## Escript Support

Batamanta can package projects that use `mix escript.build` as self-contained binaries:

```elixir
def project do
  [
    app: :my_escript_app,
    version: "0.1.0",
    batamanta: [
      format: :escript,                  # :escript or :release
      escript_module: MyEscriptApp.CLI  # Module with main/1 function
    ],
    escript: [
      main_module: MyEscriptApp.CLI
    ]
  ]
end
```

The project should have a module with a `main/1` function:

```elixir
defmodule MyEscriptApp.CLI do
  def main(args) do
    IO.puts("Escript running with args: #{inspect(args)}")
  end
end
```

**Note:** The `format: :escript` in `batamanta:` is optional if your project already has `escript:` configuration in `mix.exs` - Batamanta auto-detects escript format. But you can include it explicitly for clarity.

---

## Testing

Run the test matrix locally:

```bash
# Test across Linux distributions (requires Docker)
./docker_matrix.sh

# Run smoke tests manually
cd smoke_tests/test_cli && mix batamanta && ./test_cli-* arg1 arg2
cd smoke_tests/test_tui && mix batamanta && ./test_tui-*
cd smoke_tests/test_daemon && mix batamanta && ./test_daemon-* &
cd smoke_tests/test_escript && mix batamanta && ./test_escript --help
```

---

## License

MIT