CC ?= gcc CFLAGS ?= -std=c11 -Wall -Wextra -O2 -g SD_DIR := vendor/sashidown CJSON_DIR := vendor/cjson 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 := , # 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) # 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) -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 \ 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/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 \ $(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 HDRS = $(wildcard include/*.h) $(wildcard src/tools/*.h) $(CJSON_DIR)/cJSON.h \ .gen/models_builtin.inc .gen/web_assets.inc 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_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 .PHONY: all clean check demo sashidown-lib FORCE cmake-gcc cmake-clang asan install uninstall all: sashidown-lib cst-agent $(CST_CORE_LIB) $(CST_CORE_LIB): $(CORE_OBJ) FORCE rm -f $@ $(AR) rcs $@ $(CORE_OBJ) cmake-gcc: cmake -B build-gcc -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON cmake --build build-gcc ctest --test-dir build-gcc --output-on-failure cmake-clang: cmake -B build-clang -DCMAKE_C_COMPILER=clang -DCST_BUILD_TUI=ON cmake --build build-clang sashidown-lib: $(SD_DIR)/libsashidown.a # The sub-make decides whether the archive is stale; FORCE only makes sure it # is asked, and -j builds wait for it before linking. $(SD_DIR)/libsashidown.a: FORCE $(MAKE) -C $(SD_DIR) libsashidown.a FORCE: cst-agent: $(OBJ) $(SD_DIR)/libsashidown.a $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS) # 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) %.o: %.c $(HDRS) $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< src/tui.o: src/tui.c $(HDRS) $(SD_DIR)/include/sashidown.h $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< 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 $(HDRS) $(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 tests/test_%: tests/test_%.c $(CORE_SRC) $(HDRS) $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(CORE_SRC) $(TEST_LIBS) $(TESTS): sashidown-lib check: $(TESTS) @set -e; for t in $(TESTS); do echo "== $$t"; ./$$t; done # AddressSanitizer + UBSan build of the agent and all tests. # Under a ptrace-based sandbox (some Termux setups) LeakSanitizer aborts with # "Tracer caught signal 11"; add ASAN_OPTIONS=detect_leaks=0 there. asan: $(MAKE) clean $(MAKE) sashidown-lib $(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" cst-agent check demo: cst-agent ./cst-agent --demo-ctl --workspace . clean: rm -f $(OBJ) cst-agent $(CST_CORE_LIB) $(TESTS) .gen/models_builtin.inc \ .gen/web_assets.inc install: cst-agent install -d "$(DESTDIR)$(PREFIX)/bin" install -m 0755 cst-agent "$(DESTDIR)$(PREFIX)/bin/cst-agent" uninstall: rm -f "$(DESTDIR)$(PREFIX)/bin/cst-agent"