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)