For the last few days I have been attempting to create a nix develop shell environment. I have scoured through plenty of forum posts, blogs, example flakes, and guides. Unfortunately, I am currently unable to debug my applications on Android using it.
The goal: Replicate my Flutter + Waydroid (no Android Studio) setup I’ve used on Arch Linux.
I have tried installing packages and environment variables, but no matter what, flutter doctor just won’t detect “cmdline-tools”. Here’s its output:
$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.35.4, on NixOS 25.05 (Warbler) 6.16.8, locale
en_US.UTF-8)
[✗] Android toolchain - develop for Android devices
✗ cmdline-tools component is missing.
Try installing or updating Android Studio.
Alternatively, download the tools from
https://developer.android.com/studio#command-line-tools-only and make sure to set
the ANDROID_HOME environment variable.
See https://developer.android.com/studio/command-line for more details.
[✗] Chrome - develop for the web (Cannot find Chrome executable at google-chrome)
! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[✓] Linux toolchain - develop for Linux desktop
! Unable to access driver information using 'eglinfo'.
It is likely available from your distribution (e.g.: apt install mesa-utils)
[!] Android Studio (not installed)
[✓] Connected device (1 available)
[✓] Network resources
! Doctor found issues in 3 categories.
And here’s the flake that produced this output:
# This is a stripped down version of my project's actual flake.
# All that was removed were unnecessary comments, packages, and environment variables.
{
description = "Flutter mobile development flake";
inputs = {
nixpkgs = {
url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
};
outputs =
inputs:
let
supportedSystems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-linux"
"aarch64-darwin"
];
# This function ensures all supported systems share the same configuration.
forEachSupportedSystem =
f:
inputs.nixpkgs.lib.genAttrs supportedSystems (
system:
f {
pkgs = import inputs.nixpkgs {
inherit system;
config = {
# The joys of corporate open source.
allowUnfree = true;
android_sdk.accept_license = true;
};
};
}
);
in
{
devShells = forEachSupportedSystem (
{
pkgs ? import <nixpkgs> { },
}:
let
inherit (pkgs) lib;
# Compose a custom SDK package to reduce bloat.
androidComposition = pkgs.androidenv.composeAndroidPackages {
abiVersions = supportedSystems;
cmdLineToolsVersion = "latest";
extraLicenses = [
"android-sdk-license"
];
includeEmulator = false;
includeSystemImages = false;
includeSources = false;
};
androidSdk = androidComposition.androidsdk;
in
{
default = pkgs.mkShell {
packages = with pkgs; [
flutter
androidSdk
];
# Expose necessary libraries to the shell.
LD_LIBRARY_PATH =
with pkgs;
lib.makeLibraryPath [
# Libraries required by the project's dependencies.
jdk17
# Libraries built by the project.
# (toString ./app/example/build/linux/x64/debug/bundle)
];
JAVA_HOME = pkgs.jdk17;
};
}
);
};
}
Not flakes, but this works for me (no android studio), building flutter, and using emulators. Obviously the versions of things are picked for me, you will need to obviously season to taste.
After copying the contents of your SDK composition to mine and switching the flutter package for your flutter329, nothing has changed, I am still getting the same error from flutter doctor, except for the emulator which slightly stalls the “Connected device” check now.
I notice that you are importing your nixpkgs module from a different file (./npins). Does it point to anything other than the “nixos-unstable” channel by any chance?
Currently I am on nixos-25.05, not sure if that will make a big difference. FWIW this is what I get after entering my shell:
❯ flutter doctor -v
[✓] Flutter (Channel stable, 3.29.3, on NixOS 25.05 (Warbler) 6.17.0, locale en_GB.UTF-8)
[95ms]
• Flutter version 3.29.3 on channel stable at
/nix/store/rga4z7r7x705lns431b89gwxx47zs1zp-flutter-wrapped-3.29.3-sdk-links
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision nixpkgs000 (), 1970-01-01 00:00:00
• Engine revision cf56914b32
• Dart version 3.7.2
• DevTools version 2.42.3
[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [1,417ms]
• Android SDK at
/nix/store/79qm879lcal5px9shwmycpn0cwsrl2ag-androidsdk/libexec/android-sdk
• Platform android-35, build-tools 34.0.0
• ANDROID_SDK_ROOT =
/nix/store/79qm879lcal5px9shwmycpn0cwsrl2ag-androidsdk/libexec/android-sdk
• Java binary at:
/nix/store/xad649j61kwkh0id5wvyiab5rliprp4d-openjdk-17.0.15+6/lib/openjdk/bin/java
This JDK is specified by the JAVA_HOME environment variable.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 17.0.15+6-nixos)
! Some Android licenses not accepted. To resolve this, run: flutter doctor
--android-licenses
[✗] Chrome - develop for the web (Cannot find Chrome executable at google-chrome) [17ms]
! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[✓] Linux toolchain - develop for Linux desktop [239ms]
• clang version 19.1.7
• cmake version 3.31.6
• ninja version 1.12.1
• pkg-config version 0.29.2
[!] Android Studio (not installed) [12ms]
• Android Studio not found; download from https://developer.android.com/studio/index.html
(or visit https://flutter.dev/to/linux-android-setup for detailed instructions).
[✓] Connected device (2 available) [307ms]
• Pixel 9a (mobile) • 57281JEBF03702 • android-arm64 • Android 16 (API 36)
• Linux (desktop) • linux • linux-x64 • NixOS 25.05 (Warbler) 6.17.0
[✓] Network resources [594ms]
• All expected network resources are available.
! Doctor found issues in 3 categories.
EDIT: did you add the ANDROID_SDK_ROOT environment variable? That’s pretty important. Note that this is explicitly referenced in the flutter doctor output. I don’t think flutter finds the tools without this.
Yes, I have added it. I have also tried the non-deprecatedANDROID_HOME, but to no avail.
Also, what happens when you run ${sdk}/libexec/android-sdk/tools/bin/sdkmanager?
This is what it outputs for me:
Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema
at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156)
at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75)
at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81)
at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:73)
at com.android.sdklib.tool.sdkmanager.SdkManagerCli.main(SdkManagerCli.java:48)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525)
... 5 more
And if you take a look into the original flutter doctor output again, it states that the cmdline-tools module is missing, whilst the SDK itself is detected (not having the SDK available incurs a different error).
My greatest suspicions are:
Maybe the issue stems from how ${sdk}/libexec/android-sdk/tools is just a symlink, and not an actual directory?
Maybe the Flutter tool checks for the ${sdk}libexec/android-sdk/cmdline-tools directory itself, and doesn’t expect the contents of the cmdline-tools package to be placed in its versioned subdirectory (19.0 in my case). But I would have to double check if the file structure differs in any way from the one of the Arch package I have used previously. Except for the lack of symlinks, the file structure appears to be the same there. Though interestingly enough, the “Android toolchain” check passes despite neither $ANDROID_HOME or $ANDROID_SDK_ROOT being set.
After digging through the source code of Flutter for a little bit, I have narrowed the issue down to packages/flutter_tools/lib/src/android/android_sdk.dart, where the config value of android-sdk takes priority over environment variables.
Sure enough, after running flutter config --android-sdk $ANDROID_HOME in the development shell, flutter doctor has stopped complaining about the validity of the path, and so I have quickly appended the command to my flake’s shellHook.
From there, a few build-stopping complaints arose from Gradle, but I have managed to resolve them by adding the following configuration to the flake:
I should have remembered that (I’ve absolutely run into this in the past). I think the right answer though is to delete the flutter config, in your home directory, rather than set it with a shell hooks, because global state. With no config present the environment variable will work.
As to your last problem, enabling nix-ld is enough to banish that error. There might be a cooler solution involving patchelf, I just didn’t care enough to pursue it.
As far as I can tell, Flutter will create a config outside the project directory regardless, so using the shellHook should ensure better reproducibility (since the location of the generated sdk can change, and the flake’s goal is removing as many manual user actions as possible).
As per nix-ld and patchelf, I have no idea how to integrate either of these into my flake, but what I did find was a way of forcing Gradle to use a custom-located aapt2 binary, which I’ve managed to do by adding the following line to my project’s gradle.properties file:
But changing this might cause issues for people building the project without using the flake, so a better way to do this would be passing the gradle option via flutter run -P itself:
flutter run -d "<device name>" -P "android.aapt2FromMavenOverride=$(which aapt2)"
But of course, writing all of this out is not the most convenient thing to do, so I’m all ears for better ideas ^^
So if you want to set a config, that will work, but you are effectively dirtying the xdg configuration folder of everyone that runs the flake.
On the other bits, it’s true that setting programs.nix-ld.enable = true is not settable in the flake, outside of nixos you don’t need it, so our teams convention has just been to enable nix-ld and not bother pushing it further. It does seem like you could, based on your snippet, write a fancy wrapper to inject the argument, but that’s more effort than I’ve been willing to put in
Okay, I just checked and you’re right. Flutter doesn’t create a config on its own, and the environment variable works with it gone. I’m not sure why I didn’t test your suggestion before writing the prior reply.
As per the wrapper, it’s definitely something to consider, especially given that the tool allows for multiple calls of the -P flag (which means it wouldn’t be necessary to parse it if used outside the injection). I wonder if it wouldn’t be easier to just make an alias, though it might not work for people who call nix develop with a custom shell (-c). Then again, there are phases, but I’m unsure if they can be called with positional arguments.
Nice! If you want to contribute any of this to the troubleshooting section of the wiki page you might save a soul some time, and I guess we should probably all switch to using ANDROID_HOME.
And regarding ANDROID HOME, I’m not surprised people still use its deprecated counterpart - a Nixpkgs contributor must have gotten the story wrong, though that linked part of Android’s documentation is worded quite poorly, so I can’t blame them.
Just one more question before I move onto writing the Flutter wrapper - would I be wrong to assume that trying to bundle Waydroid into the flake is pointless, due to the fact that it requires a Systemd service to run? I think I might be better off just making a general-purpose Flutter flake instead, since it’s possible to attach the Flutter debugger to an emulator with the -d flag anyways (assuming the ADB connection is setup properly).
It also currently doesn’t have a good configuration interface in Nix (such as a default Android image source), which could be a project for another time, but as of right now, it would make integrating it into the flake extra troublesome.
I couldn’t find any helpful resources on creating Nix wrappers, and I couldn’t get wrapProgram to work with the already wrapped Flutter binary, so I’ve opted to go with a simple function in shellHook instead:
shellHook = ''
# ...
# Make `flutter` pass a custom `aapt2` binary to Gradle when appropriate.
flutter()
{
if [ "$1" = "run" ] || [ "$1" = "build" ]; then
${pkgs.flutter}/bin/flutter $@ -P "android.aapt2FromMavenOverride=${pkgs.aapt}/bin/aapt2"
else
${pkgs.flutter}/bin/flutter $@
fi
}
alias flutter=flutterCustomAapt
'';
But of course, if anyone would be able to chime in with a Nixier solution (one that would still work if the flake was run with nix develop -c <shell>), feel free.