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:
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
|
||||
Reference in New Issue
Block a user