nandi/frqpublic Fork 0
2e24e647a7766c822d13d253777204c3bf92fb7a
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Six verbs, and the last of the nix 2e24e64 · on 2e24e647a7766c822d13d253777204c3bf92fb7a · nandi · 12h ago
justfile · 261 lines · 10.7 KBMakefile Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# Six verbs over one tree.
#
# There is no nix here any more. `tools/toolchain.sh` fetches Flutter (which
# carries Dart), a JDK, the Clojure CLI and Nim as sha256-pinned tarballs into
# `.toolchain/`, and every recipe below runs inside the environment that
# script prints. One mechanism, and the same one the containers in `.modal/`
# use — which is what lets a plain Debian image build this.
#
# What the host still brings: a C compiler (Nim shells out to one), OpenSSL
# (nim.cfg is `-d:ssl`), git, curl, unzip, python3. For `run desktop` and the
# other Linux builds, GTK and the usual CMake/Ninja/pkg-config.
#
#   just build TARGET      apk desktop web lib ui app
#   just run TARGET        apk desktop web ui app
#   just test SUITE        all nim dart common live
#   just modal CONTAINER   dev web
#   just serve [PORT]      the Modal-built web bundle, on localhost
#   just tools ...         the toolchain itself

set shell := ["bash", "-euo", "pipefail", "-c"]

# Every recipe is a `#!` script. Without this, "$@" is empty in one of them —
# just interpolates into a shebang recipe rather than handing it argv.
set positional-arguments

root := justfile_directory()
tc := justfile_directory() / "tools/toolchain.sh"

default:
    @just --list

# The toolchain, directly: `just tools versions`, `just tools android`,
# `just tools exec -- flutter doctor`.
[doc('the toolchain itself: versions, android, exec -- CMD')]
tools *args:
    #!/usr/bin/env bash
    cd "{{root}}"
    exec "{{tc}}" "$@"

# Build a target.
#
#   apk        the Android app, via Gradle
#   desktop    the ClojureDart app on Flutter's Linux target
#   web        the same screens compiled to JavaScript, into build/web
#   lib        the Nim core as build/nim/libfrqcore.so
#   ui         Nim owning the state and the screens, painted by Flutter
#   app        the ClojureDart app with the Nim core as its transport only
[doc('build a target: apk desktop web lib ui app')]
build target="desktop":
    #!/usr/bin/env bash
    set -euo pipefail
    cd "{{root}}"
    case "{{target}}" in
        apk)     just _flutter apk build ;;
        desktop) just _flutter desktop build ;;
        web)     exec tools/build-web.sh build ;;
        lib)     just _nim-lib ;;
        ui)      just _flutter ui build ;;
        app)     just _flutter app build ;;
        *)       echo "usage: just build [apk|desktop|web|lib|ui|app]" >&2; exit 1 ;;
    esac

# Build a target and start it.
#
#   apk        install on a connected device and launch it
#   web        serve build/web on ARG (default 8080)
#   the rest   open the window
#
#   just run apk
#   just run web 3000
[doc('build a target and start it')]
run target="desktop" arg="":
    #!/usr/bin/env bash
    set -euo pipefail
    cd "{{root}}"
    case "{{target}}" in
        apk)     just _flutter apk run ;;
        desktop) just _flutter desktop run ;;
        web)     port="${2:-}"; exec tools/build-web.sh serve "${port:-8080}" ;;
        ui)      just _flutter ui run ;;
        app)     just _flutter app run ;;
        log)     just tools exec -- adb logcat -s flutter ;;
        *)       echo "usage: just run [apk|desktop|web|ui|app|log]" >&2; exit 1 ;;
    esac

# Test a suite.
#
#   common   what may appear in common/, read off the source. Needs no
#            toolchain at all, which is why CI runs exactly this.
#   nim      the Nim core. No Flutter, no Dart, no SDK — a rule about the IRC
#            wire format is checkable in a second.
#   dart     the Dart side of the FFI boundary, on the plain Dart VM. Passing
#            `nim` and failing this one is a marshalling bug, which is why the
#            two are separate suites.
#   live     the whole stack against a real freeq. Not in `all`: it wants a
#            network and a running server.
#
#   just test              all of them
#   just test nim tircparse    one Nim file
[doc('run a suite: all common nim dart live')]
test suite="all" *args:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "{{root}}"
    shift
    case "{{suite}}" in
        all)    just test common && just test nim && just test dart ;;
        common) exec python3 tools/check-common.py common ;;
        nim)    just _nim-test "$@" ;;
        dart)   just _nim-lib
                exec "{{tc}}" exec -- bash -c \
                    'cd dart/frq_core && dart pub get && dart test -r expanded' ;;
        live)   just _nim-lib
                exec "{{tc}}" exec -- bash -c \
                    'cd dart/frq_core && dart pub get >/dev/null && dart run tool/live_ui.dart "$@"' _ "$@" ;;
        *)      echo "usage: just test [all|common|nim|dart|live]" >&2; exit 1 ;;
    esac

# The containers in `.modal/`, run on Modal rather than here: this machine
# evaluates and Modal builds. See CLAUDE.md, which says so rather more firmly.
#
# `--shell` leaves a sandbox running with the container's own image, volumes
# and environment, and prints the command to attach to it. It blocks — an
# ephemeral app takes its sandbox down when the entrypoint returns — so attach
# from a second terminal and Ctrl-C here when done. The sandbox bills until
# you do.
#
#   just modal dev
#   just modal web --shell
[doc('run a .modal/ container on Modal')]
modal container="dev" *args:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "{{root}}"
    shift || true
    exec modal run ".modal/{{container}}/container.py" "$@"

