Last updated on

NixOS for React Native Android development

Here’s how I got a working setup for React Native Android development on NixOS.

First off, is this the best setup? No. I still don’t really know what I’m doing when it comes to Nix or NixOS. The combination here just happens to work for me. Once I learn more about Nix and NixOS I’ll optimize and make changes. Hopefully I’ll remember to update this post too.

Why do this? My pop_os install broke when upgrading nvidia drivers and I had a drive failure when trying to recover from backup. Since I had to install an OS from scratch anyway, it seemed like a good opportunity to try something new. I’d heard about NixOS and fell in love with the idea of a declarative configuration. I already have some experience with immutable operating systems from running Fedora Silverblue on my laptop, so I thought it would be a great time to try Nix.

Once I figured out basic system configuration, I needed to get my environment setup to develop a react native android application for work.

System configuration

Starting with the system wide configuration file. I enabled the adb program, included the android-udev-rules, setup flakes, and included my user in the plugdev and adbuser groups.

I also added node, yarn, and prettierd here but they should go in the project flake (I just haven’t moved them yet).

# /etc/nixos/configuration.nix

{
  config,
  pkgs,
  ...
}: {
  imports = [
    ./hardware-configuration.nix
  ];

  # ... Other lines omitted

  nixpkgs.config.allowUnfree = true;

  # NixOS doesn't have a plugdev group - needed for connecting via adb
  users.groups.plugdev = {};

  users.users.dean = {
    isNormalUser = true;
    description = "dean";
    # Not sure if both adbusers and plugdev are needed, but this works
    extraGroups = ["networkmanager" "wheel" "adbusers" "plugdev"];
    packages = with pkgs; [];
    shell = pkgs.zsh;
  };

  # Flakes enabled for the direnv setup
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
  environment.systemPackages = with pkgs; [
    wget
    unzip
    gcc # c compiler - not sure if needed, including anyways
    alejandra # .nix formatting
    wl-clipboard # clipboard integration
    nodejs_20 # This should be moved to the project flake
    nodePackages.yarn # Same with this
    prettierd # And this
    # android-tools # Couldn't get this to work: Doesn't add udev rules.
  ];

  # This is the one that correctly adds the udev rules!
  programs.adb.enable = true;

  # Not sure if this is necessary if the above is set?
  services.udev.packages = [
	pkgs.android-udev-rules
  ];

  # ... Other lines omitted

direnv is how nix will create a shell with the needed environement variables and project dependencies when we cd into the directory. I’ve added it to home manager but this could have easily gone into configuration.nix instead.

# ~/.config/home-manager/home.nix

{
  config,
  pkgs,
  ...
}: {
  home.username = "dean";
  home.homeDirectory = "/home/dean";
  home.stateVersion = "23.11"; 

  # Not sure if this is needed, had it set anyways
  targets.genericLinux.enable = true;

  programs.home-manager.enable = true;

  # ... Other lines omitted

  programs.direnv = {
    enable = true;
    enableZshIntegration = true;
    nix-direnv.enable = true;
  };
}

Project configuration

Here we create the flake that sets up all the android-sdk dependencies. I should figure out how to include nodejs and yarn and such here too.

Note: The versions listed are specific to my project.

# ~/code/my-react-native-project/flake.nix

{
  description = "My React Native project";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
    devshell.url = "github:numtide/devshell";
    flake-utils.url = "github:numtide/flake-utils";
    android.url = "github:tadfisher/android-nixpkgs";
  };

  outputs = {
    self,
    nixpkgs,
    devshell,
    flake-utils,
    android,
  }:
    {
      overlay = final: prev: {
        inherit (self.packages.${final.system}) android-sdk android-studio;
      };
    }
    // flake-utils.lib.eachSystem ["aarch64-darwin" "x86_64-darwin" "x86_64-linux"] (
      system: let
        pkgs = import nixpkgs {
          inherit system;
          config.allowUnfree = true;
          overlays = [
            devshell.overlays.default
            self.overlay
          ];
        };

        androidConfig = {
          defaultBuildToolsVersion = "34.0.0"; # This value can be passed to the devshell in the future
          sdkPkgs = android.sdk.${system} (sdkPkgs:
            with sdkPkgs; [
              # Useful packages for building and testing.
              build-tools-34-0-0
              cmdline-tools-latest
              emulator
              platform-tools
              platforms-android-34

              # Other useful packages for a development environment.
              ndk-26-1-10909125
              # skiaparser-3
              sources-android-34
            ]);
        };
      in {
        packages = {
          android-sdk = androidConfig.sdkPkgs;

          # Android Studio in nixpkgs is currently packaged for x86_64-linux only.
          android-studio = pkgs.androidStudioPackages.stable;
          # android-studio = pkgs.androidStudioPackages.beta;
          # android-studio = pkgs.androidStudioPackages.preview;
          # android-studio = pkgs.androidStudioPackage.canary;
        };

        devShell = import ./devshell.nix {inherit pkgs;};
      }
    );
}

