Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 43636aed99 | |||
| 2baa0b9a41 | |||
| 6a8b2461cd | |||
| bcb356fe69 | |||
| dd7827059a | |||
| 208e36c899 | |||
| b97ce74d1f | |||
| 155a449c4e | |||
| 4696fd6833 | |||
| 581a351fb1 | |||
| 8ce8656de2 | |||
| 1e49560f1f | |||
| e8f0b5a9de | |||
| 40287c4cfc | |||
| 0481bea44d | |||
| 9d565ca080 | |||
| 4773dd0aa2 | |||
| 6b9d9e6c4a | |||
| b4967af13e | |||
| e3dabe3e08 | |||
| 0a0a2bcb44 | |||
| 2b2a1246e7 | |||
| f78da81aa4 | |||
| 2597a092bb | |||
| 226b798407 | |||
| 58753a88d7 | |||
| edff25180e | |||
| 6271cb42b2 | |||
| 3da9181deb | |||
| 192241c7c1 | |||
| e7c2dc7734 | |||
| f7bd99ae45 | |||
| 5c41c66a0f | |||
| 93d36fddb1 | |||
| 2d751890ea | |||
| 99b113ea9d | |||
| c087b97093 | |||
| 718a2e0c06 | |||
| b6187501fd | |||
| 18e1ab6db1 | |||
| 2dec76c87a | |||
| a36a62ca14 | |||
| 28ef43264a |
@@ -214,9 +214,18 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
run: |
|
run: |
|
||||||
|
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||||
|
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||||
|
# and the SDK silently never published. Surface failures now.
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||||
|
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||||
gcloud config set project neuron-785695
|
gcloud config set project neuron-785695
|
||||||
|
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||||
|
|
||||||
VERSION="${GITHUB_SHA:0:8}"
|
VERSION="${GITHUB_SHA:0:8}"
|
||||||
|
|
||||||
@@ -268,6 +277,12 @@ jobs:
|
|||||||
# Patches ci-base:dev in-place: pulls the existing image (which has all
|
# Patches ci-base:dev in-place: pulls the existing image (which has all
|
||||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||||
|
#
|
||||||
|
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||||
|
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||||
|
# runner where DinD/Docker availability is fragile. A failure here must
|
||||||
|
# never block or redden the job — the SDK publish above is the deliverable.
|
||||||
|
continue-on-error: true
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
|
|||||||
@@ -212,12 +212,21 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
run: |
|
run: |
|
||||||
|
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||||
|
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||||
|
# and the SDK silently never published. Surface failures now.
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||||
|
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||||
gcloud config set project neuron-785695
|
gcloud config set project neuron-785695
|
||||||
|
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||||
|
|
||||||
VERSION="${GITHUB_SHA:0:8}"
|
VERSION="${GITHUB_SHA:0:8}"
|
||||||
|
|
||||||
@@ -253,6 +262,12 @@ jobs:
|
|||||||
# Patches ci-base:stage in-place: pulls the existing image (which has all
|
# Patches ci-base:stage in-place: pulls the existing image (which has all
|
||||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||||
|
#
|
||||||
|
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||||
|
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||||
|
# runner where DinD/Docker availability is fragile. A failure here must
|
||||||
|
# never block or redden the job — the SDK publish above is the deliverable.
|
||||||
|
continue-on-error: true
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
|
|||||||
@@ -288,12 +288,21 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
run: |
|
run: |
|
||||||
|
# Fail loudly: previously this step had no `set -e`, so an auth or
|
||||||
|
# upload failure was swallowed (step exited 0 on the trailing echo)
|
||||||
|
# and the SDK silently never published. Surface failures now.
|
||||||
|
set -euo pipefail
|
||||||
|
if [ -z "${GCP_SA_KEY:-}" ]; then
|
||||||
|
echo "FATAL: GCP_SA_KEY secret is empty — cannot authenticate to publish" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
echo "${GCP_SA_KEY}" > /tmp/gcp-key.json
|
||||||
apt-get install -y -qq apt-transport-https ca-certificates curl
|
apt-get install -y -qq apt-transport-https ca-certificates curl
|
||||||
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
echo "deb [trusted=yes] https://packages.cloud.google.com/apt cloud-sdk main" > /etc/apt/sources.list.d/google-cloud-sdk.list
|
||||||
apt-get update -qq && apt-get install -y google-cloud-cli
|
apt-get update -qq && apt-get install -y google-cloud-cli
|
||||||
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
||||||
gcloud config set project neuron-785695
|
gcloud config set project neuron-785695
|
||||||
|
echo "Publishing as active account: $(gcloud config get-value account 2>/dev/null)"
|
||||||
|
|
||||||
VERSION="${GITHUB_SHA:0:8}"
|
VERSION="${GITHUB_SHA:0:8}"
|
||||||
|
|
||||||
@@ -345,6 +354,12 @@ jobs:
|
|||||||
# Patches ci-base:latest in-place: pulls the existing image (which has all
|
# Patches ci-base:latest in-place: pulls the existing image (which has all
|
||||||
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
# system deps — Node, Go, gcloud, Docker CLI, etc.) and overlays the freshly
|
||||||
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
# built El SDK on top. Keeps the full ci-base rebuild fast and incremental.
|
||||||
|
#
|
||||||
|
# continue-on-error: this is a CI-cache optimization, NOT the release
|
||||||
|
# artifact. It runs Docker (pull/build/push ~600MB) on the host-mode GCE
|
||||||
|
# runner where DinD/Docker availability is fragile. A failure here must
|
||||||
|
# never block or redden the job — the SDK publish above is the deliverable.
|
||||||
|
continue-on-error: true
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
env:
|
env:
|
||||||
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
# Link to produce the engram binary
|
# Link to produce the engram binary
|
||||||
- name: Link engram binary
|
- name: Link engram binary
|
||||||
run: |
|
run: |
|
||||||
cc -std=c11 -O2 \
|
cc -std=c11 -O2 -DHAVE_CURL \
|
||||||
-I /usr/local/lib/el \
|
-I /usr/local/lib/el \
|
||||||
-o dist/engram \
|
-o dist/engram \
|
||||||
dist/engram.c \
|
dist/engram.c \
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
# Link to produce the engram binary
|
# Link to produce the engram binary
|
||||||
- name: Link engram binary
|
- name: Link engram binary
|
||||||
run: |
|
run: |
|
||||||
cc -std=c11 -O2 \
|
cc -std=c11 -O2 -DHAVE_CURL \
|
||||||
-I /usr/local/lib/el \
|
-I /usr/local/lib/el \
|
||||||
-o dist/engram \
|
-o dist/engram \
|
||||||
dist/engram.c \
|
dist/engram.c \
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
# Link to produce the engram binary
|
# Link to produce the engram binary
|
||||||
- name: Link engram binary
|
- name: Link engram binary
|
||||||
run: |
|
run: |
|
||||||
cc -std=c11 -O2 \
|
cc -std=c11 -O2 -DHAVE_CURL \
|
||||||
-I /usr/local/lib/el \
|
-I /usr/local/lib/el \
|
||||||
-o dist/engram \
|
-o dist/engram \
|
||||||
dist/engram.c \
|
dist/engram.c \
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -17,6 +17,16 @@
|
|||||||
// 4. Append dep to order after all its transitive deps
|
// 4. Append dep to order after all its transitive deps
|
||||||
// 5. Deduplicate: skip already-ordered vessels
|
// 5. Deduplicate: skip already-ordered vessels
|
||||||
|
|
||||||
|
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||||
|
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
|
||||||
|
// give elc the C prototypes so generated install.c compiles cleanly under strict
|
||||||
|
// compilers (gcc>=14 / clang) that reject implicit function declarations.
|
||||||
|
extern fn manifest_name(src: String) -> String // manifest.el
|
||||||
|
extern fn manifest_deps(src: String) -> String // manifest.el
|
||||||
|
extern fn registry_token() -> String // registry.el
|
||||||
|
extern fn registry_find(name: String, version: String) -> String // registry.el
|
||||||
|
extern fn registry_latest_version(name: String) -> String // registry.el
|
||||||
|
|
||||||
// ── Install paths ─────────────────────────────────────────────────────────────
|
// ── Install paths ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// packages_dir returns the root directory for installed vessels.
|
// packages_dir returns the root directory for installed vessels.
|
||||||
|
|||||||
@@ -14,6 +14,15 @@
|
|||||||
// EPM_REGISTRY_ORG — org name that hosts vessel repos (default: neuron-technologies)
|
// EPM_REGISTRY_ORG — org name that hosts vessel repos (default: neuron-technologies)
|
||||||
// EPM_TOKEN — Gitea personal access token (required for publish)
|
// EPM_TOKEN — Gitea personal access token (required for publish)
|
||||||
|
|
||||||
|
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||||
|
// These symbols are defined in sibling epm modules or the El runtime and are
|
||||||
|
// resolved at link time. The `extern fn` decls give elc the C prototype so the
|
||||||
|
// generated registry.c compiles cleanly under strict compilers (gcc>=14 / clang)
|
||||||
|
// that reject implicit function declarations. Signature arity must match the
|
||||||
|
// definition; return/param types are informational (all lower to el_val_t).
|
||||||
|
extern fn config(key: String) -> String // El runtime builtin
|
||||||
|
extern fn read_installed() -> String // install.el
|
||||||
|
|
||||||
// ── Config helpers ────────────────────────────────────────────────────────────
|
// ── Config helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// registry_api_url returns the Gitea API base URL with no trailing slash.
|
// registry_api_url returns the Gitea API base URL with no trailing slash.
|
||||||
|
|||||||
@@ -6,6 +6,15 @@
|
|||||||
// Depends on: registry.el (registry_latest_version, registry_find),
|
// Depends on: registry.el (registry_latest_version, registry_find),
|
||||||
// install.el (read_installed, install_vessel, installed_version)
|
// install.el (read_installed, install_vessel, installed_version)
|
||||||
|
|
||||||
|
// ── Cross-module forward declarations ─────────────────────────────────────────
|
||||||
|
// Defined in sibling epm modules; resolved at link time. The `extern fn` decls
|
||||||
|
// give elc the C prototypes so generated update.c compiles cleanly under strict
|
||||||
|
// compilers (gcc>=14 / clang) that reject implicit function declarations.
|
||||||
|
extern fn read_installed() -> String // install.el
|
||||||
|
extern fn installed_version(name: String) -> String // install.el
|
||||||
|
extern fn install_vessel(name: String, version: String) -> Bool // install.el
|
||||||
|
extern fn registry_latest_version(name: String) -> String // registry.el
|
||||||
|
|
||||||
// ── Semver helpers ────────────────────────────────────────────────────────────
|
// ── Semver helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// semver_part extracts the Nth dot-separated component from a semver string.
|
// semver_part extracts the Nth dot-separated component from a semver string.
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#ifndef EL_PLATFORM_WIN_H
|
||||||
|
#define EL_PLATFORM_WIN_H
|
||||||
|
/*
|
||||||
|
* el_platform_win.h — Windows OS-boundary shim for el_runtime.c.
|
||||||
|
*
|
||||||
|
* Branch: feat/windows-el-runtime. Included ONLY when _WIN32 is defined; the POSIX build is
|
||||||
|
* untouched. Goal: let el_runtime.c (a BSD-sockets / dlfcn / fork host) compile and link with
|
||||||
|
* mingw-w64 into a native neuron.exe, with no behavioural change to the Linux/macOS build.
|
||||||
|
*
|
||||||
|
* What it maps:
|
||||||
|
* - sockets : winsock2 (same call names: socket/bind/listen/accept/recv/send/setsockopt).
|
||||||
|
* Sockets close with closesocket() (see el_closesocket), and the stack must be
|
||||||
|
* started once with WSAStartup — done automatically via a load-time constructor.
|
||||||
|
* - dlsym : el_runtime.c uses dlsym(RTLD_DEFAULT, name) to resolve callback/tool symbols
|
||||||
|
* exported by the main module. Windows equivalent: GetProcAddress on the process
|
||||||
|
* module. Link the soul with -Wl,--export-all-symbols so the symbols are findable.
|
||||||
|
* - popen : mapped to _popen/_pclose.
|
||||||
|
* - threads : UNCHANGED. mingw-w64 ships winpthreads, so <pthread.h> + -lpthread just work.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef WIN32_LEAN_AND_MEAN
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#endif
|
||||||
|
#include <winsock2.h>
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
#include <windows.h>
|
||||||
|
#include <io.h>
|
||||||
|
#include <process.h>
|
||||||
|
|
||||||
|
/* Portable headers mingw-w64 provides (verified present). */
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h> /* strcasecmp */
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <sys/time.h> /* mingw-w64 provides gettimeofday here */
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
/* ── socket close ─────────────────────────────────────────────────────────── */
|
||||||
|
/* Winsock closes sockets with closesocket(), not close() (close() is for file fds). The POSIX
|
||||||
|
build defines the same helper as close() so the call sites are identical across platforms. */
|
||||||
|
static inline int el_closesocket(SOCKET s) { return closesocket(s); }
|
||||||
|
|
||||||
|
/* ── winsock init (once, at load) ─────────────────────────────────────────── */
|
||||||
|
static void el__win_net_init(void) {
|
||||||
|
static int inited = 0;
|
||||||
|
if (!inited) { WSADATA w; WSAStartup(MAKEWORD(2, 2), &w); inited = 1; }
|
||||||
|
}
|
||||||
|
__attribute__((constructor)) static void el__win_ctor(void) { el__win_net_init(); }
|
||||||
|
|
||||||
|
/* ── dlsym → GetProcAddress ───────────────────────────────────────────────── */
|
||||||
|
#ifndef RTLD_DEFAULT
|
||||||
|
#define RTLD_DEFAULT ((void*)0)
|
||||||
|
#endif
|
||||||
|
static inline void* el_win_dlsym(void* handle, const char* name) {
|
||||||
|
(void)handle;
|
||||||
|
return (void*)(uintptr_t)GetProcAddress(GetModuleHandleA(NULL), name);
|
||||||
|
}
|
||||||
|
#define dlsym(h, n) el_win_dlsym((h), (n))
|
||||||
|
|
||||||
|
/* ── popen / pclose ───────────────────────────────────────────────────────── */
|
||||||
|
#define popen _popen
|
||||||
|
#define pclose _pclose
|
||||||
|
|
||||||
|
/* ── misc POSIX → Win32 shims ─────────────────────────────────────────────── */
|
||||||
|
#include <direct.h> /* _mkdir */
|
||||||
|
#define mkdir(path, mode) _mkdir(path) /* POSIX mkdir(path,mode) → _mkdir(path) */
|
||||||
|
#define timegm _mkgmtime /* UTC tm → time_t */
|
||||||
|
#define fsync(fd) _commit(fd) /* no fsync() on Windows; _commit() (<io.h>) is the equiv */
|
||||||
|
|
||||||
|
/* setenv/unsetenv: not in the Windows CRT; map to _putenv_s / SetEnvironmentVariable. */
|
||||||
|
static inline int setenv(const char* name, const char* value, int overwrite) {
|
||||||
|
(void)overwrite;
|
||||||
|
return _putenv_s(name, value ? value : "");
|
||||||
|
}
|
||||||
|
static inline int unsetenv(const char* name) {
|
||||||
|
/* _putenv_s(name, "") sets VAR="" rather than removing it.
|
||||||
|
* SetEnvironmentVariableA(name, NULL) truly deletes it from the Win32
|
||||||
|
* env block; then we sync the CRT cache with _putenv("NAME="). */
|
||||||
|
SetEnvironmentVariableA(name, NULL);
|
||||||
|
size_t len = strlen(name);
|
||||||
|
char *buf = (char*)malloc(len + 2);
|
||||||
|
if (!buf) return -1;
|
||||||
|
memcpy(buf, name, len);
|
||||||
|
buf[len] = '=';
|
||||||
|
buf[len + 1] = '\0';
|
||||||
|
_putenv(buf);
|
||||||
|
free(buf);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* nanosleep — not available in MSVC/UCRT; approximate with Sleep(). */
|
||||||
|
static inline int el_nanosleep(const struct timespec *req, struct timespec *rem) {
|
||||||
|
(void)rem;
|
||||||
|
DWORD ms = (DWORD)((req->tv_sec * 1000ULL) + (req->tv_nsec / 1000000ULL));
|
||||||
|
Sleep(ms ? ms : 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#define nanosleep(req, rem) el_nanosleep((req), (rem))
|
||||||
|
|
||||||
|
/* localtime_r/gmtime_r: Windows offers localtime_s/gmtime_s with reversed arg order. */
|
||||||
|
static inline struct tm* localtime_r(const time_t* t, struct tm* out) {
|
||||||
|
return localtime_s(out, t) == 0 ? out : (struct tm*)0;
|
||||||
|
}
|
||||||
|
static inline struct tm* gmtime_r(const time_t* t, struct tm* out) {
|
||||||
|
return gmtime_s(out, t) == 0 ? out : (struct tm*)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* EL_PLATFORM_WIN_H */
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,12 @@
|
|||||||
|
|
||||||
typedef int64_t el_val_t;
|
typedef int64_t el_val_t;
|
||||||
|
|
||||||
|
/* HTTP request-handler function-pointer types. Public because soul modules (routes/chat/etc.)
|
||||||
|
* register handlers across translation units; previously defined only inside el_runtime.c, which
|
||||||
|
* made cross-module references (and the Windows build) fail. Home in the shared header. */
|
||||||
|
typedef el_val_t (*http_handler_fn)(el_val_t method, el_val_t path, el_val_t body);
|
||||||
|
typedef el_val_t (*http_handler4_fn)(el_val_t method, el_val_t path, el_val_t body, el_val_t headers);
|
||||||
|
|
||||||
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
#define EL_STR(s) ((el_val_t)(uintptr_t)(s))
|
||||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||||
#define EL_INT(v) (v)
|
#define EL_INT(v) (v)
|
||||||
@@ -626,6 +632,7 @@ el_val_t engram_load(el_val_t path);
|
|||||||
* can pass results straight through without round-tripping ElList/ElMap
|
* can pass results straight through without round-tripping ElList/ElMap
|
||||||
* through json_stringify. */
|
* through json_stringify. */
|
||||||
el_val_t engram_get_node_json(el_val_t id);
|
el_val_t engram_get_node_json(el_val_t id);
|
||||||
|
el_val_t engram_get_node_by_label(el_val_t label);
|
||||||
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
el_val_t engram_search_json(el_val_t query, el_val_t limit);
|
||||||
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
el_val_t engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||||
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
el_val_t engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||||
|
|||||||
@@ -1072,6 +1072,7 @@ el_val_t __engram_save(el_val_t path) { return engram_save
|
|||||||
el_val_t __engram_load(el_val_t path) { return engram_load(path); }
|
el_val_t __engram_load(el_val_t path) { return engram_load(path); }
|
||||||
|
|
||||||
el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); }
|
el_val_t __engram_get_node_json(el_val_t id) { return engram_get_node_json(id); }
|
||||||
|
el_val_t __engram_get_node_by_label(el_val_t label) { return engram_get_node_by_label(label); }
|
||||||
|
|
||||||
el_val_t __engram_search_json(el_val_t query, el_val_t limit) {
|
el_val_t __engram_search_json(el_val_t query, el_val_t limit) {
|
||||||
return engram_search_json(query, limit);
|
return engram_search_json(query, limit);
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ el_val_t __engram_activate(el_val_t query, el_val_t depth);
|
|||||||
el_val_t __engram_save(el_val_t path);
|
el_val_t __engram_save(el_val_t path);
|
||||||
el_val_t __engram_load(el_val_t path);
|
el_val_t __engram_load(el_val_t path);
|
||||||
el_val_t __engram_get_node_json(el_val_t id);
|
el_val_t __engram_get_node_json(el_val_t id);
|
||||||
|
el_val_t __engram_get_node_by_label(el_val_t label);
|
||||||
el_val_t __engram_search_json(el_val_t query, el_val_t limit);
|
el_val_t __engram_search_json(el_val_t query, el_val_t limit);
|
||||||
el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
el_val_t __engram_scan_nodes_json(el_val_t limit, el_val_t offset);
|
||||||
el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
el_val_t __engram_scan_nodes_by_type_json(el_val_t node_type, el_val_t limit, el_val_t offset);
|
||||||
|
|||||||
@@ -2670,6 +2670,7 @@ fn builtin_arity(name: String) -> Int {
|
|||||||
if str_eq(name, "engram_save") { return 1 }
|
if str_eq(name, "engram_save") { return 1 }
|
||||||
if str_eq(name, "engram_load") { return 1 }
|
if str_eq(name, "engram_load") { return 1 }
|
||||||
if str_eq(name, "engram_get_node_json") { return 1 }
|
if str_eq(name, "engram_get_node_json") { return 1 }
|
||||||
|
if str_eq(name, "engram_get_node_by_label") { return 1 }
|
||||||
if str_eq(name, "engram_search_json") { return 2 }
|
if str_eq(name, "engram_search_json") { return 2 }
|
||||||
if str_eq(name, "engram_scan_nodes_json") { return 2 }
|
if str_eq(name, "engram_scan_nodes_json") { return 2 }
|
||||||
if str_eq(name, "engram_neighbors_json") { return 3 }
|
if str_eq(name, "engram_neighbors_json") { return 3 }
|
||||||
|
|||||||
@@ -23,10 +23,29 @@ fn tok_at(tokens: [Any], pos: Int) -> Map<String, Any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn tok_kind(tokens: [Any], pos: Int) -> String {
|
fn tok_kind(tokens: [Any], pos: Int) -> String {
|
||||||
|
// Out-of-range reads must report the Eof sentinel so every `== "Eof"`
|
||||||
|
// termination guard in the parser fires. Without this, reading past the
|
||||||
|
// single trailing Eof token returns runtime null (el_list_get OOB -> 0),
|
||||||
|
// which matches no delimiter, letting inner parse loops append AST nodes
|
||||||
|
// forever on malformed input -> unbounded allocation -> OOM.
|
||||||
|
let n: Int = native_list_len(tokens) / 2
|
||||||
|
if pos < 0 {
|
||||||
|
return "Eof"
|
||||||
|
}
|
||||||
|
if pos >= n {
|
||||||
|
return "Eof"
|
||||||
|
}
|
||||||
native_list_get(tokens, pos * 2)
|
native_list_get(tokens, pos * 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tok_value(tokens: [Any], pos: Int) -> String {
|
fn tok_value(tokens: [Any], pos: Int) -> String {
|
||||||
|
let n: Int = native_list_len(tokens) / 2
|
||||||
|
if pos < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if pos >= n {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
native_list_get(tokens, pos * 2 + 1)
|
native_list_get(tokens, pos * 2 + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +54,12 @@ fn expect(tokens: [Any], pos: Int, kind: String) -> Int {
|
|||||||
if k == kind {
|
if k == kind {
|
||||||
return pos + 1
|
return pos + 1
|
||||||
}
|
}
|
||||||
// On mismatch just advance; error recovery is best-effort
|
// On mismatch, error recovery is best-effort. But never step PAST the Eof
|
||||||
|
// sentinel: once at Eof a mismatch means the input ended early, and
|
||||||
|
// advancing would run the cursor off the token list.
|
||||||
|
if k == "Eof" {
|
||||||
|
return pos
|
||||||
|
}
|
||||||
pos + 1
|
pos + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,14 @@ static _Thread_local ElArena _tl_arena = {NULL, 0, 0};
|
|||||||
static _Thread_local int _tl_arena_active = 0;
|
static _Thread_local int _tl_arena_active = 0;
|
||||||
|
|
||||||
/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response.
|
/* Binary-safe fs_read length — set by fs_read, consumed by http_send_response.
|
||||||
* Allows serving PNGs and other binary files without strlen truncation. */
|
* Allows serving PNGs and other binary files without strlen truncation.
|
||||||
static _Thread_local size_t _tl_fs_read_len = 0;
|
* PAIRED with the buffer pointer it describes: the length may only be applied
|
||||||
|
* to the exact buffer fs_read returned. Without the pairing, any handler that
|
||||||
|
* fs_read a file and then WRAPPED it into a larger response had that response
|
||||||
|
* truncated to the file's length (Content-Length lied AND the send stopped
|
||||||
|
* short) — the safety-contact onboarding trap, 2026-07-17. */
|
||||||
|
static _Thread_local size_t _tl_fs_read_len = 0;
|
||||||
|
static _Thread_local const char* _tl_fs_read_buf = NULL;
|
||||||
|
|
||||||
static void el_arena_track(char* p) {
|
static void el_arena_track(char* p) {
|
||||||
if (!_tl_arena_active || !p) return;
|
if (!_tl_arena_active || !p) return;
|
||||||
@@ -90,6 +96,8 @@ static void el_arena_track(char* p) {
|
|||||||
void el_request_start(void) {
|
void el_request_start(void) {
|
||||||
_tl_arena.count = 0;
|
_tl_arena.count = 0;
|
||||||
_tl_arena_active = 1;
|
_tl_arena_active = 1;
|
||||||
|
_tl_fs_read_len = 0; /* never let a previous request's file length */
|
||||||
|
_tl_fs_read_buf = NULL; /* leak into this response's byte accounting */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Called by http_worker after the El handler returns and the response is sent.
|
/* Called by http_worker after the El handler returns and the response is sent.
|
||||||
@@ -1362,11 +1370,14 @@ static void http_send_response(int fd, const char* body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const char* eff_body = is_envelope ? env_body : body;
|
const char* eff_body = is_envelope ? env_body : body;
|
||||||
/* Use the real byte count from fs_read if available (handles binary files
|
/* Use the real byte count from fs_read ONLY when this body IS the exact
|
||||||
* with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for
|
* buffer fs_read returned (binary files with embedded null bytes — PNG,
|
||||||
* normal text/JSON responses where _tl_fs_read_len is 0. */
|
* WOFF2, etc.). Any other body — wrapped, enveloped, or derived — must be
|
||||||
size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
|
* measured with strlen, or it is truncated/over-read to the file's size. */
|
||||||
|
size_t blen = (_tl_fs_read_len > 0 && eff_body == _tl_fs_read_buf)
|
||||||
|
? _tl_fs_read_len : strlen(eff_body);
|
||||||
_tl_fs_read_len = 0; /* consume — one-shot per response */
|
_tl_fs_read_len = 0; /* consume — one-shot per response */
|
||||||
|
_tl_fs_read_buf = NULL;
|
||||||
int head_only = _tl_http_head_only;
|
int head_only = _tl_http_head_only;
|
||||||
|
|
||||||
JsonBuf hdrs; jb_init(&hdrs);
|
JsonBuf hdrs; jb_init(&hdrs);
|
||||||
@@ -1438,11 +1449,22 @@ static void* http_worker(void* arg) {
|
|||||||
const char* rs = EL_CSTR(r);
|
const char* rs = EL_CSTR(r);
|
||||||
/* Copy response out BEFORE arena teardown.
|
/* Copy response out BEFORE arena teardown.
|
||||||
* For binary files, _tl_fs_read_len holds the real byte count —
|
* For binary files, _tl_fs_read_len holds the real byte count —
|
||||||
* use memcpy instead of strdup so null bytes are preserved. */
|
* use memcpy instead of strdup so null bytes are preserved.
|
||||||
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
|
* The stored length applies ONLY when the response IS the exact
|
||||||
|
* fs_read buffer; a wrapped/derived response must use strlen or
|
||||||
|
* it gets truncated (or over-read) to the file's length. */
|
||||||
|
size_t rlen;
|
||||||
|
if (_tl_fs_read_len > 0 && rs && rs == _tl_fs_read_buf) {
|
||||||
|
rlen = _tl_fs_read_len; /* raw file bytes — binary-safe */
|
||||||
|
} else {
|
||||||
|
rlen = rs ? strlen(rs) : 0;
|
||||||
|
_tl_fs_read_len = 0; /* hint doesn't describe this body */
|
||||||
|
_tl_fs_read_buf = NULL;
|
||||||
|
}
|
||||||
response = malloc(rlen + 1);
|
response = malloc(rlen + 1);
|
||||||
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
||||||
else if (response) { response[0] = '\0'; }
|
else if (response) { response[0] = '\0'; }
|
||||||
|
if (_tl_fs_read_len > 0) _tl_fs_read_buf = response; /* hint follows the copy */
|
||||||
} else {
|
} else {
|
||||||
response = el_strdup_persist("el-runtime: no http handler registered");
|
response = el_strdup_persist("el-runtime: no http handler registered");
|
||||||
}
|
}
|
||||||
@@ -1684,10 +1706,20 @@ static void* http_worker_v2(void* arg) {
|
|||||||
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
|
el_val_t hmap = http_build_headers_map(hdr_block ? hdr_block : "");
|
||||||
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
|
el_val_t r = h(EL_STR(dispatch_method), EL_STR(path), hmap, EL_STR(body));
|
||||||
const char* rs = EL_CSTR(r);
|
const char* rs = EL_CSTR(r);
|
||||||
size_t rlen = _tl_fs_read_len > 0 ? _tl_fs_read_len : (rs ? strlen(rs) : 0);
|
/* Same pairing rule as the v1 worker: the fs_read length is only
|
||||||
|
* trustworthy for the exact buffer fs_read returned. */
|
||||||
|
size_t rlen;
|
||||||
|
if (_tl_fs_read_len > 0 && rs && rs == _tl_fs_read_buf) {
|
||||||
|
rlen = _tl_fs_read_len; /* raw file bytes — binary-safe */
|
||||||
|
} else {
|
||||||
|
rlen = rs ? strlen(rs) : 0;
|
||||||
|
_tl_fs_read_len = 0; /* hint doesn't describe this body */
|
||||||
|
_tl_fs_read_buf = NULL;
|
||||||
|
}
|
||||||
response = malloc(rlen + 1);
|
response = malloc(rlen + 1);
|
||||||
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
if (response && rs) { memcpy(response, rs, rlen); response[rlen] = '\0'; }
|
||||||
else if (response) { response[0] = '\0'; }
|
else if (response) { response[0] = '\0'; }
|
||||||
|
if (_tl_fs_read_len > 0) _tl_fs_read_buf = response; /* hint follows the copy */
|
||||||
el_release(hmap);
|
el_release(hmap);
|
||||||
} else {
|
} else {
|
||||||
response = el_strdup_persist(
|
response = el_strdup_persist(
|
||||||
@@ -1803,6 +1835,7 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
|
|||||||
el_val_t fs_read(el_val_t pathv) {
|
el_val_t fs_read(el_val_t pathv) {
|
||||||
const char* path = EL_CSTR(pathv);
|
const char* path = EL_CSTR(pathv);
|
||||||
_tl_fs_read_len = 0;
|
_tl_fs_read_len = 0;
|
||||||
|
_tl_fs_read_buf = NULL;
|
||||||
if (!path) return el_wrap_str(el_strdup(""));
|
if (!path) return el_wrap_str(el_strdup(""));
|
||||||
FILE* f = fopen(path, "rb");
|
FILE* f = fopen(path, "rb");
|
||||||
if (!f) return el_wrap_str(el_strdup(""));
|
if (!f) return el_wrap_str(el_strdup(""));
|
||||||
@@ -1814,6 +1847,7 @@ el_val_t fs_read(el_val_t pathv) {
|
|||||||
size_t got = fread(buf, 1, (size_t)sz, f);
|
size_t got = fread(buf, 1, (size_t)sz, f);
|
||||||
buf[got] = '\0';
|
buf[got] = '\0';
|
||||||
_tl_fs_read_len = got; /* store real byte count for binary-safe send */
|
_tl_fs_read_len = got; /* store real byte count for binary-safe send */
|
||||||
|
_tl_fs_read_buf = buf; /* ...valid ONLY for this exact buffer */
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return el_wrap_str(buf);
|
return el_wrap_str(buf);
|
||||||
}
|
}
|
||||||
@@ -3184,8 +3218,10 @@ el_val_t json_get_raw(el_val_t json_str, el_val_t key) {
|
|||||||
const char* k = EL_CSTR(key);
|
const char* k = EL_CSTR(key);
|
||||||
const char* p = json_find_key(json, k);
|
const char* p = json_find_key(json, k);
|
||||||
/* Clear fs_read binary-length hint — result is a fresh null-terminated
|
/* Clear fs_read binary-length hint — result is a fresh null-terminated
|
||||||
* string, not the raw file bytes, so Content-Length must use strlen. */
|
* string, not the raw file bytes, so Content-Length must use strlen.
|
||||||
|
* (Kept although the pointer pairing now makes this redundant.) */
|
||||||
_tl_fs_read_len = 0;
|
_tl_fs_read_len = 0;
|
||||||
|
_tl_fs_read_buf = NULL;
|
||||||
if (!p) return el_wrap_str(el_strdup(""));
|
if (!p) return el_wrap_str(el_strdup(""));
|
||||||
const char* end = json_skip_value(p);
|
const char* end = json_skip_value(p);
|
||||||
size_t n = (size_t)(end - p);
|
size_t n = (size_t)(end - p);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// auto-generated by elc --emit-header — do not edit
|
||||||
|
extern fn el_escape(s: String) -> String
|
||||||
|
extern fn el_text(s: String) -> String
|
||||||
|
extern fn el_attr(name: String, value: String) -> String
|
||||||
|
extern fn el_div(attrs: String, children: String) -> String
|
||||||
|
extern fn el_section(attrs: String, children: String) -> String
|
||||||
|
extern fn el_article(attrs: String, children: String) -> String
|
||||||
|
extern fn el_header(attrs: String, children: String) -> String
|
||||||
|
extern fn el_footer(attrs: String, children: String) -> String
|
||||||
|
extern fn el_main(attrs: String, children: String) -> String
|
||||||
|
extern fn el_nav(attrs: String, children: String) -> String
|
||||||
|
extern fn el_aside(attrs: String, children: String) -> String
|
||||||
|
extern fn el_ul(attrs: String, children: String) -> String
|
||||||
|
extern fn el_ol(attrs: String, children: String) -> String
|
||||||
|
extern fn el_li(attrs: String, children: String) -> String
|
||||||
|
extern fn el_p(attrs: String, children: String) -> String
|
||||||
|
extern fn el_span(attrs: String, children: String) -> String
|
||||||
|
extern fn el_form(attrs: String, children: String) -> String
|
||||||
|
extern fn el_h1(attrs: String, text: String) -> String
|
||||||
|
extern fn el_h2(attrs: String, text: String) -> String
|
||||||
|
extern fn el_h3(attrs: String, text: String) -> String
|
||||||
|
extern fn el_h4(attrs: String, text: String) -> String
|
||||||
|
extern fn el_button(attrs: String, label: String) -> String
|
||||||
|
extern fn el_a(href: String, attrs: String, children: String) -> String
|
||||||
|
extern fn el_input(type_attr: String, attrs: String) -> String
|
||||||
|
extern fn el_textarea(attrs: String, value: String) -> String
|
||||||
|
extern fn el_label(for_id: String, attrs: String, children: String) -> String
|
||||||
|
extern fn el_img(src: String, alt: String, attrs: String) -> String
|
||||||
|
extern fn el_video(attrs: String, children: String) -> String
|
||||||
|
extern fn el_strong(children: String) -> String
|
||||||
|
extern fn el_em(children: String) -> String
|
||||||
|
extern fn el_code(children: String) -> String
|
||||||
|
extern fn el_pre(attrs: String, children: String) -> String
|
||||||
|
extern fn el_hr() -> String
|
||||||
|
extern fn el_br() -> String
|
||||||
|
extern fn el_html_doc(lang: String, head_html: String, body_html: String) -> String
|
||||||
|
extern fn el_meta(name: String, content: String) -> String
|
||||||
|
extern fn el_meta_charset(charset: String) -> String
|
||||||
|
extern fn el_link_stylesheet(href: String) -> String
|
||||||
|
extern fn el_script_src(src: String, defer_load: Bool) -> String
|
||||||
|
extern fn el_script_inline(js: String) -> String
|
||||||
|
extern fn el_title(text: String) -> String
|
||||||
Reference in New Issue
Block a user