| Inline the babashka scripts into the justfile 76dcc6c nandi 11d ago | 1 | # The work is here now, in recipe bodies, where it used to be in scripts/ — |
| 2 | # babashka scripts run through a scripts/bb that went looking for a babashka. |
| 3 | # That indirection bought one thing worth having, a shared way to reach nix on |
| 4 | # a host that keeps it in a container, and `nix` below is the whole of it. |
| 5 | # |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 6 | # Every recipe that builds has the same shape: outside the dev shell, re-enter |
| 7 | # it and come back to this same recipe; inside, do the work. The re-entry test |
| 8 | # is an environment variable only the shell sets — no flag to forget, and no |
| 9 | # second code path for someone who runs `nix develop --command just ...` by |
| 10 | # hand. |
| Take glimmer-vidya from gitlab, so a stranger can build this 5c40e75 nandi 19d ago | 11 | |
| Write the build in babashka, and pin the babashka 34832a8 nandi 18d ago | 12 | set shell := ["bash", "-euo", "pipefail", "-c"] |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 13 | |
| Give the terminal's classpath an nREPL, and hand recipes their arguments 664397d nandi 10d ago | 14 | # Every recipe below is a `#!` script and passes its arguments on with "$@". |
| 15 | # Without this that is empty in one — just interpolates into a shebang recipe |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 16 | # rather than handing it argv — and a recipe silently ignored its flags. |
| Give the terminal's classpath an nREPL, and hand recipes their arguments 664397d nandi 10d ago | 17 | set positional-arguments |
| 18 | |
| Inline the babashka scripts into the justfile 76dcc6c nandi 11d ago | 19 | # nix is not on every host this runs on: on the machine these recipes were |
| 20 | # written for it lives in an Arch distrobox, at the same path — which is why |
| 21 | # the container is entered rather than the tree copied into it. See CLAUDE.md. |
| 22 | nix := `command -v nix >/dev/null 2>&1 && echo nix || echo "distrobox enter arch -- nix"` |
| 23 | |
| 24 | # --max-jobs 0 is what sends the work to the `builders` entry rather than |
| 25 | # compiling it here. Left to the default, nix prefers the local machine, and a |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 26 | # cold Flutter toolchain is a lot of compiling on a laptop — for a derivation a |
| Inline the babashka scripts into the justfile 76dcc6c nandi 11d ago | 27 | # remote builder has likely built already. FRQ_MAX_JOBS=auto is the way out on |
| 28 | # a machine with no builder configured. |
| 29 | jobs := env("FRQ_MAX_JOBS", "0") |
| 30 | |
| A freeq client in jolt, as glimmer components on Vidya 4719d6f nandi 20d ago | 31 | default: |
| 32 | @just --list |
| 33 | |
| Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago | 34 | # The APK: ClojureDart compiled to Dart, then Flutter's Gradle build. |
| 35 | # |
| 36 | # Impure on purpose, and worth saying why rather than leaving it to be |
| 37 | # discovered. Gradle resolves its own dependencies over the network and |
| 38 | # installs build-tools and a platform into ANDROID_HOME as it goes, so it |
| 39 | # cannot run in a sandbox and cannot write to the store. What nix gives here |
| 40 | # is the toolchain — clojure, a JDK, Flutter, and an SDK composed by |
| 41 | # androidenv — and the recipe copies that SDK somewhere writable |
| 42 | # (flutter/.home) for Gradle to finish off. That copy and everything Gradle |
| 43 | # leaves behind are gitignored. |
| 44 | # |
| 45 | # No ndkVersion in android/app/build.gradle.kts, for the same reason: the |
| 46 | # Flutter template sets it, setting it makes Gradle fetch that exact NDK, and |
| 47 | # there is no native code here to need one. |
| 48 | # |
| 49 | # just apk build the debug APK |
| 50 | # just apk install build it and put it on a connected device |
| 51 | # just apk run install and launch |
| 52 | # just apk log logcat, filtered to this app |
| 53 | apk action="build": |
| 54 | #!/usr/bin/env bash |
| 55 | set -euo pipefail |
| 56 | cd "{{justfile_directory()}}/flutter" |
| 57 | |
| Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 7d ago | 58 | # The flake's, not an --impure --expr against whatever nixos-unstable is |
| 59 | # today: the licence config the SDK needs lives in `androidPkgsFor` now, |
| 60 | # so this is an ordinary output at the rev flake.lock pins. |
| 61 | sdk="$(nix build --no-link --print-out-paths \ |
| 62 | "{{justfile_directory()}}#android-sdk")/libexec/android-sdk" |
| Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago | 63 | |
| Let adb keep its identity when the APK recipe moves HOME bc0e14f nandi 7d ago | 64 | # adb keeps the key the phone has already trusted under the real HOME, and |
| 65 | # HOME moves below so Gradle can write into the SDK copy. Told where to |
| 66 | # look, adb keeps its identity; left to find $HOME/.android it generates a |
| 67 | # new one, the device stops recognising this machine, and the deploy ends |
| 68 | # in "no devices/emulators found" while `adb devices` in any other shell |
| 69 | # lists it perfectly well. |
| 70 | export ANDROID_USER_HOME="${ANDROID_USER_HOME:-$HOME/.android}" |
| 71 | |
| Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago | 72 | export HOME="$PWD/.home" |
| 73 | export ANDROID_HOME="$HOME/android-sdk" |
| 74 | export ANDROID_SDK_ROOT="$ANDROID_HOME" |
| 75 | mkdir -p "$HOME" |
| 76 | |
| 77 | # Gradle writes into ANDROID_HOME, so it is a copy rather than the store |
| 78 | # path. Made once and kept: re-copying would throw away the build-tools |
| 79 | # and platform Gradle installed into it on the last run. |
| 80 | if [ ! -d "$ANDROID_HOME" ]; then |
| 81 | cp -r "$sdk" "$ANDROID_HOME" |
| 82 | chmod -R u+w "$ANDROID_HOME" |
| 83 | fi |
| 84 | |
| Seed the ClojureDart caches out of the flake f2e12af nandi 7d ago | 85 | # The compile's three caches, same flake-output-and-copy shape as the SDK |
| 86 | # and for the same reason: tools.deps and pub both write into theirs, and |
| 87 | # the store is read-only. What this buys is the "Resolving dependencies… |
| 88 | # Downloading packages…" that used to open every run. |
| 89 | deps="$(nix build --no-link --print-out-paths \ |
| 90 | "{{justfile_directory()}}#cljd-deps")" |
| 91 | |
| 92 | # Neither of these follows HOME. Both are read off the JVM's user.home, |
| 93 | # which comes from /etc/passwd rather than the environment — so moving |
| 94 | # HOME below is not enough to move them, and left alone they would be the |
| 95 | # real ~/.m2 and ~/.gitlibs, shared with every other project on the box. |
| 96 | export GITLIBS="$HOME/gitlibs" |
| 97 | m2="$HOME/m2" |
| 98 | export PUB_CACHE="$HOME/.pub-cache" |
| 99 | |
| 100 | # Seeded once each and then left alone, exactly as ANDROID_HOME is: after |
| 101 | # the first compile these hold whatever the working tree has asked for |
| 102 | # since, and re-copying would throw that away. |
| 103 | seed() { |
| 104 | [ -e "$2" ] && return 0 |
| 105 | mkdir -p "$(dirname "$2")" |
| 106 | cp -r "$deps/$1" "$2" |
| 107 | chmod -R u+w "$2" |
| 108 | } |
| 109 | seed m2 "$m2" |
| 110 | seed gitlibs "$GITLIBS" |
| 111 | seed pub-cache "$PUB_CACHE" |
| 112 | seed clojuredart/cache "$PWD/.clojuredart/cache" |
| 113 | |
| Build the APK from the flake, at the rev flake.lock pins 77d4bec nandi 7d ago | 114 | # Also the flake's. `nix shell nixpkgs#...` read the registry, which is a |
| 115 | # different and unlocked nixpkgs — the Flutter that built the APK could |
| 116 | # move under it without flake.lock changing a line. |
| 117 | flutter="nix develop {{justfile_directory()}}#flutter --command" |
| Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago | 118 | |
| Seed the ClojureDart caches out of the flake f2e12af nandi 7d ago | 119 | # cljd-deps ships the analyzer project unresolved — a fixed-output |
| 120 | # derivation may not name the store, and a resolved pub project is |
| 121 | # nothing but store paths. So it is resolved here instead, offline, |
| 122 | # against the cache that derivation did fetch. ClojureDart reaches for the |
| 123 | # network only when bin/analyzer.dart is missing, and after this it is not. |
| 124 | for helper in .clojuredart/cache/*/cljd_helper; do |
| 125 | [ -d "$helper" ] || continue |
| 126 | [ -e "$helper/.dart_tool/package_config.json" ] && continue |
| 127 | ( cd "$helper" && $flutter flutter pub get --offline ) |
| 128 | done |
| 129 | |
| 130 | $flutter clojure -Sdeps "{:mvn/local-repo \"$m2\"}" -M:cljd compile |
| Build the APK, out of ClojureDart and Flutter e2b0e6b nandi 7d ago | 131 | |
| 132 | # Rewritten every run: it carries absolute store paths, and the flutter |
| 133 | # one moves whenever nixpkgs does. |
| 134 | $flutter flutter config --android-sdk "$ANDROID_HOME" >/dev/null |
| 135 | |
| 136 | apk=build/app/outputs/flutter-apk/app-debug.apk |
| 137 | adb="${ADB:-$ANDROID_HOME/platform-tools/adb}" |
| 138 | |
| 139 | case "{{action}}" in |
| 140 | build) $flutter flutter build apk --debug ;; |
| 141 | install) $flutter flutter build apk --debug && "$adb" install -r "$apk" ;; |
| 142 | run) $flutter flutter build apk --debug && "$adb" install -r "$apk" \ |
| 143 | && "$adb" shell monkey -p uk.nandi.frq -c android.intent.category.LAUNCHER 1 ;; |
| 144 | log) "$adb" logcat -s flutter ;; |
| 145 | *) echo "usage: just apk [build|install|run|log]" >&2; exit 1 ;; |
| 146 | esac |
| 147 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 148 | # What may appear in common/, checked. Needs nothing built: it reads the |
| 149 | # source, so it is the one check that runs anywhere, and CI runs exactly this. |
| Check what common/ may contain, on every push 86c2e1b nandi 7d ago | 150 | check-common: |
| 151 | #!/usr/bin/env bash |
| 152 | python3 tools/check-common.py common |
| 153 | |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 154 | # The desktop GUI: the same screens the APK paints, on Flutter's Linux target. |
| Give the terminal's classpath an nREPL, and hand recipes their arguments 664397d nandi 10d ago | 155 | # |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 156 | # This recipe and `just apk` are two targets over one tree, and the split is |
| 157 | # the one the APK already draws. Everything under `common/` — the screens, the |
| 158 | # cells, `frq.io` — is shared; what differs is who answers the host. So this |
| 159 | # is `just apk` with the Android half taken out: the same `clojure -M:cljd compile` over the same flutter/src, |
| Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago | 160 | # then Flutter's Linux target rather than its Android one. CMake and Ninja |
| 161 | # instead of Gradle, `flutter/linux/` as the runner, no SDK and no JDK. |
| 162 | # |
| 163 | # Still impure, for one of the two reasons `apk` is: pub.dev resolution and |
| 164 | # Flutter's own engine artifacts are network. What it does NOT need is the |
| 165 | # writable-ANDROID_HOME dance — nothing here writes into the store — so there |
| 166 | # is no `flutter/.home` on this path. |
| 167 | # |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 168 | # nixGL because Flutter paints through GL, and off NixOS the driver is the |
| 169 | # host's. |
| Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago | 170 | # |
| 171 | # just flutter-desktop build the debug bundle |
| 172 | # just flutter-desktop run build it and open the window |
| 173 | flutter-desktop action="build": |
| 174 | #!/usr/bin/env bash |
| 175 | set -euo pipefail |
| 176 | cd "{{justfile_directory()}}" |
| 177 | if [ -z "${FRQ_FLUTTER_DESKTOP:-}" ]; then |
| 178 | exec {{nix}} develop .#flutter-desktop --max-jobs {{jobs}} \ |
| 179 | --command just flutter-desktop "$@" |
| 180 | fi |
| 181 | cd flutter |
| 182 | |
| Seed the ClojureDart caches out of the flake f2e12af nandi 7d ago | 183 | # The same three caches `just apk` seeds, in the same place and out of the |
| 184 | # same flake output — one compiler, one set of dependencies, and no reason |
| 185 | # for the two frontends to keep a copy each. `.home/` is `apk`'s directory |
| 186 | # by name and this is the only thing put there from here, which is the |
| 187 | # point: whichever recipe runs first pays for the copy and the other finds |
| 188 | # it warm. |
| 189 | # |
| 190 | # No `nix build` here, unlike `apk`: this recipe is already inside the |
| 191 | # shell that names FRQ_CLJD_DEPS by the time it gets this far, and `apk` |
| 192 | # needs the path before it enters anything. |
| 193 | # |
| 194 | # m2 and gitlibs are set here for the reason they are set there — the JVM |
| 195 | # reads user.home out of /etc/passwd, so neither follows HOME and left |
| 196 | # alone they are the real ~/.m2 and ~/.gitlibs. |
| 197 | export PUB_CACHE="$PWD/.home/.pub-cache" |
| 198 | export GITLIBS="$PWD/.home/gitlibs" |
| 199 | m2="$PWD/.home/m2" |
| 200 | |
| 201 | seed() { |
| 202 | [ -e "$2" ] && return 0 |
| 203 | mkdir -p "$(dirname "$2")" |
| 204 | cp -r "$FRQ_CLJD_DEPS/$1" "$2" |
| 205 | chmod -R u+w "$2" |
| 206 | } |
| 207 | seed m2 "$m2" |
| 208 | seed gitlibs "$GITLIBS" |
| 209 | seed pub-cache "$PUB_CACHE" |
| 210 | seed clojuredart/cache "$PWD/.clojuredart/cache" |
| 211 | |
| 212 | # Resolved here rather than in cljd-deps, which was not allowed to name |
| 213 | # the store — see the same loop in `apk`. |
| 214 | for helper in .clojuredart/cache/*/cljd_helper; do |
| 215 | [ -d "$helper" ] || continue |
| 216 | [ -e "$helper/.dart_tool/package_config.json" ] && continue |
| 217 | ( cd "$helper" && flutter pub get --offline ) |
| 218 | done |
| 219 | |
| 220 | clojure -Sdeps "{:mvn/local-repo \"$m2\"}" -M:cljd compile |
| Paint the desktop with Flutter too, beside libcosmic fc5f63c nandi 7d ago | 221 | flutter build linux --debug |
| 222 | |
| 223 | # x64/arm64 is Flutter's own name for the host arch, not uname's. |
| 224 | case "$(uname -m)" in |
| 225 | x86_64) arch=x64 ;; |
| 226 | aarch64) arch=arm64 ;; |
| 227 | *) echo "unknown arch $(uname -m)" >&2; exit 1 ;; |
| 228 | esac |
| 229 | bundle="build/linux/$arch/debug/bundle" |
| 230 | |
| 231 | case "{{action}}" in |
| 232 | build) echo "built $PWD/$bundle/frq" ;; |
| 233 | run) |
| 234 | runner=() |
| 235 | [ -e /run/current-system ] || runner=("$NIXGL") |
| 236 | exec "${runner[@]}" "$bundle/frq" |
| 237 | ;; |
| 238 | *) echo "usage: just flutter-desktop [build|run]" >&2; exit 1 ;; |
| 239 | esac |
| A shell that is already the devShell, and two recipes to reach it e853593 nandi 5d ago | 240 | |
| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 241 | # The third frontend: the same screens again, compiled to JavaScript. |
| 242 | # |
| 243 | # `flutter-desktop` with the Linux half taken out. One `clojure -M:cljd |
| 244 | # compile` over the same flutter/src and common/, then Flutter's web target |
| 245 | # instead of its Linux one — dart2js instead of CMake and Ninja, and a |
| 246 | # directory of static files instead of a bundle with an executable in it. |
| 247 | # |
| Three tarballs where a devShell was 5ce66d5 nandi yesterday | 248 | # And the one target with no nix in it. The other two need the host: a JDK |
| 249 | # and the Android SDK for `apk`, GTK and a C++ toolchain and nixGL for |
| 250 | # `flutter-desktop`. This one needs a Dart, a JVM and a browser, and the |
| 251 | # browser is not ours — so `tools/toolchain.sh` fetches the first two as |
| 252 | # pinned tarballs into `.toolchain/` and there is nothing left for a devShell |
| 253 | # to supply. That is what lets the container in `.modal/flutter-web/` drop |
| 254 | # its image build too: same script, same three pins, no store to populate. |
| 255 | # |
| 256 | # Impure for the reason the other two are: pub.dev resolution and Flutter's |
| 257 | # engine artifacts are network, and now the toolchain is as well — pinned by |
| 258 | # sha256, which is the reproducibility that was worth having out of the store. |
| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 259 | # |
| 260 | # The entry point is `frq.main-web`, not `frq.main`: path_provider has no web |
| 261 | # implementation, so the `getApplicationSupportDirectory` that `frq.main` |
| 262 | # awaits throws MissingPluginException before any widget is built. The web |
| 263 | # entry installs `frq.io.web` — localStorage behind the same seam — and awaits |
| 264 | # nothing. `frq.net.dart` is still the socket half, so connecting will want a |
| 265 | # WebSocket before this does more than paint. |
| 266 | # |
| Three tarballs where a devShell was 5ce66d5 nandi yesterday | 267 | # One build and no `--debug` variant, because there is nothing to gain from |
| 268 | # one: dart2js at -O1 measured 52.5s against the release build's 49.8s on the |
| 269 | # same source change here, so a second, larger bundle would buy noise. See |
| 270 | # `tools/build-web.sh`, which writes the numbers down. |
| 271 | # |
| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 272 | # just flutter-web build build/web |
| 273 | # just flutter-web serve build it and serve it on $PORT (8080) |
| Three tarballs where a devShell was 5ce66d5 nandi yesterday | 274 | # just flutter-web serve 3000 ...on another port |
| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 275 | flutter-web action="build" port="8080": |
| 276 | #!/usr/bin/env bash |
| 277 | set -euo pipefail |
| Three tarballs where a devShell was 5ce66d5 nandi yesterday | 278 | # A wrapper and nothing else. The build is a shell script because the |
| 279 | # container runs it too, and a container that had to install `just` to |
| 280 | # start would be one dependency away from the point. |
| 281 | exec "{{justfile_directory()}}/tools/build-web.sh" {{action}} {{port}} |
| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 282 | |
| A shell that is already the devShell, and two recipes to reach it e853593 nandi 5d ago | 283 | # The containers in `.modal/`, run on Modal rather than here. This machine |
| 284 | # evaluates and Modal builds — see CLAUDE.md, which says so rather more |
| 285 | # firmly — and these two recipes are the whole interface to that. |
| 286 | # |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 287 | # Named for where the work happens, the way `flutter-desktop` is named for |
| 288 | # what paints: there is no re-entry test here because nothing |
| A Mesa nobody needed, and the AppImage that carried it 9db383c nandi 23h ago | 289 | # re-enters. There is no `nix` variable either, and that used to be because |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 290 | # nix ran out there — now it is because neither of these containers has any |
| 291 | # nix in it at all. |
| A shell that is already the devShell, and two recipes to reach it e853593 nandi 5d ago | 292 | # |
| 293 | # just modal flutter-dev the incremental Flutter loop |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 294 | # just modal flutter-web the web bundle |
| 295 | modal container="flutter-dev" *args: |
| A shell that is already the devShell, and two recipes to reach it e853593 nandi 5d ago | 296 | #!/usr/bin/env bash |
| 297 | set -euo pipefail |
| 298 | cd "{{justfile_directory()}}" |
| 299 | shift |
| 300 | exec modal run ".modal/{{container}}/container.py" "$@" |
| 301 | |
| A third target, and the seam that was already waiting for it f54ca45 nandi 2d ago | 302 | # The Modal-built web bundle, served from this machine on localhost. |
| 303 | # |
| 304 | # Not a local build: `modal volume get` pulls what `just modal flutter-web` |
| 305 | # already compiled out of the devshell volume, so this needs no Flutter, no |
| 306 | # Dart and no nix — only python, which the flake shell has and so does the |
| 307 | # machine. |
| 308 | # |
| 309 | # It exists for one reason, and the reason is the auth broker rather than |
| 310 | # convenience. freeq's broker finishes an OAuth login by redirecting the |
| 311 | # browser to `return_to`, and it will only redirect to an origin on its |
| 312 | # allowlist: its own https hosts, and `http://localhost` or `http://127.0.0.1` |
| 313 | # on ANY port. A build served from anywhere else — the Modal URL included — |
| 314 | # gets `400 Invalid return_to URL` and can never complete a Bluesky sign-in, |
| 315 | # no matter what the client does. localhost is the one allowlisted origin we |
| 316 | # can serve from, so this is how Bluesky sign-in is tested. |
| 317 | # |
| 318 | # Guest and app-password sign-in need none of this; they work on the deployed |
| 319 | # URL, because neither goes near the broker. |
| 320 | # |
| 321 | # just web-local fetch and serve on :8080 |
| 322 | # just web-local 3000 another port; any port is allowlisted |
| 323 | web-local port="8080": |
| 324 | #!/usr/bin/env bash |
| 325 | set -euo pipefail |
| 326 | cd "{{justfile_directory()}}" |
| 327 | out="{{justfile_directory()}}/.web-local" |
| 328 | mkdir -p "$out" |
| 329 | echo "fetching the Modal-built bundle…" |
| 330 | # --force: this is a mirror of the volume, and a stale file left behind |
| 331 | # would be served in preference to the one just built. |
| 332 | modal volume get --force devshell frq-flutter-web/flutter/build/web "$out" |
| 333 | echo |
| 334 | echo " http://localhost:{{port}}" |
| 335 | echo |
| 336 | echo "Bluesky sign-in works here and not on the Modal URL: the broker" |
| 337 | echo "allowlists localhost on any port. Put wss://irc.freeq.at/irc in the" |
| 338 | echo "Server field — a browser has no TCP." |
| 339 | exec python3 -m http.server {{port}} --bind 127.0.0.1 --directory "$out/web" |
| 340 | |
| A shell that is already the devShell, and two recipes to reach it e853593 nandi 5d ago | 341 | # A sandbox left running with the container's own image, volumes and |
| 342 | # environment, and the command to get into it. `modal shell --image` cannot |
| 343 | # be pointed at a published Modal image like arch-nix, so attaching to a |
| 344 | # running sandbox is the only way to get a shell that is the container. |
| 345 | # |
| 346 | # It blocks: an ephemeral app stops when its entrypoint returns and takes the |
| 347 | # sandbox with it. Attach from a second terminal, and Ctrl-C here when done — |
| 348 | # the sandbox bills until you do. |
| 349 | # |
| 350 | # just modal-shell flutter-dev, the usual one |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 351 | # just modal-shell flutter-web the web bundle container |
| A shell that is already the devShell, and two recipes to reach it e853593 nandi 5d ago | 352 | modal-shell container="flutter-dev": |
| 353 | #!/usr/bin/env bash |
| 354 | set -euo pipefail |
| 355 | cd "{{justfile_directory()}}" |
| 356 | exec modal run ".modal/{{container}}/container.py" --shell |
| One frontend where there were three, and a core that is not Clojure 438b247 nandi 21h ago | 357 | |
| 358 | # The Nim core's test suite. |
| 359 | # |
| 360 | # It needs no Flutter, no Dart and no Android SDK — which is the point of |
| 361 | # having the logic here rather than under `common/`: a rule about the IRC wire |
| 362 | # format can be checked in a second, on any machine, without a toolchain that |
| 363 | # takes minutes to enter. |
| 364 | # |
| 365 | # just nim-test the whole suite |
| 366 | # just nim-test tircparse one file |
| 367 | nim-test file="": |
| 368 | #!/usr/bin/env bash |
| 369 | set -euo pipefail |
| 370 | cd "{{justfile_directory()}}" |
| 371 | if [ -z "${FRQ_NIM:-}" ]; then |
| 372 | exec {{nix}} develop .#nim --max-jobs {{jobs}} --command just nim-test "$@" |
| 373 | fi |
| 374 | cd nim |
| 375 | if [ -n "{{file}}" ]; then |
| 376 | exec nim c -r --hints:off --path:src "tests/{{file}}.nim" |
| 377 | fi |
| 378 | for t in tests/t*.nim; do |
| 379 | echo "== $t" |
| 380 | nim c -r --hints:off --path:src "$t" |
| 381 | done |
| 382 | |
| 383 | # The Nim core as a shared library, into build/nim. |
| 384 | # |
| 385 | # `--mm:orc` rather than the default: this is a library loaded by a Dart |
| 386 | # process that owns its own lifetime, so reference counting with a cycle |
| 387 | # collector is the memory model that does not need a GC thread of its own or a |
| 388 | # stack it can scan. |
| 389 | # |
| 390 | # `-d:release` and not `-d:danger`: the bounds checks are what turn a |
| 391 | # malformed line off a socket into an exception instead of a read past the end |
| 392 | # of a buffer, and this parses exactly that. |
| 393 | nim-lib: |
| 394 | #!/usr/bin/env bash |
| 395 | set -euo pipefail |
| 396 | cd "{{justfile_directory()}}" |
| 397 | if [ -z "${FRQ_NIM:-}" ]; then |
| 398 | exec {{nix}} develop .#nim --max-jobs {{jobs}} --command just nim-lib |
| 399 | fi |
| 400 | out="{{justfile_directory()}}/build/nim" |
| 401 | mkdir -p "$out" |
| 402 | cd nim |
| 403 | nim c --app:lib --mm:orc -d:release --hints:off \ |
| 404 | --path:src --out:"$out/libfrqcore.so" src/frq_core.nim |
| 405 | echo "built $out/libfrqcore.so" |
| 406 | nm -D --defined-only "$out/libfrqcore.so" | grep ' T frq_' || true |
| The binding is Dart, and it works f7aea3b nandi 20h ago | 407 | |
| 408 | # The Dart side of the Nim boundary, on the plain Dart VM. |
| 409 | # |
| 410 | # No Flutter, no emulator, no ClojureDart — `dart/frq_core` is ordinary Dart |
| 411 | # over `dart:ffi` and is not a Flutter package, so the test that proves the |
| 412 | # marshalling runs in a second. Passing `just nim-test` and failing this one is a |
| 413 | # marshalling bug, which is the whole reason the two suites are separate. |
| 414 | # |
| 415 | # Builds the library first: the test dlopens a real .so and there is no point |
| 416 | # reporting that it could not find one. |
| 417 | dart-test: |
| 418 | #!/usr/bin/env bash |
| 419 | set -euo pipefail |
| 420 | cd "{{justfile_directory()}}" |
| 421 | if [ -z "${FRQ_DART:-}" ]; then |
| 422 | # nim-lib before the re-entry, not after: it enters a shell of its own |
| 423 | # and doing it on the far side would build the library twice. |
| 424 | just nim-lib |
| 425 | exec {{nix}} develop .#dart --max-jobs {{jobs}} --command just dart-test |
| 426 | fi |
| 427 | cd dart/frq_core |
| 428 | dart pub get |
| 429 | dart test -r expanded |
| Nim owns the screen, Dart owns the pixels 43a02c2 nandi 20h ago | 430 | |
| 431 | # The Nim spike: a Flutter window whose screens come from Nim. |
| 432 | # |
| 433 | # No ClojureDart on this path at all — not `frq.main`, not `common/`, not a |
| 434 | # `.cljd` file. `lib/main_nim.dart` asks the Nim core for a widget tree and |
| 435 | # paints it, and every tap goes back as an event id. See `nim/src/frq/ui.nim`. |
| 436 | # |
| 437 | # Impure and deliberately so: this is a spike, so it runs `flutter` directly |
| 438 | # out of the desktop shell rather than going through the nix build, and |
| 439 | # `flutter pub get` resolves the path dependency on `dart/frq_core` in place. |
| 440 | # Nothing here is on the way to a release. |
| 441 | # |
| 442 | # just nim-spike open the window |
| 443 | # just nim-spike build just build it |
| 444 | nim-spike action="run": |
| 445 | #!/usr/bin/env bash |
| 446 | set -euo pipefail |
| 447 | cd "{{justfile_directory()}}" |
| 448 | if [ -z "${FRQ_FLUTTER_DESKTOP:-}" ]; then |
| 449 | # The library first, in its own shell — the app dlopens it at startup |
| 450 | # and a missing .so is a blank window with a StateError behind it. |
| 451 | just nim-lib |
| 452 | exec {{nix}} develop .#flutter-desktop --max-jobs {{jobs}} \ |
| 453 | --command just nim-spike "$@" |
| 454 | fi |
| 455 | cd flutter |
| 456 | flutter pub get |
| The window connects 1d62d1a nandi 20h ago | 457 | # The Nim core links OpenSSL for the TLS on :6697, and the process that |
| 458 | # dlopens it has to be able to find one. Prepended here rather than set in |
| 459 | # the shell, so nixGL's own loader path is left alone. |
| 460 | export LD_LIBRARY_PATH="${FRQ_OPENSSL_LIB:-}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" |
| Nim owns the screen, Dart owns the pixels 43a02c2 nandi 20h ago | 461 | runner=() |
| 462 | [ -e /run/current-system ] || runner=("$NIXGL") |
| 463 | case "{{action}}" in |
| 464 | build) exec "${runner[@]}" flutter build linux --debug -t lib/main_nim.dart ;; |
| 465 | run) exec "${runner[@]}" flutter run -d linux -t lib/main_nim.dart ;; |
| 466 | *) echo "usage: just nim-spike [run|build]" >&2; exit 1 ;; |
| 467 | esac |
| 468 | |
| 469 | # The spike's widget tests: Nim's tree, as Flutter widgets, driven by taps. |
| 470 | # |
| 471 | # Headless — no GL, no window — which is what makes this the proof rather than |
| 472 | # a screenshot. A screenshot shows that something painted; this shows the round |
| 473 | # trip closes: a tap reaches Nim, its state moves, the new tree comes back and |
| 474 | # the widgets change to match. |
| 475 | nim-spike-test: |
| 476 | #!/usr/bin/env bash |
| 477 | set -euo pipefail |
| 478 | cd "{{justfile_directory()}}" |
| 479 | if [ -z "${FRQ_FLUTTER_DESKTOP:-}" ]; then |
| 480 | just nim-lib |
| 481 | exec {{nix}} develop .#flutter-desktop --max-jobs {{jobs}} \ |
| 482 | --command just nim-spike-test |
| 483 | fi |
| 484 | cd flutter |
| 485 | flutter pub get |
| 486 | flutter test test/nim_renderer_test.dart |
| 487 | |
| 488 | # What the Nim boundary costs per frame. |
| 489 | # |
| 490 | # The spike rebuilds the whole screen in Nim and ships it as JSON on every |
| 491 | # event, which is the obvious objection to the design. This is the number that |
| 492 | # answers it — or doesn't. |
| 493 | nim-bench: |
| 494 | #!/usr/bin/env bash |
| 495 | set -euo pipefail |
| 496 | cd "{{justfile_directory()}}" |
| 497 | if [ -z "${FRQ_DART:-}" ]; then |
| 498 | just nim-lib |
| 499 | exec {{nix}} develop .#dart --max-jobs {{jobs}} --command just nim-bench |
| 500 | fi |
| 501 | cd dart/frq_core |
| 502 | dart pub get >/dev/null |
| 503 | dart run test/bench.dart |
| A message in #test, from Nim 35994d4 nandi 20h ago | 504 | |
| 505 | # The spike, end to end, against a real freeq. |
| 506 | # |
| 507 | # Connects, registers, joins #test and says a line — all of it through the |
| 508 | # FFI, so what it proves is Nim's socket, Nim's TLS, Nim's IRC registration |
| 509 | # and the Dart boundary over the lot. |
| 510 | # |
| 511 | # Not in any test suite, and not in CI: it needs a network and it sends a |
| 512 | # message to a public channel. Run it when you mean to. |
| 513 | # |
| 514 | # just nim-live irc.freeq.at, a random nick |
| 515 | # just nim-live irc.freeq.at mynick "a line" |
| 516 | # FRQ_TRACE=1 just nim-live ...and every line on the wire |
| 517 | nim-live *args: |
| 518 | #!/usr/bin/env bash |
| 519 | set -euo pipefail |
| 520 | cd "{{justfile_directory()}}" |
| 521 | if [ -z "${FRQ_DART:-}" ]; then |
| 522 | just nim-lib |
| 523 | exec {{nix}} develop .#dart --max-jobs {{jobs}} --command just nim-live "$@" |
| 524 | fi |
| 525 | shift || true |
| 526 | cd dart/frq_core |
| 527 | dart pub get >/dev/null |
| 528 | exec dart run tool/live_send.dart "$@" |