Compare commits

..

5 Commits

Author SHA1 Message Date
Julien Calixte
ac70d8926f docs(postmortems): record Path 2 -- libgit2 runs on device 2026-07-05 21:28:47 +02:00
Julien Calixte
b216a67c3b chore(firmware): add git2 build/flash/monitor just recipes 2026-07-05 21:28:47 +02:00
Julien Calixte
2b268fe168 feat(firmware): wire libgit2 component + bind git2 in system mode
Adds the extra_components metadata, git2 as an optional dep behind the
`git` feature (default-features off -> no openssl-sys/libssh2-sys), and a
git_smoke bin gated on it. libgit2-sys/libz-sys run in system mode against
fake pkg-config files (empty Libs) so they emit no link flags -- symbols
come from the esp-idf component. Proven on device: git2 version + a blob
SHA1 through the mbedTLS backend.
2026-07-05 21:28:47 +02:00
Julien Calixte
bea3058004 feat(firmware): add libgit2 esp-idf component (CMake + mbedTLS shims)
Builds vendored libgit2 as an esp-idf component so it inherits the
lwip/mbedtls/pthread/vfs/newlib include+link graph -- the include cascade
that sank CFLAGS injection never appears. Configured for mbedTLS via a
hand-written git2_features.h. Small port surface, libgit2 sources
untouched: poll.h shim, lstat==stat (esp_port.h), p_mmap via malloc+read
(esp_map.c), and identity/symlink stubs (esp_stubs.c). Registers empty
when LIBGIT2_SRC is unset so the editor build is unaffected.
2026-07-05 21:28:47 +02:00
Julien Calixte
8883a55df6 feat(firmware): vendor libgit2 v1.9.4 as a git submodule
Pins the exact version libgit2-sys 0.18.5 expects, so git2's safe API can
bind it in system mode. Sits under the esp-idf component that builds it;
clone with --recurse-submodules to fetch the source.
2026-07-05 21:28:47 +02:00
14 changed files with 551 additions and 4 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "firmware/components/libgit2/vendor"]
path = firmware/components/libgit2/vendor
url = https://github.com/libgit2/libgit2.git

View File

