Dean Sallinen
  • Uses
  • Wrote
  • Now
  • 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