Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Modules

a module is a file, and what it is is the value of its last form. there is no export list and no namespace:

; lib/math.naj
(define (double x) (* x 2))
(list (cons 'double double))
(define math (load "lib/math.naj"))
(say ((cdr (assq 'double math)) 21))
42

an assoc list is convenient, not required. a module may answer a single procedure, a number, or a task’s address.

Two ways in

load takes a path, need takes a name. a name is resolved against naj.lock, an ordinary file the program reads:

((narju-lock 1)
 (modules
  (json
   (url "https://github.com/thorn/naj-json")
   (rev "a1b2c3...")
   (path "github.com/thorn/naj-json")
   (file "json.naj"))))

rev is the identity. a module is a git revision, never a version number and never a semantics. two revisions of one repository are two directories in the store, and ('host files) '(root) says where the store is.

resolution is a read and an assq. nothing here fetches, which is what lets a program run under a build system with no network.

(say (attempt (lambda () (need 'nope))))
('throw 'no-such-module 'nope)

A module resolves against its own lock

main.naj locks json to one repository. the module one carries its own naj.lock, which locks json to a different one.

(say (cdr (assq 'which (need 'json))))
(say (cdr (assq 'mine (need 'one))))
'v1
'v2

a name means what the module that wrote it meant. one flat lock would make a name global, turning a diamond into a conflict and a conflict into version solving. here a diamond is two directories and nobody solves anything. what makes that affordable is that there are no cells: two instances of one module cannot drift apart, so having two costs disk and nothing else.

load is module-relative for the same reason. inside a loaded file, load and need are rebound to that module’s own directory, so a module’s paths mean the same thing wherever the store put it.

Writing the lock

naj --lock reads naj.deps and writes naj.lock. it is the only entry point that registers ('host modules).

((json (url "https://github.com/thorn/naj-json")
       (ref "main")
       (file "json.naj")))

ref defaults to HEAD. naming a branch pins what it points at now rather than tracking it: what gets written down is the resolved revision.

naj --lock also fetches the whole reachable graph, since a dependency’s own dependencies must be in the store before it can resolve them. nothing is merged. each was already pinned by whoever named it, so the closure is a graph walk rather than a solver.

the manifest has no version marker and the lock does, because the manifest is written by hand and the lock is read by a program that must be able to refuse a format it does not know.

The other reader

naj --lock and nix build are the split nix flake lock and nix build already are, so the flake reads the same file:

naj-modules = narju.lib.najModules pkgs ./naj.lock;
# ...
naj --modules ${naj-modules} main.naj

najModules walks the same graph with builtins.fetchGit and symlinks each revision to $out/<path>/<rev>, the layout need joins. the sandbox then needs no network, and the binary running in it has no peer registered that could reach one.

readLock matches the file with a regex instead of parsing it. narju’s reader is four lines of assq and nix has no reader for s-expressions, so writing one there would be a second implementation of the format. the cost is that a hand-reflowed lock is not read by it.