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?

18 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!
6 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
1 Like

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!

2 Likes

This looks cool, but I get:

org-babel-execute-src-block: No org-babel-execute function for nix!

Is there any special setup needed? I added “nix” to 'org-babel-do-load-languages but still…

You need to have the package that org bael will load as it is not part of emacs.
This is theesm/ob-nix: a simple org-babel language extension to evaluate nix expressions with nix-instantiate - Codeberg.org
NB I have not actually tried it out.

The nix-shell declarations in the org file are still not picked up it seems. I. e. with code block shell cowsay is not found except when already installed outside the org file scope.

I get the same error as you when I hit Ctrl-C-Ctrl-C on the named nix-shell code block.
However when I hit Ctrl-C-Ctrl-C on the code blocks that reference the nix-shell it does work.