Files
typewriter/firmware/justfile
Julien Calixte 78958d9de6 feat(justfile): split SD provisioning into init/load/provision
Replace the single seed-sd recipe with three entry points sharing
private helpers: init (repo + config), load (repo copy only), provision
(config only). Each excludes gitignored paths via git's own resolution
(so node_modules and firmware/.env never reach the card), writes
/sd/typoena.conf from firmware/.env (Wi-Fi + PAT + git identity, PAT
never printed), and ejects the card so it goes straight into Typoena.
2026-07-11 01:44:38 +02:00

284 lines
13 KiB
Makefile

# Typoena firmware — common commands.
# Recipes source ~/export-esp.sh themselves (LIBCLANG_PATH + Xtensa GCC),
# so no per-shell setup is needed before calling just.
# Load firmware/.env (Wi-Fi creds for the network spikes) into recipe env.
# Absent .env is fine — the editor build ignores TW_* entirely.
set dotenv-load := true
esp_env := ". ~/export-esp.sh &&"
elf := "target/xtensa-esp32s3-espidf/release/firmware"
elf_wifi := "target/xtensa-esp32s3-espidf/release/wifi_tls"
elf_sd := "target/xtensa-esp32s3-espidf/release/sd_fat"
elf_git := "target/xtensa-esp32s3-espidf/release/git_smoke"
elf_git_push := "target/xtensa-esp32s3-espidf/release/git_push"
elf_git_sync := "target/xtensa-esp32s3-espidf/release/git_sync"
# Custom partition table (adds the `storage` FAT partition for the git working
# copy). Only the git-push flash applies it — the editor flash keeps the default
# single-app layout, so this change is scoped to Spike 7's finish.
partition_table := justfile_directory() + "/partitions.csv"
# Spike 7 Path 2 — env for the git2/libgit2 build. LIBGIT2_SRC points at the
# vendored libgit2 submodule (v1.9.4, matches libgit2-sys 0.18.5). pkgconfig/
# holds fake .pc files so libgit2-sys + libz-sys run in system mode and the
# symbols come from the esp-idf libgit2 component. Only the git recipes set this,
# so the editor build never compiles libgit2.
libgit2_src := justfile_directory() + "/components/libgit2/vendor"
git_env := "LIBGIT2_SRC=" + libgit2_src + " LIBGIT2_NO_VENDOR=1 PKG_CONFIG_ALLOW_CROSS=1 PKG_CONFIG_LIBDIR=" + justfile_directory() + "/pkgconfig"
# list recipes
default:
@just --list
# compile (release)
build:
{{esp_env}} cargo build --release --bin firmware
# build + flash + open serial monitor
flash:
{{esp_env}} cargo run --release --bin firmware
# serial monitor only, with decoded backtraces
monitor:
espflash monitor --elf {{elf}}
# Spike 6 — build the Wi-Fi + TLS spike (needs TW_WIFI_SSID / TW_WIFI_PASS in .env)
build-wifi:
{{esp_env}} cargo build --release --bin wifi_tls
# Spike 6 — flash + monitor the Wi-Fi + TLS spike
flash-wifi:
{{esp_env}} cargo run --release --bin wifi_tls
# serial monitor for the Wi-Fi spike, with decoded backtraces
monitor-wifi:
espflash monitor --elf {{elf_wifi}}
# Spike 3 — build the SD/FAT spike (no .env needed)
build-sd:
{{esp_env}} cargo build --release --bin sd_fat
# Spike 3 — flash + monitor the SD/FAT spike
flash-sd:
{{esp_env}} cargo run --release --bin sd_fat
# serial monitor for the SD spike, with decoded backtraces
monitor-sd:
espflash monitor --elf {{elf_sd}}
# Spike 7 Path 2 — build the git2/libgit2 smoke (git2 safe API on device)
build-git:
{{esp_env}} {{git_env}} cargo build --release --bin git_smoke --features git
# Spike 7 Path 2 — flash + monitor the git2/libgit2 smoke
flash-git:
{{esp_env}} {{git_env}} cargo run --release --bin git_smoke --features git
# serial monitor for the git smoke, with decoded backtraces
monitor-git:
espflash monitor --elf {{elf_git}}
# Spike 7 finish — build the on-device git push (Wi-Fi + SNTP + flash-FAT + libgit2)
build-git-push:
{{esp_env}} {{git_env}} cargo build --release --bin git_push --features git
# Spike 7 finish — flash + monitor. espflash applies the custom partition table
# (so the `storage` FAT partition exists) and sets 16 MB flash. Uses espflash
# directly, not `cargo run`, so the table is applied only to this binary.
flash-git-push: build-git-push
espflash flash --monitor --partition-table {{partition_table}} --flash-size 16mb {{elf_git_push}}
# serial monitor for the git push, with decoded backtraces
monitor-git-push:
espflash monitor --elf {{elf_git_push}}
# Milestone #2A — build the persistent-clone publish cycle (clone + fast-forward push)
build-git-sync:
{{esp_env}} {{git_env}} cargo build --release --bin git_sync --features git
# Milestone #2A — flash + monitor. Applies the same custom partition table as
# git-push (the `storage` FAT partition holds the persistent clone).
flash-git-sync: build-git-sync
espflash flash --monitor --partition-table {{partition_table}} --flash-size 16mb {{elf_git_sync}}
# serial monitor for the git sync, with decoded backtraces
monitor-git-sync:
espflash monitor --elf {{elf_git_sync}}
# detect board, print chip/MAC/flash size
info:
espflash board-info
# list USB serial devices
ports:
@ls /dev/cu.usb* 2>/dev/null || echo "no USB serial device found"
# ─── SD card provisioning ────────────────────────────────────────────────────
# Prepare an SD card on a computer so it can go straight into Typoena (the
# decision in docs/notes/git-sync-images-and-repo-size.md): the device never
# cold-clones 566 MB over Wi-Fi + mbedTLS; a laptop copies the clone onto the
# card via a reader, and the device only ever takes the `open` + fast-forward
# path. Three entry points, each ejecting the card when done:
#
# just init ~/code/notes # full prep of a fresh card: repo + config
# just load ~/code/notes # (re)copy just the notes repo → /sd/repo
# just provision # (re)write just the config from firmware/.env
#
# Add a /Volumes/<name> as the last arg if more than one card is mounted.
# The `_`-prefixed recipes are shared internals (hidden from `just --list`).
sd_repo_dir := "repo"
# Full prep of a fresh card: copy the notes repo + write config, then eject.
# Run this once per card. <repo-src> is a clone made on this computer.
init repo_src sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
vol="$(just _card "{{sd_volume}}" | tail -n1)"
just _load-repo "{{repo_src}}" "$vol"
just _write-conf "$vol"
just _eject "$vol"
# (Re)copy just the notes repo to /sd/repo (full clone, gitignored paths
# excluded), then eject — e.g. to refresh the card after big upstream changes.
load repo_src sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
vol="$(just _card "{{sd_volume}}" | tail -n1)"
just _load-repo "{{repo_src}}" "$vol"
just _eject "$vol"
# (Re)write just the config (Wi-Fi + PAT + git identity) from firmware/.env,
# then eject — e.g. to rotate the PAT or switch networks without touching repo/.
provision sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
vol="$(just _card "{{sd_volume}}" | tail -n1)"
just _write-conf "$vol"
just _eject "$vol"
# Resolve the target card volume. Prints the /Volumes/<name> path as the last
# stdout line (callers capture it); all diagnostics go to stderr. Prefers an
# explicit name; else auto-detects exactly one ejectable + external volume, and
# refuses on 0 or >1 — a wrong guess here means rsync --delete wipes the wrong
# disk's repo/.
_card sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
want="{{sd_volume}}"
if [ -n "$want" ]; then
vol="/Volumes/$want"
[ -d "$vol" ] || { echo "error: '$vol' is not mounted" >&2; exit 1; }
else
cands=()
for v in /Volumes/*; do
[ -d "$v" ] || continue
info="$(diskutil info "$v" 2>/dev/null || true)"
if echo "$info" | grep -qiE 'Ejectable:[[:space:]]+Yes' \
&& echo "$info" | grep -qiE 'Device Location:[[:space:]]+External|Removable Media:[[:space:]]+(Removable|Yes)'; then
cands+=("$v")
fi
done
case "${#cands[@]}" in
0) echo "error: no removable card detected under /Volumes — insert an SD card (FAT32)" >&2; exit 1 ;;
1) vol="${cands[0]}" ;;
*) echo "error: multiple removable volumes — name one as the volume arg:" >&2
printf ' %s\n' "${cands[@]#/Volumes/}" >&2; exit 1 ;;
esac
fi
# The device won't mount a non-FAT card (esp_vfs_fat, format_if_mount_failed=false).
fs="$(diskutil info "$vol" 2>/dev/null | grep -iE 'File System Personality' | sed 's/.*:[[:space:]]*//' || true)"
case "$fs" in
*FAT32*|*MS-DOS*) : ;;
*) echo "warning: '$vol' is '$fs', not FAT32 — the device may fail to mount it" >&2 ;;
esac
echo "$vol"
# Copy a full clone of the notes repo to <vol>/repo. Everything the repo's
# .gitignore ignores is excluded (node_modules is 3.9 GB, gitignored, never in
# .git) — the device needs .git + the checkout (~720 MB), not the JS deps or
# secrets like firmware/.env. Copying (not a fresh `git clone` of the local
# path) preserves origin → GitHub so on-device fetch/push still work.
_load-repo repo_src vol:
#!/usr/bin/env bash
set -euo pipefail
src="{{repo_src}}"; src="${src%/}" # strip trailing slash
[ -d "$src/.git" ] || { echo "error: '$src' is not a git repo (no .git/)"; exit 1; }
origin="$(git -C "$src" remote get-url origin 2>/dev/null || true)"
case "$origin" in
https://*|git@*|ssh://*|git://*) echo "source origin: $origin" ;;
"") echo "warning: '$src' has no 'origin' remote — the device can't fetch/push after loading" ;;
*) echo "warning: origin is '$origin' (looks like a local path, not a remote) —"
echo " the device fetch/push will fail; set origin to the GitHub URL first" ;;
esac
if [ -n "${TW_REMOTE_URL:-}" ] && [ -n "$origin" ] && [ "$origin" != "${TW_REMOTE_URL}" ]; then
echo "warning: origin ($origin) != TW_REMOTE_URL (${TW_REMOTE_URL})"
fi
dest="{{vol}}/{{sd_repo_dir}}"
echo "copying repo: $src -> $dest"
mkdir -p "$dest"
# Build the exclude list from git's own ignore resolution (.gitignore +
# .git/info/exclude + global). --directory collapses a fully-ignored dir
# (node_modules/, 3.9 GB) to one line. Driving off git — not rsync's own
# dir-merge — means we never list a tracked file or anything under .git/, so
# a .gitignore pattern like `logs/` or `*.pack` can't corrupt the clone.
ignore_list="$(mktemp)"
trap 'rm -f "$ignore_list"' EXIT
git -C "$src" -c core.quotePath=false ls-files \
--others --ignored --exclude-standard --directory --no-empty-directory \
> "$ignore_list"
echo "excluding $(wc -l < "$ignore_list" | tr -d ' ') gitignored path(s) (incl. node_modules, .env)"
# -rt (no perms/owner/symlinks — meaningless on FAT), --modify-window=1 for
# FAT's 2 s timestamp granularity, --delete to mirror (re-runs stay clean),
# -P for progress on the ~700 MB copy. Scoped to repo/, never the card root.
rsync -rtP --delete --modify-window=1 --exclude-from="$ignore_list" "$src/" "$dest/"
# Write <vol>/typoena.conf (Wi-Fi + PAT + git identity) from firmware/.env
# (already loaded via `set dotenv-load`), the same file the build uses — so no
# re-typing creds, no prompts, no network. The PAT is written to the file but
# only ever reported as set/MISSING, never echoed. FAT has no file permissions,
# so physical custody of the card is the control: use a fine-grained PAT
# (contents:write on just the notes repo) so a lost card is a one-token revoke.
_write-conf vol:
#!/usr/bin/env bash
set -euo pipefail
conf="{{vol}}/typoena.conf"
if [ -n "${TW_WIFI_SSID:-}${TW_REMOTE_URL:-}${TW_PAT:-}" ]; then
{
printf '# Typoena runtime config — generated by `just provision` from firmware/.env.\n'
printf '# Plaintext secrets on removable media: keep the card safe; scope TW_PAT\n'
printf '# to contents:write on just the notes repo. `key=value`, `#` = comment.\n'
printf 'TW_WIFI_SSID=%s\n' "${TW_WIFI_SSID:-}"
printf 'TW_WIFI_PASS=%s\n' "${TW_WIFI_PASS:-}"
printf 'TW_REMOTE_URL=%s\n' "${TW_REMOTE_URL:-}"
printf 'TW_GH_USER=%s\n' "${TW_GH_USER:-}"
printf 'TW_PAT=%s\n' "${TW_PAT:-}"
printf 'TW_AUTHOR_NAME=%s\n' "${TW_AUTHOR_NAME:-}"
printf 'TW_AUTHOR_EMAIL=%s\n' "${TW_AUTHOR_EMAIL:-}"
} > "$conf"
mask() { if [ -n "${1:-}" ]; then printf 'set'; else printf 'MISSING'; fi; }
echo "wrote $conf (values reused from firmware/.env, never printed):"
echo " wifi: ssid=$(mask "${TW_WIFI_SSID:-}") pass=$(mask "${TW_WIFI_PASS:-}")"
echo " git: remote=$(mask "${TW_REMOTE_URL:-}") gh_user=$(mask "${TW_GH_USER:-}") pat=$(mask "${TW_PAT:-}")"
echo " author: name=$(mask "${TW_AUTHOR_NAME:-}") email=$(mask "${TW_AUTHOR_EMAIL:-}")"
else
echo "note: no TW_* values in firmware/.env — leaving any existing $conf untouched"
echo " (copy .env.example → firmware/.env and fill in Wi-Fi + PAT to provision)"
fi
# Flush + eject so the card can go straight into Typoena.
_eject vol:
#!/usr/bin/env bash
set -euo pipefail
sync
echo "ejecting {{vol}}"
if diskutil eject "{{vol}}" >/dev/null 2>&1; then
echo "✅ card ejected — remove it and insert into Typoena"
else
echo "⚠️ eject failed (a file may still be open) — eject '{{vol}}' from Finder before removing"
fi