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.
This commit is contained in:
Julien Calixte
2026-07-05 21:28:47 +02:00
parent 8883a55df6
commit bea3058004
6 changed files with 318 additions and 0 deletions

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