@@ -192,10 +192,12 @@ up.
the C cross-compiles; the include cascade does not converge via CFLAGS
injection (path 1 dead end). Decision: **path 2** (libgit2 as an esp-idf
component with `USE_HTTPS=mbedTLS`).
- [ ] Path 2: add libgit2 as an esp-idf component (component CMake +
`REQUIRES lwip mbedtls pthread newlib vfs`, `USE_HTTPS=mbedTLS`), then bind
from Rust. Solves the include cascade *and* TLS; likely removes the need
for a custom subtransport.
- [x] Path 2: add libgit2 as an esp-idf component — **compiles AND links
2026-07-05** (Gate A + Gate B). libgit2 1.9.4 built as a component with
`REQUIRES mbedtls lwip pthread vfs newlib`; the include cascade vanished as
predicted. mbedTLS wired directly (`GIT_MBEDTLS` + `GIT_SHA1/256_MBEDTLS`).
A `git_smoke` bin calling `git_libgit2_init/version/shutdown` links clean:
538 `git_*` functions in the ELF, +514 KB text. See "Path 2 result" below.
- [x] Flash the PSRAM build and confirm the SPIRAM heap region — **done
2026-07-05**: 8192K pool added to heap, memory test OK.
- [x] Run the desktop spike against a real GitHub test repo — **done 2026-07-05**
@@ -205,9 +207,112 @@ up.
- [ ] Revise the `git` module section of the technical doc (it still describes
gix crates/transport) once the device path is confirmed.
## Path 2 result — libgit2 compiles and links on xtensa (Gate A + Gate B)
The bet paid off. libgit2 **1.9.4** (the exact version `libgit2-sys 0.18.5`
vendors, chosen so `git2`'s safe Rust API can bind it in system mode later)
builds as an esp-idf component and links into a real image.
**Why a component beat Path 1.** Registering libgit2 with
`REQUIRES mbedtls lwip pthread vfs newlib` makes it inherit those components'
include + link graph. Path 1's manual CFLAGS injection died because resolving
one component's headers exposes the next (`arpa/inet.h``sys/ioctl.h` → …).
The component model walks that graph for us — the cascade never appeared.
**mbedTLS, not OpenSSL.** The libgit2-sys wrapper only offers
openssl/securetransport/winhttp, but the C library has an mbedTLS backend
(`streams/mbedtls.c`, `hash/mbedtls.c`). A hand-written `git2_features.h` selects
`GIT_HTTPS` + `GIT_MBEDTLS` + `GIT_SHA1_MBEDTLS` + `GIT_SHA256_MBEDTLS`, so TLS
and hashing reuse the mbedtls esp-idf already ships (and Spike 6 validated).
**The port surface was small** — four shims, libgit2 sources untouched (so we
never fork 1.9.4):
| Gap on esp-idf (picolibc + VFS) | Shim |
|---|---|
| no top-level `<poll.h>` (only `<sys/poll.h>`) | forwarding `poll.h` on the include path |
| `lstat` absent (no symlinks) | `#define lstat stat`, force-included via `esp_port.h` |
| `<sys/mman.h>` absent | `esp_map.c``p_mmap` via `git__malloc` + `read` (pack pages land in PSRAM) |
| `getuid`/`geteuid`/`getgid`/`getppid`/`getpgid`/`getsid`/`getpwuid_r`/`readlink`/`utimes` declared but not implemented | `esp_stubs.c` — single-root-user, no-user-db, no-symlink answers |
Also: gcc 14 promoted `-Wimplicit-function-declaration` /
`-Wincompatible-pointer-types` to hard errors; this pre-gcc14 C trips them
benignly, so the component downgrades them to warnings. `unix/process.c`
(fork/`sys/wait.h`) is excluded — only the SSH-exec transport we don't enable
uses it.
**Verification.** A throwaway `git_smoke` bin (`git_libgit2_init` /
`_version` / `_shutdown` via three hand externs) links with **zero undefined
references**: `nm` shows **538 `git_*` text symbols** in the ELF (`git_index_*`,
`git_repository_*`, `git_commit_*`, `git_remote_*`), the four shims present,
+514 KB `.text` (negligible against 16 MB flash).
**Gate C — RAN ON HARDWARE 2026-07-05.** Flashed `git_smoke` to the S3; the
linked library reports `1.9.4`, `git_libgit2_init() -> 1` (global init ran —
registers the mbedTLS stream + HTTP transport + hash backends),
`git_libgit2_shutdown() -> 0`, clean. No crash/assert/hang. So libgit2 +
mbedTLS **compiles, links, and executes** on the ESP32-S3 — the full Path 2
de-risk. Still unproven: an actual `repository_init``commit``push` over
mbedTLS HTTPS (needs Wi-Fi/SNTP from Spike 6 + a working-copy location).
**Build mechanics learned.** The component is wired via
`[[package.metadata.esp-idf-sys.extra_components]]` `component_dirs`, pointed at
on-disk source through a `LIBGIT2_SRC` env var (probe stage — not yet vendored).
esp-idf-sys emits no `rerun-if-*`, so editing the *root* Cargo.toml or the
component doesn't retrigger its build script once it has succeeded; forcing a
reconfigure means `rm -rf target/**/.fingerprint/esp-idf-sys-*` (cheap — the
159 MB cmake cache in the OUT_DIR persists, so only the changed component
recompiles).
**Gate D — `git2` safe-API binding LINKS 2026-07-05.** Replaced the hand
externs with the real path: the `git2` crate (default-features off, so no
openssl-sys/libssh2-sys) bound to our component via `libgit2-sys` in **system
mode** (`LIBGIT2_NO_VENDOR=1`). The trick: we don't want libgit2-sys to build
*or* link anything — esp-idf already links `liblibgit2.a` inside its component
group (verified in `build.ninja`: `esp-idf/libgit2/liblibgit2.a` sits in the
`libespidf.elf` `LINK_LIBRARIES`, and the group is repeated ~6× so libgit2's
refs to mbedtls/lwip resolve). So a **fake pkg-config with empty `Libs`**
(`firmware/pkgconfig/{libgit2,zlib}.pc`, found via `PKG_CONFIG_LIBDIR` +
`PKG_CONFIG_ALLOW_CROSS=1`) makes both libgit2-sys's and libz-sys's probes
succeed while emitting nothing; the symbols come from the component. `git_smoke`
now uses `git2::Version` + `Oid::hash_object` and links with zero undefined refs
`nm` confirms `git_odb_hash`, `git_oid_tostr`, `git_error_last`, and
`mbedtls_sha1_starts` all **defined** in the ELF.
**Build gotcha (important):** esp-idf-sys forwards the app's link args only when
its build script reruns, and emits no `rerun-if-*`. After the component set
changes, the forwarded args go stale — `rm -rf
target/**/.fingerprint/esp-idf-sys-*` before building forces a fresh forward
(that was why the first git2 link failed with undefined `git_*`).
**Build-gating done:** `git2` is an optional dep behind the `git` feature, and
`git_smoke` has `required-features = ["git"]`, so the editor build never pulls
libgit2-sys/pkg-config. The component's CMake now registers *empty* when
`LIBGIT2_SRC` is unset, so `just build` (no env) still works.
**Open decisions before commit** (deliberately not done yet):
1. **Vendoring** — the component points at `~/.cargo`'s unpacked source via
`LIBGIT2_SRC`; not reproducible. Needs a submodule pinned to `v1.9.4` (or a
vendored copy). This also lets the `just flash-git` recipe drop the env vars.
2. **Component build burden**`extra_components` still compiles all ~200
libgit2 files on a clean build even for the editor (cached after; Rust side
is already gated). Accept, or gate the C compile too.
3. ~~Runtime (Gate D on HW)~~**DONE 2026-07-05.** `just flash-git` on the S3:
`git2 crate is talking to libgit2 1.9.4`, then `sha1(blob "hello") =
b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0` + "hash matches" — i.e. git2 →
libgit2 → mbedTLS SHA1 all ran correctly on device. Full chain proven.
4. **The real thing**`repository_init``commit``push` over mbedTLS
HTTPS (needs Wi-Fi/SNTP from Spike 6 + a working-copy location).
## Artifacts (this session)
- `spikes/spike7-git-push/` — the desktop spike crate (`src/main.rs`,
`Cargo.toml`, `README.md`, `.env.example`).
- `firmware/components/libgit2/` — the esp-idf component (uncommitted probe):
`CMakeLists.txt`, `git2_features.h`, `poll.h`, `esp_port.h`, `esp_map.c`,
`esp_stubs.c`.
- `firmware/src/bin/git_smoke.rs` + Cargo.toml `[[bin]]`/`extra_components`
(uncommitted probe wiring).
- ADR-004 — outcome note appended (kill-switch fired → libgit2).
- `docs/v0.1-mvp-technical.md` — risk-table row updated (gix push → libgit2).

View File

@@ -29,6 +29,15 @@ name = "sd_fat"
path = "src/bin/sd_fat.rs"
harness = false
# Spike 7 Path 2 — libgit2 link/run smoke via the git2 safe API. Gated behind
# the `git` feature so the editor build never pulls libgit2-sys/pkg-config.
# Build: cargo build --release --bin git_smoke --features git (env in justfile).
[[bin]]
name = "git_smoke"
path = "src/bin/git_smoke.rs"
harness = false
required-features = ["git"]
[profile.release]
opt-level = "s"
@@ -40,9 +49,17 @@ esp-idf-sys = { git = "https://github.com/esp-rs/esp-idf-sys.git" }
esp-idf-hal = { git = "https://github.com/esp-rs/esp-idf-hal.git" }
esp-idf-svc = { git = "https://github.com/esp-rs/esp-idf-svc.git" }
[features]
# Pulls the git2 safe API. libgit2 itself is built by the esp-idf component
# (firmware/components/libgit2/) with mbedTLS; git2/libgit2-sys are used in
# system mode (LIBGIT2_NO_VENDOR=1) purely for the Rust bindings, so we disable
# their default features to avoid dragging in openssl-sys/libssh2-sys.
git = ["dep:git2"]
[dependencies]
anyhow = "1"
log = "0.4"
git2 = { version = "0.20", default-features = false, optional = true }
esp-idf-svc = { version = "0.52.1", features = ["critical-section", "embassy-time-driver", "embassy-sync"] }
# Remove `generic-queue-8` if you plan to use `embassy-time` WITH `embassy-executor`
embassy-time = { version = "0.5", features = ["generic-queue-8"] }
@@ -53,3 +70,10 @@ embedded-svc = "0.29"
[build-dependencies]
embuild = "0.33"
# TEMPORARY — Spike 7 Path 2 Gate A probe. Builds libgit2 as an esp-idf
# component (see firmware/components/libgit2/). Compiled against on-disk source
# via LIBGIT2_SRC. Revert if the probe fails; promote (vendor the source) if it
# passes.
[[package.metadata.esp-idf-sys.extra_components]]
component_dirs = ["components/libgit2"]

View File

@@ -0,0 +1,108 @@
# libgit2 as an ESP-IDF component (Spike 7, Path 2 — Gate A probe).
#
# Builds libgit2 1.9.4 (the exact version libgit2-sys 0.18.5 expects, so git2's
# safe Rust API can bind it in system mode later) from an on-disk source tree
# pointed to by $ENV{LIBGIT2_SRC}, configured for mbedTLS.
#
# Why a component and not CFLAGS injection (Path 1): registering as a component
# makes libgit2 inherit the lwip / mbedtls / pthread / vfs / newlib include and
# link graph. Path 1 died because manually injecting one component's includes
# cascades into the next (arpa/inet.h -> sys/ioctl.h -> ...). The component
# model resolves that graph for us.
#
# GATE A SCOPE: prove libgit2's C compiles under the esp-idf/xtensa toolchain.
# Not wired to any Rust caller yet — esp-idf builds all components (COMPONENTS
# is empty in esp-idf-sys's project template), so this compiles regardless.
set(LG2 $ENV{LIBGIT2_SRC})
if(NOT LG2 OR NOT EXISTS "${LG2}/include/git2.h")
# No source provided → register an empty component and bail. This keeps the
# editor build (`just build`, no env) working: esp-idf builds every
# component, but with nothing to compile this one is a no-op. Only `git`
# builds set LIBGIT2_SRC. (Once libgit2 is vendored into the repo this
# branch becomes dead — LIBGIT2_SRC will always resolve.)
message(STATUS "libgit2 component: LIBGIT2_SRC unset/invalid — empty component (git feature off)")
idf_component_register()
return()
endif()
# Source list mirrors libgit2-sys's build.rs, with two deltas:
# - hashing/TLS = mbedtls (one hash file) instead of sha1dc + openssl
# - bundled zlib compiled in (deps/zlib) instead of linking libz-sys
# The transports/ and streams/ dirs are compiled wholesale; each file guards
# itself on the feature macros in git2_features.h.
file(GLOB LG2_SRCS
"${LG2}/src/libgit2/*.c"
"${LG2}/src/libgit2/transports/*.c"
"${LG2}/src/libgit2/streams/*.c"
"${LG2}/src/util/*.c"
"${LG2}/deps/llhttp/*.c"
"${LG2}/deps/xdiff/*.c"
"${LG2}/deps/pcre/*.c"
"${LG2}/deps/zlib/*.c"
)
list(APPEND LG2_SRCS
"${LG2}/src/util/allocators/failalloc.c"
"${LG2}/src/util/allocators/stdalloc.c"
"${LG2}/src/util/unix/realpath.c"
"${LG2}/src/util/hash/mbedtls.c" # SHA1 + SHA256 via mbedtls
"${CMAKE_CURRENT_LIST_DIR}/esp_map.c" # p_mmap via malloc+read (no <sys/mman.h>)
"${CMAKE_CURRENT_LIST_DIR}/esp_stubs.c" # getuid/readlink/utimes/... stubs
# NOTE: unix/map.c replaced by esp_map.c — picolibc has no <sys/mman.h>.
# NOTE: unix/process.c deliberately excluded — needs fork()/sys/wait.h,
# only used by the SSH-exec transport we don't enable.
)
list(LENGTH LG2_SRCS LG2_NSRC)
if(LG2_NSRC LESS 50)
message(FATAL_ERROR "libgit2 glob returned only ${LG2_NSRC} files — LIBGIT2_SRC likely wrong")
endif()
message(STATUS "libgit2 component: ${LG2_NSRC} source files from ${LG2}")
idf_component_register(
SRCS ${LG2_SRCS}
INCLUDE_DIRS "${LG2}/include"
PRIV_INCLUDE_DIRS
"${CMAKE_CURRENT_LIST_DIR}" # our hand-written git2_features.h
"${LG2}/src/libgit2"
"${LG2}/src/util"
"${LG2}/src/util/hash"
"${LG2}/deps/llhttp"
"${LG2}/deps/xdiff"
"${LG2}/deps/pcre"
"${LG2}/deps/zlib"
REQUIRES mbedtls lwip pthread vfs newlib
)
# PCRE (bundled regex backend) compile-time config — copied from libgit2-sys's
# build.rs. Global -D because libgit2 shares one config.h across the tree.
target_compile_definitions(${COMPONENT_LIB} PRIVATE
GIT_REGEX_BUILTIN=1
HAVE_STDINT_H=1
HAVE_MEMMOVE=1
NO_RECURSE=1
NEWLINE=10
POSIX_MALLOC_THRESHOLD=10
LINK_SIZE=2
PARENS_NEST_LIMIT=250
MATCH_LIMIT=10000000
MATCH_LIMIT_RECURSION=MATCH_LIMIT
MAX_NAME_SIZE=32
MAX_NAME_COUNT=10000
)
# Force-include our platform shims into every libgit2 TU (lstat==stat, etc.).
target_compile_options(${COMPONENT_LIB} PRIVATE
"-include" "${CMAKE_CURRENT_LIST_DIR}/esp_port.h"
)
# gcc 14 promoted these from warnings to hard errors; this vendored C (last
# tested on gcc <14) trips them benignly (int32_t/long pointer aliasing on a
# 32-bit target; internal iterator callback casts). Downgrade to warnings.
# Missing POSIX functions surface as implicit-declaration warnings, then as
# unresolved symbols at link — that's how we enumerate the shims still needed.
target_compile_options(${COMPONENT_LIB} PRIVATE
-Wno-error=implicit-function-declaration
-Wno-error=incompatible-pointer-types
-Wno-error=implicit-int
)

View File

@@ -0,0 +1,84 @@
/*
* esp_map.c — p_mmap/p_munmap for libgit2 on esp-idf (Spike 7, Path 2).
*
* Replaces src/util/unix/map.c, which needs <sys/mman.h> (absent on
* picolibc/esp-idf). libgit2 uses p_mmap read-only, to view pack files and the
* index. We emulate it by allocating a buffer and reading the range into it.
* Allocations go through git__malloc, so with PSRAM in the heap they land in
* the 8 MB external RAM rather than the ~340 KB internal DRAM.
*
* Limitation: writable/shared mappings are not written back (libgit2 does not
* mmap for writing in the paths we exercise). If that ever changes it will
* surface at runtime, not here.
*/
#include "git2_util.h"
#include "map.h"
#include <unistd.h>
#include <string.h>
#include <errno.h>
int git__page_size(size_t *page_size)
{
*page_size = 4096;
return 0;
}
int git__mmap_alignment(size_t *alignment)
{
*alignment = 4096;
return 0;
}
int p_mmap(git_map *out, size_t len, int prot, int flags, int fd, off64_t offset)
{
unsigned char *data;
size_t got = 0;
GIT_UNUSED(prot);
GIT_UNUSED(flags);
GIT_MMAP_VALIDATE(out, len, prot, flags);
out->data = NULL;
out->len = 0;
data = git__malloc(len);
GIT_ERROR_CHECK_ALLOC(data);
if (lseek(fd, offset, SEEK_SET) < 0) {
git_error_set(GIT_ERROR_OS, "failed to seek for mmap emulation");
git__free(data);
return -1;
}
while (got < len) {
ssize_t n = read(fd, data + got, len - got);
if (n < 0) {
if (errno == EINTR)
continue;
git_error_set(GIT_ERROR_OS, "failed to read for mmap emulation");
git__free(data);
return -1;
}
if (n == 0)
break; /* short file: zero-fill the tail, like a real mapping */
got += (size_t)n;
}
if (got < len)
memset(data + got, 0, len - got);
out->data = data;
out->len = len;
return 0;
}
int p_munmap(git_map *map)
{
GIT_ASSERT_ARG(map);
git__free(map->data);
map->data = NULL;
map->len = 0;
return 0;
}

View File

@@ -0,0 +1,20 @@
/*
* esp_port.h — platform shims for building libgit2 on esp-idf, force-included
* into every libgit2 TU via `-include` (Spike 7, Path 2).
*
* esp-idf's C library is picolibc and its filesystem layer is the VFS, which
* has no symlink concept. So POSIX calls libgit2 assumes on "unix" are either
* absent or degenerate here. Shimming them in a force-included header keeps
* libgit2's own sources untouched (important: we don't want to fork 1.9.4).
*/
#ifndef LIBGIT2_ESPIDF_PORT_H
#define LIBGIT2_ESPIDF_PORT_H
#include <sys/stat.h>
/* No symlinks on FAT/LittleFS: lstat is just stat. */
#ifndef lstat
#define lstat stat
#endif
#endif

View File

@@ -0,0 +1,57 @@
/*
* esp_stubs.c — POSIX identity/symlink stubs for libgit2 on esp-idf
* (Spike 7, Path 2).
*
* picolibc *declares* these in <unistd.h>/<pwd.h> but does not implement them:
* esp-idf has no users, groups, processes, or symlinks. libgit2 calls them
* while resolving config paths, ownership checks, and temp names. We provide
* definitions that model "a single root user, no user database, no symlinks",
* which is the truthful shape of the device's flat filesystem.
*/
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#include <errno.h>
#include <sys/time.h>
/* One implicit root user/group. */
uid_t getuid(void) { return 0; }
uid_t geteuid(void) { return 0; }
gid_t getgid(void) { return 0; }
gid_t getegid(void) { return 0; }
/* No process hierarchy. */
pid_t getppid(void) { return 0; }
pid_t getpgid(pid_t pid) { (void)pid; return 0; }
pid_t getsid(pid_t pid) { (void)pid; return 0; }
/* No user database: report "no such user" so libgit2 falls back to $HOME. */
int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen,
struct passwd **result)
{
(void)uid;
(void)pwd;
(void)buf;
(void)buflen;
*result = NULL;
return 0;
}
/* No symlinks on FAT/LittleFS: nothing is ever a symbolic link. */
ssize_t readlink(const char *path, char *buf, size_t bufsiz)
{
(void)path;
(void)buf;
(void)bufsiz;
errno = EINVAL; /* POSIX: EINVAL == "named file is not a symbolic link" */
return -1;
}
/* VFS has no utimes(); accept and ignore (file mtime is cosmetic here). */
int utimes(const char *path, const struct timeval times[2])
{
(void)path;
(void)times;
return 0;
}

