menuconfig

This commit is contained in:
2026-04-18 15:37:24 +08:00
parent c587215e80
commit ddacc3e863
7 changed files with 848 additions and 7 deletions

4
.gitignore vendored
View File

@@ -12,3 +12,7 @@ compile_commands.json
CTestTestfile.cmake CTestTestfile.cmake
Testing/ Testing/
.cmake/ .cmake/
# menuconfig generated
configs/menuconfig/.config.json
configs/menuconfig/config.cmake

View File

@@ -117,6 +117,56 @@ set(CLKS_ARCH "x86_64" CACHE STRING "Target CLKS arch")
set(LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/clks/arch/${CLKS_ARCH}/linker.ld") set(LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/clks/arch/${CLKS_ARCH}/linker.ld")
set(USER_LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/cleonos/c/user.ld") set(USER_LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/cleonos/c/user.ld")
set(KELF_LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/cleonos/c/kelf.ld") set(KELF_LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/cleonos/c/kelf.ld")
set(CLEONOS_MENUCONFIG_CMAKE "${CMAKE_SOURCE_DIR}/configs/menuconfig/config.cmake" CACHE FILEPATH "menuconfig generated CMake config")
if(EXISTS "${CLEONOS_MENUCONFIG_CMAKE}")
include("${CLEONOS_MENUCONFIG_CMAKE}")
cl_log_info("menuconfig loaded from ${CLEONOS_MENUCONFIG_CMAKE}")
endif()
function(cl_set_bool_cache VAR_NAME DEFAULT_VALUE DOC_TEXT)
if(NOT DEFINED ${VAR_NAME})
set(${VAR_NAME} ${DEFAULT_VALUE} CACHE BOOL "${DOC_TEXT}")
else()
set(${VAR_NAME} ${${VAR_NAME}} CACHE BOOL "${DOC_TEXT}" FORCE)
endif()
endfunction()
function(cl_bool_to_int BOOL_VAR OUT_VAR)
if(${BOOL_VAR})
set(${OUT_VAR} 1 PARENT_SCOPE)
else()
set(${OUT_VAR} 0 PARENT_SCOPE)
endif()
endfunction()
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_AUDIO ON "Initialize kernel audio subsystem")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_MOUSE ON "Initialize kernel mouse subsystem")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_DESKTOP ON "Initialize desktop compositor subsystem")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_DRIVER_MANAGER ON "Initialize kernel driver manager")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_KELF ON "Enable KELF app dispatcher")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_USERLAND_AUTO_EXEC ON "Auto exec /shell/shell.elf on boot")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_HEAP_SELFTEST ON "Run heap selftest at boot")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_EXTERNAL_PSF ON "Load /system/tty.psf external font")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_KEYBOARD ON "Initialize keyboard input subsystem")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_ELFRUNNER_PROBE ON "Run ELFRUNNER kernel ELF probe")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_KLOGD_TASK ON "Enable scheduler klogd task")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_KWORKER_TASK ON "Enable scheduler kworker task")
cl_set_bool_cache(CLEONOS_CLKS_ENABLE_USRD_TASK ON "Enable scheduler usrd task")
cl_bool_to_int(CLEONOS_CLKS_ENABLE_AUDIO CLKS_CFG_AUDIO_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_MOUSE CLKS_CFG_MOUSE_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_DESKTOP CLKS_CFG_DESKTOP_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_DRIVER_MANAGER CLKS_CFG_DRIVER_MANAGER_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_KELF CLKS_CFG_KELF_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_USERLAND_AUTO_EXEC CLKS_CFG_USERLAND_AUTO_EXEC_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_HEAP_SELFTEST CLKS_CFG_HEAP_SELFTEST_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_EXTERNAL_PSF CLKS_CFG_EXTERNAL_PSF_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_KEYBOARD CLKS_CFG_KEYBOARD_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_ELFRUNNER_PROBE CLKS_CFG_ELFRUNNER_PROBE_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_KLOGD_TASK CLKS_CFG_KLOGD_TASK_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_KWORKER_TASK CLKS_CFG_KWORKER_TASK_INT)
cl_bool_to_int(CLEONOS_CLKS_ENABLE_USRD_TASK CLKS_CFG_USRD_TASK_INT)
set(CFLAGS_COMMON set(CFLAGS_COMMON
-std=c11 -std=c11
@@ -132,6 +182,19 @@ set(CFLAGS_COMMON
set(ARCH_CFLAGS set(ARCH_CFLAGS
-DCLKS_ARCH_X86_64=1 -DCLKS_ARCH_X86_64=1
"-DCLKS_CFG_AUDIO=${CLKS_CFG_AUDIO_INT}"
"-DCLKS_CFG_MOUSE=${CLKS_CFG_MOUSE_INT}"
"-DCLKS_CFG_DESKTOP=${CLKS_CFG_DESKTOP_INT}"
"-DCLKS_CFG_DRIVER_MANAGER=${CLKS_CFG_DRIVER_MANAGER_INT}"
"-DCLKS_CFG_KELF=${CLKS_CFG_KELF_INT}"
"-DCLKS_CFG_USERLAND_AUTO_EXEC=${CLKS_CFG_USERLAND_AUTO_EXEC_INT}"
"-DCLKS_CFG_HEAP_SELFTEST=${CLKS_CFG_HEAP_SELFTEST_INT}"
"-DCLKS_CFG_EXTERNAL_PSF=${CLKS_CFG_EXTERNAL_PSF_INT}"
"-DCLKS_CFG_KEYBOARD=${CLKS_CFG_KEYBOARD_INT}"
"-DCLKS_CFG_ELFRUNNER_PROBE=${CLKS_CFG_ELFRUNNER_PROBE_INT}"
"-DCLKS_CFG_KLOGD_TASK=${CLKS_CFG_KLOGD_TASK_INT}"
"-DCLKS_CFG_KWORKER_TASK=${CLKS_CFG_KWORKER_TASK_INT}"
"-DCLKS_CFG_USRD_TASK=${CLKS_CFG_USRD_TASK_INT}"
-m64 -m64
-mno-red-zone -mno-red-zone
-mcmodel=kernel -mcmodel=kernel
@@ -393,6 +456,9 @@ set(USER_SHELL_COMMAND_APPS
foreach(SRC IN LISTS USER_APP_MAIN_SOURCES) foreach(SRC IN LISTS USER_APP_MAIN_SOURCES)
get_filename_component(_stem "${SRC}" NAME_WE) get_filename_component(_stem "${SRC}" NAME_WE)
string(REGEX REPLACE "_main$" "" _app_name "${_stem}") string(REGEX REPLACE "_main$" "" _app_name "${_stem}")
string(TOUPPER "${_app_name}" _app_name_upper)
string(REGEX REPLACE "[^A-Z0-9]" "_" _app_name_key "${_app_name_upper}")
set(_app_enabled_var "CLEONOS_USER_APP_${_app_name_key}")
list(FIND USER_KELF_APP_NAMES "${_app_name}" _shadowed_by_kmain) list(FIND USER_KELF_APP_NAMES "${_app_name}" _shadowed_by_kmain)
if(NOT _shadowed_by_kmain EQUAL -1) if(NOT _shadowed_by_kmain EQUAL -1)
@@ -400,6 +466,17 @@ foreach(SRC IN LISTS USER_APP_MAIN_SOURCES)
continue() continue()
endif() endif()
if(NOT DEFINED ${_app_enabled_var})
set(${_app_enabled_var} ON CACHE BOOL "Build user app ${_app_name}")
else()
set(${_app_enabled_var} ${${_app_enabled_var}} CACHE BOOL "Build user app ${_app_name}" FORCE)
endif()
if(NOT ${_app_enabled_var})
cl_log_info("menuconfig: skip disabled user app ${_app_name}")
continue()
endif()
list(FIND USER_APP_NAMES "${_app_name}" _dup_name_idx) list(FIND USER_APP_NAMES "${_app_name}" _dup_name_idx)
if(NOT _dup_name_idx EQUAL -1) if(NOT _dup_name_idx EQUAL -1)
cl_log_error("duplicate user app name: ${_app_name}") cl_log_error("duplicate user app name: ${_app_name}")
@@ -456,6 +533,20 @@ endforeach()
foreach(SRC IN LISTS USER_APP_KMAIN_SOURCES) foreach(SRC IN LISTS USER_APP_KMAIN_SOURCES)
get_filename_component(_stem "${SRC}" NAME_WE) get_filename_component(_stem "${SRC}" NAME_WE)
string(REGEX REPLACE "_kmain$" "" _app_name "${_stem}") string(REGEX REPLACE "_kmain$" "" _app_name "${_stem}")
string(TOUPPER "${_app_name}" _app_name_upper)
string(REGEX REPLACE "[^A-Z0-9]" "_" _app_name_key "${_app_name_upper}")
set(_app_enabled_var "CLEONOS_USER_APP_${_app_name_key}")
if(NOT DEFINED ${_app_enabled_var})
set(${_app_enabled_var} ON CACHE BOOL "Build user app ${_app_name}")
else()
set(${_app_enabled_var} ${${_app_enabled_var}} CACHE BOOL "Build user app ${_app_name}" FORCE)
endif()
if(NOT ${_app_enabled_var})
cl_log_info("menuconfig: skip disabled user app ${_app_name}")
continue()
endif()
list(FIND USER_APP_NAMES "${_app_name}" _dup_name_idx) list(FIND USER_APP_NAMES "${_app_name}" _dup_name_idx)
if(NOT _dup_name_idx EQUAL -1) if(NOT _dup_name_idx EQUAL -1)
@@ -690,9 +781,23 @@ add_custom_target(clean-all
-P "${CL_LOG_EMIT_SCRIPT}" -P "${CL_LOG_EMIT_SCRIPT}"
) )
find_package(Python3 COMPONENTS Interpreter QUIET)
if(Python3_Interpreter_FOUND)
add_custom_target(menuconfig
COMMAND ${Python3_EXECUTABLE} "${CMAKE_SOURCE_DIR}/scripts/menuconfig.py"
USES_TERMINAL
)
else()
add_custom_target(menuconfig
COMMAND ${CMAKE_COMMAND} -E echo "python3 not found; run scripts/menuconfig.py manually"
USES_TERMINAL
)
endif()
add_custom_target(cleonos-help add_custom_target(cleonos-help
COMMAND ${CMAKE_COMMAND} -E echo "CLeonOS CMake build system (x86_64 only)" COMMAND ${CMAKE_COMMAND} -E echo "CLeonOS CMake build system (x86_64 only)"
COMMAND ${CMAKE_COMMAND} -E echo " cmake -S . -B build-cmake" COMMAND ${CMAKE_COMMAND} -E echo " cmake -S . -B build-cmake"
COMMAND ${CMAKE_COMMAND} -E echo " cmake --build build-cmake --target menuconfig"
COMMAND ${CMAKE_COMMAND} -E echo " cmake --build build-cmake --target setup" COMMAND ${CMAKE_COMMAND} -E echo " cmake --build build-cmake --target setup"
COMMAND ${CMAKE_COMMAND} -E echo " cmake --build build-cmake --target userapps" COMMAND ${CMAKE_COMMAND} -E echo " cmake --build build-cmake --target userapps"
COMMAND ${CMAKE_COMMAND} -E echo " cmake --build build-cmake --target iso" COMMAND ${CMAKE_COMMAND} -E echo " cmake --build build-cmake --target iso"

View File

@@ -15,6 +15,8 @@ LIMINE_BIN_DIR ?=
OBJCOPY_FOR_TARGET ?= OBJCOPY_FOR_TARGET ?=
OBJDUMP_FOR_TARGET ?= OBJDUMP_FOR_TARGET ?=
READELF_FOR_TARGET ?= READELF_FOR_TARGET ?=
PYTHON ?= python3
MENUCONFIG_ARGS ?=
ifeq ($(strip $(CMAKE_GENERATOR)),) ifeq ($(strip $(CMAKE_GENERATOR)),)
GEN_ARG := GEN_ARG :=
@@ -49,7 +51,7 @@ ifneq ($(strip $(READELF_FOR_TARGET)),)
CMAKE_PASSTHROUGH_ARGS += -DREADELF_FOR_TARGET=$(READELF_FOR_TARGET) CMAKE_PASSTHROUGH_ARGS += -DREADELF_FOR_TARGET=$(READELF_FOR_TARGET)
endif endif
.PHONY: all configure reconfigure setup setup-tools setup-limine kernel userapps ramdisk-root ramdisk iso run debug clean clean-all help .PHONY: all configure reconfigure menuconfig setup setup-tools setup-limine kernel userapps ramdisk-root ramdisk iso run debug clean clean-all help
all: iso all: iso
@@ -60,6 +62,17 @@ reconfigure:
> @rm -rf $(CMAKE_BUILD_DIR) > @rm -rf $(CMAKE_BUILD_DIR)
> @$(MAKE) configure CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) CMAKE_GENERATOR="$(CMAKE_GENERATOR)" CMAKE_EXTRA_ARGS="$(CMAKE_EXTRA_ARGS)" NO_COLOR="$(NO_COLOR)" LIMINE_SKIP_CONFIGURE="$(LIMINE_SKIP_CONFIGURE)" LIMINE_REF="$(LIMINE_REF)" LIMINE_REPO="$(LIMINE_REPO)" LIMINE_DIR="$(LIMINE_DIR)" LIMINE_BIN_DIR="$(LIMINE_BIN_DIR)" OBJCOPY_FOR_TARGET="$(OBJCOPY_FOR_TARGET)" OBJDUMP_FOR_TARGET="$(OBJDUMP_FOR_TARGET)" READELF_FOR_TARGET="$(READELF_FOR_TARGET)" > @$(MAKE) configure CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) CMAKE_GENERATOR="$(CMAKE_GENERATOR)" CMAKE_EXTRA_ARGS="$(CMAKE_EXTRA_ARGS)" NO_COLOR="$(NO_COLOR)" LIMINE_SKIP_CONFIGURE="$(LIMINE_SKIP_CONFIGURE)" LIMINE_REF="$(LIMINE_REF)" LIMINE_REPO="$(LIMINE_REPO)" LIMINE_DIR="$(LIMINE_DIR)" LIMINE_BIN_DIR="$(LIMINE_BIN_DIR)" OBJCOPY_FOR_TARGET="$(OBJCOPY_FOR_TARGET)" OBJDUMP_FOR_TARGET="$(OBJDUMP_FOR_TARGET)" READELF_FOR_TARGET="$(READELF_FOR_TARGET)"
menuconfig:
> @if command -v $(PYTHON) >/dev/null 2>&1; then \
> $(PYTHON) scripts/menuconfig.py $(MENUCONFIG_ARGS); \
> elif command -v python >/dev/null 2>&1; then \
> python scripts/menuconfig.py $(MENUCONFIG_ARGS); \
> else \
> echo "python3/python not found"; \
> exit 1; \
> fi
> @$(MAKE) configure CMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) CMAKE_GENERATOR="$(CMAKE_GENERATOR)" CMAKE_EXTRA_ARGS="$(CMAKE_EXTRA_ARGS)" NO_COLOR="$(NO_COLOR)" LIMINE_SKIP_CONFIGURE="$(LIMINE_SKIP_CONFIGURE)" LIMINE_REF="$(LIMINE_REF)" LIMINE_REPO="$(LIMINE_REPO)" LIMINE_DIR="$(LIMINE_DIR)" LIMINE_BIN_DIR="$(LIMINE_BIN_DIR)" OBJCOPY_FOR_TARGET="$(OBJCOPY_FOR_TARGET)" OBJDUMP_FOR_TARGET="$(OBJDUMP_FOR_TARGET)" READELF_FOR_TARGET="$(READELF_FOR_TARGET)"
setup: configure setup: configure
> @$(CMAKE) --build $(CMAKE_BUILD_DIR) --target setup > @$(CMAKE) --build $(CMAKE_BUILD_DIR) --target setup
@@ -107,6 +120,7 @@ clean-all:
help: help:
> @echo "CLeonOS (CMake-backed wrapper)" > @echo "CLeonOS (CMake-backed wrapper)"
> @echo " make configure" > @echo " make configure"
> @echo " make menuconfig"
> @echo " make setup" > @echo " make setup"
> @echo " make userapps" > @echo " make userapps"
> @echo " make iso" > @echo " make iso"

View File

@@ -26,6 +26,54 @@
#include <clks/types.h> #include <clks/types.h>
#include <clks/userland.h> #include <clks/userland.h>
#ifndef CLKS_CFG_AUDIO
#define CLKS_CFG_AUDIO 1
#endif
#ifndef CLKS_CFG_MOUSE
#define CLKS_CFG_MOUSE 1
#endif
#ifndef CLKS_CFG_DESKTOP
#define CLKS_CFG_DESKTOP 1
#endif
#ifndef CLKS_CFG_DRIVER_MANAGER
#define CLKS_CFG_DRIVER_MANAGER 1
#endif
#ifndef CLKS_CFG_KELF
#define CLKS_CFG_KELF 1
#endif
#ifndef CLKS_CFG_HEAP_SELFTEST
#define CLKS_CFG_HEAP_SELFTEST 1
#endif
#ifndef CLKS_CFG_EXTERNAL_PSF
#define CLKS_CFG_EXTERNAL_PSF 1
#endif
#ifndef CLKS_CFG_KEYBOARD
#define CLKS_CFG_KEYBOARD 1
#endif
#ifndef CLKS_CFG_ELFRUNNER_PROBE
#define CLKS_CFG_ELFRUNNER_PROBE 1
#endif
#ifndef CLKS_CFG_KLOGD_TASK
#define CLKS_CFG_KLOGD_TASK 1
#endif
#ifndef CLKS_CFG_KWORKER_TASK
#define CLKS_CFG_KWORKER_TASK 1
#endif
#ifndef CLKS_CFG_USRD_TASK
#define CLKS_CFG_USRD_TASK 1
#endif
static void clks_task_klogd(u64 tick) { static void clks_task_klogd(u64 tick) {
static u64 last_emit = 0ULL; static u64 last_emit = 0ULL;
@@ -61,15 +109,21 @@ static void clks_task_kworker(u64 tick) {
} }
static void clks_task_kelfd(u64 tick) { static void clks_task_kelfd(u64 tick) {
#if CLKS_CFG_KELF
clks_service_heartbeat(CLKS_SERVICE_KELF, tick); clks_service_heartbeat(CLKS_SERVICE_KELF, tick);
clks_kelf_tick(tick); clks_kelf_tick(tick);
#else
(void)tick;
#endif
} }
static void clks_task_usrd(u64 tick) { static void clks_task_usrd(u64 tick) {
clks_service_heartbeat(CLKS_SERVICE_USER, tick); clks_service_heartbeat(CLKS_SERVICE_USER, tick);
clks_exec_tick(tick); clks_exec_tick(tick);
clks_userland_tick(tick); clks_userland_tick(tick);
#if CLKS_CFG_DESKTOP
clks_desktop_tick(tick); clks_desktop_tick(tick);
#endif
clks_tty_tick(tick); clks_tty_tick(tick);
clks_shell_tick(tick); clks_shell_tick(tick);
} }
@@ -81,11 +135,8 @@ void clks_kernel_main(void) {
struct clks_heap_stats heap_stats; struct clks_heap_stats heap_stats;
struct clks_scheduler_stats sched_stats; struct clks_scheduler_stats sched_stats;
struct clks_fs_node_info fs_system_dir = {0}; struct clks_fs_node_info fs_system_dir = {0};
void *heap_probe = CLKS_NULL;
u64 syscall_ticks; u64 syscall_ticks;
u64 fs_root_children; u64 fs_root_children;
const void *tty_psf_blob = CLKS_NULL;
u64 tty_psf_size = 0ULL;
clks_serial_init(); clks_serial_init();
@@ -139,7 +190,8 @@ void clks_kernel_main(void) {
clks_log_hex(CLKS_LOG_INFO, "HEAP", "TOTAL_BYTES", heap_stats.total_bytes); clks_log_hex(CLKS_LOG_INFO, "HEAP", "TOTAL_BYTES", heap_stats.total_bytes);
clks_log_hex(CLKS_LOG_INFO, "HEAP", "FREE_BYTES", heap_stats.free_bytes); clks_log_hex(CLKS_LOG_INFO, "HEAP", "FREE_BYTES", heap_stats.free_bytes);
heap_probe = clks_kmalloc(128); #if CLKS_CFG_HEAP_SELFTEST
void *heap_probe = clks_kmalloc(128);
if (heap_probe == CLKS_NULL) { if (heap_probe == CLKS_NULL) {
clks_log(CLKS_LOG_ERROR, "HEAP", "KMALLOC SELFTEST FAILED"); clks_log(CLKS_LOG_ERROR, "HEAP", "KMALLOC SELFTEST FAILED");
@@ -147,6 +199,9 @@ void clks_kernel_main(void) {
clks_log(CLKS_LOG_INFO, "HEAP", "KMALLOC SELFTEST OK"); clks_log(CLKS_LOG_INFO, "HEAP", "KMALLOC SELFTEST OK");
clks_kfree(heap_probe); clks_kfree(heap_probe);
} }
#else
clks_log(CLKS_LOG_WARN, "CFG", "HEAP SELFTEST DISABLED BY MENUCONFIG");
#endif
clks_fs_init(); clks_fs_init();
@@ -164,6 +219,10 @@ void clks_kernel_main(void) {
} }
if (boot_fb != CLKS_NULL) { if (boot_fb != CLKS_NULL) {
#if CLKS_CFG_EXTERNAL_PSF
const void *tty_psf_blob;
u64 tty_psf_size = 0ULL;
tty_psf_blob = clks_fs_read_all("/system/tty.psf", &tty_psf_size); tty_psf_blob = clks_fs_read_all("/system/tty.psf", &tty_psf_size);
if (tty_psf_blob != CLKS_NULL && clks_fb_load_psf_font(tty_psf_blob, tty_psf_size) == CLKS_TRUE) { if (tty_psf_blob != CLKS_NULL && clks_fb_load_psf_font(tty_psf_blob, tty_psf_size) == CLKS_TRUE) {
@@ -173,39 +232,82 @@ void clks_kernel_main(void) {
} else { } else {
clks_log(CLKS_LOG_WARN, "TTY", "EXTERNAL PSF LOAD FAILED, USING BUILTIN"); clks_log(CLKS_LOG_WARN, "TTY", "EXTERNAL PSF LOAD FAILED, USING BUILTIN");
} }
#else
clks_log(CLKS_LOG_WARN, "CFG", "EXTERNAL PSF LOADING DISABLED BY MENUCONFIG");
#endif
} }
clks_exec_init(); clks_exec_init();
#if CLKS_CFG_AUDIO
clks_audio_init(); clks_audio_init();
#else
clks_log(CLKS_LOG_WARN, "CFG", "AUDIO DISABLED BY MENUCONFIG");
#endif
#if CLKS_CFG_KEYBOARD
clks_keyboard_init(); clks_keyboard_init();
#else
clks_log(CLKS_LOG_WARN, "CFG", "KEYBOARD DISABLED BY MENUCONFIG");
#endif
#if CLKS_CFG_MOUSE
clks_mouse_init(); clks_mouse_init();
#else
clks_log(CLKS_LOG_WARN, "CFG", "MOUSE DISABLED BY MENUCONFIG");
#endif
#if CLKS_CFG_DESKTOP
clks_desktop_init(); clks_desktop_init();
#else
clks_log(CLKS_LOG_WARN, "CFG", "DESKTOP DISABLED BY MENUCONFIG");
#endif
if (clks_userland_init() == CLKS_FALSE) { if (clks_userland_init() == CLKS_FALSE) {
clks_log(CLKS_LOG_ERROR, "USER", "USERLAND INIT FAILED"); clks_log(CLKS_LOG_ERROR, "USER", "USERLAND INIT FAILED");
clks_cpu_halt_forever(); clks_cpu_halt_forever();
} }
#if CLKS_CFG_DRIVER_MANAGER
clks_driver_init(); clks_driver_init();
#else
clks_log(CLKS_LOG_WARN, "CFG", "DRIVER MANAGER DISABLED BY MENUCONFIG");
#endif
#if CLKS_CFG_KELF
clks_kelf_init(); clks_kelf_init();
#else
clks_log(CLKS_LOG_WARN, "CFG", "KELF DISABLED BY MENUCONFIG");
#endif
clks_scheduler_init(); clks_scheduler_init();
#if CLKS_CFG_KLOGD_TASK
if (clks_scheduler_add_kernel_task_ex("klogd", 4U, clks_task_klogd) == CLKS_FALSE) { if (clks_scheduler_add_kernel_task_ex("klogd", 4U, clks_task_klogd) == CLKS_FALSE) {
clks_log(CLKS_LOG_WARN, "SCHED", "FAILED TO ADD KLOGD TASK"); clks_log(CLKS_LOG_WARN, "SCHED", "FAILED TO ADD KLOGD TASK");
} }
#else
clks_log(CLKS_LOG_WARN, "SCHED", "KLOGD TASK DISABLED BY MENUCONFIG");
#endif
#if CLKS_CFG_KWORKER_TASK
if (clks_scheduler_add_kernel_task_ex("kworker", 3U, clks_task_kworker) == CLKS_FALSE) { if (clks_scheduler_add_kernel_task_ex("kworker", 3U, clks_task_kworker) == CLKS_FALSE) {
clks_log(CLKS_LOG_WARN, "SCHED", "FAILED TO ADD KWORKER TASK"); clks_log(CLKS_LOG_WARN, "SCHED", "FAILED TO ADD KWORKER TASK");
} }
#else
clks_log(CLKS_LOG_WARN, "SCHED", "KWORKER TASK DISABLED BY MENUCONFIG");
#endif
#if CLKS_CFG_KELF
if (clks_scheduler_add_kernel_task_ex("kelfd", 5U, clks_task_kelfd) == CLKS_FALSE) { if (clks_scheduler_add_kernel_task_ex("kelfd", 5U, clks_task_kelfd) == CLKS_FALSE) {
clks_log(CLKS_LOG_WARN, "SCHED", "FAILED TO ADD KELFD TASK"); clks_log(CLKS_LOG_WARN, "SCHED", "FAILED TO ADD KELFD TASK");
} }
#else
clks_log(CLKS_LOG_WARN, "SCHED", "KELFD TASK DISABLED BY MENUCONFIG");
#endif
#if CLKS_CFG_USRD_TASK
if (clks_scheduler_add_kernel_task_ex("usrd", 4U, clks_task_usrd) == CLKS_FALSE) { if (clks_scheduler_add_kernel_task_ex("usrd", 4U, clks_task_usrd) == CLKS_FALSE) {
clks_log(CLKS_LOG_WARN, "SCHED", "FAILED TO ADD USRD TASK"); clks_log(CLKS_LOG_WARN, "SCHED", "FAILED TO ADD USRD TASK");
} }
#else
clks_log(CLKS_LOG_WARN, "SCHED", "USRD TASK DISABLED BY MENUCONFIG");
#endif
sched_stats = clks_scheduler_get_stats(); sched_stats = clks_scheduler_get_stats();
clks_log_hex(CLKS_LOG_INFO, "SCHED", "TASK_COUNT", sched_stats.task_count); clks_log_hex(CLKS_LOG_INFO, "SCHED", "TASK_COUNT", sched_stats.task_count);
@@ -214,9 +316,13 @@ void clks_kernel_main(void) {
clks_elfrunner_init(); clks_elfrunner_init();
#if CLKS_CFG_ELFRUNNER_PROBE
if (clks_elfrunner_probe_kernel_executable() == CLKS_FALSE) { if (clks_elfrunner_probe_kernel_executable() == CLKS_FALSE) {
clks_log(CLKS_LOG_ERROR, "ELF", "KERNEL ELF PROBE FAILED"); clks_log(CLKS_LOG_ERROR, "ELF", "KERNEL ELF PROBE FAILED");
} }
#else
clks_log(CLKS_LOG_WARN, "CFG", "ELFRUNNER PROBE DISABLED BY MENUCONFIG");
#endif
clks_syscall_init(); clks_syscall_init();
@@ -228,11 +334,15 @@ void clks_kernel_main(void) {
clks_shell_init(); clks_shell_init();
#if CLKS_CFG_USRD_TASK
if (clks_userland_shell_auto_exec_enabled() == CLKS_TRUE) { if (clks_userland_shell_auto_exec_enabled() == CLKS_TRUE) {
clks_log(CLKS_LOG_INFO, "SHELL", "DEFAULT ENTER USER SHELL MODE"); clks_log(CLKS_LOG_INFO, "SHELL", "DEFAULT ENTER USER SHELL MODE");
} else { } else {
clks_log(CLKS_LOG_INFO, "SHELL", "KERNEL SHELL ACTIVE"); clks_log(CLKS_LOG_INFO, "SHELL", "KERNEL SHELL ACTIVE");
} }
#else
clks_log(CLKS_LOG_WARN, "SHELL", "USRD TASK DISABLED; INTERACTIVE SHELL TICK OFF");
#endif
clks_log_hex(CLKS_LOG_INFO, "TTY", "COUNT", (u64)clks_tty_count()); clks_log_hex(CLKS_LOG_INFO, "TTY", "COUNT", (u64)clks_tty_count());
clks_log_hex(CLKS_LOG_INFO, "TTY", "ACTIVE", (u64)clks_tty_active()); clks_log_hex(CLKS_LOG_INFO, "TTY", "ACTIVE", (u64)clks_tty_active());

View File

@@ -7,6 +7,10 @@
#define CLKS_USERLAND_RETRY_INTERVAL 500ULL #define CLKS_USERLAND_RETRY_INTERVAL 500ULL
#ifndef CLKS_CFG_USERLAND_AUTO_EXEC
#define CLKS_CFG_USERLAND_AUTO_EXEC 1
#endif
static clks_bool clks_user_shell_ready = CLKS_FALSE; static clks_bool clks_user_shell_ready = CLKS_FALSE;
static clks_bool clks_user_shell_exec_requested_flag = CLKS_FALSE; static clks_bool clks_user_shell_exec_requested_flag = CLKS_FALSE;
static clks_bool clks_user_shell_exec_enabled = CLKS_FALSE; static clks_bool clks_user_shell_exec_enabled = CLKS_FALSE;
@@ -84,7 +88,7 @@ clks_bool clks_userland_init(void) {
clks_user_shell_ready = CLKS_FALSE; clks_user_shell_ready = CLKS_FALSE;
clks_user_shell_exec_requested_flag = CLKS_FALSE; clks_user_shell_exec_requested_flag = CLKS_FALSE;
clks_user_shell_exec_enabled = CLKS_TRUE; clks_user_shell_exec_enabled = (CLKS_CFG_USERLAND_AUTO_EXEC != 0) ? CLKS_TRUE : CLKS_FALSE;
clks_user_launch_attempt_count = 0ULL; clks_user_launch_attempt_count = 0ULL;
clks_user_launch_success_count = 0ULL; clks_user_launch_success_count = 0ULL;
clks_user_launch_fail_count = 0ULL; clks_user_launch_fail_count = 0ULL;
@@ -107,7 +111,11 @@ clks_bool clks_userland_init(void) {
return CLKS_FALSE; return CLKS_FALSE;
} }
clks_log(CLKS_LOG_INFO, "USER", "USER SHELL AUTO EXEC ENABLED"); if (clks_user_shell_exec_enabled == CLKS_TRUE) {
clks_log(CLKS_LOG_INFO, "USER", "USER SHELL AUTO EXEC ENABLED");
} else {
clks_log(CLKS_LOG_WARN, "USER", "USER SHELL AUTO EXEC DISABLED BY MENUCONFIG");
}
return CLKS_TRUE; return CLKS_TRUE;
} }

View File

@@ -0,0 +1,82 @@
{
"features": [
{
"key": "CLEONOS_CLKS_ENABLE_AUDIO",
"title": "Audio Driver Init",
"description": "Initialize kernel audio subsystem during boot.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_MOUSE",
"title": "PS/2 Mouse Input",
"description": "Initialize kernel PS/2 mouse input subsystem.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_DESKTOP",
"title": "TTY2 Desktop",
"description": "Enable desktop compositor tick/update path on TTY2.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_DRIVER_MANAGER",
"title": "Driver Manager",
"description": "Initialize kernel ELF driver manager.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_KELF",
"title": "KELF Executor",
"description": "Enable kernel ELF app dispatcher and kelfd task.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_USERLAND_AUTO_EXEC",
"title": "Auto Enter User Shell",
"description": "Auto-exec /shell/shell.elf after kernel boot.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_HEAP_SELFTEST",
"title": "Heap Selftest",
"description": "Run kmalloc/kfree selftest during kernel boot.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_EXTERNAL_PSF",
"title": "Load External PSF Font",
"description": "Load /system/tty.psf and apply it to framebuffer TTY.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_KEYBOARD",
"title": "PS/2 Keyboard Input",
"description": "Initialize PS/2 keyboard input subsystem.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_ELFRUNNER_PROBE",
"title": "ELFRUNNER Probe",
"description": "Probe kernel ELF runtime metadata after ELFRUNNER init.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_KLOGD_TASK",
"title": "Scheduler Task: klogd",
"description": "Enable periodic klogd maintenance task.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_KWORKER_TASK",
"title": "Scheduler Task: kworker",
"description": "Enable periodic kernel worker service-heartbeat task.",
"default": true
},
{
"key": "CLEONOS_CLKS_ENABLE_USRD_TASK",
"title": "Scheduler Task: usrd",
"description": "Enable user/runtime dispatch task (shell tick, tty tick, exec tick).",
"default": true
}
]
}

518
scripts/menuconfig.py Normal file
View File

@@ -0,0 +1,518 @@
#!/usr/bin/env python3
"""
CLeonOS menuconfig
Interactive feature selector that writes:
- configs/menuconfig/.config.json
- configs/menuconfig/config.cmake
Design:
- CLKS options come from configs/menuconfig/clks_features.json
- User-space app options are discovered dynamically from *_main.c / *_kmain.c
"""
from __future__ import annotations
import argparse
import os
import json
import re
import sys
import textwrap
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, List, Tuple
try:
import curses
except Exception:
curses = None
ROOT_DIR = Path(__file__).resolve().parent.parent
APPS_DIR = ROOT_DIR / "cleonos" / "c" / "apps"
MENUCONFIG_DIR = ROOT_DIR / "configs" / "menuconfig"
CLKS_FEATURES_PATH = MENUCONFIG_DIR / "clks_features.json"
CONFIG_JSON_PATH = MENUCONFIG_DIR / ".config.json"
CONFIG_CMAKE_PATH = MENUCONFIG_DIR / "config.cmake"
@dataclass(frozen=True)
class OptionItem:
key: str
title: str
description: str
default: bool
def normalize_bool(raw: object, default: bool) -> bool:
if isinstance(raw, bool):
return raw
if isinstance(raw, (int, float)):
return raw != 0
if isinstance(raw, str):
text = raw.strip().lower()
if text in {"1", "on", "true", "yes", "y"}:
return True
if text in {"0", "off", "false", "no", "n"}:
return False
return default
def sanitize_token(name: str) -> str:
token = re.sub(r"[^A-Za-z0-9]+", "_", name.strip().upper())
token = token.strip("_")
return token or "UNKNOWN"
def load_clks_options() -> List[OptionItem]:
if not CLKS_FEATURES_PATH.exists():
raise RuntimeError(f"missing CLKS feature file: {CLKS_FEATURES_PATH}")
raw = json.loads(CLKS_FEATURES_PATH.read_text(encoding="utf-8"))
if not isinstance(raw, dict) or "features" not in raw or not isinstance(raw["features"], list):
raise RuntimeError(f"invalid feature format in {CLKS_FEATURES_PATH}")
options: List[OptionItem] = []
for entry in raw["features"]:
if not isinstance(entry, dict):
continue
key = str(entry.get("key", "")).strip()
title = str(entry.get("title", key)).strip()
description = str(entry.get("description", "")).strip()
default = normalize_bool(entry.get("default", True), True)
if not key:
continue
options.append(OptionItem(key=key, title=title, description=description, default=default))
if not options:
raise RuntimeError(f"no CLKS feature options in {CLKS_FEATURES_PATH}")
return options
def discover_user_apps() -> List[OptionItem]:
main_paths = sorted(APPS_DIR.glob("*_main.c"))
kmain_paths = sorted(APPS_DIR.glob("*_kmain.c"))
kmain_names = set()
for path in kmain_paths:
name = path.stem
if name.endswith("_kmain"):
kmain_names.add(name[:-6])
final_apps: List[Tuple[str, str]] = []
for path in main_paths:
name = path.stem
if not name.endswith("_main"):
continue
app = name[:-5]
if app in kmain_names:
continue
if app.endswith("drv"):
section = "driver"
elif app == "hello":
section = "root"
else:
section = "shell"
final_apps.append((app, section))
for app in sorted(kmain_names):
final_apps.append((app, "system"))
final_apps.sort(key=lambda item: (item[1], item[0]))
options: List[OptionItem] = []
for app, section in final_apps:
key = f"CLEONOS_USER_APP_{sanitize_token(app)}"
title = f"{app}.elf [{section}]"
description = f"Build and package user app '{app}' into ramdisk/{section}."
options.append(OptionItem(key=key, title=title, description=description, default=True))
return options
def load_previous_values() -> Dict[str, bool]:
if not CONFIG_JSON_PATH.exists():
return {}
try:
raw = json.loads(CONFIG_JSON_PATH.read_text(encoding="utf-8"))
except Exception:
return {}
if not isinstance(raw, dict):
return {}
out: Dict[str, bool] = {}
for key, value in raw.items():
if not isinstance(key, str):
continue
out[key] = normalize_bool(value, False)
return out
def init_values(options: Iterable[OptionItem], previous: Dict[str, bool], use_defaults: bool) -> Dict[str, bool]:
values: Dict[str, bool] = {}
for item in options:
if not use_defaults and item.key in previous:
values[item.key] = previous[item.key]
else:
values[item.key] = item.default
return values
def print_section(title: str, options: List[OptionItem], values: Dict[str, bool]) -> None:
print()
print(f"== {title} ==")
for idx, item in enumerate(options, start=1):
mark = "x" if values.get(item.key, item.default) else " "
print(f"{idx:3d}. [{mark}] {item.title}")
print("Commands: <number> toggle, a enable-all, n disable-all, i <n> info, b back")
def section_loop(title: str, options: List[OptionItem], values: Dict[str, bool]) -> None:
while True:
print_section(title, options, values)
raw = input(f"{title}> ").strip()
if not raw:
continue
lower = raw.lower()
if lower in {"b", "back", "q", "quit"}:
return
if lower in {"a", "all", "on"}:
for item in options:
values[item.key] = True
continue
if lower in {"n", "none", "off"}:
for item in options:
values[item.key] = False
continue
if lower.startswith("i "):
token = lower[2:].strip()
if token.isdigit():
idx = int(token)
if 1 <= idx <= len(options):
item = options[idx - 1]
state = "ON" if values.get(item.key, item.default) else "OFF"
print()
print(f"[{idx}] {item.title}")
print(f"key: {item.key}")
print(f"state: {state}")
print(f"desc: {item.description}")
continue
print("invalid info index")
continue
if raw.isdigit():
idx = int(raw)
if 1 <= idx <= len(options):
item = options[idx - 1]
values[item.key] = not values.get(item.key, item.default)
else:
print("invalid index")
continue
print("unknown command")
def _safe_addnstr(stdscr, y: int, x: int, text: str, attr: int = 0) -> None:
h, w = stdscr.getmaxyx()
if y < 0 or y >= h or x >= w:
return
max_len = max(0, w - x - 1)
if max_len <= 0:
return
try:
stdscr.addnstr(y, x, text, max_len, attr)
except Exception:
pass
def _option_enabled(values: Dict[str, bool], item: OptionItem) -> bool:
return values.get(item.key, item.default)
def _set_all(values: Dict[str, bool], options: List[OptionItem], enabled: bool) -> None:
for item in options:
values[item.key] = enabled
def _run_ncurses_section(stdscr, title: str, options: List[OptionItem], values: Dict[str, bool]) -> None:
selected = 0
top = 0
while True:
stdscr.erase()
h, w = stdscr.getmaxyx()
if h < 10 or w < 40:
_safe_addnstr(stdscr, 0, 0, "Terminal too small for menuconfig (need >= 40x10).", curses.A_BOLD)
_safe_addnstr(stdscr, 2, 0, "Resize terminal then press any key, or ESC to go back.")
key = stdscr.getch()
if key in (27,):
return
continue
list_top = 2
desc_area = 4
help_area = 2
list_bottom = h - (desc_area + help_area) - 1
visible = max(1, list_bottom - list_top + 1)
if selected < 0:
selected = 0
if selected >= len(options):
selected = max(0, len(options) - 1)
if selected < top:
top = selected
if selected >= top + visible:
top = selected - visible + 1
if top < 0:
top = 0
_safe_addnstr(stdscr, 0, 0, f"CLeonOS menuconfig / {title}", curses.A_REVERSE)
_safe_addnstr(stdscr, 1, 0, f"Items: {len(options)}")
for row in range(visible):
idx = top + row
if idx >= len(options):
break
item = options[idx]
mark = "x" if _option_enabled(values, item) else " "
line = f"{idx + 1:3d}. [{mark}] {item.title}"
attr = curses.A_REVERSE if idx == selected else 0
_safe_addnstr(stdscr, list_top + row, 0, line, attr)
if options:
cur = options[selected]
key_line = f"{cur.key}"
desc_line = cur.description
wrapped = textwrap.wrap(desc_line, max(10, w - 2))
_safe_addnstr(stdscr, list_bottom + 1, 0, key_line, curses.A_DIM)
for i, part in enumerate(wrapped[: max(1, desc_area - 1)]):
_safe_addnstr(stdscr, list_bottom + 2 + i, 0, part)
_safe_addnstr(
stdscr,
h - 2,
0,
"Arrows/jk move Space toggle a all-on n all-off PgUp/PgDn Home/End",
curses.A_DIM,
)
_safe_addnstr(stdscr, h - 1, 0, "Enter/ESC/q back", curses.A_DIM)
stdscr.refresh()
key = stdscr.getch()
if key in (27, ord("q"), ord("Q"), curses.KEY_LEFT, curses.KEY_ENTER, 10, 13):
return
if key in (curses.KEY_UP, ord("k"), ord("K")):
selected -= 1
continue
if key in (curses.KEY_DOWN, ord("j"), ord("J")):
selected += 1
continue
if key == curses.KEY_PPAGE:
selected -= visible
continue
if key == curses.KEY_NPAGE:
selected += visible
continue
if key == curses.KEY_HOME:
selected = 0
continue
if key == curses.KEY_END:
selected = max(0, len(options) - 1)
continue
if key == ord(" "):
if options:
item = options[selected]
values[item.key] = not _option_enabled(values, item)
continue
if key in (ord("a"), ord("A")):
_set_all(values, options, True)
continue
if key in (ord("n"), ord("N")):
_set_all(values, options, False)
continue
def _run_ncurses_main(stdscr, clks_options: List[OptionItem], user_options: List[OptionItem], values: Dict[str, bool]) -> bool:
try:
curses.curs_set(0)
except Exception:
pass
stdscr.keypad(True)
selected = 0
while True:
stdscr.erase()
h, _w = stdscr.getmaxyx()
clks_on = sum(1 for item in clks_options if _option_enabled(values, item))
user_on = sum(1 for item in user_options if _option_enabled(values, item))
items = [
f"CLKS features ({clks_on}/{len(clks_options)} enabled)",
f"User apps ({user_on}/{len(user_options)} enabled)",
"Save and Exit",
"Quit without Saving",
]
_safe_addnstr(stdscr, 0, 0, "CLeonOS menuconfig (ncurses)", curses.A_REVERSE)
_safe_addnstr(stdscr, 1, 0, "Enter open/select, Arrow/jk move, s save, q quit", curses.A_DIM)
base = 3
for i, text in enumerate(items):
attr = curses.A_REVERSE if i == selected else 0
_safe_addnstr(stdscr, base + i, 0, text, attr)
_safe_addnstr(stdscr, h - 1, 0, "Tip: Space toggles options inside sections.", curses.A_DIM)
stdscr.refresh()
key = stdscr.getch()
if key in (ord("q"), ord("Q"), 27):
return False
if key in (ord("s"), ord("S")):
return True
if key in (curses.KEY_UP, ord("k"), ord("K")):
selected = (selected - 1) % len(items)
continue
if key in (curses.KEY_DOWN, ord("j"), ord("J")):
selected = (selected + 1) % len(items)
continue
if key in (curses.KEY_ENTER, 10, 13):
if selected == 0:
_run_ncurses_section(stdscr, "CLKS", clks_options, values)
elif selected == 1:
_run_ncurses_section(stdscr, "USER", user_options, values)
elif selected == 2:
return True
else:
return False
continue
def interactive_menu_ncurses(clks_options: List[OptionItem], user_options: List[OptionItem], values: Dict[str, bool]) -> bool:
if curses is None:
raise RuntimeError("python curses module unavailable (install python3-curses / ncurses)")
if "TERM" not in os.environ or not os.environ["TERM"]:
raise RuntimeError("TERM is not set; cannot start ncurses UI")
return bool(curses.wrapper(lambda stdscr: _run_ncurses_main(stdscr, clks_options, user_options, values)))
def write_outputs(all_values: Dict[str, bool], ordered_options: List[OptionItem]) -> None:
MENUCONFIG_DIR.mkdir(parents=True, exist_ok=True)
ordered_keys = [item.key for item in ordered_options]
output_values: Dict[str, bool] = {key: all_values[key] for key in ordered_keys if key in all_values}
CONFIG_JSON_PATH.write_text(
json.dumps(output_values, ensure_ascii=True, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
lines = [
"# Auto-generated by scripts/menuconfig.py",
"# Do not edit manually unless you know what you are doing.",
'set(CLEONOS_MENUCONFIG_LOADED ON CACHE BOOL "CLeonOS menuconfig loaded" FORCE)',
]
for item in ordered_options:
value = "ON" if all_values.get(item.key, item.default) else "OFF"
lines.append(f'set({item.key} {value} CACHE BOOL "{item.title}" FORCE)')
CONFIG_CMAKE_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
def show_summary(clks_options: List[OptionItem], user_options: List[OptionItem], values: Dict[str, bool]) -> None:
clks_on = sum(1 for item in clks_options if values.get(item.key, item.default))
user_on = sum(1 for item in user_options if values.get(item.key, item.default))
print()
print("========== CLeonOS menuconfig ==========")
print(f"1) CLKS features : {clks_on}/{len(clks_options)} enabled")
print(f"2) User features : {user_on}/{len(user_options)} enabled")
print("s) Save and exit")
print("q) Quit without saving")
def interactive_menu(clks_options: List[OptionItem], user_options: List[OptionItem], values: Dict[str, bool]) -> bool:
while True:
show_summary(clks_options, user_options, values)
choice = input("Select> ").strip().lower()
if choice == "1":
section_loop("CLKS", clks_options, values)
continue
if choice == "2":
section_loop("USER", user_options, values)
continue
if choice in {"s", "save"}:
return True
if choice in {"q", "quit"}:
return False
print("unknown selection")
def parse_set_overrides(values: Dict[str, bool], kv_pairs: List[str]) -> None:
for pair in kv_pairs:
if "=" not in pair:
raise RuntimeError(f"invalid --set entry: {pair!r}, expected KEY=ON|OFF")
key, raw = pair.split("=", 1)
key = key.strip()
if not key:
raise RuntimeError(f"invalid --set entry: {pair!r}, empty key")
values[key] = normalize_bool(raw, False)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="CLeonOS menuconfig")
parser.add_argument("--defaults", action="store_true", help="ignore previous .config and use defaults")
parser.add_argument("--non-interactive", action="store_true", help="save config without opening interactive menu")
parser.add_argument("--plain", action="store_true", help="use legacy plain-text menu instead of ncurses")
parser.add_argument(
"--set",
action="append",
default=[],
metavar="KEY=ON|OFF",
help="override one option before save (can be repeated)",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
clks_options = load_clks_options()
user_options = discover_user_apps()
all_options = clks_options + user_options
previous = load_previous_values()
values = init_values(all_options, previous, use_defaults=args.defaults)
parse_set_overrides(values, args.set)
should_save = args.non_interactive
if not args.non_interactive:
if not sys.stdin.isatty():
raise RuntimeError("menuconfig requires interactive tty (or use --non-interactive)")
if args.plain:
should_save = interactive_menu(clks_options, user_options, values)
else:
should_save = interactive_menu_ncurses(clks_options, user_options, values)
if not should_save:
print("menuconfig: no changes saved")
return 0
write_outputs(values, all_options)
print(f"menuconfig: wrote {CONFIG_JSON_PATH}")
print(f"menuconfig: wrote {CONFIG_CMAKE_PATH}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except RuntimeError as exc:
print(f"menuconfig error: {exc}", file=sys.stderr)
raise SystemExit(1)