Files
CST-agent/CMakeLists.txt
T
WHCandCursor 5538ed3412
ci / tcc-check (push) Successful in 1m8s
ci / linux (aarch64, 0) (push) Failing after 1h0m3s
ci / linux (x86, 1) (push) Failing after 1h0m3s
ci / linux (x86_64, 1) (push) Failing after 1h0m2s
ci / windows (push) Failing after 50s
refactor: archive GUI clients; keep minimal TUI with hooks/API
Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus
libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 22:49:23 +08:00

451 lines
17 KiB
CMake

cmake_minimum_required(VERSION 3.16)
project(cst-agent C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
option(CST_BUILD_TUI "Build ncurses/PDCurses TUI" ON)
option(CST_BUILD_WEBUI "Build the HTTP/SSE engine API (--web, cst-webui/1)" ON)
option(CST_BUILD_APP "Build archived Qt front end in archive/qt-app (experimental)" OFF)
option(CST_NATIVE_ARCH "Pass -march=native to sashidown" OFF)
option(CST_BUNDLE_BUSYBOX "Install placeholder/scripts for busybox beside binary" ON)
if(WIN32)
enable_language(RC)
# Cross builds link libgcc/winpthreads/curl statically; MSYS2's curl is a DLL.
if(CMAKE_CROSSCOMPILING)
set(_CST_STATIC_DEFAULT ON)
else()
set(_CST_STATIC_DEFAULT OFF)
endif()
option(CST_WIN_STATIC "Link Windows executables with -static" ${_CST_STATIC_DEFAULT})
endif()
# Windows: UTF-8 manifest (activeCodePage, longPathAware) and version resource
# on every executable, so tests run with the same process code page as the app.
function(cst_win_exe t)
if(NOT WIN32)
return()
endif()
target_sources(${t} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/res/windows/cst-agent.rc")
target_include_directories(${t} PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/include" "${CMAKE_CURRENT_SOURCE_DIR}/res/windows")
target_compile_definitions(${t} PRIVATE _WIN32_WINNT=0x0A00)
if(CST_WIN_STATIC)
target_link_options(${t} PRIVATE -static)
endif()
endfunction()
set(SD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/sashidown")
if(CST_NATIVE_ARCH)
set(_SD_MAKE_EXTRA CST_NATIVE_ARCH=1)
else()
set(_SD_MAKE_EXTRA "")
endif()
# sashidown has a plain Makefile; CMAKE_MAKE_PROGRAM may be ninja.
find_program(CST_MAKE NAMES make gmake mingw32-make REQUIRED)
# Separate archive for MinGW so Linux host builds keep libsashidown.a
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(SASHIDOWN_A "${SD_DIR}/libsashidown-mingw.a")
# Use whatever archiver CMake found for the active toolchain. Hardcoding
# "x86_64-w64-mingw32-ar" only works on cross-build hosts, where binutils
# installs prefixed names; MSYS2's mingw-w64 toolchain ships plain ar.exe
# and ranlib.exe, so the hardcoded names are not found.
set(_MINGW_AR "${CMAKE_AR}")
set(_MINGW_RANLIB "${CMAKE_RANLIB}")
add_custom_command(
OUTPUT "${SASHIDOWN_A}"
COMMAND ${CST_MAKE} -C "${SD_DIR}" clean
COMMAND ${CST_MAKE} -C "${SD_DIR}" libsashidown.a
CC=${CMAKE_C_COMPILER} AR=${_MINGW_AR} RANLIB=${_MINGW_RANLIB}
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SD_DIR}/libsashidown.a" "${SASHIDOWN_A}"
COMMAND ${CST_MAKE} -C "${SD_DIR}" clean
COMMENT "Building libsashidown for MinGW"
VERBATIM)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Android")
# The host archive has to stay host-built (the CLI, the tests and the
# Makefile link it), so the NDK build keeps its own copy, one per ABI.
# CMake's compiler is plain clang (the --target/--sysroot flags live in its
# own variables), so the vendor make gets the per-target wrapper instead:
# aarch64-linux-android24-clang carries both.
get_filename_component(_SD_CC_BIN "${CMAKE_C_COMPILER}" DIRECTORY)
string(REPLACE "-none" "" _SD_TRIPLE "${CMAKE_C_COMPILER_TARGET}")
set(_SD_CC "${_SD_CC_BIN}/${_SD_TRIPLE}-clang")
if(NOT EXISTS "${_SD_CC}")
set(_SD_CC "${CMAKE_C_COMPILER}")
endif()
set(SASHIDOWN_A "${SD_DIR}/libsashidown-${ANDROID_ABI}.a")
add_custom_command(
OUTPUT "${SASHIDOWN_A}"
COMMAND ${CST_MAKE} -C "${SD_DIR}" clean
COMMAND ${CST_MAKE} -C "${SD_DIR}" libsashidown.a
CC=${_SD_CC} AR=${CMAKE_AR} RANLIB=${CMAKE_RANLIB}
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SD_DIR}/libsashidown.a" "${SASHIDOWN_A}"
COMMAND ${CST_MAKE} -C "${SD_DIR}" clean
COMMENT "Building libsashidown for Android (${ANDROID_ABI})"
VERBATIM)
else()
set(SASHIDOWN_A "${SD_DIR}/libsashidown.a")
add_custom_command(
OUTPUT "${SASHIDOWN_A}"
COMMAND ${CST_MAKE} -C "${SD_DIR}" libsashidown.a CC=${CMAKE_C_COMPILER} ${_SD_MAKE_EXTRA}
COMMENT "Building libsashidown.a"
VERBATIM)
endif()
add_custom_target(sashidown DEPENDS "${SASHIDOWN_A}")
find_package(CURL REQUIRED)
find_package(Threads REQUIRED)
set(CJSON_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/cjson")
set(CST_CORE_SOURCES
vendor/cjson/cJSON.c
vendor/monocypher/monocypher.c
vendor/monocypher/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/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/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
src/hermit_glyph.c
src/net.c
src/http_srv.c
src/web_md.c
)
# Built-in model data as a C byte list (included by model_registry.c).
set(CST_GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/gen")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/models_builtin.json")
# The WebUI shell (web/*) as C byte arrays, the same trick as the model data.
file(GLOB CST_WEB_ASSETS "${CMAKE_CURRENT_SOURCE_DIR}/web/*")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${CST_WEB_ASSETS})
set(_cst_web_inc "/* Generated by CMake from web/ - do not edit. */\n")
set(_cst_web_table "")
foreach(_f ${CST_WEB_ASSETS})
if(IS_DIRECTORY "${_f}")
continue()
endif()
get_filename_component(_name "${_f}" NAME)
string(REGEX REPLACE "[^A-Za-z0-9]" "_" _var "wui_asset_${_name}")
file(READ "${_f}" _hex HEX)
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _hex "${_hex}")
set(_ctype "application/octet-stream")
if(_name MATCHES "\\.html$")
set(_ctype "text/html; charset=utf-8")
elseif(_name MATCHES "\\.js$")
set(_ctype "text/javascript; charset=utf-8")
elseif(_name MATCHES "\\.css$")
set(_ctype "text/css; charset=utf-8")
elseif(_name MATCHES "\\.svg$")
set(_ctype "image/svg+xml")
endif()
string(APPEND _cst_web_inc "static const unsigned char ${_var}[] = {${_hex}0x00};\n")
string(APPEND _cst_web_table " {\"${_name}\", \"${_ctype}\", ${_var}, sizeof(${_var}) - 1},\n")
endforeach()
string(APPEND _cst_web_inc "static const wui_asset_t wui_assets[] = {\n${_cst_web_table}};\n")
string(APPEND _cst_web_inc "static const int wui_asset_count = (int)(sizeof(wui_assets) / sizeof(wui_assets[0]));\n")
file(WRITE "${CST_GEN_DIR}/web_assets.inc" "${_cst_web_inc}")
if(NOT CST_BUILD_WEBUI)
list(APPEND CST_CORE_SOURCES src/webui_stub.c)
else()
list(APPEND CST_CORE_SOURCES src/webui.c)
endif()
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/src/models_builtin.json" _cst_models_hex HEX)
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _cst_models_hex "${_cst_models_hex}")
file(WRITE "${CST_GEN_DIR}/models_builtin.inc" "${_cst_models_hex}\n")
include_directories("${CST_GEN_DIR}")
if(NOT CST_BUILD_WEBUI)
add_compile_definitions(CST_NO_WEBUI=1)
endif()
if(WIN32)
# MinGW has no <regex.h>: musl's TRE engine, UTF-8 aware (vendor/tre-regex)
include_directories(vendor/tre-regex)
set(_CST_TRE_SOURCES
vendor/tre-regex/regcomp.c vendor/tre-regex/regexec.c
vendor/tre-regex/regerror.c vendor/tre-regex/tre-mem.c)
list(APPEND CST_CORE_SOURCES ${_CST_TRE_SOURCES})
# Upstream code, kept as is: CST_WIN_CFLAGS=-Werror must not apply to it.
set_source_files_properties(${_CST_TRE_SOURCES} PROPERTIES COMPILE_OPTIONS "-w")
endif()
# --- the engine as a library ---------------------------------------------
# cstcore is the core without main()/TUI: the CLI links it; archived clients
# (archive/qt-app, archive/desktop-tauri) can embed the same objects in-process.
add_library(cstcore STATIC ${CST_CORE_SOURCES})
add_dependencies(cstcore sashidown)
target_include_directories(cstcore PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${SD_DIR}/include"
"${CJSON_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/vendor/monocypher"
)
set_source_files_properties(
vendor/monocypher/monocypher.c
vendor/monocypher/monocypher-ed25519.c
PROPERTIES COMPILE_OPTIONS "-w")
target_compile_definitions(cstcore PRIVATE _GNU_SOURCE)
if(WIN32)
target_compile_definitions(cstcore PRIVATE _WIN32_WINNT=0x0A00)
target_link_libraries(cstcore PUBLIC
"${SASHIDOWN_A}" CURL::libcurl Threads::Threads
ws2_32 crypt32 secur32 bcrypt advapi32 user32)
else()
target_compile_definitions(cstcore PRIVATE _XOPEN_SOURCE_EXTENDED)
target_link_libraries(cstcore PUBLIC
"${SASHIDOWN_A}" CURL::libcurl Threads::Threads m)
endif()
if(CST_BUILD_APP)
# Frozen reference GUI; not part of the default Hermit delivery.
add_subdirectory(archive/qt-app)
endif()
set(CST_SOURCES src/main.c)
if(CST_BUILD_TUI)
list(APPEND CST_SOURCES src/textsel.c src/tui.c)
else()
list(APPEND CST_SOURCES src/tui_stub.c)
endif()
add_executable(cst-agent ${CST_SOURCES})
set_target_properties(cst-agent PROPERTIES OUTPUT_NAME hermit)
add_dependencies(cst-agent sashidown)
cst_win_exe(cst-agent)
target_include_directories(cst-agent PRIVATE
include
"${SD_DIR}/include"
"${CJSON_DIR}"
)
target_compile_definitions(cst-agent PRIVATE _GNU_SOURCE)
if(NOT WIN32)
target_compile_definitions(cst-agent PRIVATE _XOPEN_SOURCE_EXTENDED)
endif()
# cstcore carries sashidown, curl, threads and libm as usage requirements.
target_link_libraries(cst-agent PRIVATE cstcore)
set(CST_WIN_CURSES "ncurses" CACHE STRING "Windows TUI backend: ncurses or pdcurses")
set_property(CACHE CST_WIN_CURSES PROPERTY STRINGS ncurses pdcurses)
if(CST_BUILD_TUI AND WIN32 AND CST_WIN_CURSES STREQUAL "ncurses")
# ncursesw built by scripts/build_third_party_mingw.sh ncurses (or MSYS2's
# mingw-w64-ucrt-x86_64-ncurses); headers live in include/ncursesw.
find_path(NCURSESW_INCLUDE_DIR ncurses.h PATH_SUFFIXES ncursesw include/ncursesw)
find_library(NCURSESW_LIBRARY NAMES ncursesw)
if(NOT NCURSESW_INCLUDE_DIR OR NOT NCURSESW_LIBRARY)
message(FATAL_ERROR "CST_WIN_CURSES=ncurses needs ncursesw "
"(scripts/build_third_party_mingw.sh ncurses), or -DCST_WIN_CURSES=pdcurses")
endif()
# ncurses.h includes <ncursesw/ncurses_dll.h>, so the parent is needed too
get_filename_component(NCURSESW_PARENT_DIR ${NCURSESW_INCLUDE_DIR} DIRECTORY)
target_include_directories(cst-agent PRIVATE ${NCURSESW_INCLUDE_DIR} ${NCURSESW_PARENT_DIR})
target_link_libraries(cst-agent PRIVATE ${NCURSESW_LIBRARY})
target_compile_definitions(cst-agent PRIVATE CST_USE_NCURSES=1 NCURSES_STATIC=1 NCURSES_WIDECHAR=1)
message(STATUS "TUI: ncursesw (${NCURSESW_LIBRARY})")
elseif(CST_BUILD_TUI)
if(WIN32)
# PDCurses (pdcurses / curses). Users may set PDCurses_ROOT.
# PATH_SUFFIXES also covers the layout MSYS2's mingw-w64-pdcurses uses,
# which puts the headers under include/pdcurses/ rather than include/.
find_path(PDCURSES_INCLUDE_DIR curses.h
HINTS ENV PDCurses_ROOT "${PDCurses_ROOT}"
PATH_SUFFIXES include pdcurses include/pdcurses)
find_library(PDCURSES_LIBRARY
NAMES pdcurses pdcurses.dll curses
HINTS ENV PDCurses_ROOT "${PDCurses_ROOT}"
PATH_SUFFIXES lib)
if(PDCURSES_INCLUDE_DIR AND PDCURSES_LIBRARY)
target_include_directories(cst-agent PRIVATE ${PDCURSES_INCLUDE_DIR})
target_link_libraries(cst-agent PRIVATE ${PDCURSES_LIBRARY})
# PDCurses suffixes its exported symbols with an ABI tag (endwin_u64_4400
# etc.). MSYS2 builds the UTF-8 wide variant, so PDC_FORCE_UTF8 has to be
# defined or curses.h maps to the w64 names and the link fails.
target_compile_definitions(cst-agent PRIVATE
CST_USE_PDCURSES=1 PDC_FORCE_UTF8=1)
message(STATUS "TUI: PDCurses (${PDCURSES_LIBRARY})")
else()
message(FATAL_ERROR "CST_BUILD_TUI=ON on Windows requires PDCurses (set PDCurses_ROOT)")
endif()
else()
set(CURSES_NEED_WIDE TRUE)
find_package(Curses)
if(CURSES_FOUND)
target_include_directories(cst-agent PRIVATE ${CURSES_INCLUDE_DIRS})
target_link_libraries(cst-agent PRIVATE ${CURSES_LIBRARIES})
else()
# Fallback common on Linux distros
target_link_libraries(cst-agent PRIVATE ncursesw)
endif()
target_compile_definitions(cst-agent PRIVATE CST_USE_NCURSES=1)
endif()
else()
target_compile_definitions(cst-agent PRIVATE CST_HEADLESS_ONLY=1)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Windows" OR WIN32)
# curl Schannel + networking; winmm is needed by PDCurses (PlaySoundW)
target_link_libraries(cst-agent PRIVATE
ws2_32 crypt32 secur32 bcrypt advapi32 user32 gdi32 winmm)
endif()
# --- tests ---
enable_testing()
set(_CST_SMALL_TESTS test_ctl_frontier test_ctl_heredoc test_json_baseline
test_replay_starvation test_replay_real test_mm_tool test_textsel
test_pdf_text test_markdown)
foreach(t ${_CST_SMALL_TESTS})
if(t STREQUAL test_textsel)
add_executable(${t} tests/${t}.c src/textsel.c src/os_compat.c src/os_win.c
src/util.c)
if(WIN32)
target_link_libraries(${t} PRIVATE advapi32 user32)
endif()
elseif(t STREQUAL test_pdf_text)
add_executable(${t} tests/${t}.c src/pdf_text.c src/os_compat.c src/os_win.c
src/util.c)
if(WIN32)
target_link_libraries(${t} PRIVATE advapi32 user32)
endif()
elseif(t STREQUAL test_markdown)
add_executable(${t} tests/${t}.c)
target_include_directories(${t} PRIVATE "${SD_DIR}/include")
target_link_libraries(${t} PRIVATE "${SASHIDOWN_A}")
add_dependencies(${t} sashidown)
elseif(t STREQUAL test_ctl_frontier OR t STREQUAL test_ctl_heredoc)
add_executable(${t} tests/${t}.c src/ctl_parse.c)
elseif(t STREQUAL test_json_baseline)
add_executable(${t} tests/${t}.c src/json_baseline.c vendor/cjson/cJSON.c)
elseif(t STREQUAL test_replay_starvation OR t STREQUAL test_replay_real)
# cst_test_util.h static helpers reference os_*; gcc -O0 keeps unused ones.
# On Windows os_win.c also needs util.c (cst_buf_*, cst_utf8_valid).
add_executable(${t} tests/${t}.c src/ctl_parse.c src/json_baseline.c
vendor/cjson/cJSON.c src/os_compat.c src/os_win.c src/util.c)
if(WIN32)
target_link_libraries(${t} PRIVATE advapi32 user32)
endif()
else()
add_executable(${t} tests/test_mm_tool.c src/mm_tool.c)
endif()
target_include_directories(${t} PRIVATE include "${CJSON_DIR}")
target_compile_definitions(${t} PRIVATE _GNU_SOURCE)
if(NOT WIN32)
target_link_libraries(${t} PRIVATE m Threads::Threads)
endif()
cst_win_exe(${t})
add_test(NAME ${t} COMMAND ${t} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
endforeach()
# Tests that link the whole core (no TUI).
set(_CST_CORE_TESTS test_llm_body test_prefix_stable test_sse_decode
test_apply_patch test_session_roundtrip test_store test_attach
test_artifacts
test_shell test_diff test_tools_fs test_tools_ext test_registry
test_undo test_config test_models test_model_registry test_sandbox
test_keymap test_shell_compact test_skills
test_plugins test_mcp test_subagent test_runtime test_realm test_discover
test_peer_auth test_parliament test_lowil
test_win_utf8 test_os_dir test_tmpdir
test_tool_meta test_embed)
if(CST_BUILD_WEBUI)
# test_webui drives the real server; the stub has nothing to test.
list(APPEND _CST_CORE_TESTS test_webui)
endif()
foreach(t ${_CST_CORE_TESTS})
# Link the shared cstcore archive instead of recompiling every core .c.
add_executable(${t} tests/${t}.c)
target_include_directories(${t} PRIVATE include "${CJSON_DIR}" "${SD_DIR}/include")
target_compile_definitions(${t} PRIVATE _GNU_SOURCE)
target_link_libraries(${t} PRIVATE cstcore)
add_dependencies(${t} sashidown)
if(WIN32)
target_link_libraries(${t} PRIVATE ws2_32 crypt32 secur32 bcrypt advapi32 user32)
endif()
cst_win_exe(${t})
add_test(NAME ${t} COMMAND ${t} WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
endforeach()
# --- busybox bundle helpers ---
if(CST_BUNDLE_BUSYBOX)
install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/third_party/busybox/README.md"
DESTINATION .
OPTIONAL)
if(WIN32 AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/busybox/busybox.exe")
install(PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/busybox/busybox.exe" DESTINATION .)
endif()
endif()
if(WIN32)
install(TARGETS cst-agent RUNTIME DESTINATION .)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf" DESTINATION .)
endif()
else()
include(GNUInstallDirs)
install(TARGETS cst-agent RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf")
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf"
DESTINATION ${CMAKE_INSTALL_DATADIR}/hermit)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-icons.ttf"
DESTINATION ${CMAKE_INSTALL_DATADIR}/fonts/hermit)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-frames")
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/res/hermit-frames/"
DESTINATION ${CMAKE_INSTALL_DATADIR}/hermit/frames)
endif()
endif()
message(STATUS "Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER}")
message(STATUS "CST_BUILD_TUI=${CST_BUILD_TUI}")