View File

@@ -0,0 +1,37 @@
/*
* git2_features.h — hand-written feature config for building libgit2 as an
* ESP-IDF component (Spike 7, Path 2).
*
* libgit2's CMake normally generates this from git2_features.h.in. We write it
* by hand so the build is driven by ESP-IDF's component CMake instead. The one
* substantive choice vs. the libgit2-sys vendored build: TLS + hashing use
* *mbedTLS* (which ESP-IDF already ships and uses for its own TLS), not
* OpenSSL/SecureTransport. That backend exists in the C library but the
* libgit2-sys Rust wrapper never exposes it — the reason for Path 2.
*/
#ifndef INCLUDE_features_h__
#define INCLUDE_features_h__
#define GIT_THREADS 1
#define GIT_TRACE 1
#define GIT_ARCH_32 1
/* Bundled backends: no system deps to hunt for on the device. */
#define GIT_REGEX_BUILTIN 1
#define GIT_HTTPPARSER_BUILTIN 1
#define GIT_COMPRESSION_BUILTIN 1
/* TLS via ESP-IDF's mbedTLS (streams/mbedtls.c). */
#define GIT_HTTPS 1
#define GIT_MBEDTLS 1
/* Hashing via mbedTLS too (util/hash/mbedtls.c) — one crypto backend, and it
* lets us skip the sha1dc collision-detection sources. */
#define GIT_SHA1_MBEDTLS 1
#define GIT_SHA256_MBEDTLS 1
/* Socket readiness via poll()/select() (both provided by lwip + vfs). */
#define GIT_IO_POLL 1
#define GIT_IO_SELECT 1
#endif

