DEV Community

engnzdl
engnzdl

Posted on

I built a cross-platform Android emulator manager with Tauri 2 + Rust — no Android Studio required

I got tired of three things at once: the Android emulator CLI is tedious, Android Studio is a multi-gigabyte install for just running AVDs, and every decent alternative is either closed-source or expensive. So I built Enmulator — a desktop app that manages Android Virtual Devices end to end, ships on Windows/macOS/Linux, and doesn't need Android Studio installed.

It's Tauri 2 on the backend (Rust, 13 modules), React 18 on the frontend, and it's GPLv3.

What it actually does

  • Create / start / stop / clone / delete AVDs, plus batch operations across multiple devices
  • 66 pre-loaded device profiles across 16 brands (Pixel, Samsung, Xiaomi, OnePlus, realme…)
  • Device identity spoofing: IMEI, operator, phone number, WiFi MAC, timezone, locale
  • Root: adb root for google_apis images, and Magisk via rootAVD for everything else
  • Per-device proxy, GPS mocking, screen recording, clipboard sync, live logcat, a split-panel file explorer
  • A headless REST API (Actix-web) so you can drive it from CI or a script

The whole point was to make emulator setup feel like a product, not a shell-script chore.

Why Tauri instead of Electron

A tool that spawns emulator processes shouldn't eat 300 MB of RAM on its own. Tauri uses the OS webview instead of bundling Chromium, so the installer is small and idle memory is low. The backend being Rust mattered more than I expected: adb calls are blocking Command invocations, and being able to move them off the main thread with plain std::thread — without async ceremony — kept the UI responsive for free.

The tradeoff is real too: the macOS/Windows/Linux build matrix means three toolchains and three sets of platform quirks (more on that below).

Architecture

The Rust side is a flat set of modules, each owning one concern:

  • sdk.rs — locate the Android SDK, adb, emulator, avdmanager binaries
  • avd_manager.rs — create/delete AVDs, write config.ini (resolution, DPI, hardware keyboard)
  • emulator.rs — process management + port allocation (each emulator gets a console port)
  • adb_bridge.rs — the adb shell / install / push / pull wrappers
  • fingerprint.rs — the identity-spoofing engine
  • bypass.rs — build.prop patching for detection bypass
  • extras.rs — GPS, clipboard, screen record, logcat streams
  • api_server.rs — the REST layer

The frontend is a few focused components: a create wizard, per-device cards, an identity panel, the file explorer, quick actions. Nothing clever — state lives in the backend, the UI just calls commands over Tauri's IPC.

The parts that were actually hard

Fingerprint spoofing is a moving target

The naive version is adb shell setprop with the IMEI/operator values. That works for some properties and silently fails for others — and "silently fails" is the worst outcome because the user thinks they're spoofed when they aren't.

The reality on modern Android:

  • ro.product.* (brand, model, manufacturer) are immutable — set at boot by the system image, setprop can't touch them.
  • IMEI/SIM fields (persist.radio.imei, etc.) stopped being settable via setprop around API 29.
  • The reliable route for read-only props is Magisk's resetprop, which patches the property store in memory.

So the identity engine is now Magisk-aware: if the device is rooted with Magisk, it uses resetprop for ro.product.* and ro.build.fingerprint; otherwise it falls back to setprop for the fields that still allow it. The app also reports which fields applied and which failed, instead of one big "success" that might be a lie.

Magisk integration without Android Studio

Rooting an emulator normally means patching the ramdisk. Enmulator bundles rootAVD, which does exactly that: it grabs ramdisk.img from the system image, patches it with Magisk, and pushes it back.

The gotcha I learned: the ramdisk is shared per system image, not per AVD. Root one device on a given image and every AVD using that image is rooted. That's worth documenting loud, because it surprises people.

Also adb root only works on google_apis (non-Play-Store) images — Play Store images reject it outright. So the app offers both paths and tells you which one applies to your image.

Windows wants to show you console windows

Every adb / emulator spawn on Windows popped a black console window. The fix is CREATE_NO_WINDOW (0x08000000) in the process creation flags — one line, but it has to wrap every command invocation, so it went into a shared sdk::new_command() helper used everywhere.

Windows also likes to prepend \\?\ to long paths, which broke resolving the rootAVD submodule. dunce::canonicalize fixed it by stripping the prefix.

Shipping three platforms from one repo

The release pipeline is a GitHub Actions matrix (ubuntu / windows / macos) that produces a .deb, .AppImage, .msi, and .dmg with stable filenames, then uploads them to the release. The thing that bit me: the default GITHUB_TOKEN can't update a release — it needs permissions: contents: write in the workflow. Took a failed run to learn that.

I also did a git history cleanup before open-sourcing: the repo's .git was 483 MB because old target/ and node_modules blobs were stuck in history. git filter-repo brought it to 6.7 MB. Same code, 72× smaller clone.

Honest limitations

  • ARM64 system images only run natively on Apple Silicon — x86_64 everywhere else.
  • The detection-bypass and fingerprint features are for testing on software you own/are authorized to test. There's a disclaimer in the README for a reason.
  • This is a solo project, so some edges (snapshot management, exotic device configs) are rougher than a team-built tool.

Try it

Repo: github.com/engnzdl/enmulator

Installers for macOS / Windows / Linux are on the releases page. If you poke at it and hit something, open an issue — and if it's useful, a star helps others find it.

Built with Tauri 2, Rust, and React. GPLv3.

Top comments (0)