Porting workflow
This guide is the end-to-end process for porting one official raylib example to jank, as practiced across the first 50 ports. Follow it top to bottom and a port lands as one self-contained, tested, registered commit.
The ports themselves live in raylib-jank-demo, one Leiningen project per example, so the steps below run there. This repo keeps the process write-up next to the interop rules it depends on.
1. Pick from the queue
The official raylib example list is the queue, and demos.edn in raylib-jank-demo lists what is already ported. Markers tell you the cost up front:
- (no marker): pure raylib, port directly (these are all done now)
- ๐ผ๏ธ: uses a
RenderTexture(supported; seelines_drawing.jank) - ๐๏ธ: uses raygui controls; swap them for keyboard controls (see raygui-to-keyboard.md)
- โ๏ธ: uses the low-level
rlglAPI. Turns out to work directly:rlgl.his installed next toraylib.hand its functions live inlibraylib, so(:include "rlgl.h")is all it takes (proof:rlgl_triangle.jank)
2. Port from the definitive C source
The authoritative originals are raylib's own example programs, under examples/{core,shapes,text,...}/ (easing functions in examples/shapes/reasings.h). You already have a checkout: the vendored submodule at jank-raylib-sys/raylib/examples/ in raylib-jank-demo is pinned to the same raylib commit the raylib-sys package builds, so it is the copy whose API actually matches.
Port from the C, never from an intermediate binding. Ports-of-ports drift; if you find an existing Clojure or Lisp translation of an example, treat it as a hint and check it against the C.
Keep formulas and update ordering faithful to the C (a code review of the easings testbed verified all 28 easing formulas term-by-term against reasings.h; that fidelity is the standard). When jank forces a deviation (no mutable arrays, capped pool sizes, keyboard instead of raygui), say so in the namespace docstring:
(ns net.b12n.raylib-jnk.scenes.bullet-hell
"raylib [shapes] example - bullet hell, ported to jank.
...controls...
Based on raylib/examples/shapes/shapes_bullet_hell.c
(jank-native: bullets are a vector of maps rebuilt per frame instead
of a mutable C array, and MAX-BULLETS is 5000 rather than the C's
500000 calloc headroom - the reset-at-cap behavior is the same.)"
(:include "raylib.h" "math.h"))
Docstring format: title line, controls, Based on <C file>, then a (jank-native: ...) note for intentional deviations. File names use underscores, namespaces use kebab, and the directory is the kebab name: bullet_hell.jank โ net.b12n.raylib-jnk.scenes.bullet-hell in bullet-hell/src/net/b12n/raylib_jnk/scenes/. Comments must be ASCII (an em-dash trips the lexer).
Before writing a new construct, grep raylib-jank-demo's demos for a sibling that already uses it; every proven idiom has at least one committed demo there, and the guide pages index them by theme.
Every example sets (cpp/SetConfigFlags cpp/FLAG_WINDOW_HIGHDPI) before InitWindow so windows scale with the monitor DPI (drawing stays at the C's logical resolution); include it in new ports. SetConfigFlags ORs each call into its state, so it stacks with FLAG_MSAA_4X_HINT etc. Exceptions: window-flags (a flag-state demo) and the two highdpi-* examples (which manage DPI flags themselves).
3. Register the example (same commit)
In raylib-jank-demo each example is its own Leiningen project, and its README's "Adding a demo" section is the checklist:
<demo>/src/net/b12n/raylib_jnk/scenes/<demo>.jank, with the namespacenet.b12n.raylib-jnk.scenes.<demo>and a-main<demo>/project.cljand<demo>/bb.edn, copied from any existing demo- a line in
demos.edn bb gen, which adds the root task and the gallery entry- the demo's own
docs/guide/index.md, and its recording in both<demo>/docs/demos/anddocs/demos/
Do not defer any of these; the registration IS part of the port.
4. Smoke-test headless
Check paren balance BEFORE the first compile. A jank compile costs 30-60 s; a strict reader loop is instant and catches the classic extra-close-paren at the recur tail (which cost one wasted compile on input_gestures_testbed). jank sources read fine with the JVM reader:
cd <name> # inside raylib-jank-demo
clojure -M -e "
(let [text (slurp \"src/net/b12n/raylib_jnk/scenes/<name_with_underscores>.jank\")
r (java.io.PushbackReader. (java.io.StringReader. text))]
(try
(loop [forms []]
(let [form (read {:read-cond :allow :eof ::eof} r)]
(if (= form ::eof)
(println :total-forms (count forms) :ok)
(recur (conj forms form)))))
(catch Exception e (println :ERROR (.getMessage e)))))"
:total-forms N :ok means balanced; :ERROR Unmatched delimiter means fix before compiling. (A strict read loop, not read-string - the single-form and (do ...)-wrapped variants both miss trailing imbalance.)
macOS has no timeout and this harness blocks a foreground sleep, so the reliable one-shot is a perl alarm:
cd <name> # inside raylib-jank-demo
perl -e 'alarm 25; exec @ARGV' \
lein run --disable-sandbox > /tmp/run.log 2>&1
echo "exit=$?"
grep -icE "error|exception|Mismatched|small_real|small_integer|invalid object" /tmp/run.log
Reading the result:
exit=142(SIGALRM): the example compiled, opened its window, and survived 25 s of the frame loop. This is the success signal.exit=1or an early exit: compilation or startup failed; the log has the compiler error.- The
grepmust print0. The markers are the jank/raylib failure vocabulary:Mismatched(if-branch type clash),small_real/small_integer(int/real API mismatch),invalid object(bad conversion).
First compile of a new example takes ~60โ75 s; cached recompiles ~15 s.
Key-gated paths need a probe run
A 25 s headless run only exercises code that runs unconditionally. If the interesting path hides behind input (a hover, a key, a generation count), temporarily force the state, run, then revert before committing:
penrose_tile.jank: forcedgen 2+ prebuilt tokens to exercise the L-system, then reverted.input_box.jank: forcedon-text? trueand seeded the name from the ASCII table so the caret/MeasureTextpath ran, then reverted.
Note the probe in the commit message so reviewers know the gated path was actually executed.
5. Commit
- One example per commit when practical (
demos.ednrows interleave if you batch two; fine occasionally, but singles keep history greppable). - Subject:
<name>: port <official_source_name>. - Body: the interesting jank-native decisions, and any NEW interop lesson the port surfaced.
- Stage files by explicit path; never
git add -A/./-u. - If the port surfaced a new lesson, add it to the relevant guide page in the same commit.
Debugging a port that won't compile
Bisect: cut the example down to a minimal draw loop, then add one construct back at a time; each lein run recompiles the one changed module in ~30โ60 s. The compiler error vocabulary and what each message actually means is in type-checking-and-coercion.md and cpp-interop-toolbox.md.