In devshell.nix we create a new shell environment with all the required packages and environment variables.

# ~/code/my-react-native-project/devshell.nix

# Documentation: https://github.com/numtide/devshell
{pkgs}:
with pkgs;
  devshell.mkShell {
    name = "android-project";
    motd = ''
      Entered the Android app development environment.
    '';
    env = [
      {
        name = "ANDROID_HOME";
        value = "${android-sdk}/share/android-sdk";
      }
      {
        name = "ANDROID_SDK_ROOT";
        value = "${android-sdk}/share/android-sdk";
      }
      {
        name = "JAVA_HOME";
        value = jdk17.home;
      }
      {
        name = "GRADLE_OPTS";
        value = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${aapt}/bin/aapt2"; # Using the nixpkgs aapt2 to resolve an issue with dynamically linked executables
      }
      {
        name = "PATH";
        prefix = "${android-sdk}/share/android-sdk/emulator";
      }
      {
        name = "PATH";
        prefix = "${android-sdk}/share/android-sdk/platform-tools";
      }
    ];
    commands = [
      {
        help = "take screenshot of connected android device";
        name = "adbcap";
        command = "adb exec-out screencap -p > /tmp/screen-$(date +%Y-%m-%d-%H.%M.%S).png";
      }
    ];
    packages = [
      android-studio
      android-sdk
      gradle
      jdk17
      aapt
      # here is where I'd add nodejs and yarn 
    ];
  }

And finally, the .envrc needed for direnv to know to use the flake.

# ~/code/my-react-native-project/.envrc

if ! has nix_direnv_version || ! nix_direnv_version 2.1.1; then
  source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.1.1/direnvrc" "sha256-b6qJ4r34rbE23yWjMqbmu3ia2z4b2wIlZUksBke/ol0="
fi
use flake

Note: you’ll need to add the changes in git otherwise you’ll get some error stating flake.nix is not found. git add -A && git commit -m "Add flake.nix" worked for me.

Now with all of that set up, when I cd into my project directory I get a change to my prompt confirming my environment is setup.

~
 cd code/my-react-native-project
direnv: loading ~/code/my-react-native-project/.envrc
direnv: using flake
direnv: nix-direnv: using cached dev shell
Entered the Android app development environment.

direnv: export +ANDROID_HOME +ANDROID_SDK_ROOT +DEVSHELL_DIR +GRADLE_OPTS +IN_NIX_SHELL +JAVA_HOME +NIXPKGS_PATH +PRJ_DATA_DIR +PRJ_ROOT +name ~PATH ~XDG_DATA_DIRS

~/code/my-react-native-project main*
android-project-env echo $ANDROID_HOME
/nix/store/74iwz121cnycgr5zcx1fkb4r1j8czncx-android-sdk-env/share/android-sdk

I can now run adb devices and see my connected device. This was hard to get working, but the combination of user groups and adb in configuration.nix seems to do the trick.

Also, I used to select Transfer Files when connecting my device via USB but for some reason I now need to select Transfer Photos. That’s also part of why it was so confusing to setup. Looking into the udev rules that are added by adb, it’s something to do with the model of phone I have. Your mileage may vary.

~/code/my-react-native-project main*
android-project-env adb devices
List of devices attached
58301a9d	device

Now I can start my react-native project as usual and get back to work.

Resources