Make `just build`/`just flash` the nominal product build (--features git + LIBGIT2_SRC), and move the fast editor-only build to `build-light`/ `flash-light`. Verified the git default links libgit2 into the firmware ELF (1.2 MB) cleanly.
373 lines
18 KiB
Makefile
373 lines
18 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) — the nominal product build: full firmware WITH git
|
|
# publishing (compiles libgit2 + mbedTLS, links git2). For fast iteration on the
|
|
# editor / EPD / USB / SD without paying for libgit2, use `build-light`.
|
|
build:
|
|
{{esp_env}} {{git_env}} cargo build --release --bin firmware --features git
|
|
|
|
# build + flash + open serial monitor (full firmware, see `build`)
|
|
flash:
|
|
{{esp_env}} {{git_env}} cargo run --release --bin firmware --features git
|
|
|
|
# LIGHT editor build — no git: no libgit2 component (LIBGIT2_SRC unset) and no
|
|
# git2 crate (`git` feature off), so it builds much faster. `:sync` saves locally
|
|
# and skips the push. Use for anything but exercising publish.
|
|
build-light:
|
|
{{esp_env}} cargo build --release --bin firmware
|
|
|
|
# light build + flash + open serial monitor (see `build-light`)
|
|
flash-light:
|
|
{{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 (derive + prompt)
|
|
#
|
|
# Config (Wi-Fi + PAT + git identity) needs no firmware/.env: each value is
|
|
# resolved from firmware/.env if present, else derived from tools the machine
|
|
# already has (git config, gh, the active Wi-Fi network + its Keychain
|
|
# password), else asked for at an interactive prompt with the derived value as
|
|
# the default. The PAT is always typed by hand — never derived (a broad
|
|
# `gh auth token` on a plaintext card would defeat the scoped-token model).
|
|
#
|
|
# 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" "{{repo_src}}"
|
|
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), then eject — e.g. to
|
|
# rotate the PAT or switch networks without touching repo/. Values are derived +
|
|
# prompted (see the section header); firmware/.env is an optional override. With
|
|
# no repo arg, the git remote is derived from the card's existing clone.
|
|
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/"
|
|
|
|
# Resolve + write <vol>/typoena.conf (Wi-Fi + PAT + git identity). Each value
|
|
# runs a ladder: firmware/.env (loaded via `set dotenv-load`) → derived from
|
|
# tools already on the machine (git config, gh, the active Wi-Fi network + its
|
|
# System-keychain password) → interactive prompt, with the derived value as the
|
|
# default. So a newcomer needs no .env: they Enter through the defaults and paste
|
|
# a PAT once. The PAT is never derived (a broad `gh auth token` on plaintext
|
|
# removable media would defeat the scoped-token model) and 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. <repo_src> (optional) seeds the git remote; without it the
|
|
# remote is derived from the card's existing clone.
|
|
_write-conf vol repo_src="":
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
conf="{{vol}}/typoena.conf"
|
|
repo_src="{{repo_src}}"; repo_src="${repo_src%/}"
|
|
interactive=1; [ -t 0 ] || interactive=0
|
|
|
|
# ask "label" "default" → chosen value on stdout; prompt shown on stderr (so
|
|
# it survives the $(...) capture). Non-interactive → returns the default.
|
|
ask() {
|
|
local label="$1" def="${2:-}" ans
|
|
if [ "$interactive" = 0 ]; then printf '%s' "$def"; return; fi
|
|
read -r -p " $label${def:+ [$def]}: " ans || ans=""
|
|
printf '%s' "${ans:-$def}"
|
|
}
|
|
# like ask but hides input (secrets). A present default reads as "keep current".
|
|
ask_secret() {
|
|
local label="$1" def="${2:-}" ans
|
|
if [ "$interactive" = 0 ]; then printf '%s' "$def"; return; fi
|
|
read -r -s -p " $label${def:+ [keep current]}: " ans || ans=""; printf '\n' >&2
|
|
printf '%s' "${ans:-$def}"
|
|
}
|
|
|
|
# ── derive defaults (silent; empty when a tool is missing/unauthed) ──────────
|
|
# remote: .env → source repo's origin → the card's existing clone.
|
|
remote="${TW_REMOTE_URL:-}"
|
|
if [ -z "$remote" ] && [ -n "$repo_src" ] && [ -d "$repo_src/.git" ]; then
|
|
remote="$(git -C "$repo_src" remote get-url origin 2>/dev/null || true)"
|
|
fi
|
|
if [ -z "$remote" ] && [ -d "{{vol}}/{{sd_repo_dir}}/.git" ]; then
|
|
remote="$(git -C "{{vol}}/{{sd_repo_dir}}" remote get-url origin 2>/dev/null || true)"
|
|
fi
|
|
a_name="${TW_AUTHOR_NAME:-$(git config user.name 2>/dev/null || true)}"
|
|
a_email="${TW_AUTHOR_EMAIL:-$(git config user.email 2>/dev/null || true)}"
|
|
gh_user="${TW_GH_USER:-}"
|
|
[ -z "$gh_user" ] && gh_user="$(gh api user --jq .login 2>/dev/null || true)"
|
|
# ssid: .env → the Mac's active Wi-Fi network.
|
|
wifi_if="$(networksetup -listallhardwareports 2>/dev/null | awk '/Wi-Fi/{getline; print $2; exit}')"
|
|
ssid="${TW_WIFI_SSID:-}"
|
|
[ -z "$ssid" ] && ssid="$(networksetup -getairportnetwork "${wifi_if:-en0}" 2>/dev/null | sed -n 's/^Current Wi-Fi Network: //p')"
|
|
wifi_pass="${TW_WIFI_PASS:-}"
|
|
pat="${TW_PAT:-}"
|
|
|
|
# ── confirm / fill via prompts ──────────────────────────────────────────────
|
|
[ "$interactive" = 1 ] && echo "configuring $conf — press Enter to accept each [default]:" >&2
|
|
ssid="$(ask "Wi-Fi SSID" "$ssid")"
|
|
# Keychain read happens after the SSID is final (the user may have edited it).
|
|
# Reading a System-keychain Wi-Fi password can pop a macOS auth dialog — only
|
|
# attempt it interactively so scripted runs never trigger a surprise prompt.
|
|
if [ -z "$wifi_pass" ] && [ -n "$ssid" ] && [ "$interactive" = 1 ]; then
|
|
echo " looking up Wi-Fi password for '$ssid' in Keychain (approve the macOS dialog if it appears)…" >&2
|
|
wifi_pass="$(security find-generic-password -wa "$ssid" 2>/dev/null || true)"
|
|
fi
|
|
wifi_pass="$(ask_secret "Wi-Fi password" "$wifi_pass")"
|
|
remote="$(ask "Git remote URL" "$remote")"
|
|
gh_user="$(ask "GitHub username" "$gh_user")"
|
|
pat="$(ask_secret "GitHub PAT (fine-grained, contents:write)" "$pat")"
|
|
a_name="$(ask "Commit author name" "$a_name")"
|
|
a_email="$(ask "Commit author email" "$a_email")"
|
|
|
|
# ── require the essentials (a blank config ships a dead device) ──────────────
|
|
missing=""
|
|
[ -n "$ssid" ] || missing="$missing Wi-Fi-SSID"
|
|
[ -n "$remote" ] || missing="$missing git-remote-URL"
|
|
[ -n "$pat" ] || missing="$missing GitHub-PAT"
|
|
if [ -n "$missing" ]; then
|
|
echo "error: missing required config:$missing" >&2
|
|
[ "$interactive" = 0 ] && echo " (no TTY — set these in firmware/.env or run interactively)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ── write ───────────────────────────────────────────────────────────────────
|
|
{
|
|
printf '# Typoena runtime config — generated by `just init`/`provision`.\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' "$ssid"
|
|
printf 'TW_WIFI_PASS=%s\n' "$wifi_pass"
|
|
printf 'TW_REMOTE_URL=%s\n' "$remote"
|
|
printf 'TW_GH_USER=%s\n' "$gh_user"
|
|
printf 'TW_PAT=%s\n' "$pat"
|
|
printf 'TW_AUTHOR_NAME=%s\n' "$a_name"
|
|
printf 'TW_AUTHOR_EMAIL=%s\n' "$a_email"
|
|
} > "$conf"
|
|
mask() { if [ -n "${1:-}" ]; then printf 'set'; else printf 'MISSING'; fi; }
|
|
echo "wrote $conf (secrets never printed):"
|
|
echo " wifi: ssid=$(mask "$ssid") pass=$(mask "$wifi_pass")"
|
|
echo " git: remote=$(mask "$remote") gh_user=$(mask "$gh_user") pat=$(mask "$pat")"
|
|
echo " author: name=$(mask "$a_name") email=$(mask "$a_email")"
|
|
|
|
# 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
|