commit 95faf8ee1e90ee174a3a590abafecc77bd6d3af1
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Wed, 23 Sep 2026 21:10:02 -0300
readme, justfile: shape the jfm collection for Odin scripts
Scripts written in Odin need a home for shared packages. This is the jfm
collection: eight packages, one runner, tests and a three-target
type-check. The README is the reference the following commits fill in,
the justfile holds the recipes, odinfmt.json and ols.json point the
formatter and the language server at the collection root, and .gitignore
keeps build/ out of the tree.
Diffstat:
5 files changed, 162 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/README.md b/README.md
@@ -0,0 +1,88 @@
+# jfm — Odin for scripts
+
+A collection of small packages and one runner that make Odin comfortable for
+the scripts Python and bash usually get. Everything builds on `core:`; the
+only linked dependency is libcurl through `vendor:curl`.
+
+```odin
+#!/usr/bin/env odin-run
+package main
+
+import "core:fmt"
+import "jfm:prelude"
+import "jfm:sh"
+import "jfm:path"
+
+must :: prelude.must
+die :: prelude.die
+
+main :: proc() {
+ context = prelude.init()
+ branch := must(sh.out("git rev-parse --abbrev-ref HEAD"))
+ for f in must(path.walk(".")) {
+ fmt.println(branch, f)
+ }
+}
+```
+
+`chmod +x` and run it. The first run compiles; later runs start the cached
+binary.
+
+## Packages
+
+| Package | Job |
+|-----------|-----|
+| `prelude` | `must`, `die`, `env`, `args`; arena or debug allocator; logfmt log file plus `deaths.log` audit trail |
+| `sh` | `out`, `lines`, `ok`, `run`, `capture` through the shell; `exec`, `exec_run` with argv; `which`, `quote`, `error` |
+| `http` | `get`, `post`, `post_json`, `get_json`, `download`, `request` over libcurl |
+| `path` | `expand`, `join`, `mkdirs`, `read`, `read_lines`, `write`, `append_file`, `list`, `walk`, `temp_dir`, `same`, per-user app dirs |
+| `timefmt` | strftime `format`, `local`, `parse`; `iso`, `stamp`, `date`, `duration` |
+| `debug` | guard-byte debug allocator (origin: sonar, which now imports this copy) |
+| `flow` | `width`, `each`, `manage`: lock-free worker pools where each worker owns one state slot and the caller merges afterwards |
+
+`tools/odin-run` is the runner. Every package reads on its own; the doc
+comment at the top of each file is the reference.
+
+## Conventions
+
+- Scripts never free. `prelude.init` puts a growing arena in
+ `context.allocator`; the exit reclaims it. `ODIN_SCRIPT_DEBUG=1` swaps in
+ the debug allocator, which reports overflow, double free and write after
+ free at exit.
+- Anything that can fail returns `(value, ok)` or `(value, os.Error)` so
+ `must(...)` wraps it. `die` writes the message and the call site to the
+ script log, to stderr, and to one shared `deaths.log`.
+- `prelude` never writes to stdout. Stdout is the script's data channel.
+- Logs: `<log dir>/odin/<name>/<name>.log`, rotated to `.1` at 8 MiB, and
+ `<log dir>/odin/deaths.log`. The log dir is `~/.local/state` on Linux,
+ `~/Library/Logs` on macOS and `%LOCALAPPDATA%` on Windows. `ODIN_LOG=debug`
+ echoes everything to stderr; the default echoes warning and above.
+- Platform splits live in `_unix.odin` / `_windows.odin` files. `just check`
+ type-checks every package for `linux_amd64`, `darwin_arm64` and
+ `windows_amd64` from one machine.
+
+## Runner
+
+```
+odin-run script.odin [args...] compile if stale, then run
+odin-run -clean drop the cache
+```
+
+Cache key: script bytes and path, every `.odin` in the collection, the odin
+binary, target and flags. `ODIN_RUN_FLAGS="-debug"` builds unoptimised;
+`ODIN_RUN_VERBOSE=1` prints the build line. On Windows the shebang does
+nothing; call `odin-run script.odin` or associate `.odin` with it. When
+`vendor/curl/lib/libcurl.dll` exists under the Odin root it is copied beside
+the cached binary.
+
+## Recipes
+
+```
+just build debug odin-run just release optimised odin-run
+just test all package tests just check 3-target type-check
+just install odin-run -> ~/.local/bin (BINDIR overrides)
+just example run examples/hello.odin just clean
+```
+
+`just install` bakes this checkout's path into the runner as the `jfm`
+collection root; `ODIN_RUN_COLLECTION` overrides it.
diff --git a/justfile b/justfile
@@ -0,0 +1,57 @@
+# Standard recipes: build, release, clean, test, install, plus check and example.
+#
+# just build debug odin-run -> build/debug/odin-run
+# just release optimised odin-run -> build/release/odin-run
+# just test run every package's tests
+# just check type-check every package for linux, darwin and windows
+# just install release odin-run into ~/.local/bin with this checkout baked in
+# just example compile and run examples/hello.odin through the collection
+# just clean remove build/
+
+odin := env("ODIN", "odin")
+root := justfile_directory()
+flags := "-vet -strict-style -collection:jfm=" + root
+exe := if os() == "windows" { ".exe" } else { "" }
+bindir := env("BINDIR", home_directory() / ".local" / "bin")
+packages := "prelude sh http path timefmt debug flow tar"
+targets := "linux_amd64 darwin_arm64 windows_amd64"
+
+# `just` alone lists the recipes.
+default:
+ @just --list --unsorted
+
+# Debug odin-run -> build/debug/odin-run
+build:
+ mkdir -p build/debug
+ {{odin}} build tools/odin-run -debug {{flags}} -define:JFM_ROOT={{root}} -out:build/debug/odin-run{{exe}}
+
+# Optimised odin-run -> build/release/odin-run
+release:
+ mkdir -p build/release
+ {{odin}} build tools/odin-run -o:speed {{flags}} -define:JFM_ROOT={{root}} -out:build/release/odin-run{{exe}}
+
+# Run every package's tests
+test:
+ mkdir -p build/test
+ for p in {{packages}}; do {{odin}} test $p {{flags}} -out:build/test/$p{{exe}} || exit 1; done
+
+# Type-check every package and the runner for each target
+check:
+ for t in {{targets}}; do \
+ for p in {{packages}}; do {{odin}} check $p {{flags}} -no-entry-point -target:$t || exit 1; done; \
+ {{odin}} check tools/odin-run {{flags}} -target:$t || exit 1; \
+ {{odin}} check examples/hello.odin -file {{flags}} -target:$t || exit 1; \
+ done
+
+# Install odin-run into ~/.local/bin (override with BINDIR)
+install: release
+ mkdir -p {{bindir}}
+ cp build/release/odin-run{{exe}} {{bindir}}/odin-run{{exe}}
+
+# Compile and run the example script
+example: build
+ ODIN_RUN_VERBOSE=1 build/debug/odin-run{{exe}} examples/hello.odin
+
+# Remove build/
+clean:
+ rm -rf build
diff --git a/odinfmt.json b/odinfmt.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
+ "character_width": 100,
+ "tabs": true,
+ "tabs_width": 4,
+ "newline_style": "LF",
+ "align_constant_definitions": true
+}
diff --git a/ols.json b/ols.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json",
+ "collections": [
+ { "name": "jfm", "path": "." }
+ ],
+ "enable_semantic_tokens": true,
+ "enable_snippets": true
+}