Files
CST-agent/Makefile
T
WHCandCursor ea38247161
ci / tcc-check (push) Failing after 40s
ci / linux (aarch64, 0) (push) Skipped
ci / linux (x86, 1) (push) Skipped
ci / linux (x86_64, 1) (push) Skipped
ci / windows (push) Skipped
feat: Hermit 1.1.0 — HERMIT_* env, Tauri desktop shell
Wire hermit_getenv across the engine, rebrand WebUI/systemd/docs, and
add apps/desktop (Tauri 2 + React) with a DeepSeek-Harness-like UI.
Qt app/ is frozen; full libcstcore FFI is scaffolded behind --features engine.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 13:15:47 +08:00

410 lines
17 KiB
Makefile

CC ?= gcc
CFLAGS ?= -std=c11 -Wall -Wextra -O2 -g
SD_DIR := vendor/sashidown
CJSON_DIR := vendor/cjson
MONO_DIR := vendor/monocypher
PKG_CONFIG ?= pkg-config
PREFIX ?= /usr/local
DESTDIR ?=
UNAME_S := $(shell uname -s 2>/dev/null)
UNAME_O := $(shell uname -o 2>/dev/null)
comma := ,
# Optional ccache: USE_CCACHE=1 forces it; USE_CCACHE=0 disables; else auto.
USE_CCACHE ?= auto
ifeq ($(USE_CCACHE),1)
CC := $(shell command -v ccache >/dev/null && echo "ccache $(CC)" || echo "$(CC)")
else ifeq ($(USE_CCACHE),auto)
ifneq ($(findstring ccache,$(CC)),ccache)
CC := $(shell command -v ccache >/dev/null && echo "ccache $(CC)" || echo "$(CC)")
endif
endif
# Android (Termux): pthreads live in libc.
PTHREAD_LIBS := -lpthread
ifeq ($(UNAME_O),Android)
PTHREAD_LIBS :=
endif
# macOS: Homebrew ncurses/curl are keg-only, so point pkg-config at them.
# (macOS ships GNU make 3.81, whose $(shell) ignores exported variables.)
PKG_ENV :=
ifeq ($(UNAME_S),Darwin)
BREW_PREFIX := $(shell brew --prefix 2>/dev/null)
ifneq ($(BREW_PREFIX),)
PKG_ENV := PKG_CONFIG_PATH="$(BREW_PREFIX)/opt/ncurses/lib/pkgconfig:$(BREW_PREFIX)/opt/curl/lib/pkgconfig:$$PKG_CONFIG_PATH"
endif
CPPFLAGS += -D_DARWIN_C_SOURCE
endif
PKGC = $(PKG_ENV) $(PKG_CONFIG)
# STATIC=1: fully static link (the musl release builds).
STATIC ?= 0
LINK_STATIC :=
ifeq ($(STATIC),1)
PKGC += --static
LINK_STATIC := -static
endif
LDFLAGS += $(LINK_STATIC)
# Wide-character curses: ncursesw, else ncurses (macOS/BSD build it wide).
CURSES_PC := $(shell for p in ncursesw ncurses; do $(PKGC) --exists $$p 2>/dev/null && { echo $$p; exit 0; }; done)
ifneq ($(CURSES_PC),)
CURSES_CFLAGS := $(shell $(PKGC) --cflags $(CURSES_PC))
CURSES_LIBS := $(shell $(PKGC) --libs $(CURSES_PC))
else ifeq ($(UNAME_S),Darwin)
CURSES_LIBS := -lncurses
else
CURSES_LIBS := -lncursesw
endif
CURL_CFLAGS := $(shell $(PKGC) --cflags libcurl 2>/dev/null)
CURL_LIBS := $(shell $(PKGC) --libs libcurl 2>/dev/null)
ifeq ($(strip $(CURL_LIBS)),)
CURL_LIBS := -lcurl
endif
# WebUI (HTTP + SSE front end): src/net.c src/http_srv.c src/web_md.c
# src/webui.c. CST_WEBUI=0 swaps in the stub (--web then prints a hint).
CST_WEBUI ?= 1
PKG_CFLAGS := $(CURL_CFLAGS) $(CURSES_CFLAGS)
PKG_LIBS := $(CURL_LIBS) $(CURSES_LIBS)
# Upstream Monocypher: keep -Werror builds green without patching the vendor.
$(MONO_DIR)/monocypher.o $(MONO_DIR)/monocypher-ed25519.o: %.o: %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -w -c -o $@ $<
# tcc writes no rpath of its own, so shared libs found via pkg-config -L
# (Nix store, /opt prefixes) would not load at run time.
RPATH_LIBS :=
ifneq ($(findstring tcc,$(notdir $(CC))),)
ifneq ($(STATIC),1)
RPATH_LIBS := $(patsubst -L%,-Wl$(comma)-rpath$(comma)%,$(filter -L%,$(PKG_LIBS)))
endif
endif
# Keep POSIX declarations visible for shared test helpers (notably symlink()).
CPPFLAGS += -Iinclude -I.gen -I$(SD_DIR)/include -I$(CJSON_DIR) -I$(MONO_DIR) \
-D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE_EXTENDED $(PKG_CFLAGS)
LDFLAGS += -L$(SD_DIR)
LDLIBS += -lsashidown -lm $(PKG_LIBS) $(PTHREAD_LIBS) $(RPATH_LIBS)
WEB_DIR := web
WEB_ASSETS := $(wildcard $(WEB_DIR)/*.html) $(wildcard $(WEB_DIR)/*.js) \
$(wildcard $(WEB_DIR)/*.css) $(wildcard $(WEB_DIR)/*.svg)
ifeq ($(CST_WEBUI),0)
WEB_SRC := src/net.c src/http_srv.c src/web_md.c src/webui_stub.c
# The stub body is guarded by this, so the switch has to define it too.
CPPFLAGS += -DCST_NO_WEBUI=1
else
WEB_SRC := src/net.c src/http_srv.c src/web_md.c src/webui.c
endif
CORE_SRC = \
$(CJSON_DIR)/cJSON.c \
$(MONO_DIR)/monocypher.c \
$(MONO_DIR)/monocypher-ed25519.c \
src/util.c \
src/ctl_parse.c \
src/os_compat.c \
src/os_win.c \
src/sandbox.c \
src/sandbox_exec.c \
src/keymap.c \
src/skills.c \
src/extensions.c src/plugins.c src/mcp.c src/subagent.c src/attach.c src/clipboard.c \
src/pdf_text.c \
src/session.c src/runtime.c src/worktree.c src/realm.c src/discover.c \
src/peer_auth.c src/parliament.c src/parliament_daemon.c src/parliament_cli.c \
src/tools/tools.c \
src/tools/patch.c \
src/tools/registry.c \
src/tools/fs_tools.c \
src/tools/agent_tools.c \
src/tools/web.c \
src/tools/session_tools.c \
src/tools/diff.c \
src/undo.c \
src/permissions.c \
src/settings.c \
src/llm_openai.c \
src/agent_loop.c \
src/agent_store.c \
src/json_baseline.c \
src/creds.c src/provider_switch.c \
src/mm_tool.c \
src/models.c src/model_registry.c \
src/metrics.c \
src/hermit_glyph.c \
$(WEB_SRC)
SRC = $(CORE_SRC) src/textsel.c src/tui.c src/main.c
OBJ = $(SRC:.c=.o)
# The core as a static library: what the Qt app embeds in-process (app/), so
# the GUI links the same engine the CLI/TUI/WebUI run. No TUI, no main.
CORE_OBJ = $(CORE_SRC:.c=.o)
CST_CORE_LIB = libcstcore.a
# Generated embeds; per-file .d files (from -MMD) track the rest.
GEN_INCS = .gen/models_builtin.inc .gen/web_assets.inc
DEPFILES = $(OBJ:.o=.d)
TESTS = \
tests/test_ctl_frontier \
tests/test_ctl_heredoc \
tests/test_json_baseline \
tests/test_replay_starvation \
tests/test_replay_real \
tests/test_mm_tool \
tests/test_llm_body \
tests/test_prefix_stable \
tests/test_sse_decode \
tests/test_apply_patch \
tests/test_session_roundtrip \
tests/test_store \
tests/test_attach \
tests/test_artifacts \
tests/test_pdf_text \
tests/test_shell \
tests/test_diff \
tests/test_tools_fs \
tests/test_tools_ext \
tests/test_registry \
tests/test_tool_meta \
tests/test_embed \
tests/test_undo \
tests/test_config \
tests/test_models \
tests/test_model_registry \
tests/test_sandbox \
tests/test_keymap \
tests/test_textsel \
tests/test_markdown \
tests/test_shell_compact \
tests/test_skills \
tests/test_plugins \
tests/test_mcp \
tests/test_subagent \
tests/test_runtime \
tests/test_prompt_contract \
tests/test_realm \
tests/test_discover \
tests/test_peer_auth \
tests/test_parliament \
tests/test_lowil \
tests/test_win_utf8 \
tests/test_os_dir \
tests/test_tmpdir
# test_webui drives the real server; there is nothing to test with the stub.
ifneq ($(CST_WEBUI),0)
TESTS += tests/test_webui
endif
TEST_SYS_LIBS = -lm $(PTHREAD_LIBS) $(LINK_STATIC)
TEST_LIBS = -L$(SD_DIR) -lsashidown $(CURL_LIBS) $(RPATH_LIBS) $(TEST_SYS_LIBS)
# cst_test_util.h has static os_* helpers; gcc drops them when unused, tcc
# keeps them, so the small tests link the OS layer too.
TEST_OS_SRC = src/os_compat.c src/os_win.c src/util.c
# hermit_getenv is in os_compat.c (HERMIT_/CST_ dual env).
.PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang asan install \
uninstall install-service help hermit-font cst-agent
all: sashidown-lib hermit $(CST_CORE_LIB)
# Compat alias: many scripts still say `make cst-agent`.
cst-agent: hermit
help:
@echo "Hermit build targets (prefer: make -j\$$(nproc))"
@echo " all / hermit incremental build (ccache auto if installed)"
@echo " check build + run unit tests"
@echo " asan AddressSanitizer rebuild + check"
@echo " hermit-font bake PNG frames + hermit-icons.ttf (Pillow+fonttools)"
@echo " install PREFIX=$(PREFIX); hermit + font + cst-agent symlink"
@echo " install-service systemd --user parliament unit"
@echo "Env: USE_CCACHE=0|1|auto STATIC=1 CST_WEBUI=0 CC=tcc"
@echo " HERMIT_ICONS=auto|mini|font HERMIT_*/CST_* env"
@echo "Release: scripts/package_linux.sh ARCH | scripts/ci_windows_cross.sh"
@echo "Fast install: scripts/bootstrap_linux.sh --release"
$(CST_CORE_LIB): $(CORE_OBJ)
rm -f $@
$(AR) rcs $@ $(CORE_OBJ)
cmake-gcc:
cmake -B build-gcc -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON
cmake --build build-gcc -j
ctest --test-dir build-gcc --output-on-failure -j
cmake-clang:
cmake -B build-clang -DCMAKE_C_COMPILER=clang -DCST_BUILD_TUI=ON
cmake --build build-clang -j
# Always ask the sub-make; it no-ops when libsashidown.a is current.
sashidown-lib:
$(MAKE) -C $(SD_DIR) libsashidown.a
hermit: $(OBJ) sashidown-lib
$(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS)
@ln -sfr $@ cst-agent 2>/dev/null || ln -sf hermit cst-agent
hermit-font:
@python3 scripts/build_hermit_font.py || \
echo "hermit-font: skipped (need Pillow+fonttools; try: nix-shell -p python3Packages.pillow python3Packages.fonttools)"
# Built-in model data as a C byte list (included by model_registry.c).
.gen/models_builtin.inc: src/models_builtin.json
@mkdir -p .gen
od -An -v -tx1 $< | sed 's/\([0-9a-f][0-9a-f]\)/0x\1,/g' > $@.tmp && mv $@.tmp $@
# The WebUI shell is embedded the same way, one array per file.
.gen/web_assets.inc: $(WEB_ASSETS) scripts/embed_web.sh
@mkdir -p .gen
@sh scripts/embed_web.sh $@ $(WEB_DIR)
# Incremental rebuilds: -MMD tracks real includes instead of every header.
COMPILE_C = $(CC) $(CFLAGS) $(CPPFLAGS) -MMD -MP -c -o $@ $<
%.o: %.c
$(COMPILE_C)
src/model_registry.o: .gen/models_builtin.inc
src/webui.o: .gen/web_assets.inc
src/tui.o: $(SD_DIR)/include/sashidown.h
tests/test_ctl_frontier: tests/test_ctl_frontier.c src/ctl_parse.c include/ctl.h $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_frontier.c src/ctl_parse.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_ctl_heredoc: tests/test_ctl_heredoc.c src/ctl_parse.c include/ctl.h $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_heredoc.c src/ctl_parse.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_json_baseline: tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_replay_starvation: tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_replay_real: tests/test_replay_real.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_replay_real.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_mm_tool: tests/test_mm_tool.c src/mm_tool.c include/mm_tool.h $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_mm_tool.c src/mm_tool.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_textsel: tests/test_textsel.c src/textsel.c $(TEST_OS_SRC) include/cst_textsel.h
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_textsel.c src/textsel.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_pdf_text: tests/test_pdf_text.c src/pdf_text.c include/cst_pdf.h $(TEST_OS_SRC)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_pdf_text.c src/pdf_text.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
tests/test_markdown: tests/test_markdown.c $(SD_DIR)/libsashidown.a
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_markdown.c $(LDFLAGS) -lsashidown -lm
# Core tests reuse libcstcore.a — avoid recompiling ~35 .c files per binary.
# Do not depend on the phony sashidown-lib target or every test relinks each check.
tests/test_%: tests/test_%.c $(CST_CORE_LIB) $(SD_DIR)/libsashidown.a
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(CST_CORE_LIB) $(TEST_LIBS)
$(SD_DIR)/libsashidown.a: sashidown-lib
# Build test binaries with make -j; run them one-by-one for readable logs.
check: sashidown-lib $(TESTS)
@set -e; for t in $(TESTS); do echo "== $$t"; ./$$t; done
# AddressSanitizer + UBSan rebuild (clears objects so sanitizer flags apply).
asan:
$(MAKE) clean
$(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" \
sashidown-lib hermit check
demo: hermit
./hermit --demo-ctl --workspace .
clean:
rm -f $(OBJ) $(DEPFILES) hermit cst-agent $(CST_CORE_LIB) $(TESTS) \
.gen/models_builtin.inc .gen/web_assets.inc
# Absolute checkout path for hermit-update (survives cd). Override with HERMIT_SRC.
HERMIT_SRC ?= $(abspath .)
install: hermit
install -d "$(DESTDIR)$(PREFIX)/bin"
install -m 0755 hermit "$(DESTDIR)$(PREFIX)/bin/hermit"
ln -sfr "$(DESTDIR)$(PREFIX)/bin/hermit" "$(DESTDIR)$(PREFIX)/bin/cst-agent" 2>/dev/null || \
ln -sf hermit "$(DESTDIR)$(PREFIX)/bin/cst-agent"
@# hermit-update / hermit-service (cst-* compat symlinks)
@printf '%s\n' '#!/usr/bin/env bash' \
'export HERMIT_PREFIX="$${HERMIT_PREFIX:-$${CST_AGENT_PREFIX:-$(PREFIX)}}"' \
'export CST_AGENT_PREFIX="$$HERMIT_PREFIX"' \
'export HERMIT_DIR="$${HERMIT_DIR:-$${CST_AGENT_DIR:-$(HERMIT_SRC)}}"' \
'export CST_AGENT_DIR="$$HERMIT_DIR"' \
'exec bash "$$HERMIT_DIR/scripts/bootstrap_linux.sh" "$$HERMIT_DIR" "$$@"' \
> "$(DESTDIR)$(PREFIX)/bin/hermit-update"
@chmod 0755 "$(DESTDIR)$(PREFIX)/bin/hermit-update"
@ln -sfr "$(DESTDIR)$(PREFIX)/bin/hermit-update" "$(DESTDIR)$(PREFIX)/bin/cst-agent-update" 2>/dev/null || \
ln -sf hermit-update "$(DESTDIR)$(PREFIX)/bin/cst-agent-update"
@printf '%s\n' '#!/usr/bin/env bash' \
'set -euo pipefail' \
'BIN="$(PREFIX)/bin/hermit"' \
'cmd="$${1:-status}"' \
'case "$$cmd" in' \
'status) "$$BIN" --version; if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active hermit-parliament.service >/dev/null 2>&1; then systemctl --user status hermit-parliament.service --no-pager; elif command -v systemctl >/dev/null 2>&1 && systemctl --user is-active cst-agent-parliament.service >/dev/null 2>&1; then systemctl --user status cst-agent-parliament.service --no-pager; else "$$BIN" --service-stop >/dev/null 2>&1 || true; echo "parliament service: not managed by systemd --user"; fi ;;' \
'install-unit) make -C "$(HERMIT_SRC)" install-service PREFIX="$(PREFIX)" ;;' \
'start) systemctl --user start hermit-parliament.service 2>/dev/null || systemctl --user start cst-agent-parliament.service ;;' \
'stop) systemctl --user stop hermit-parliament.service 2>/dev/null || true; systemctl --user stop cst-agent-parliament.service 2>/dev/null || true; "$$BIN" --service-stop || true ;;' \
'restart) systemctl --user restart hermit-parliament.service 2>/dev/null || systemctl --user restart cst-agent-parliament.service ;;' \
'*) echo "usage: hermit-service {status|install-unit|start|stop|restart}" >&2; exit 2 ;;' \
'esac' \
> "$(DESTDIR)$(PREFIX)/bin/hermit-service"
@chmod 0755 "$(DESTDIR)$(PREFIX)/bin/hermit-service"
@ln -sfr "$(DESTDIR)$(PREFIX)/bin/hermit-service" "$(DESTDIR)$(PREFIX)/bin/cst-agent-service" 2>/dev/null || \
ln -sf hermit-service "$(DESTDIR)$(PREFIX)/bin/cst-agent-service"
@if [ ! -f res/hermit-icons.ttf ]; then $(MAKE) hermit-font || true; fi
@if [ -f res/hermit-icons.ttf ]; then \
install -d "$(DESTDIR)$(PREFIX)/share/hermit"; \
install -d "$(DESTDIR)$(PREFIX)/share/fonts/hermit"; \
install -m 0644 res/hermit-icons.ttf "$(DESTDIR)$(PREFIX)/share/hermit/hermit-icons.ttf"; \
install -m 0644 res/hermit-icons.ttf "$(DESTDIR)$(PREFIX)/share/fonts/hermit/hermit-icons.ttf"; \
if [ -n "$(HOME)" ] && [ "$(DESTDIR)" = "" ]; then \
install -d "$(HOME)/.local/share/fonts"; \
install -m 0644 res/hermit-icons.ttf "$(HOME)/.local/share/fonts/hermit-icons.ttf"; \
command -v fc-cache >/dev/null 2>&1 && fc-cache -f "$(HOME)/.local/share/fonts" >/dev/null 2>&1 || true; \
fi; \
echo "installed Hermit Icons → $(PREFIX)/share/fonts/hermit/hermit-icons.ttf"; \
else \
echo "warning: res/hermit-icons.ttf missing (run: make hermit-font)" >&2; \
fi
@if [ -d res/hermit-frames ]; then \
install -d "$(DESTDIR)$(PREFIX)/share/hermit/frames"; \
cp -a res/hermit-frames/. "$(DESTDIR)$(PREFIX)/share/hermit/frames/"; \
fi
@echo "installed hermit-update → $(PREFIX)/bin/hermit-update"
# Optional systemd --user unit for the parliament seat service.
install-service: install
install -d "$(DESTDIR)$(HOME)/.config/systemd/user"
sed -e 's|@BIN@|$(PREFIX)/bin/hermit|g' \
scripts/hermit-parliament.service.in \
> "$(DESTDIR)$(HOME)/.config/systemd/user/hermit-parliament.service"
ln -sfr "$(DESTDIR)$(HOME)/.config/systemd/user/hermit-parliament.service" \
"$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service" 2>/dev/null || \
ln -sf hermit-parliament.service \
"$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service"
@echo "Installed user unit. Enable with:"
@echo " systemctl --user daemon-reload"
@echo " systemctl --user enable --now hermit-parliament.service"
@echo "(set HERMIT_/CST_ REALM_PASSWORD / INSTANCE_ID in the drop-in or environment)"
uninstall:
rm -f "$(DESTDIR)$(PREFIX)/bin/hermit" "$(DESTDIR)$(PREFIX)/bin/cst-agent"
rm -f "$(DESTDIR)$(PREFIX)/bin/hermit-update" "$(DESTDIR)$(PREFIX)/bin/cst-agent-update"
rm -f "$(DESTDIR)$(PREFIX)/bin/hermit-service" "$(DESTDIR)$(PREFIX)/bin/cst-agent-service"
rm -f "$(DESTDIR)$(PREFIX)/share/hermit/hermit-icons.ttf"
rm -f "$(DESTDIR)$(PREFIX)/share/fonts/hermit/hermit-icons.ttf"
rm -rf "$(DESTDIR)$(PREFIX)/share/hermit/frames"
rm -f "$(HOME)/.local/share/fonts/hermit-icons.ttf"
rm -f "$(HOME)/.config/systemd/user/hermit-parliament.service"
rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service"
-include $(DEPFILES)