View File

@@ -0,0 +1,12 @@
/*
* poll.h shim for the libgit2 esp-idf component (Spike 7, Path 2).
*
* libgit2's posix.h does `#include <poll.h>` when GIT_IO_POLL is set, but
* esp-idf's newlib ships poll() under <sys/poll.h> only — there is no
* top-level <poll.h>. This forwarding header (on the component's private
* include path) bridges the gap without touching libgit2's sources.
*/
#ifndef LIBGIT2_ESPIDF_POLL_SHIM_H
#define LIBGIT2_ESPIDF_POLL_SHIM_H
#include <sys/poll.h>
#endif

View File

@@ -10,6 +10,15 @@ 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"
# 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:
@@ -51,6 +60,18 @@ flash-sd:
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}}
# detect board, print chip/MAC/flash size
info:
espflash board-info

View File

@@ -0,0 +1,14 @@
# Fake pkg-config file for the libgit2 esp-idf component (Spike 7, Path 2).
#
# libgit2-sys in system mode (LIBGIT2_NO_VENDOR=1) probes for a system libgit2
# via pkg-config; without one it aborts, with one it emits link flags. We don't
# want it to build OR link anything — esp-idf already builds liblibgit2.a and
# puts it in the final link group (that's how git_smoke's 538 symbols resolved).
# So this .pc makes the probe *succeed* (right version, in range [1.9.4,1.10.0))
# while emitting NOTHING (empty Libs/Cflags). The symbols come from the
# component; libgit2-sys supplies only its hand-written Rust bindings.
Name: libgit2
Description: libgit2 built as an esp-idf component (mbedTLS)
Version: 1.9.4
Libs:
Cflags:

