From ddacc3e863f1f8bf75b8f271fa5275cbc0918dfd Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sat, 18 Apr 2026 15:37:24 +0800 Subject: [PATCH] menuconfig --- .gitignore | 4 + CMakeLists.txt | 105 ++++++ Makefile | 16 +- clks/kernel/kmain.c | 118 +++++- clks/kernel/userland.c | 12 +- configs/menuconfig/clks_features.json | 82 ++++ scripts/menuconfig.py | 518 ++++++++++++++++++++++++++ 7 files changed, 848 insertions(+), 7 deletions(-) create mode 100644 configs/menuconfig/clks_features.json create mode 100644 scripts/menuconfig.py diff --git a/.gitignore b/.gitignore index 3e1b0a5..0aeb3fc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ compile_commands.json CTestTestfile.cmake Testing/ .cmake/ + +# menuconfig generated +configs/menuconfig/.config.json +configs/menuconfig/config.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index c071ab3..6dc497e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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(USER_LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/cleonos/c/user.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 -std=c11 @@ -132,6 +182,19 @@ set(CFLAGS_COMMON set(ARCH_CFLAGS -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 -mno-red-zone -mcmodel=kernel @@ -393,6 +456,9 @@ set(USER_SHELL_COMMAND_APPS foreach(SRC IN LISTS USER_APP_MAIN_SOURCES) get_filename_component(_stem "${SRC}" NAME_WE) 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) if(NOT _shadowed_by_kmain EQUAL -1) @@ -400,6 +466,17 @@ foreach(SRC IN LISTS USER_APP_MAIN_SOURCES) continue() 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) if(NOT _dup_name_idx EQUAL -1) cl_log_error("duplicate user app name: ${_app_name}") @@ -456,6 +533,20 @@ endforeach() foreach(SRC IN LISTS USER_APP_KMAIN_SOURCES) get_filename_component(_stem "${SRC}" NAME_WE) 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) if(NOT _dup_name_idx EQUAL -1) @@ -690,9 +781,23 @@ add_custom_target(clean-all -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 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 --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 userapps" COMMAND ${CMAKE_COMMAND} -E echo " cmake --build build-cmake --target iso" diff --git a/Makefile b/Makefile index 601abf7..e886c25 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,8 @@ LIMINE_BIN_DIR ?= OBJCOPY_FOR_TARGET ?= OBJDUMP_FOR_TARGET ?= READELF_FOR_TARGET ?= +PYTHON ?= python3 +MENUCONFIG_ARGS ?= ifeq ($(strip $(CMAKE_GENERATOR)),) GEN_ARG := @@ -49,7 +51,7 @@ ifneq ($(strip $(READELF_FOR_TARGET)),) CMAKE_PASSTHROUGH_ARGS += -DREADELF_FOR_TARGET=$(READELF_FOR_TARGET) 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 @@ -60,6 +62,17 @@ reconfigure: > @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)" +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 > @$(CMAKE) --build $(CMAKE_BUILD_DIR) --target setup @@ -107,6 +120,7 @@ clean-all: help: > @echo "CLeonOS (CMake-backed wrapper)" > @echo " make configure" +> @echo " make menuconfig" > @echo " make setup" > @echo " make userapps" > @echo " make iso" diff --git a/clks/kernel/kmain.c b/clks/kernel/kmain.c index 7fb16d3..81f7ccd 100644 --- a/clks/kernel/kmain.c +++ b/clks/kernel/kmain.c @@ -26,6 +26,54 @@ #include #include +#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 u64 last_emit = 0ULL; @@ -61,15 +109,21 @@ static void clks_task_kworker(u64 tick) { } static void clks_task_kelfd(u64 tick) { +#if CLKS_CFG_KELF clks_service_heartbeat(CLKS_SERVICE_KELF, tick); clks_kelf_tick(tick); +#else + (void)tick; +#endif } static void clks_task_usrd(u64 tick) { clks_service_heartbeat(CLKS_SERVICE_USER, tick); clks_exec_tick(tick); clks_userland_tick(tick); +#if CLKS_CFG_DESKTOP clks_desktop_tick(tick); +#endif clks_tty_tick(tick); clks_shell_tick(tick); } @@ -81,11 +135,8 @@ void clks_kernel_main(void) { struct clks_heap_stats heap_stats; struct clks_scheduler_stats sched_stats; struct clks_fs_node_info fs_system_dir = {0}; - void *heap_probe = CLKS_NULL; u64 syscall_ticks; u64 fs_root_children; - const void *tty_psf_blob = CLKS_NULL; - u64 tty_psf_size = 0ULL; 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", "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) { 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_kfree(heap_probe); } +#else + clks_log(CLKS_LOG_WARN, "CFG", "HEAP SELFTEST DISABLED BY MENUCONFIG"); +#endif clks_fs_init(); @@ -164,6 +219,10 @@ void clks_kernel_main(void) { } 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); 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 { 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(); +#if CLKS_CFG_AUDIO clks_audio_init(); +#else + clks_log(CLKS_LOG_WARN, "CFG", "AUDIO DISABLED BY MENUCONFIG"); +#endif +#if CLKS_CFG_KEYBOARD clks_keyboard_init(); +#else + clks_log(CLKS_LOG_WARN, "CFG", "KEYBOARD DISABLED BY MENUCONFIG"); +#endif +#if CLKS_CFG_MOUSE clks_mouse_init(); +#else + clks_log(CLKS_LOG_WARN, "CFG", "MOUSE DISABLED BY MENUCONFIG"); +#endif +#if CLKS_CFG_DESKTOP clks_desktop_init(); +#else + clks_log(CLKS_LOG_WARN, "CFG", "DESKTOP DISABLED BY MENUCONFIG"); +#endif if (clks_userland_init() == CLKS_FALSE) { clks_log(CLKS_LOG_ERROR, "USER", "USERLAND INIT FAILED"); clks_cpu_halt_forever(); } +#if CLKS_CFG_DRIVER_MANAGER clks_driver_init(); +#else + clks_log(CLKS_LOG_WARN, "CFG", "DRIVER MANAGER DISABLED BY MENUCONFIG"); +#endif +#if CLKS_CFG_KELF clks_kelf_init(); +#else + clks_log(CLKS_LOG_WARN, "CFG", "KELF DISABLED BY MENUCONFIG"); +#endif clks_scheduler_init(); +#if CLKS_CFG_KLOGD_TASK 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"); } +#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) { 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) { 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) { 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(); 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(); +#if CLKS_CFG_ELFRUNNER_PROBE if (clks_elfrunner_probe_kernel_executable() == CLKS_FALSE) { 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(); @@ -228,11 +334,15 @@ void clks_kernel_main(void) { clks_shell_init(); +#if CLKS_CFG_USRD_TASK if (clks_userland_shell_auto_exec_enabled() == CLKS_TRUE) { clks_log(CLKS_LOG_INFO, "SHELL", "DEFAULT ENTER USER SHELL MODE"); } else { 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", "ACTIVE", (u64)clks_tty_active()); diff --git a/clks/kernel/userland.c b/clks/kernel/userland.c index 58a2c60..a360964 100644 --- a/clks/kernel/userland.c +++ b/clks/kernel/userland.c @@ -7,6 +7,10 @@ #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_exec_requested_flag = 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_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_success_count = 0ULL; clks_user_launch_fail_count = 0ULL; @@ -107,7 +111,11 @@ clks_bool clks_userland_init(void) { 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; } diff --git a/configs/menuconfig/clks_features.json b/configs/menuconfig/clks_features.json new file mode 100644 index 0000000..ea3cc86 --- /dev/null +++ b/configs/menuconfig/clks_features.json @@ -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 + } + ] +} diff --git a/scripts/menuconfig.py b/scripts/menuconfig.py new file mode 100644 index 0000000..5371531 --- /dev/null +++ b/scripts/menuconfig.py @@ -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: toggle, a enable-all, n disable-all, i 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)