Utensil's Zettelkasten-style forest of evergreen notes on math and tech.
utensil.tngl.sh/forest/
1{
2 description = "utensil forest SSG toolchains — tectonic + forester + WASM pkgs (pinned, cache-reusable; mirrors blog's flake pattern)";
3
4 inputs = {
5 # Pinned nixpkgs — nixos-unstable as of 2026-05-29 (sha 64c08a7ca0…). Recent
6 # enough that pkgs.tectonic / pkgs.wasm-pack carry .version, and forester
7 # should be available. Full commit hash so input resolution doesn't go via
8 # the GH API.
9 nixpkgs.url = "github:NixOS/nixpkgs/64c08a7ca051951c8eae34e3e3cb1e202fe36786";
10 };
11
12 outputs = { self, nixpkgs }:
13 let
14 # Linux only — we build/run on the GH Actions runner (x86_64) and on
15 # tangled Spindle (x86_64). aarch64 included for local nix develop.
16 systems = [ "x86_64-linux" "aarch64-linux" ];
17 forAll = nixpkgs.lib.genAttrs systems;
18 pkgsFor = system: import nixpkgs { inherit system; };
19
20 # ---- forest-tectonic ---------------------------------------------------
21 # tectonic LaTeX engine + pre-warmed CTAN cache (built from a tiny doc
22 # so the render container starts with the packages it needs already
23 # downloaded). Replaces TeX Live 2025 scheme-full (~7GB) — tectonic
24 # binary ~30-50MB, cache ~200-300MB.
25 #
26 # NB: needs network at build time to populate the cache. Within nix
27 # sandbox=false. Fine on the depot job (build-depot.yml runs with
28 # sandbox=false in nix.conf to match blog's pattern); render then
29 # reuses the snapshot offline.
30 # Literal version strings everywhere — older nixpkgs wrap tectonic with
31 # a derivation that lacks an exposed .version, and we don't want the
32 # flake eval to depend on that. Bump as we move pins.
33 tectonicVersion = "0.15-with-cache";
34 foresterVersion = "5ab7277";
35 wasmPkgsVersion = "wgputoy-60d0bec+egglog-8d9b10e+rhai-9fa8066";
36 texliveVersion = "small";
37
38 # ---- forest-texlive ---------------------------------------------------
39 # forester internally shells out to plain `latex` (the DVI engine) for
40 # inline-math rendering — tectonic only writes PDF, isn't a drop-in.
41 # nixery rejects `texliveSmall` from the URL path (its Docker name
42 # rules forbid uppercase). So bundle it via the flake instead — same
43 # NAR closure pattern as tectonic + forester.
44 forestTexliveFor = pkgs:
45 # forester's inline-math job.tex uses \documentclass{standalone}
46 # plus \input{diagrams}/\input{string-diagrams} which pull in
47 # tikz-cd + xy. scheme-small alone misses standalone.cls; combine
48 # explicitly so we know what's in the closure.
49 # Empirical list — derived from parsing the latex log of pipeline
50 # 22716. 19 unique tex/<dir> directories were actually loaded;
51 # map each to its nixpkgs texlive attribute. scheme-medium was
52 # 1.2 GB on R2; scheme-small + the precise extras below should
53 # land ~75-150 MB. If the next render exposes more, add them
54 # here — every loaded path in the latex log maps to one
55 # texlive.<dir> attribute.
56 let texCombo = pkgs.texlive.combine {
57 inherit (pkgs.texlive) scheme-small
58 # Common across most forester preambles
59 standalone preview varwidth adjustbox collectbox
60 tikz-cd pgf mathtools stmaryrd
61 # tikz extras forester pulls (tikzmark, calc, fit are all inside
62 # tikz but tikzmark is a separately-packaged library)
63 tikzmark
64 # newpx font family + its non-propagated deps
65 newpx newtx kastrup fontaxes mweights
66 # Loaded directly per the 22716 log
67 amscls amsfonts amsmath
68 etoolbox xpatch xstring xkeyval
69 carlisle l3packages
70 graphics-def
71 # iter 96 — Latin Modern + CM-Super + math-friendly fonts so
72 # dvisvgm's kpathsea finds OTF/PFB for native font references
73 # tectonic emits in XDV. (iter 95 hit 'l3keys2e missing' in
74 # nixpkgs; that pkg is part of l3packages already which we
75 # have. Drop bare-l3keys2e + any other un-attribute'd names
76 # if they show up.)
77 lm lm-math cm-super pxfonts mathpazo psnfss
78 # Algorithm packages — used by ag-0018, uts-001F, uts-0018.
79 # `algpseudocodex` may not be a top-level attr in older
80 # nixpkgs (was added 2023); keep as best-effort and drop if
81 # build-depot errors on it.
82 algorithmicx pseudo tcolorbox
83 # forester package dependencies
84 todonotes utfsym
85 # newpx auxiliary fonts (tx fonts for math chains)
86 pgfplots tipa physics;
87 };
88 in pkgs.stdenv.mkDerivation {
89 pname = "forest-texlive";
90 version = "${texliveVersion}+standalone+tikz-cd";
91 dontUnpack = true;
92 dontConfigure = true;
93 nativeBuildInputs = [ texCombo ];
94 buildPhase = ''
95 runHook preBuild
96 ${texCombo}/bin/latex --version | head -1
97 ${texCombo}/bin/kpsewhich standalone.cls
98 runHook postBuild
99 '';
100 installPhase = ''
101 runHook preInstall
102 mkdir -p $out
103 ln -s ${texCombo}/bin $out/bin
104 ln -s ${texCombo}/share $out/share || true
105 printf '%s\n' \
106 'export PATH="'$out'/bin:''${PATH:-}"' \
107 > $out/env.sh
108 runHook postInstall
109 '';
110 meta.description = "texlive scheme-small + standalone/tikz-cd/xy combo for forester";
111 };
112
113 # Build tectonic from master @ 2025-10-06 (which supports the .ttb
114 # bundle format from doronbehar's nix overlay in tectonic#1269 comment
115 # 3551606513). nixpkgs's tectonic 0.16.9 is too old for the TL2024
116 # bundle; we need post-2025-10-06.
117 tectonicUnstableFor = pkgs: pkgs.tectonic-unwrapped.overrideAttrs (final: prev: {
118 version = "0.15.0-unstable-2025-10-06";
119 src = pkgs.fetchFromGitHub {
120 owner = "tectonic-typesetting";
121 repo = "tectonic";
122 rev = "d7f3275adf6b501fc21122a1873912e970bf52ba";
123 hash = "sha256-4p5ZU1O75xcE4pDUs1xwZkFkxJ+g3Rm9LL5Cog96Sm8=";
124 };
125 patches = [];
126 cargoPatches = [];
127 cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
128 inherit (final) src;
129 hash = "sha256-J3q0dtQ/qb/72b6A40TNYDIZvDrUSpxF3SFjhc+X0fw=";
130 };
131 # Skip the sandbox-incompatible segfault test.
132 checkFlags = (prev.checkFlags or []) ++ [
133 "--skip=tests::no_segfault_after_failed_compilation"
134 ];
135 });
136
137 tectonicBundleUrl = "https://bebopbamf-tex.syd1.cdn.digitaloceanspaces.com/texlive2024-0312.ttb";
138
139 # dvisvgm 3.6 (Dec 2025) — fresh from upstream. nixpkgs's texlive bundles
140 # an older dvisvgm that crashes with `basic_string::substr` on some
141 # forest trees. Build standalone via autotools; we put the texlive
142 # combine's texmf-dist next to the binary so kpathsea finds .pro files.
143 dvisvgmNewerFor = pkgs: pkgs.stdenv.mkDerivation rec {
144 pname = "dvisvgm";
145 version = "3.6";
146 src = pkgs.fetchurl {
147 url = "https://github.com/mgieseki/dvisvgm/releases/download/${version}/dvisvgm-${version}.tar.gz";
148 hash = "sha256-JkRrs7EHOf8JJcnkFrdtLSIgdcnV3Pr+biFGCdBy7Ro=";
149 };
150 nativeBuildInputs = with pkgs; [ pkg-config ];
151 buildInputs = with pkgs; [
152 freetype ghostscript brotli woff2 zlib
153 # libkpathsea ships in texlive.bin.core (no top-level pkgs.kpathsea
154 # in this nixpkgs revision).
155 texlive.bin.core
156 ];
157 # iter 73 — patch TWO upstream bugs that together break XDV font
158 # fallback for tectonic-produced output (ag-0018 / uts-001F /
159 # ca-000J tikz-cd):
160 #
161 # (1) FontManager.cpp:313 — `*it` returns the format-extension
162 # literal "otf"/"ttf" from the format-array, NOT the resolved
163 # font path. NativeFontImpl ends up with path="otf".
164 # (2) FontEngine.cpp:93 — setFont's sys:// gate uses `||` where
165 # it should be `&&`; lets any name with size <= 6 (including
166 # literal "otf") fall into substr(6) which throws on size<6.
167 #
168 # iter 70 patched (2) alone via substituteInPlace (silently failed,
169 # likely nix --replace deprecation). iter 71 used inline sed which
170 # ran but unmasked (1) as a "can't read font file otf" error since
171 # the literal "otf" path was still being set up by FontManager.
172 # iter 72 moved to a proper patch file (operator request). iter 73
173 # extends that patch to fix (1) as well — same file, just adds the
174 # FontManager hunk so the fallback re-resolves the real path.
175 patches = [ ./nix-patches/dvisvgm-fontengine-substr-fix.patch ];
176 # autotools default — configure + make + make install.
177 };
178
179 forestTectonicFor = pkgs: pkgs.stdenv.mkDerivation {
180 pname = "forest-tectonic";
181 # Includes dvisvgm now too — nixery rejects the standalone dvisvgm
182 # pkg name; shipping it alongside tectonic in the same NAR avoids
183 # the manifest miss.
184 version = "${tectonicVersion}+dvisvgm+ttb2024";
185
186 # No source — we build off the master tectonic plus a tiny
187 # warmup .tex file that pulls every package the real forest build
188 # is expected to need.
189 dontUnpack = true;
190 dontConfigure = true;
191
192 nativeBuildInputs = [
193 (tectonicUnstableFor pkgs) pkgs.cacert
194 # Custom dvisvgm 3.6 (newer than nixpkgs's bundled one) to avoid
195 # the basic_string::substr crash on some forest documents.
196 (dvisvgmNewerFor pkgs)
197 # texlive runtime tree for kpathsea — provides texmf.cnf + the
198 # dvips PostScript headers (tex.pro / texps.pro / special.pro
199 # / color.pro). We don't use the bundled dvisvgm from this combo.
200 (pkgs.texlive.combine { inherit (pkgs.texlive) scheme-small dvips graphics; })
201 pkgs.makeWrapper
202 ];
203
204 buildPhase = ''
205 runHook preBuild
206 export HOME=$TMPDIR
207 export TECTONIC_CACHE_DIR=$HOME/.cache/Tectonic
208 mkdir -p "$TECTONIC_CACHE_DIR"
209 # Warmup: representative doc that loads the heavy packages forest
210 # uses. Add \usepackage{...} lines as forest LaTeX surfaces more.
211 cat > warmup.tex <<'WARMUP'
212 \documentclass[a4paper]{article}
213 \usepackage{amsmath, amssymb, amsthm}
214 \usepackage{xcolor}
215 \usepackage{tikz}
216 \usepackage{hyperref}
217 \usepackage{listings}
218 \begin{document}
219 Hello world.
220 \end{document}
221 WARMUP
222 # Pin to the BebopBamf TL2024 .ttb bundle (tectonic#1269 community
223 # workaround — official bundles stopped at TL2022). Newer
224 # pseudo.sty in TL2024 auto-registers /tcb/pseudo/* keys forest
225 # expects, so the runtime CTAN override becomes redundant.
226 tectonic --bundle ${tectonicBundleUrl} --outdir . warmup.tex || true
227 ls -la "$TECTONIC_CACHE_DIR" || true
228 runHook postBuild
229 '';
230
231 installPhase = ''
232 runHook preInstall
233 mkdir -p $out/bin $out/share
234 ln -s ${tectonicUnstableFor pkgs}/bin/tectonic $out/bin/tectonic
235 # dvisvgm via texlive.combine. kpathsea uses dvisvgm's argv0
236 # to locate texmf-dist/dvips/base/*.pro etc. A plain symlink
237 # would leave argv0 pointing at $out/bin/dvisvgm (and kpathsea
238 # would search $out/share/texmf-dist — empty). Instead write a
239 # tiny wrapper that exec's the full-path binary under the
240 # combine's bindir; kpathsea then finds the .pro headers.
241 # Newer dvisvgm 3.6 from source. kpathsea uses argv[0] to compute
242 # SELFAUTOLOC. A plain symlink lets the kernel resolve it BEFORE
243 # exec, so argv[0] = the dvisvgm-3.6 store path → kpathsea looks
244 # at dvisvgm-3.6/share/... (empty). Instead write a shell wrapper
245 # using bash's `exec -a` to FORCE argv[0] = $out/bin/dvisvgm, so
246 # kpathsea uses $out/share/texmf-dist (symlinked below).
247 dvisvgm_bin=${dvisvgmNewerFor pkgs}/bin/dvisvgm
248 texlive_drv=${pkgs.texlive.combine { inherit (pkgs.texlive) scheme-small dvips graphics; }}
249 test -x "$dvisvgm_bin"
250 # Stage the runtime tree at $out/share/<...> so kpathsea finds it
251 # via $out/bin/dvisvgm's SELFAUTOLOC = $out/bin → ../share/...
252 for d in texmf-dist texmf-config texmf-var web2c; do
253 if [ -d "$texlive_drv/share/$d" ]; then
254 ln -s "$texlive_drv/share/$d" "$out/share/$d"
255 fi
256 done
257 # Locate ALL texmf.cnf files. texlive.combine writes a config
258 # override at share/texmf-config/web2c/texmf.cnf, while the main
259 # one (with PSHEADERS / DVIPSHEADERS / TEXMF definitions) lives at
260 # share/texmf-dist/web2c/texmf.cnf. kpathsea needs BOTH dirs in
261 # TEXMFCNF or it only reads the override and misses the search-
262 # path defaults → tex.pro / texps.pro / special.pro / color.pro
263 # show up as "not found".
264 texmfcnf_dirs=""
265 while IFS= read -r f; do
266 d=$(dirname "$f")
267 case ":$texmfcnf_dirs:" in
268 *":$d:"*) ;;
269 *) texmfcnf_dirs="''${texmfcnf_dirs:+$texmfcnf_dirs:}$d" ;;
270 esac
271 done < <(find "$texlive_drv/share" -name texmf.cnf -type f 2>/dev/null)
272 if [ -z "$texmfcnf_dirs" ]; then
273 echo "ERROR: texmf.cnf not found in $texlive_drv/share" >&2
274 find "$texlive_drv/share" -maxdepth 4 -type d >&2
275 exit 1
276 fi
277 echo "TEXMFCNF dirs: $texmfcnf_dirs"
278 # PostScript headers (tex.pro, texps.pro, special.pro, color.pro,
279 # finclude.pro) from the dvips package. Iter 37's symlink approach
280 # to share/texmf-dist didn't reliably surface dvips/base in the
281 # rendered closure (render.yml's `find` returned empty). Just COPY
282 # them into $out/share/dvips/base — known fixed path, no symlink
283 # traversal mysteries, and they ride with the forest-tectonic NAR
284 # closure independently of where texlive.combine stashes them.
285 mkdir -p $out/share/dvips/base
286 echo "Staging dvips .pro files at $out/share/dvips/base:"
287 # texlive.combine output is a tree of symlinks into per-package
288 # derivations. Plain `find` doesn't traverse them — use -L to
289 # follow symlinks so .pro files inside the dvips pkg are found.
290 for pro in tex.pro texps.pro special.pro color.pro finclude.pro; do
291 found=$(find -L "$texlive_drv" -name "$pro" -type f 2>/dev/null | head -1)
292 if [ -n "$found" ]; then
293 cp -L "$found" $out/share/dvips/base/
294 echo " ✓ $pro from $found"
295 else
296 echo " ✗ $pro MISSING in $texlive_drv"
297 fi
298 done
299 echo "Staged contents:"
300 ls -la $out/share/dvips/base/ 2>&1 || true
301 actual_dvips_dir="$out/share/dvips"
302 # Wrapper preserves argv[0] = $out/bin/dvisvgm so kpathsea
303 # SELFAUTOLOC = $out/bin. Plain `exec` (no -a) would let the
304 # kernel pass the dvisvgm-3.6 path as argv[0].
305 # TEXMF is the umbrella search-cascade — set it AND TEXMFDIST so
306 # texmf.cnf's $TEXMF/dvips/base lookup resolves to the combine.
307 cat > $out/bin/dvisvgm <<EOF
308 #!${pkgs.bash}/bin/bash
309 export TEXMFCNF="$texmfcnf_dirs"
310 export TEXMFDIST="$texlive_drv/share/texmf-dist"
311 export TEXMFROOT="$texlive_drv/share"
312 export TEXMF="$texlive_drv/share/texmf-dist"
313 export PSHEADERS=".:$actual_dvips_dir/base:$actual_dvips_dir/config:$actual_dvips_dir"
314 export TEXPSHEADERS="\$PSHEADERS"
315 export DVIPSHEADERS="\$PSHEADERS"
316 if [ -n "''${FOREST_KPSEDEBUG:-}" ]; then
317 export KPATHSEA_DEBUG=64
318 fi
319 exec -a "\$0" "$dvisvgm_bin" "\$@"
320 EOF
321 chmod +x $out/bin/dvisvgm
322 # snapshot the warmed cache under $out/share so render workflow
323 # can `cp -r $out/share/tectonic-cache ~/.cache/Tectonic`.
324 if [ -d "$TECTONIC_CACHE_DIR" ]; then
325 cp -r "$TECTONIC_CACHE_DIR" $out/share/tectonic-cache
326 fi
327 # env.sh — source this on Spindle to wire HOME + cache + PATH
328 cat > $out/env.sh <<EOSH
329 export PATH="$out/bin:''${PATH:-}"
330 export TECTONIC_CACHE_DIR="''${HOME:-/tmp}/.cache/Tectonic"
331 mkdir -p "''${TECTONIC_CACHE_DIR}"
332 if [ -d "$out/share/tectonic-cache" ]; then
333 cp -rn "$out/share/tectonic-cache/." "''${TECTONIC_CACHE_DIR}/" 2>/dev/null || true
334 fi
335 EOSH
336 runHook postInstall
337 '';
338
339 meta.description = "tectonic LaTeX engine + pre-warmed CTAN cache for forest";
340 };
341
342 # ---- forest-forester ---------------------------------------------------
343 # Built from sr.ht commit 5ab7277 via opam (same pin as gh-pages.yml).
344 # The closure (opam switch + forester binary) is captured under $out
345 # so the render workflow can nix-store --import and put forester on PATH.
346 #
347 # NB: opam needs network at build time → sandbox=false on the depot.
348 forestForesterFor = pkgs: pkgs.stdenv.mkDerivation {
349 pname = "forest-forester";
350 version = foresterVersion;
351
352 dontUnpack = true;
353 dontConfigure = true;
354
355 nativeBuildInputs = with pkgs; [
356 opam ocaml dune_3 gcc gnumake pkg-config
357 libev cacert openssl gmp
358 git curl
359 ];
360 buildInputs = with pkgs; [ libev openssl gmp ];
361
362 buildPhase = ''
363 runHook preBuild
364 export HOME=$TMPDIR
365 export OPAMROOT=$HOME/.opam
366 export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
367 # OPAMNODEPEXTS=1 stops opam from calling apt-cache / brew (it doesn't
368 # have them in a nix sandbox; depexts are satisfied via nativeBuildInputs).
369 # OPAMYES + assume-yes avoids interactive prompts on init failure.
370 export OPAMNODEPEXTS=1 OPAMYES=1 OPAMNOSANDBOXING=1
371 opam init --bare --no-setup --disable-sandboxing -y
372 # Use ocaml-system (the nixpkgs ocaml we provide) so opam doesn't try
373 # to compile ocaml-base-compiler from source (which then tries apt-cache
374 # for system deps and fails on nixos containers).
375 opam switch create forester --empty -y
376 eval "$(opam env --switch=forester)"
377 opam install ocaml-system -y
378 opam pin add forester 'git+https://git.sr.ht/~jonsterling/ocaml-forester#5ab7277' --no-action -y
379 opam install forester -y
380 # nix stdenv doesn't ship `which`; check via command -v instead.
381 command -v forester || { echo "::error::forester binary missing after opam install"; exit 1; }
382 forester --version || true
383 runHook postBuild
384 '';
385
386 installPhase = ''
387 runHook preInstall
388 mkdir -p $out/bin $out/share/opam
389 # Capture the forester binary directly.
390 cp "$OPAMROOT/forester/bin/forester" $out/bin/forester
391 # Also capture the opam switch directory so any forester runtime
392 # data (xsl templates etc.) is preserved.
393 cp -r "$OPAMROOT/forester/share" $out/share/opam/ || true
394 cat > $out/env.sh <<EOSH
395 export PATH="$out/bin:''${PATH:-}"
396 EOSH
397 runHook postInstall
398 '';
399
400 meta.description = "forester (Jon Sterling, sr.ht 5ab7277) — opam-built closure";
401 };
402
403 # ---- forest-wasm-pkgs --------------------------------------------------
404 # Clones each upstream Rust→WASM crate at the pinned hash from build.sh,
405 # runs wasm-pack build --target web --release, captures ONLY the pkg/
406 # outputs (NOT the Rust toolchain, NOT the target/ dir). This is the
407 # operator's "ship only the WASM, not the Rust" optimization — pkg/
408 # dirs are 10s-100s of KB each.
409 forestWasmPkgsFor = pkgs:
410 let
411 # `path` mirrors build.sh's `lib_path` arg — relative to the work
412 # dir (where we clone the repo), points at the directory wasm-pack
413 # runs in. For wgputoy/rhaiscript the cloned repo IS the wasm-pack
414 # crate (path = name). For egglog the wasm-pack crate is a subdir
415 # of the clone (path = "egglog/web-demo").
416 pins = [
417 { name = "wgputoy"; url = "https://github.com/compute-toys/wgpu-compute-toy.git";
418 hash = "60d0bec4bd912a54d5049f2c28c1bd6a0916e5ec";
419 path = "wgputoy"; }
420 { name = "egglog"; url = "https://github.com/egraphs-good/egglog.git";
421 hash = "8d9b10ec712106b21d10b7bf45d10c0f9d1d09c7";
422 path = "egglog/web-demo"; }
423 { name = "rhaiscript"; url = "https://github.com/rhaiscript/playground";
424 hash = "9fa80661bc9eb69363ac86879826dcd8ccb604af";
425 path = "rhaiscript"; }
426 ];
427 in pkgs.stdenv.mkDerivation {
428 pname = "forest-wasm-pkgs";
429 version = wasmPkgsVersion;
430
431 dontUnpack = true;
432 dontConfigure = true;
433
434 nativeBuildInputs = with pkgs; [
435 rustc cargo wasm-pack bun nodejs binaryen git cacert
436 patch # for nix/patches/wgputoy-surface-error-other.patch
437 # wgputoy's .cargo/config.toml asks for lld as the linker.
438 lld
439 ];
440
441 buildPhase = ''
442 runHook preBuild
443 export HOME=$TMPDIR
444 export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt
445 mkdir -p $TMPDIR/work
446 cd $TMPDIR/work
447 ${pkgs.lib.concatMapStrings (p: ''
448 echo "=== building ${p.name} ==="
449 git clone --depth 1 "${p.url}" "${p.name}"
450 # git fetch/checkout target the cloned repo (egglog/, wgputoy/, …).
451 ( cd "${p.name}"
452 git fetch --depth 1 origin "${p.hash}"
453 git checkout "${p.hash}"
454 )
455 # wasm-pack runs in ${p.path} — for egglog the wasm-pack crate
456 # lives at egglog/web-demo (a subdir of the clone); for the
457 # others, path == name.
458 ( cd "${p.path}"
459 if [ "${p.name}" = "wgputoy" ]; then
460 # Two diff/context patches against upstream src/lib.rs +
461 # Cargo.toml — `patch -p1` fails loudly if upstream drifts,
462 # vs a bare sed that would silently move. Tracked in
463 # utensil/forest#54 (item 1) + #55 (wasm-opt strategy).
464 patch -p1 --no-backup-if-mismatch < ${./nix/patches/wgputoy-surface-error-other.patch}
465 test "$(grep -c 'SurfaceError::Other' src/lib.rs)" -eq 2 \
466 || { echo "::error::wgputoy patch produced unexpected arm count"; exit 1; }
467 patch -p1 --no-backup-if-mismatch < ${./nix/patches/wgputoy-wasm-opt-bulk-memory.patch}
468 fi
469 wasm-pack build --target web --release --out-dir pkg . -- --locked
470 ls -la pkg
471 )
472 '') pins}
473 runHook postBuild
474 '';
475
476 installPhase = ''
477 runHook preInstall
478 mkdir -p $out/pkg
479 ${pkgs.lib.concatMapStrings (p: ''
480 mkdir -p $out/pkg/${p.name}
481 cp -r $TMPDIR/work/${p.path}/pkg/. $out/pkg/${p.name}/
482 '') pins}
483 ls -la $out/pkg
484 runHook postInstall
485 '';
486
487 meta.description = "Pre-built WASM pkgs for forest (wgputoy + egglog + rhaiscript)";
488 };
489
490 in {
491 packages = forAll (system:
492 let pkgs = pkgsFor system; in {
493 forest-tectonic = forestTectonicFor pkgs;
494 forest-forester = forestForesterFor pkgs;
495 forest-wasm-pkgs = forestWasmPkgsFor pkgs;
496 # iter 95 — RE-ENABLED per operator directive after 30+ iters of
497 # tectonic-only experiments couldn't get dvisvgm to load LM OTF
498 # fonts that tectonic's XDV emits as native font_def names
499 # (lmroman10-italic, lmromancaps10-regular, etc.). texlive.combine
500 # produces a real TEXMF tree with ls-R, which dvisvgm's kpathsea
501 # actually honors at runtime — should restore the cmcsc10/cmti10
502 # rendering and close the visible-overlap gap on uts-001F's pseudo
503 # block + ag-0018.
504 forest-texlive = forestTexliveFor pkgs;
505 }
506 );
507
508 # `nix develop` shells the toolchain — useful for local sanity checks.
509 devShells = forAll (system:
510 let pkgs = pkgsFor system; in {
511 default = pkgs.mkShell {
512 packages = [
513 pkgs.tectonic pkgs.opam pkgs.bun pkgs.just pkgs.biome
514 pkgs.uv pkgs.xsltproc pkgs.nodejs pkgs.wasm-pack
515 ];
516 };
517 }
518 );
519 };
520}