diff --git a/firmware/components/libgit2/CMakeLists.txt b/firmware/components/libgit2/CMakeLists.txt new file mode 100644 index 0000000..602bcdf --- /dev/null +++ b/firmware/components/libgit2/CMakeLists.txt @@ -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 ) + "${CMAKE_CURRENT_LIST_DIR}/esp_stubs.c" # getuid/readlink/utimes/... stubs + # NOTE: unix/map.c replaced by esp_map.c — picolibc has no . + # 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 +) diff --git a/firmware/components/libgit2/esp_map.c b/firmware/components/libgit2/esp_map.c new file mode 100644 index 0000000..1a04836 --- /dev/null +++ b/firmware/components/libgit2/esp_map.c @@ -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 (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 +#include +#include + +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; +} diff --git a/firmware/components/libgit2/esp_port.h b/firmware/components/libgit2/esp_port.h new file mode 100644 index 0000000..faefe64 --- /dev/null +++ b/firmware/components/libgit2/esp_port.h @@ -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 + +/* No symlinks on FAT/LittleFS: lstat is just stat. */ +#ifndef lstat +#define lstat stat +#endif + +#endif diff --git a/firmware/components/libgit2/esp_stubs.c b/firmware/components/libgit2/esp_stubs.c new file mode 100644 index 0000000..90a6b82 --- /dev/null +++ b/firmware/components/libgit2/esp_stubs.c @@ -0,0 +1,57 @@ +/* + * esp_stubs.c — POSIX identity/symlink stubs for libgit2 on esp-idf + * (Spike 7, Path 2). + * + * picolibc *declares* these in / 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 +#include +#include +#include +#include + +/* 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; +} diff --git a/firmware/components/libgit2/git2_features.h b/firmware/components/libgit2/git2_features.h new file mode 100644 index 0000000..a120d20 --- /dev/null +++ b/firmware/components/libgit2/git2_features.h @@ -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 diff --git a/firmware/components/libgit2/poll.h b/firmware/components/libgit2/poll.h new file mode 100644 index 0000000..4400871 --- /dev/null +++ b/firmware/components/libgit2/poll.h @@ -0,0 +1,12 @@ +/* + * poll.h shim for the libgit2 esp-idf component (Spike 7, Path 2). + * + * libgit2's posix.h does `#include ` when GIT_IO_POLL is set, but + * esp-idf's newlib ships poll() under only — there is no + * top-level . 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 +#endif