gcc -O0 keeps unused cst_test_util.h helpers that need os_*; clang GC'd them so only the gcc CMake step failed. Prefer ghfast/ghproxy.net for the llvm-mingw tarball (gitclone 500 / github unreachable on the runner).
298 lines
11 KiB
CMake
298 lines
11 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_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)
|
|
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/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/mm_tool.c
|
|
src/models.c
|
|
src/metrics.c
|
|
)
|
|
if(WIN32)
|
|
# MinGW has no <regex.h>: musl's TRE engine, UTF-8 aware (vendor/tre-regex)
|
|
include_directories(vendor/tre-regex)
|
|
list(APPEND CST_CORE_SOURCES
|
|
vendor/tre-regex/regcomp.c vendor/tre-regex/regexec.c
|
|
vendor/tre-regex/regerror.c vendor/tre-regex/tre-mem.c)
|
|
endif()
|
|
|
|
set(CST_SOURCES ${CST_CORE_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()
|
|
|
|
target_link_libraries(cst-agent PRIVATE
|
|
"${SASHIDOWN_A}"
|
|
CURL::libcurl
|
|
Threads::Threads
|
|
)
|
|
if(NOT WIN32)
|
|
target_link_libraries(cst-agent PRIVATE m)
|
|
endif()
|
|
|
|
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_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_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
|
|
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)
|
|
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)
|
|
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).
|
|
foreach(t test_llm_body test_sse_decode test_apply_patch test_session_roundtrip
|
|
test_shell test_diff test_tools_fs test_registry
|
|
test_undo test_config test_models 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)
|
|
add_executable(${t} tests/${t}.c ${CST_CORE_SOURCES})
|
|
target_include_directories(${t} PRIVATE include "${CJSON_DIR}")
|
|
target_compile_definitions(${t} PRIVATE _GNU_SOURCE)
|
|
target_link_libraries(${t} PRIVATE CURL::libcurl Threads::Threads)
|
|
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}")
|