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.