# The Modal-built web bundle, served from this machine on localhost.
#
# Not a local build: `modal volume get` pulls down what `just modal
# web` already compiled, so this needs only python3.
#
# It exists for the auth broker rather than for convenience. freeq's broker
# finishes an OAuth login by redirecting to `return_to`, and only to an origin
# on its allowlist: its own https hosts, and http://localhost or
# http://127.0.0.1 on ANY port. Served from anywhere else — the Modal URL
# included — a Bluesky sign-in gets `400 Invalid return_to URL` and can never
# complete. Guest and app-password sign-in work on the deployed URL; neither
# goes near the broker.
[doc('serve the Modal-built web bundle on localhost')]
serve port="8080":
    #!/usr/bin/env bash
    set -euo pipefail
    cd "{{root}}"
    out="{{root}}/.web-local"
    mkdir -p "$out"
    echo "fetching the Modal-built bundle…"
    # --force: this is a mirror of the volume, and a stale file left behind
    # would be served in preference to the one just built.
    modal volume get --force devshell frq-web/flutter/build/web "$out"
    echo
    echo "  http://localhost:{{port}}"
    echo
    echo "Bluesky sign-in works here and not on the Modal URL. Put"
    echo "wss://irc.freeq.at/irc in the Server field — a browser has no TCP."
    exec python3 -m http.server {{port}} --bind 127.0.0.1 --directory "$out/web"

# --- the work behind the verbs ------------------------------------------

# The Nim core as a shared library, into build/nim.
#
# `--mm:orc` and not refc, and it is not a preference: refc gives each thread
# its own GC heap, so the Socket the reader and writer threads share is a ref
# from another heap and dereferencing it segfaults. ORC's heap is shared.
#
# `-d:release` and not `-d:danger`: the bounds checks are what turn a
# malformed line off a socket into an exception rather than a read past the
# end of a buffer, and this parses exactly that.
[private]
_nim-lib:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "{{root}}"
    out="{{root}}/build/nim"
    mkdir -p "$out"
    exec "{{tc}}" exec -- bash -euo pipefail -c '
        cd nim
        nim c --app:lib --mm:orc -d:release --hints:off \
            --path:src --out:"'"$out"'/libfrqcore.so" src/frq_core.nim
        echo "built '"$out"'/libfrqcore.so"
        nm -D --defined-only "'"$out"'/libfrqcore.so" | grep " T frq_" || true'

[private]
_nim-test *args:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "{{root}}"
    exec "{{tc}}" exec -- bash -euo pipefail -c '
        cd nim
        if [ -n "${1:-}" ]; then
            exec nim c -r --hints:off --path:src "tests/$1.nim"
        fi
        for t in tests/t*.nim; do
            echo "== $t"
            nim c -r --hints:off --path:src "$t"
        done' _ "$@"

# The three Flutter targets, which differ only in what they compile and which
# entry point they paint.
#
#   apk      ClojureDart, then Gradle. Impure on purpose: Gradle resolves its
#            own dependencies over the network and has sdkmanager install a
#            platform and build-tools into ANDROID_HOME as it goes, so the
#            SDK has to be writable — which is what `just tools android` gets
#            it. Everything it leaves behind is under `.toolchain/` and
#            gitignored.
#   desktop  the same `clojure -M:cljd compile`, Flutter's Linux target.
#   ui       no ClojureDart at all: `lib/main_nim.dart` asks the Nim core for
#            a widget tree and paints it.
#   app      `frq.main-nim` is `frq.main` with one line changed —
#            `frq.net.nim/install!` where it said `frq.net.dart/install!`.
#            Every screen, cell and action is the one that was already there.
[private]
_flutter target action:
    #!/usr/bin/env bash
    set -euo pipefail
    cd "{{root}}"
    case "{{target}}" in
        apk) "{{tc}}" android ;;
        ui|app) just _nim-lib ;;
    esac
    exec "{{tc}}" exec -- bash -euo pipefail -c '
        cd flutter
        # Nim resolves OpenSSL through dynlib at run time; without the host
        # library on the loader path `newContext` dies in a SIGSEGV that says
        # nothing about SSL.
        export LD_LIBRARY_PATH="${FRQ_OPENSSL_LIB:-}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
        cljd() { clojure -Sdeps "{:mvn/local-repo \"$FRQ_M2\"}" -M:cljd compile "$@"; }

        case "$1:$2" in
            apk:*)
                cljd
                # Rewritten every run: it carries absolute paths.
                flutter config --android-sdk "$ANDROID_HOME" >/dev/null
                flutter build apk --debug
                apk=build/app/outputs/flutter-apk/app-debug.apk
                [ "$2" = run ] || exit 0
                adb install -r "$apk"
                exec adb shell monkey -p uk.nandi.frq \
                    -c android.intent.category.LAUNCHER 1 ;;
            desktop:build) cljd; exec flutter build linux --debug ;;
            desktop:run)   cljd; exec flutter run -d linux ;;
            ui:build)      flutter pub get
                           exec flutter build linux --debug -t lib/main_nim.dart ;;
            ui:run)        flutter pub get
                           exec flutter run -d linux -t lib/main_nim.dart ;;
            app:build)     flutter pub get; cljd frq.main-nim
                           exec flutter build linux --debug -t lib/main_nim_app.dart ;;
            app:run)       flutter pub get; cljd frq.main-nim
                           exec flutter run -d linux -t lib/main_nim_app.dart ;;
        esac' _ "{{target}}" "{{action}}"