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
573
April 25, 2021, 4:28pm
2
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
jhoch
January 5, 2025, 1:59pm
7
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…
mark
February 2, 2025, 9:31pm
8
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.
573
February 6, 2025, 10:13am
9
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.