Want a nix flake for using Jupyter Notebooks with R kernel

(also posted on SO - https://stackoverflow.com/questions/79889469/nix-flake-for-using-jupyter-notebooks-with-r-kernel)

Goal

I want to be able to call nix-shell (or nix develop) followed by jupyter lab, and have a jupyter lab instance with an R kernel.

So far

Asking SO-AI and making some tweaks, I have the following flake.nix file:

{
  inputs = {
    # nixpkgs 25.01 - https://github.com/NixOS/nixpkgs/releases/tag/25.01
    nixpkgs.url = "github:NixOS/nixpkgs/11cb3517b3af6af300dd6c055aeda73c9bf52c48";
  };

  outputs = { self, nixpkgs }: let
    system = "aarch64-darwin"; # change to "aarch64-linux" or others if needed
    pkgs = import nixpkgs { inherit system; };
  in {
    devShells.${system}.default = pkgs.mkShell {
      buildInputs = with pkgs; [
        python312
        python312Packages.jupyterlab
        python312Packages.ipykernel
        R
        rPackages.IRkernel
        # useful to have common R tools:
        rPackages.tidyverse
      ];

      shellHook = ''
        export LC_ALL=en_US.UTF-8
        export LANG=en_US.UTF-8

        # Ensure Jupyter uses a runtime dir inside the temporary dev-shell profile,
        # so kernel specs written below are visible to jupyter run in this shell.
        mkdir -p "./.jupyter_runtime"

        # Register IRkernel into the Jupyter kernelspecs visible to this shell.
        # Use --user so it writes into a user kernels directory (under JUPYTER_DATA_DIR/JUPYTER_RUNTIME_DIR settings).
        if ! jupyter kernelspec list 2>/dev/null | grep -q '^ir '; then
          echo "Registering IRkernel for this dev-shell..."
          Rscript -e 'require("IRkernel")'
          Rscript -e 'IRkernel::installspec(prefix = "./.jupyter_runtime")'
        fi

        echo "Dev shell ready. Start JupyterLab with: jupyter lab"
      '';
    };
  };
}

The environment installs correctly, but the shell hook generates errors:

Error in file(file, ifelse(append, "a", "w")) : 
  cannot open the connection
Calls: <Anonymous> -> write -> cat -> file
In addition: Warning message:
In file(file, ifelse(append, "a", "w")) :
  cannot open file '/tmp/nix-shell.dKvmub/Rtmpal6d2K/file4c816c5dae9/kernelspec/kernel.json': Permission denied
Execution halted
rm: cannot remove '/tmp/nix-shell.dKvmub/Rtmpal6d2K/file4c816c5dae9/kernelspec/kernel.json': Permission denied
rm: cannot remove '/tmp/nix-shell.dKvmub/Rtmpal6d2K/file4c816c5dae9/kernelspec/kernel.js': Permission denied
rm: cannot remove '/tmp/nix-shell.dKvmub/Rtmpal6d2K/file4c816c5dae9/kernelspec/logo-64x64.png': Permission denied
rm: cannot remove '/tmp/nix-shell.dKvmub/Rtmpal6d2K/file4c816c5dae9/kernelspec/logo-svg.svg': Permission denied

And if I then run jupyter lab, the browser window loads correctly but there is no available R kernel.

I imagine this is because IRkernel::installspec() is trying to modify files in the Jupyter install path, which by nix design are immutable, so the “registration” fails and the R kernel remains inaccessible to Jupyter. But I don’t know how to fix it; I thought specifying prefix = ... would fix the issue but it didn’t change anything.

nvm, figured it out. the resulting flake looks like this:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
  };

  outputs =
    {
      self,
      nixpkgs,
    }:

    let
      system = "aarch64-darwin";
      pkgs = import nixpkgs { inherit system; };
    in
    {
      devShells.${system}.default = pkgs.mkShell {
        buildInputs = with pkgs; [
          python312
          python312Packages.jupyterlab
          python312Packages.ipykernel
          R
          rPackages.IRkernel
          # useful to have common R tools:
          rPackages.tidyverse
        ];

        shellHook = ''
          export LC_ALL=en_US.UTF-8
          export LANG=en_US.UTF-8

          # Ensure Jupyter uses a runtime dir inside the temporary dev-shell profile,
          # so kernel specs written below are visible to jupyter run in this shell.
          mkdir -p "./.jupyter_runtime"

          # Register IRkernel into the Jupyter kernelspecs visible to this shell.
          # Use --user so it writes into a user kernels directory (under JUPYTER_DATA_DIR/JUPYTER_RUNTIME_DIR settings).
          if ! jupyter kernelspec list --json 2>/dev/null | grep -q 'IRkernel::main()'; then
            echo "Registering IRkernel for this dev-shell..."
            jupyter kernelspec install ${pkgs.rPackages.IRkernel}/library/IRkernel/kernelspec --user
          fi

          echo "Dev shell ready. Start JupyterLab with: jupyter lab"
        '';
      };
    };

}

The main solution was to install the kernel with jupyter kernelspec install --user, instead of the SO-AI-recommended Rscript -e 'IRkernel::installspec(...)'

There was also a small detail to recognizing an already-installed kernel, to prevent installing the kernel twice. Specifically, SO-AI was using jupyter kernelspec list and checking whether anything in the output matched grep -q '^ir '. However, using jupyter kernelspec install ... installed the R-kernel with the name kernelspec into a user folder, so jupyter kernelspec list didn’t actually output any text with ir in it.

Instead, I changed it to run jupyter kernelspec list --json (notably with the --json flag), which outputs a lot of information about each kernel, including the run commands to start the kernel. And in the case of the R-kernel, the start command IRkernel::main() is distinguishable from any other kernels, regardless of what the R-kernel is named. So now it conditions on that to determine whether an R-kernel is registered or not.

1 Like