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?

15 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

Loads a shell from a temporary directory using the envrc package to provide buffer-local direnv integration.

Here is a self-contained example:

#+begin_src sh :results output
hello
#+end_src

#+begin_src python :results drawer output
  from sympy import *
  def out(s):
    print("\[ " + s + " \]")

  x = symbols('x')
  functions = [
    x*2,
    ln(x),
    csc(x),
    atan(x),
    exp(x**2)
  ]

  for f in functions:
    out(latex(f) + " \\to " + latex(integrate(f)))
#+end_src

* Document Setup                                                   :noexport:

#+name: nix-shell
#+begin_src nix :tangle (nix-shell-get-direnv-path "shell.nix") :mkdirp t :noeval t
  { pkgs ? import <nixpkgs> {}, pythonPackages ? pkgs.python3Packages }:
  pkgs.mkShell {
    buildInputs = [
      pythonPackages.sympy
      pkgs.hello
    ];
  }
#+end_src

#+name: nix-shell-load-direnv
#+begin_src emacs-lisp :results silent
  (require 'envrc)
  (org-sbe "nix-shell-get-direnv-path-defun")
  (org-babel-tangle)
  (let ((default-directory (nix-shell-get-direnv-path "")))
      (envrc-allow))
#+end_src

** Auxiliary functions.

#+name: nix-shell-get-direnv-path-defun
#+begin_src emacs-lisp
  (defun nix-shell-get-direnv-path (path)
    (let* ((tmpdir-basename (format-time-string "env%m%d%H")) ; For demo purpose. You can use ID property to get per-file location.
           (tmpdir (format "/tmp/%s/%s" tmpdir-basename path)))
       tmpdir))
#+end_src

#+name: nix-shell-dotenvrc
#+begin_src sh :tangle (nix-shell-get-direnv-path ".envrc") :mkdirp t :noeval t
  use nix
#+end_src

** Local Variables

# Local Variables:
# eval: (org-sbe "nix-shell-load-direnv")
# End:

I made an Emacs package for this.

Cheers!

1 Like