Skip to content

Instantly share code, notes, and snippets.

@mattchrist
Last active May 10, 2024 05:41
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattchrist/c27ab7c4bbc80fbc99f96e4daa575e47 to your computer and use it in GitHub Desktop.
Save mattchrist/c27ab7c4bbc80fbc99f96e4daa575e47 to your computer and use it in GitHub Desktop.

Using Nix Shells in Org-mode Source Blocks

Using the shell source type and the :shebang option, we can execute org-mode source blocks in a nix-shell. Using this, org-mode can define the dependencies and runtime available for literate programming documents.

Hello World!

Create a shell.nix file

{pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  buildInputs = [ pkgs.hello ];
}

Tangle it with `M-x org-babel-tangle`.

hello

This also works with other interpreters:

Python

{pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  buildInputs = [ pkgs.python3 ];
  name = "World";
}

Tangle again.

import os
print("Hello, {}!".format(os.environ['name']))

Nodejs

{pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
  buildInputs = [ pkgs.nodejs ];
  name = "Emacs";
}
console.log(`Hello, ${process.env.name}!`);

Node is a bit special, the second-line shebang doesn’t work, since #! is not valid nodejs syntax. But thanks to this bug/feature #!nix-shell content in other parts of the file are still processed by nix-shell. In this shebang we wrap the second line in grave symbols and newlines to make it both valid for nix-shell and nodejs.

Future work

  • Make syntax highlighting work right, emacs doesn’t know that the python and javascript code need to be highlighted differently from shell scripts
  • Figure out better way to load nix-shell code than tangling to a temporary directory
  • custom org-babel-execute mode for nix-shells
@AntonHakansson
Copy link

AntonHakansson commented Jan 21, 2024

This is an alternative method that I discovered.
It relies on envrc package to load a direnv environment from a temporary directory.
I have only tested it with shell and python so far but it might work with other src blocks.
Notice that the python src block does not require any extra setup.

#+title: org-babel and nix-shell integration hack

* Usage
** Shell

#+begin_src sh :results output
hello
#+end_src

#+results:
: Hello, world!

** Python
Compute the integrals of some sympy expressions.

#+name: python-with-matplotlib-from-nix-shell
#+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

#+results: python-with-matplotlib-from-nix-shell
:results:
\[ 2 x \to x^{2} \]
\[ \log{\left(x \right)} \to x \log{\left(x \right)} - x \]
\[ \csc{\left(x \right)} \to \frac{\log{\left(\cos{\left(x \right)} - 1 \right)}}{2} - \frac{\log{\left(\cos{\left(x \right)} + 1 \right)}}{2} \]
\[ \operatorname{atan}{\left(x \right)} \to x \operatorname{atan}{\left(x \right)} - \frac{\log{\left(x^{2} + 1 \right)}}{2} \]
\[ e^{x^{2}} \to \frac{\sqrt{\pi} \operatorname{erfi}{\left(x \right)}}{2} \]
:end:

* 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")) ; You can use ID property to get per-file location
           (tmpdir (format "/tmp/%s/%s" tmpdir-basename path)))
       tmpdir))
#+end_src

The =shell.nix= for this buffer will be located at src_elisp[]{(nix-shell-get-direnv-path "")} {{{results(=/tmp/20240121T144415/=)}}}.

#+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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment