on master help sign in
Add a flake that builds the desks and runs every check

Jibb has no native binary: what it ships is desks, source a ship builds. So `nix build` writes the three desks, my-app, jibb and jibb-dev, exactly as `tools/ship sync` commits them, with a new ship-free `tools/ship assemble --out DIR`, and compiles jibb.hoon and jibb-import.hoon under urbit eval to check that they build. `nix flake check` builds that and two checks, each one tools/check run in the sandbox: `--group eval`, and on Linux `--group ship` against a copy of a fake ~zod that the flake boots from the brass pill in about half a minute. The pins: nixpkgs from the nixos-unstable channel tarball, for vere 4.6 (channels.nixos.org fetches where GitHub's archive downloads are blocked, as in a Claude cloud container), click from urbit/tools by git, and the pill from the urbit/urbit commit vendor/ comes from. `nix develop` gives urbit, python3 and click, with $JIBB_CLICK set, and `nix build .#fakezod` the booted pier. The ship checks need Linux, since tools/ship reads /proc; on macOS the flake runs the eval group only. Only x86_64-linux has been tried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U4FzmTL265QGvDaa6pxF4f

commitf583172008841f48d68679f827b878940d0d58e5 authorClaude <noreply@anthropic.com> dateMon 2 Nov 2026, 16:31 (9 hours ago) parent0a86d7c
6 files changed+291−4
.gitignore+2 README.md+18−1 docs/development.md+14−3 flake.lock+41 flake.nix+192 tools/ship+24
.gitignore+2
@@ -1,2 +1,4 @@
11__pycache__/
22*.pyc
3+result
4+result-*
README.md+18−1
@@ -14,6 +14,23 @@ Milestone 1 of the doc's eight, the parser and the `%jibb` mark, is done; milest
1414- `tools/`: dev tooling: `golden`, `hoon` and `doc-blocks`, with their self-test in `testdata/`, `ship`, and `check`, which runs every check.
1515- `docs/`: the doc's tabs as Markdown in `spec/`, the status, the open questions, the development guide, the compiler's map, and briefs for milestones in `prompts/`.
1616
17+## Building and checking with Nix
18+
19+The flake (`flake.nix`, pinned by `flake.lock`) builds the desks and runs every check, with vere 4.6, Python 3, click and the brass pill pinned:
20+
21+```
22+nix build # result/my-app, result/jibb, result/jibb-dev: the desks
23+nix flake check # every check: the eval group and, on Linux, the ship group on a fresh fake ~zod
24+nix develop # a shell with urbit, python3 and click, for running the tools by hand
25+nix build .#fakezod # a freshly booted fake ~zod pier (Linux)
26+```
27+
28+Jibb has no native binary: what it ships is desks, Hoon and Jibb source that a ship builds. So `nix build` writes the three desks exactly as `tools/ship sync` would commit them (it runs `tools/ship assemble`), and compiles `jibb.hoon` and `jibb-import.hoon` under `urbit eval` to check that they build.
29+
30+`nix flake check` builds that too, then runs `tools/check --group eval` and, on Linux, `tools/check --group ship` against a copy of a fake ~zod it boots from the pill. Everything runs in the Nix sandbox without network, so a check can't pass because of something on your machine. Each group is one build; a failure prints the failing checks' output, and a passing build keeps every check's log in its output. On four CPUs it took about 20 minutes, the two groups building side by side: 18 minutes for the eval group, most of it `type-todo-full-test`, and 10 for the ship group. To run a single check, use `nix develop -c tools/check <name>` (see [docs/development.md](docs/development.md#before-pushing)).
31+
32+Limits: the ship checks need Linux, since `tools/ship` finds the ship's processes in `/proc`, so on macOS `nix flake check` runs only the eval group. Only `x86_64-linux` has been tried; the flake also declares `aarch64-linux` and `aarch64-darwin`, the other platforms nixpkgs builds vere for.
33+
1734## Golden tests
1835
1936Each case, `tests/golden/<case>/`, holds `source.jibb`, `about.txt` (where it comes from, what it tests, the expected outcome), and the expected outputs: `tokens.txt` from the layout pass and `tree.txt` from the parser. A file holds the output plus one trailing newline. A case may also have a `path.txt` specifying its logical `/ui/.../*.jibb` path. An `expect-types:` line in `about.txt` opts it into the provisional `golden-types` checker output in `types.txt`; `--update` creates it. A case can select Hoon molds in `imports.txt` and provide each module in `sur/<module>.txt`, or select marks in `marks.txt` and provide their source in `mar/<name>.txt`. `modules.txt` selects Jibb source fixtures; their trees go in `module-trees.txt`, and the partial checker can resolve a subset of library imports. See [docs/development.md](docs/development.md).
@@ -78,7 +95,7 @@ tools/ship log # the end of its log
7895tools/ship stop
7996```
8097
81−`tools/ship sync <desk>` assembles a desk from the repo's folders, creates it if it's missing (as `|new-desk` does), mounts it, deletes from the desk through Clay the files the repo doesn't have, makes the mount hold exactly the repo's files, runs `|commit` and waits until the desk matches the repo, checking every file. If Clay rejects the commit, it prints Clay's error, which the log doesn't show.
98+`tools/ship assemble --out <dir> [<desk> ...]` writes the desks' files to `<dir>/<desk>` as sync assembles them, without a ship; `nix build` uses it. `tools/ship sync <desk>` assembles a desk from the repo's folders, creates it if it's missing (as `|new-desk` does), mounts it, deletes from the desk through Clay the files the repo doesn't have, makes the mount hold exactly the repo's files, runs `|commit` and waits until the desk matches the repo, checking every file. If Clay rejects the commit, it prints Clay's error, which the log doesn't show.
8299
83100| Desk | Assembled from |
84101| --- | --- |
docs/development.md+14−3
@@ -20,7 +20,17 @@ The source-backed generic-union guard can be run under eval with `tools/type-uni
2020
2121The tools need Python 3 and `urbit` (vere) on `PATH`; the ship tools also need a fake `~zod` and click. Everything so far ran with vere 4.6 on Linux.
2222
23−On a usual machine:
23+With Nix (and flakes), `nix develop` gives a shell with vere 4.6, Python 3 and click, as the flake pins them, and sets `$JIBB_CLICK`. `nix build .#fakezod` boots a fake `~zod` from the pinned brass pill in about half a minute; copy it to where `tools/ship` expects it:
24+
25+```
26+mkdir -p /root/urbit
27+cp -r "$(nix build .#fakezod --no-link --print-out-paths)" /root/urbit/zod
28+chmod -R u+w /root/urbit/zod
29+```
30+
31+`nix flake check` needs neither step, since it boots a ship of its own; see the [README](../README.md#building-and-checking-with-nix).
32+
33+Without Nix, on a usual machine:
2434
25351. Install vere as [docs.urbit.org](https://docs.urbit.org) says, and get a brass pill.
26362. Boot a fake ship where `tools/ship` expects it, or point `--pier` or `$JIBB_PIER` elsewhere. Booting takes a few minutes:
@@ -31,7 +41,7 @@ On a usual machine:
31413. Get click, the command-line client for a ship's `conn.sock`, from [urbit/tools](https://github.com/urbit/tools), at `/root/src/tools/pkg/click/click` or wherever `--click` or `$JIBB_CLICK` says.
32424. Optionally, clone [urbit/urbit](https://github.com/urbit/urbit) to read Arvo's source. `vendor/SOURCES.txt` names the commit the vendored files come from.
3343
34−In a Claude cloud container, the network blocks urbit.org, bootstrap.urbit.org, nixos.org's installer and GitHub's release downloads. This worked on 2026-10-29, and has to be redone in each new container:
44+In a Claude cloud container, the network blocks urbit.org, bootstrap.urbit.org, nixos.org's installer and GitHub's release downloads. When Nix is already installed, as it was on 2026-11-02, the flake does all of the below: it fetches nixpkgs from channels.nixos.org, click with git and the pill from media.githubusercontent.com, which the network allows. Otherwise, this worked on 2026-10-29, and has to be redone in each new container:
3545
3646```
3747# Nix, single-user
@@ -117,7 +127,7 @@ git clone https://github.com/urbit/tools /root/src/tools
117127| `tools/clay-fixtures` | Syncs `my-app`, builds its example `/sur` files and marks through Clay, checks selected mold and mark projections and the partial checker, and records Ford and mark subject shapes on the fake ship |
118128| `tools/hoon` | Evaluates Hoon with the library in scope, as `tools/hoon '(golden /a/b/jibb (crip "++ main view"))'`; `--cord name=file` binds a file's text, `-f` reads the expression from a file |
119129| `tools/doc-blocks` | Checks the doc's `jibb` blocks against the `doc-*` golden cases: `tools/doc-blocks docs/spec` |
120−| `tools/ship` | Runs the fake ship: `start`, `stop`, `status`, `log`, `sync <desk>`, `test`, `gen-tests`, `check-commit`, `scry`, `hoon`, `run`, `poke`, `commit`, `mount`, `unmount` |
130+| `tools/ship` | Runs the fake ship: `start`, `stop`, `status`, `log`, `sync <desk>`, `assemble --out <dir>` (no ship needed), `test`, `gen-tests`, `check-commit`, `scry`, `hoon`, `run`, `poke`, `commit`, `mount`, `unmount` |
121131| `tools/testdata/selftest` | Tests `tools/golden` and `tools/hoon` against a stub library, and `tools/doc-blocks` against Markdown it writes |
122132| `tools/testdata/ship-selftest` | Tests `tools/ship`'s test generator and runner against a stub library on the ship |
123133| `tools/testdata/clay-fixtures-selftest` | Tests temporary Clay fixture cleanup and signal handling without a ship |
@@ -129,6 +139,7 @@ git clone https://github.com/urbit/tools /root/src/tools
129139```
130140tools/check # every check; it exits 0 only when all pass
131141tools/check --group eval # only the checks that need no ship, as CI runs them
142+nix flake check # the same checks in the Nix sandbox, on a fresh fake ~zod
132143```
133144
134145`tools/check` runs every check the repo has. Their one list is `CHECKS` in the script, so a new check goes there, as well as in the table above; `tools/check --list` prints it with each check's command. The eval group, the 62 checks that need only `urbit` and Python, runs several at a time (`-j`, by default one per CPU), the slowest first. The ship group, `ship-selftest`, `ship-test`, `check-commit` and `clay-fixtures`, then runs one check at a time on the ship `tools/ship` talks to; `tools/check` starts it if it isn't running and stops it again afterwards. Name checks, or globs, to run only those: `tools/check golden 'type-todo-*'`. A failing check's output follows its FAIL line; `-v` prints every check's output, and `--logs DIR` keeps it all.
flake.locknew file+41
@@ -0,0 +1,41 @@
1+{
2+ "nodes": {
3+ "nixpkgs": {
4+ "locked": {
5+ "lastModified": 1793540411,
6+ "narHash": "sha256-us2GRWGXbOQpN1blyJGvmdx+g4bv9GoFYq5iiOHRiZ8=",
7+ "rev": "0bb2eedc20e4739f29e8045034f2828ad7054f8f",
8+ "type": "tarball",
9+ "url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1106360.0bb2eedc20e4/nixexprs.tar.xz"
10+ },
11+ "original": {
12+ "type": "tarball",
13+ "url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
14+ }
15+ },
16+ "root": {
17+ "inputs": {
18+ "nixpkgs": "nixpkgs",
19+ "urbit-tools": "urbit-tools"
20+ }
21+ },
22+ "urbit-tools": {
23+ "flake": false,
24+ "locked": {
25+ "lastModified": 1750452841,
26+ "narHash": "sha256-2IoU8tGR92g8SaoDZdMxbzh2VLr/63XUCbrPS3nestg=",
27+ "ref": "refs/heads/master",
28+ "rev": "c9c91ce142cfe85edbed320138f21c7213aceaab",
29+ "revCount": 19,
30+ "type": "git",
31+ "url": "https://github.com/urbit/tools"
32+ },
33+ "original": {
34+ "type": "git",
35+ "url": "https://github.com/urbit/tools"
36+ }
37+ }
38+ },
39+ "root": "root",
40+ "version": 7
41+}
flake.nixnew file+192
@@ -0,0 +1,192 @@
1+{
2+ description = "Jibb, a UI language for Urbit: its desks, and the repo's checks";
3+
4+ inputs = {
5+ # A channel tarball rather than github:NixOS/nixpkgs, so the flake
6+ # also fetches where GitHub's archive downloads are blocked, as in a
7+ # Claude cloud container. flake.lock pins the exact release.
8+ # nixos-unstable has vere 4.6, which the tools were written against.
9+ nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
10+ # click, which tools/ship formats its requests to the ship with.
11+ urbit-tools = {
12+ url = "git+https://github.com/urbit/tools";
13+ flake = false;
14+ };
15+ };
16+
17+ outputs = { self, nixpkgs, urbit-tools }:
18+ let
19+ # The platforms nixpkgs builds vere for.
20+ systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
21+ forSystems = f: nixpkgs.lib.genAttrs systems
22+ (system: f nixpkgs.legacyPackages.${system});
23+ inherit (nixpkgs) lib;
24+ fs = lib.fileset;
25+
26+ # What the desks are assembled from, and the tools that assemble
27+ # them and compile their libraries.
28+ deskFiles = fs.unions [
29+ ./jibb
30+ ./jibb-dev
31+ ./my-app
32+ ./vendor
33+ ./tests
34+ ./tools/ship
35+ ./tools/golden_case.py
36+ ./tools/hoon
37+ ./tools/urbit_eval.py
38+ ./tools/noun.py
39+ ];
40+ # What the checks read: the desks' files, the tools and the doc's
41+ # tabs, but not the other docs, so editing those reruns nothing.
42+ checkFiles = fs.unions [ deskFiles ./tools ./docs/spec ];
43+ source = fileset: fs.toSource { root = ./.; inherit fileset; };
44+
45+ perSystem = pkgs:
46+ let
47+ inherit (pkgs) python3 urbit;
48+ isLinux = pkgs.stdenv.hostPlatform.isLinux;
49+
50+ click = pkgs.stdenvNoCC.mkDerivation {
51+ pname = "click";
52+ version = "0-unstable-${builtins.substring 0 8 urbit-tools.lastModifiedDate}";
53+ src = urbit-tools;
54+ nativeBuildInputs = [ pkgs.makeWrapper ];
55+ buildInputs = [ pkgs.bash ];
56+ installPhase = ''
57+ runHook preInstall
58+ # click runs click-format from its own folder.
59+ install -Dm755 -t $out/libexec/click \
60+ pkg/click/click pkg/click/click-format
61+ makeWrapper $out/libexec/click/click $out/bin/click \
62+ --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils pkgs.gnused ]} \
63+ --prefix PATH : ${pkgs.netcat}/bin
64+ runHook postInstall
65+ '';
66+ meta = {
67+ description = "Thin client for a running Urbit ship's conn.sock";
68+ homepage = "https://github.com/urbit/tools";
69+ license = lib.licenses.mit;
70+ mainProgram = "click";
71+ };
72+ };
73+
74+ # The brass pill from the urbit/urbit commit vendor/ is copied
75+ # from (vendor/SOURCES.txt). GitHub serves it from Git LFS.
76+ pill = pkgs.fetchurl {
77+ name = "brass.pill";
78+ url = "https://media.githubusercontent.com/media/urbit/urbit/50c7f5239610fc72263df7aa99b4d171cfe9d408/bin/brass.pill";
79+ hash = "sha256-tLq9FOnxrL20IbPDHCFe5jetZpG344K5nG/YmR5iNwg=";
80+ };
81+
82+ # A freshly booted fake ~zod: the pier itself. Booting takes
83+ # about half a minute; the ship checks start a copy of it.
84+ fakezod = pkgs.runCommand "fakezod" {
85+ nativeBuildInputs = [ urbit ];
86+ } ''
87+ urbit -F zod -B ${pill} -t -x -c zod
88+ test -d zod/.urb/log
89+ mv zod $out
90+ '';
91+
92+ # The desks, as tools/ship assembles them for the ship:
93+ # $out/my-app, $out/jibb and $out/jibb-dev, the test desk. The
94+ # check phase compiles the two libraries under urbit eval.
95+ desks = pkgs.stdenvNoCC.mkDerivation {
96+ # No version in the name, so a commit that changes no desk
97+ # file reuses the last build.
98+ name = "jibb-desks";
99+ src = source deskFiles;
100+ nativeBuildInputs = [ python3 ];
101+ nativeCheckInputs = [ urbit ];
102+ dontConfigure = true;
103+ buildPhase = ''
104+ runHook preBuild
105+ patchShebangs tools > /dev/null
106+ tools/ship assemble --out desks
107+ runHook postBuild
108+ '';
109+ doCheck = true;
110+ checkPhase = ''
111+ runHook preCheck
112+ for lib in jibb jibb-import; do
113+ echo "compiling $lib.hoon"
114+ tools/hoon --lib desks/jibb-dev/lib/$lib.hoon '~'
115+ done
116+ runHook postCheck
117+ '';
118+ installPhase = ''
119+ runHook preInstall
120+ mv desks $out
121+ runHook postInstall
122+ '';
123+ # The files are the desks' as they are: nothing to patch.
124+ dontFixup = true;
125+ meta = {
126+ description = "The Jibb desks: my-app, jibb and jibb-dev";
127+ platforms = systems;
128+ };
129+ };
130+
131+ # A check: tools/check run on the repo with the given arguments.
132+ check = name: args: extra: pkgs.stdenvNoCC.mkDerivation ({
133+ name = "jibb-check-${name}";
134+ src = source checkFiles;
135+ nativeBuildInputs = [ python3 urbit ];
136+ dontConfigure = true;
137+ dontInstall = true;
138+ dontFixup = true;
139+ buildPhase = ''
140+ runHook preBuild
141+ patchShebangs tools > /dev/null
142+ mkdir $out
143+ tools/check ${args} --logs $out
144+ runHook postBuild
145+ '';
146+ } // extra);
147+
148+ # Every check that needs only urbit and Python, at once.
149+ evalChecks = check "eval" "--group eval -j $NIX_BUILD_CORES" { };
150+
151+ # The checks that need a ship, on a writable copy of fakezod.
152+ shipChecks = check "ship" "--group ship" {
153+ preBuild = ''
154+ cp -r ${fakezod} $TMPDIR/zod
155+ chmod -R u+w $TMPDIR/zod
156+ export JIBB_PIER=$TMPDIR/zod
157+ export JIBB_CLICK=${lib.getExe click}
158+ '';
159+ # A failed run shows the end of the ship's log too.
160+ failureHook = ''
161+ echo "the end of the ship's log, $TMPDIR/zod.log:"
162+ tail -n 100 $TMPDIR/zod.log || true
163+ '';
164+ };
165+ in
166+ {
167+ packages = {
168+ inherit desks click;
169+ default = desks;
170+ } // lib.optionalAttrs isLinux { inherit fakezod; };
171+
172+ checks = {
173+ inherit desks;
174+ eval = evalChecks;
175+ } // lib.optionalAttrs isLinux { ship = shipChecks; };
176+
177+ devShells.default = pkgs.mkShell {
178+ packages = [ python3 urbit click ];
179+ # tools/ship looks for click here; the pier stays
180+ # /root/urbit/zod unless $JIBB_PIER says otherwise.
181+ JIBB_CLICK = lib.getExe click;
182+ };
183+ };
184+
185+ all = forSystems perSystem;
186+ in
187+ {
188+ packages = lib.mapAttrs (_: s: s.packages) all;
189+ checks = lib.mapAttrs (_: s: s.checks) all;
190+ devShells = lib.mapAttrs (_: s: s.devShells) all;
191+ };
192+}
tools/ship+24
@@ -5,6 +5,7 @@
55 tools/ship stop stop it
66 tools/ship status running or not; desks, revisions, mounts
77 tools/ship sync my-app assemble a desk from its folders, |commit
8+ tools/ship assemble --out DIR write the desks' files to DIR, no ship
89 tools/ship test sync the jibb-dev test desk, run -test
910 tools/ship gen-tests write tests/lib/jibb.hoon from tests/golden
1011 tools/ship check-commit prove that |commit stores .jibb files
@@ -1761,6 +1762,21 @@ def cmd_sync(ship, args):
17611762 sync(ship, desk, lib=lib, golden=args.golden, timeout=args.timeout)
17621763
17631764
1765+def cmd_assemble(ship, args):
1766+ """Write desks' files, as sync assembles them, into <out>/<desk>."""
1767+ for desk in args.desks or sorted(DESKS):
1768+ a = assemble(desk)
1769+ into = os.path.join(args.out, desk)
1770+ if os.path.exists(into):
1771+ raise Unable('%s already exists' % show(into))
1772+ for rel in sorted(a.files):
1773+ path = os.path.join(into, rel)
1774+ os.makedirs(os.path.dirname(path), exist_ok=True)
1775+ with open(path, 'wb') as f:
1776+ f.write(a.files[rel][0])
1777+ say('%s: %d files in %s' % (desk, len(a.files), show(into)))
1778+
1779+
17641780def cmd_gen_tests(ship, args):
17651781 golden = args.golden or GOLDEN
17661782 out = args.out or TESTS_LIB
@@ -2319,6 +2335,13 @@ def main(argv):
23192335 p.add_argument('--timeout', type=int, default=120,
23202336 help='seconds to wait for the commit')
23212337
2338+ p = sub.add_parser('assemble', help='write desks, as sync assembles '
2339+ 'them, into a folder; needs no ship')
2340+ p.add_argument('desks', nargs='*', metavar='desk',
2341+ help='the desks (default: all of them)')
2342+ p.add_argument('--out', required=True,
2343+ help='the folder; each desk goes in <out>/<desk>')
2344+
23222345 p = sub.add_parser('test', help='sync the jibb-dev desk and run -test '
23232346 'on its /tests')
23242347 g = p.add_mutually_exclusive_group()
@@ -2383,6 +2406,7 @@ def main(argv):
23832406 'status': lambda: cmd_status(ship, args),
23842407 'log': lambda: cmd_log(ship, args),
23852408 'sync': lambda: cmd_sync(ship, args),
2409+ 'assemble': lambda: cmd_assemble(ship, args),
23862410 'test': lambda: cmd_test(ship, args),
23872411 'gen-tests': lambda: cmd_gen_tests(ship, args),
23882412 'check-commit': lambda: cmd_check_commit(ship, args),