Compare commits
5 Commits
9f36d786d0
...
ac70d8926f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac70d8926f | ||
|
|
b216a67c3b | ||
|
|
2b268fe168 | ||
|
|
bea3058004 | ||
|
|
8883a55df6 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "firmware/components/libgit2/vendor"]
|
||||
path = firmware/components/libgit2/vendor
|
||||
url = https://github.com/libgit2/libgit2.git
|
||||
@@ -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).
|
||||
|
||||
@@ -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"]
|
||||
|
||||
108
firmware/components/libgit2/CMakeLists.txt
Normal file
108
firmware/components/libgit2/CMakeLists.txt
Normal 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
|
||||
)
|
||||
84
firmware/components/libgit2/esp_map.c
Normal file
84
firmware/components/libgit2/esp_map.c
Normal 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;
|
||||
}
|
||||
20
firmware/components/libgit2/esp_port.h
Normal file
20
firmware/components/libgit2/esp_port.h
Normal 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
|
||||
57
firmware/components/libgit2/esp_stubs.c
Normal file
57
firmware/components/libgit2/esp_stubs.c
Normal 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;
|
||||
}
|
||||
37
firmware/components/libgit2/git2_features.h
Normal file
37
firmware/components/libgit2/git2_features.h
Normal 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
|
||||
12
firmware/components/libgit2/poll.h
Normal file
12
firmware/components/libgit2/poll.h
Normal 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
|
||||
1
firmware/components/libgit2/vendor
Submodule
1
firmware/components/libgit2/vendor
Submodule
Submodule firmware/components/libgit2/vendor added at f7164261c9
@@ -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
|
||||
|
||||
14
firmware/pkgconfig/libgit2.pc
Normal file
14
firmware/pkgconfig/libgit2.pc
Normal 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:
|
||||
16
firmware/pkgconfig/zlib.pc
Normal file
16
firmware/pkgconfig/zlib.pc
Normal 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:
|
||||
45
firmware/src/bin/git_smoke.rs
Normal file
45
firmware/src/bin/git_smoke.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user