View File

@@ -0,0 +1,16 @@
# Fake pkg-config file for zlib (Spike 7, Path 2).
#
# libz-sys is a non-optional dependency of libgit2-sys. Left to itself it would
# either grab the host (macOS) zlib or vendor-build its own libz.a — and either
# way its zlib symbols would collide with the zlib we bundle *inside* the
# libgit2 component (deps/zlib, which lives in esp-idf's link group where the
# ordering is safe). This .pc makes libz-sys's probe succeed while emitting
# NOTHING, so it contributes no symbols and the component's bundled zlib wins.
#
# PKG_CONFIG_LIBDIR is pointed at this directory only, so neither probe can fall
# through to the host's real zlib.pc.
Name: zlib
Description: zlib bundled into the libgit2 esp-idf component
Version: 1.3.1
Libs:
Cflags:

View File

@@ -0,0 +1,45 @@
//! Spike 7 — Path 2 Gate D: the `git2` safe API on device.
//!
//! Gates A/B/C proved libgit2 compiles, links, and runs via hand externs. This
//! replaces those with the real integration path: the `git2` crate's safe Rust
//! API, bound to our esp-idf-built libgit2 through `libgit2-sys` in system mode
//! (LIBGIT2_NO_VENDOR=1 + the fake pkg-config in firmware/pkgconfig/). If this
//! links and runs, the desktop spike's add/commit/push code transfers to device.
//!
//! Two checks, both in-memory (no filesystem or network needed yet):
//! 1. git2::Version — proves the safe wrapper reaches the linked library.
//! 2. Oid::hash_object — computes a blob SHA1 through libgit2's ODB, which
//! runs the mbedTLS hash backend. The expected hash is known, so a correct
//! value proves the whole path (git2 -> libgit2 -> mbedtls) end to end.
use esp_idf_svc::sys;
use git2::{ObjectType, Oid};
fn main() {
sys::link_patches();
esp_idf_svc::log::EspLogger::initialize_default();
log::info!("Typoena — Spike 7 Path 2 Gate D (git2 safe API on device)");
let (major, minor, patch) = git2::Version::get().libgit2_version();
log::info!("git2 crate is talking to libgit2 {major}.{minor}.{patch}");
// `git hash-object` of the 5 bytes "hello" is a fixed, well-known value.
match Oid::hash_object(ObjectType::Blob, b"hello") {
Ok(oid) => {
log::info!("sha1(blob \"hello\") = {oid}");
if oid.to_string() == "b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0" {
log::info!("hash matches `git hash-object` — mbedTLS SHA1 backend correct");
} else {
log::warn!("hash MISMATCH — mbedTLS SHA1 backend produced the wrong digest");
}
}
Err(e) => log::error!("Oid::hash_object failed: {e}"),
}
log::info!("✅ git2 safe API linked and ran on device");
loop {
std::thread::sleep(std::time::Duration::from_secs(60));
}
}