nix-darwin, but for your phone. Declare Android apps and reachable device state in Nix, inspect a read-only plan, then converge over adb.
android-rebuild plan --flake .#pixel --serial DEVICE_SERIAL
android-rebuild switch --flake .#pixel --serial DEVICE_SERIAL
android-rebuild status --flake .#pixel --serial DEVICE_SERIAL # drift since last switch
android-rebuild assist --watch --flake .#pixel --serial DEVICE_SERIAL
android-rebuild bootstrap --flake .#pixel --serial DEVICE_SERIAL # wiped-device rebuildnix-android works at adb-shell privilege on a stock, locked-bootloader device. It requires neither root nor replacing the OS, weakening verified boot, or installing Nix on the phone. GrapheneOS is the first-class target; an AOSP emulator is the mutation test bench.
Status: alpha-ready. The app and settings converge loop works end to end on the AOSP bench, has completed an additive install plus a reversible setting round trip on a locked GrapheneOS device, and passes the release packages and device-free checks on a physical Apple Silicon Mac. The pinned
macos-15job repeats that gate after publication. See docs/PLAN.md, and do not aim an unreviewedswitchorbootstrapat a daily phone.
- F-Droid and third-party F-Droid repository apps
- GitHub/Gitea release APKs, direct vendor-URL APKs (bare URL, a Signal-style update-manifest JSON, or a Steam-style HTML-discovered link), and local APK files
- Google Play apps as user-confirmed presence assertions with Play-specific assistance
- other attended apps as generic presence assertions
- credential-free Obtainium release-source and App Manager signer import adapters
- raw Android settings keys, dark mode, and Private DNS
- default browser, SMS, dialer, and home roles
- runtime permission grants/revocations, writable permission flags, and package app-ops
- per-app locales, input-method selection, global Data Saver, and user app-link choices
- package disablement/suspension and battery-optimization exemptions
- optional cleanup of undeclared user-installed apps
- resumable wiped-device bootstrap across reproducible and consent-bound apps
The default is additive: undeclared apps are left alone. Every device command
requires an explicit adb serial, the declared ABI must match the target, and
plan never writes to the device (it may evaluate, fetch, and build locally).
See docs/LIMITS.md for the exact boundary between reconciled,
ensure-only, attended, and unreachable state.
{
device = {
name = "pixel";
abi = "arm64-v8a";
user = 0;
};
apps.fdroid.packages = [
"org.fdroid.fdroid"
"com.termux"
];
apps.play = [ "com.spotify.music" ];
apps.cleanup = "none";
android = {
darkMode = true;
privateDns = "opportunistic";
permissions."com.termux".grant = [
"android.permission.POST_NOTIFICATIONS"
];
permissions."com.termux".flags."android.permission.POST_NOTIFICATIONS" = [ "user-set" ];
appOps."com.termux".RUN_IN_BACKGROUND = "allow";
locales."com.termux" = [ "en-US" ];
dataSaver.enabled = true;
batteryOptimization.exempt = [ "com.termux" ];
};
}F-Droid locks are resolved through a certificate-authenticated entry.jar,
the signed entry.json index hash, and the per-APK hash. GitHub/Gitea assets
are package-ID checked when locked. Every resulting artifact is fetched by
hash into the Nix store. The engine needs no network once the complete converge
closure—including controller tools and APKs—is present locally. Missing paths
require a fetch from nixpkgs, the recorded app source, or a configured binary
cache.
- docs/USING.md — bootstrap, complete option surface, CLI, safety, and semantics
- docs/OPTIONS.md — every option, generated from the typed module definitions
- docs/LIMITS.md — what adb-shell cannot or does not manage
- docs/SUPPORT.md — supported stock-Pixel/GrapheneOS target contract
- docs/CAPABILITIES.md — auditable ADB read/write/import-to-Nix map
- docs/DEVELOPING.md — architecture, checks, and emulator workflow
- docs/PRIMITIVES.md — device-tested adb capability matrix
- docs/IMPORT.md — faithful import model, schema, and prior art
- docs/PLAN.md — release gate and post-0.1 roadmap
nix-android can be its own configuration flake or an input to an existing
flake. The latter adds androidConfigurations.<device> beside existing
nixosConfigurations and darwinConfigurations; see the existing-flake and
multi-controller examples in docs/USING.md.
direnv --version # install direnv first if this fails
direnv allow
just check
just emu # x86_64 Linux with systemd only
nix run .#android-rebuild -- plan --flake .#bench --serial emulator-5554Licensed under the MIT License.