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
573
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!
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