dorker 0.1.4

Docker for headless Android emulator environments
<div align="center">
  <h1>dorker</h1>

  <p><strong>Docker for headless Android emulator environments</strong></p>

  <p>
    <img alt="License" src="https://img.shields.io/badge/license-MIT-green">
    <img alt="Rust" src="https://img.shields.io/badge/rust-1.85%2B-orange">
    <img alt="Edition" src="https://img.shields.io/badge/edition-2024-blue">
    <a href="https://crates.io/crates/dorker"><img alt="crates.io" src="https://img.shields.io/crates/v/dorker.svg"></a>
  </p>

  <p>
    <a href="#install">Install</a>
    &nbsp;·&nbsp;
    <a href="#quickstart">Quickstart</a>
    &nbsp;·&nbsp;
    <a href="#development">Development</a>
  </p>
</div>

---

## Install

Requires [Rust](https://rustup.rs) **1.85+** and `~/.cargo/bin` on your `PATH`.

```bash
cargo install dorker
```

Verify:

```bash
dorker --help
```

<details>
<summary><strong>Build from source</strong> — for development or unreleased changes</summary>

```bash
git clone https://github.com/cesarferreira/dorker.git
cd dorker
cargo install --path . --locked
# or
make install-release
```

Debug install (faster compile, larger binary):

```bash
make install
```

Run without installing:

```bash
make build-release
./target/release/dorker
```

</details>

<a id="quickstart"></a>
## Quickstart

Requires the Android SDK (`emulator`, `platform-tools`) and an installed system
image (`dorker pull` installs one). macOS Apple Silicon and Linux x86_64 with KVM are
validated. No Docker needed.

```bash
# Check your setup, then install a system image if one is missing
dorker doctor
dorker pull android:35

# Boot a headless emulator and use it
dorker run -d --name test android:35
dorker ps
dorker exec test "getprop ro.build.version.sdk"
dorker install test ./app-debug.apk
dorker screenshot test shot.png      # waits up to 10s for a non-blank frame
dorker logs test                     # add -f to follow

# Stop it, bring it back (cold boot, keeps its data), then delete it
dorker stop test
dorker start test
dorker rm test
```

### Describe your environment in a Dorkerfile

A `Dorkerfile` (next to your app, like a Dockerfile) declares the Android environment: the
base image, resources, the APK to install, settings, and a snapshot to start from.

```dorkerfile
# Dorkerfile
FROM android:35

MEMORY 2GB
CPUS 2
DISK 4GB
DEVICE pixel_7
RESOLUTION 1080x2400
HEADLESS true

# Paths are relative to this file's folder.
INSTALL ./app/build/outputs/apk/debug/app-debug.apk

# Turn off animations: faster, less flaky UI tests.
SETTING window_animation_scale 0
SETTING transition_animation_scale 0
SETTING animator_duration_scale 0

# Save a snapshot so instances start in a few seconds,
# and open the app once an instance is ready.
SNAPSHOT ready
CMD launch com.example.app
```

Then build the image once and run it as many times as you like:

```bash
# Install the base image named by FROM, if it is missing
dorker pull

# Build once: installs your APK, applies settings, saves the snapshot
dorker build -t myapp:latest .

# Run as often as you like; each run starts from the snapshot
dorker run -d --name t1 myapp:latest
dorker screenshot t1 shot.png
dorker test t1 \
  --runner com.example.app.test/androidx.test.runner.AndroidJUnitRunner
dorker rm -f t1

# Or run in the foreground; the instance is removed on Ctrl-C
dorker run --rm myapp:latest
```

An identical rebuild is a cache hit (instant), and every `dorker run` gets its own isolated
copy, so instances never share state. To try it without your own app, use the sample:

```bash
# Build the tiny sample APK (needs the Android build-tools and a JDK)
examples/basic/make-apk.sh

dorker build -t basic examples/basic
dorker run -d --name b1 basic
dorker exec b1 "pm list packages | grep dorker"
dorker rm -f b1
```

Every directive is documented in [docs/dorkerfile.md](docs/dorkerfile.md). More examples:
[examples/basic](examples/basic), [examples/instrumentation](examples/instrumentation) and
[examples/counter-app](examples/counter-app), a real Gradle app with Espresso tests
(`./dev.sh` runs Gradle, `dorker build`, `run`, a screenshot and the tests). The example APKs
are not committed: build them with each example's `make-apk.sh` (or Gradle for the counter app).

Each instance gets its own private AVD under `~/.dorker` (override with
`DORKER_HOME`). Your existing AVDs are never touched. `dorker run --rm` removes the
instance when it stops. `ps`, `images` and `doctor` accept `--json`.

Measured on an Apple M4 Pro, API 35, 2GB: cold boot about 15s, snapshot restore
about 4s. Host memory is about 2.5GB after a cold boot and 3.3GB after a restore.
Details and caveats: [docs/benchmarks.md](docs/benchmarks.md).

### Test, benchmark, automate

```bash
dorker init                          # write a starter Dorkerfile
dorker test t1 \
  --runner com.example.test/androidx.test.runner.AndroidJUnitRunner
dorker test t1 --runner ... --class com.example.FooTest#bar

# Boot, restore, launch, disk and idle numbers
dorker bench myapp:latest --runs 3 --json

# Also time the plain official emulator, for comparison
dorker bench android:35 --baseline
```

`dorker test` prints a summary, preserves the raw instrumentation output and logcat
under the instance directory, and exits 7 if anything failed.
See [examples/instrumentation](examples/instrumentation) for a runnable example.

### Tests

```bash
# Unit, CLI and fake-SDK lifecycle/failure tests (no emulator needed)
cargo test

# End-to-end: boots real emulators (about 3 minutes)
cargo test -- --ignored --test-threads=1
```

### Use it from AI agents

`dorker mcp` is a Model Context Protocol server: an agent can start instances, install apps,
run commands, take screenshots, read logs and run tests, all with structured results.

```bash
claude mcp add dorker -- dorker mcp
```

See [docs/mcp.md](docs/mcp.md) for the tools, other clients and security notes. The same
operations are available to scripts as `--json` on `run`, `start`, `exec`, `install`,
`screenshot`, `test`, `ps`, `images`, `doctor` and `bench`.

### Diagnostics

`dorker doctor` colours its report on a terminal (green ok, yellow warn, bold red FAIL, fixes
in cyan). Colour is off when piped, `NO_COLOR` turns it off, `CLICOLOR_FORCE=1` forces it on,
and `--json` is never coloured.

`dorker -v <command>` (or `DORKER_VERBOSE=1`) prints what dorker decides and runs to stderr: the
located SDK, the boot mode and why, the exact emulator command line and environment, state
transitions and timings. Each instance also keeps its raw emulator output in
`~/.dorker/instances/<id>/emulator.log`.

### Exit codes

| Code | Meaning |
|---|---|
| 0 | success |
| 1 | other error |
| 2 | invalid configuration or arguments (including Dorkerfile errors) |
| 3 | missing dependency (SDK, emulator, adb, system image); `doctor` failures |
| 4 | image build failed |
| 5 | instance failed to start or boot |
| 6 | adb operation failed |
| 7 | instrumentation tests failed |
| 8 | name, port or build conflict, or not enough free disk space |

`dorker exec` returns the guest command's own exit status.

See [docs/architecture.md](docs/architecture.md) for design and limitations.

## Limitations

- **Platform:** validated on macOS Apple Silicon (primary) and Linux x86_64 with KVM (Ubuntu 24.04, an Azure VM with nested virtualisation; unit tests and the full emulator end-to-end suite pass). Linux arm64 and bare-metal Linux are untested; Windows is unsupported. Linux is slower in our measurements (cold boot about 29s, snapshot restore about 9s; see [docs/benchmarks.md](docs/benchmarks.md)).
- **System images are installed on request, not automatically.** `dorker pull android:35` runs Google's `android sdk install` (the replacement for the deprecated `sdkmanager`; older command-line tools fall back to `sdkmanager`). The new CLI accepts the Android SDK licences (and the ARM translation licence for arm64 images) for you without prompting, so `pull` says so first, and Google documents that it collects usage data (opt out with `android --no-metrics`); `dorker run`/`build` print this command when an image is missing. The download path itself was only verified for the already-installed and nonexistent-package cases, not a real multi-GB download.
- **Snapshots are strict.** A change of emulator version, system-image revision, memory or CPUs invalidates an image's snapshot. dorker then cold boots (about 15s) and says so. Snapshots are host-specific and not portable between machines.
- **Memory:** a 2GB guest used about 2.5GB of host memory after a cold boot and 3.3GB after a snapshot restore on macOS (3-3.7GB on Linux). Idle CPU is dominated by Android background work and takes about a minute to settle.
- **Builds take about 45s** because dorker reboots the guest to persist provisioned state.
- **`CMD`** only supports `launch <package>`. `dorker start` always cold boots (about 14s) because restoring the image snapshot would roll back the instance's data. There is software rendering (`swiftshader_indirect`) is the default; `GPU <mode>` in the Dorkerfile selects another, but none was faster for snapshot restore in our measurements (see docs/benchmarks.md) and no automatic test-APK discovery.
- **Concurrency:** creating instances concurrently is safe, and `stop`, `rm` and `start` on the same instance are serialised by a per-instance lock (the second command waits up to 60s, then exits 8).
- **Orphans:** emulator processes that dorker started but lost track of (CLI killed mid-start) are reported by `dorker ps`, not adopted or killed automatically.
- `examples/basic` and `examples/instrumentation` use tiny hand-built APKs; `examples/counter-app` is the realistic one.

<a id="development"></a>
## Development

Common tasks via the `Makefile`:

```bash
make              # check + build + test
make build        # debug build
make build-release
make install      # install debug binary
make install-release
make run ARGS="doctor"
make check        # cargo check + clippy
make fmt          # format
make lint         # fmt check + clippy
make test
make clean
make demo         # install + show --help
```

Releasing (requires [cargo-release](https://github.com/crate-ci/cargo-release) and [git-cliff](https://github.com/orhun/git-cliff)):

```bash
make release                  # default minor bump
make release LEVEL=patch      # patch bump
make release LEVEL=major      # major bump
```

The pre-release hook regenerates `CHANGELOG.md` with `git-cliff` from your conventional-commit history (grouped into Features, Bug Fixes, etc. per `cliff.toml`) and commits it alongside the version bump. Pushing the resulting `v*` tag triggers the release workflow, which builds the multi-platform binaries and publishes a GitHub Release whose notes are generated by `git-cliff` from the same config.

## License

MIT