Nix-shells in Emacs Org-mode source blocks

I discovered this week that using the :shebang option for org-mode source blocks, we can execute source blocks within a nix-shell.

Using this, org-mode can define the dependencies and runtime available for literate programming org-mode documents.

Define your shell.nix in a source block

  #+begin_src nix :tangle /tmp/shell.nix
    {pkgs ? import <nixpkgs> {} }:
    pkgs.mkShell {
      buildInputs = [ pkgs.hello ];
    }
  #+end_src

Tangle it with ‘M-x org-babel-tangle’

Then you can use this shell:

  #+begin_src shell :shebang "#!/usr/bin/env nix-shell\n#!nix-shell /tmp/shell.nix -i bash --pure" :results output
    hello
  #+end_src

  #+RESULTS:
  : Hello, World!

This also works for other interpreted languages:
Python:

  #+begin_src nix :tangle /tmp/python.nix
    {pkgs ? import <nixpkgs> {} }:
    pkgs.mkShell {
      buildInputs = [ pkgs.python3 ];
      name = "World";
    }
  #+end_src
  
  #+begin_src shell :shebang "#!/usr/bin/env nix-shell\n#!nix-shell /tmp/python.nix -i python --pure" :results output
    import os
    print("Hello, {}!".format(os.environ['name']))
  #+end_src

  #+RESULTS:
  : Hello, World!

Nodejs:

    {pkgs ? import <nixpkgs> {} }:
    pkgs.mkShell {
      buildInputs = [ pkgs.nodejs ];
      name = "Emacs";
    }
  #+end_src

  
  #+begin_src shell :shebang "#!/usr/bin/env nix-shell\n`\n#!nix-shell /tmp/node.nix -i node --pure\n`" :results output
    console.log(`Hello, ${process.env.name}!`);
  #+end_src

  #+RESULTS:
  : Hello, Emacs!

Example org doc:

Thanks for reading! Any ideas on how to make this better or other ways to use this paradigm?

14 Likes

Indeed cool. Sounds like a polyglot in-emacs jupyter.

Thanks for sharing this @mattchrist. I like this approach. Previously I was using nix-buffer GitHub - shlevy/nix-buffer: nix-shell for emacs buffers. You can set the :python and :shebang at the top of the file so that you don’t have to redefine it each time.

#+PROPERTY: header-args:python :python "nix-shell /tmp/shell.nix --pure --run python"
#+PROPERTY: header-args:shell :shebang "#!/usr/bin/env nix-shell\n#!nix-shell /tmp/shell.nix -i bash --pure"

I got the following to work for bash and python.

#+begin_src nix :tangle /tmp/shell.nix :mkdirp t
  { pkgs ? import <nixpkgs> { }, pythonPackages ? pkgs.python3Packages }:

  pkgs.mkShell {
    buildInputs = [
      pythonPackages.numpy
    ];
  }
#+end_src

#+begin_src shell :shebang "#!/usr/bin/env nix-shell\n#!nix-shell /tmp/shell.nix -i bash --pure" :results output
  echo "hello"
#+end_src

#+RESULTS:
: hello

#+begin_src python :python "nix-shell /tmp/shell.nix --pure --run python" :results output
  import numpy

  print('Hello, World!')
#+end_src

#+RESULTS:
: Hello, World!
5 Likes

Trying to run this over tramp does break the shebang.

Example

#+begin_src nix :tangle /ssh:user@host:/tmp/shell.nix
{pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  buildInputs = with pkgs; [
    hello
  ];
}
#+end_src

#+begin_src shell :dir /ssh:user@host:~/ :shebang "#!/usr/bin/env nix-shell\n#!nix-shell /tmp/shell.nix -i bash --pure" :results output
hello
#+end_src

#+RESULTS:

*Org-Babel Error Output*

zsh:1: no such file or directory: /ssh:user@host:/tmp/sh-script-wcS20a

It is trying to run zsh /ssh:user@host:/tmp/sh-script-wcS20a instead of just the ./tmp/sh-script-wcS20a.

/tmp/sh-script-wcS20a

#!/usr/bin/env nix-shell
#!nix-shell /tmp/shell.nix -i bash --pure

hello

EDIT: Workaround

This does work.

#+begin_src shell :results output
ssh user@host
nix-shell -p hello --run bash
hello
hostname
whoami
#+end_src

#+RESULTS:
: Hallo, Welt!
: host
: user

noweb?

But how can i use noweb <> to run the shell on the remote host without writing the shell.nix file onto the remote first?

#+name: shellnix
#+begin_src nix
{pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  buildInputs = with pkgs; [
    hello
  ];
}
#+end_src
#+begin_src shell :results output
ssh user@host
nix-shell -p hello --run bash
hello
hostname
whoami
#+end_src

#+RESULTS:
: Hallo, Welt!
: host
: user