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 WebUI (--web)" ON) option(CST_BUILD_APP "Build the Qt front end in app/ (in-process engine, no port)" 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 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/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/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 : 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 and the Qt front # end in app/ (CST_BUILD_APP) embeds the very same objects in-process, so the # engine is compiled once per build instead of once per front end. 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}" ) 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) add_subdirectory(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}) 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 , 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_attach test_shell test_diff test_tools_fs 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_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}) add_executable(${t} tests/${t}.c ${CST_CORE_SOURCES}) # The core now contains src/web_md.c, which needs sashidown's header and # archive exactly like the Makefile build does. target_include_directories(${t} PRIVATE include "${CJSON_DIR}" "${SD_DIR}/include") target_compile_definitions(${t} PRIVATE _GNU_SOURCE) target_link_libraries(${t} PRIVATE "${SASHIDOWN_A}" CURL::libcurl Threads::Threads) add_dependencies(${t} sashidown) if(NOT WIN32) target_link_libraries(${t} PRIVATE m) else() 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 .) else() include(GNUInstallDirs) install(TARGETS cst-agent RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() message(STATUS "Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER}") message(STATUS "CST_BUILD_TUI=${CST_BUILD_TUI}")