diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ccb72ca..eafbd327 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,10 +73,16 @@ jobs: - name: Check module dependency layering run: python3 scripts/module_depgraph.py --check + - name: Check declared module layers vs includes + run: python3 scripts/module_depgraph.py --check-layers + - name: Report dependency graph if: always() run: python3 scripts/module_depgraph.py + - name: Check test source manifest + run: python3 scripts/check_test_manifest.py + - name: Test release.py run: python3 -m unittest discover -s scripts/tests -p "test_*.py" -v diff --git a/AGENTS.md b/AGENTS.md index b436987a..f9c3ceb3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ export XDG_RUNTIME_DIR=/tmp/xdg-runtime && mkdir -p "$XDG_RUNTIME_DIR" && chmod export ALSOFT_DRIVERS=null # OpenAL Soft null backend (no audio card) ``` -- Unit tests (full suite, ~1400 tests, a couple of minutes): +- Unit tests (full suite, ~1500 per-case tests, a couple of minutes): `VK_ICD_FILENAMES=... ALSOFT_DRIVERS=... xvfb-run -a make test/linux-debug` - `make test/*` runs tests per case (process-isolated; this is the fast path on CI) and injects fast headless defaults for ClassicScenes diff --git a/CMakeLists.txt b/CMakeLists.txt index 43d7b33f..285ed3bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,14 @@ set(EVENGINE_PREBUILT_THIRD_PARTY "" CACHE PATH "Compatibility alias for EVENGINE_THIRD_PARTY_BINARY_DIR") set(EVENGINE_THIRD_PARTY_ROOT "" CACHE PATH "Deprecated compatibility alias for EVENGINE_THIRD_PARTY_BINARY_DIR") +if(EVENGINE_PREBUILT_THIRD_PARTY) + message(DEPRECATION + "EVENGINE_PREBUILT_THIRD_PARTY is deprecated; use EVENGINE_THIRD_PARTY_BINARY_DIR") +endif() +if(EVENGINE_THIRD_PARTY_ROOT) + message(DEPRECATION + "EVENGINE_THIRD_PARTY_ROOT is deprecated; use EVENGINE_THIRD_PARTY_BINARY_DIR") +endif() if(NOT EVENGINE_THIRD_PARTY_BINARY_DIR) if(EVENGINE_PREBUILT_THIRD_PARTY) set(EVENGINE_THIRD_PARTY_BINARY_DIR "${EVENGINE_PREBUILT_THIRD_PARTY}" CACHE PATH @@ -45,6 +53,11 @@ include(CTest) # This is the main function of the build script function(main) + # Central include/link-usage target for every engine module, platform + # library and test runner. Replaces the old global include_directories() / + # link_directories() sprawl: targets opt in by linking eve_engine_includes, + # and the third-party install trees register their include/link dirs here. + add_library(eve_engine_includes INTERFACE) get_available_build() load_third_party() load_webgpu_backend() @@ -140,6 +153,7 @@ function(main) # content from the result. See cmake/module_manifest.cmake. include(${CMAKE_SOURCE_DIR}/cmake/module_manifest.cmake) include(${CMAKE_SOURCE_DIR}/cmake/thirdparty_libs.cmake) + include(${CMAKE_SOURCE_DIR}/cmake/system_libraries.cmake) if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten" AND NOT EVENGINE_PROFILE) # The browser runtime drops the modules whose third-party deps were cut. set(EVENGINE_PROFILE "web") @@ -164,6 +178,11 @@ function(main) # Platform CMake may set EVENGINE_PLATFORM_LIB; clear stale cache from other platforms. unset(EVENGINE_PLATFORM_LIB CACHE) add_subdirectory(platform/${BUILD_PLATFORM}) + # Platform libraries (android / ios / macosx / webgpu) also compile against + # the shared engine include set. + if(EVENGINE_PLATFORM_LIB AND TARGET ${EVENGINE_PLATFORM_LIB}) + target_link_libraries(${EVENGINE_PLATFORM_LIB} PUBLIC eve_engine_includes) + endif() add_subdirectory(src/engine) add_subdirectory(src/modules) add_subdirectory(src/scripts) @@ -214,6 +233,9 @@ macro(create_module libname folder) else() add_library(${libname} OBJECT ${module_src}) endif() + # Opt the module into the shared engine include set (was global + # include_directories before; see eve_engine_includes in main()). + target_link_libraries(${libname} PUBLIC eve_engine_includes) add_custom_target(check_sources_${folder} ALL @@ -235,7 +257,7 @@ endmacro() # This function will set the include directories for the project function(set_include) - include_directories( + target_include_directories(eve_engine_includes INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/src/engine ${CMAKE_CURRENT_SOURCE_DIR}/src/modules ${CMAKE_CURRENT_SOURCE_DIR}/src/scripts @@ -298,9 +320,10 @@ function(set_include) message(FATAL_ERROR "Android NDK libvulkan not found (need ANDROID_PLATFORM >= 24)") endif() if(Vulkan_INCLUDE_DIRS) - include_directories(${Vulkan_INCLUDE_DIRS}) + target_include_directories(eve_engine_includes INTERFACE ${Vulkan_INCLUDE_DIRS}) else() - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/third-party/vulkan) + target_include_directories(eve_engine_includes INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/third-party/vulkan) endif() set(EVENGINE_VULKAN_LIB ${ANDROID_VULKAN_LIB} CACHE INTERNAL "Vulkan link target") message(STATUS "Vulkan (Android): ${ANDROID_VULKAN_LIB}") @@ -360,16 +383,17 @@ function(set_include) set(EVENGINE_MOLTENVK_FRAMEWORK_DIR "${_eve_moltenvk_framework_dir}" CACHE INTERNAL "MoltenVK framework search path") set(EVENGINE_MOLTENVK_STATIC_LIB "${_eve_moltenvk_static_lib}" CACHE INTERNAL "MoltenVK static archive") if(Vulkan_INCLUDE_DIRS) - include_directories(${Vulkan_INCLUDE_DIRS}) + target_include_directories(eve_engine_includes INTERFACE ${Vulkan_INCLUDE_DIRS}) else() - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/third-party/vulkan) + target_include_directories(eve_engine_includes INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/third-party/vulkan) endif() # Placeholder when using -framework; static mode sets the .a below in engine CMake. set(EVENGINE_VULKAN_LIB "" CACHE INTERNAL "Vulkan link target") message(STATUS "Vulkan (iOS MoltenVK ${_eve_moltenvk_link_mode}): ${EVENGINE_MOLTENVK_XCFRAMEWORK}") elseif(Vulkan_FOUND AND Vulkan_INCLUDE_DIRS AND Vulkan_LIBRARIES) message(STATUS "Vulkan found: ${Vulkan_LIBRARY}") - include_directories(${Vulkan_INCLUDE_DIRS}) + target_include_directories(eve_engine_includes INTERFACE ${Vulkan_INCLUDE_DIRS}) set(EVENGINE_VULKAN_LIB Vulkan::Vulkan CACHE INTERNAL "Vulkan link target") elseif(APPLE) message(FATAL_ERROR @@ -401,16 +425,16 @@ macro(get_available_build) # Cross-compiling for Emscripten (web/WASM, browser WebGPU) must win over host detection. if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") list(APPEND AVAILABLE_PLATFORM "webgpu") - set(BUILD_PLATFORM "webgpu" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") + set(BUILD_PLATFORM "webgpu" CACHE STRING "The build platform (${AVAILABLE_PLATFORM})") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) # Cross-compiling for Android (NDK toolchain) must win over host Apple/Linux detection. elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) list(APPEND AVAILABLE_PLATFORM "android") - set(BUILD_PLATFORM "android" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") + set(BUILD_PLATFORM "android" CACHE STRING "The build platform (${AVAILABLE_PLATFORM})") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS") list(APPEND AVAILABLE_PLATFORM "ios") - set(BUILD_PLATFORM "ios" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") + set(BUILD_PLATFORM "ios" CACHE STRING "The build platform (${AVAILABLE_PLATFORM})") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) elseif(UNIX) if(APPLE) @@ -418,13 +442,13 @@ macro(get_available_build) list(APPEND AVAILABLE_PLATFORM "ios") list(APPEND AVAILABLE_PLATFORM "android") list(APPEND AVAILABLE_PLATFORM "webgpu") - set(BUILD_PLATFORM "macosx" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") + set(BUILD_PLATFORM "macosx" CACHE STRING "The build platform (${AVAILABLE_PLATFORM})") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) else() # Linux list(APPEND AVAILABLE_PLATFORM "linux") list(APPEND AVAILABLE_PLATFORM "android") list(APPEND AVAILABLE_PLATFORM "webgpu") - set(BUILD_PLATFORM "linux" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") + set(BUILD_PLATFORM "linux" CACHE STRING "The build platform (${AVAILABLE_PLATFORM})") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) endif() else() @@ -432,7 +456,7 @@ macro(get_available_build) list(APPEND AVAILABLE_PLATFORM "win32") list(APPEND AVAILABLE_PLATFORM "android") list(APPEND AVAILABLE_PLATFORM "webgpu") - set(BUILD_PLATFORM "win32" CACHE STRING "The build platfrom (${AVAILABLE_PLATFORM})") + set(BUILD_PLATFORM "win32" CACHE STRING "The build platform (${AVAILABLE_PLATFORM})") MESSAGE("Build Platform: ${BUILD_PLATFORM}") SET_PROPERTY(CACHE BUILD_PLATFORM PROPERTY STRINGS ${AVAILABLE_PLATFORM}) endif() @@ -488,7 +512,7 @@ macro(get_available_build) CACHE INTERNAL "Exact basenames excluded from module source scan") endif() - message("Availabe: ${AVAILABLE_PLATFORM}") + message("Available: ${AVAILABLE_PLATFORM}") if("${BUILD_PLATFORM}" IN_LIST AVAILABLE_PLATFORM) message("Build Platform: ${BUILD_PLATFORM}") @@ -590,16 +614,18 @@ function(check_third_party_project name repo) "${_eve_tp_binary}") endif() add_custom_target(${name}) - include_directories("${_eve_tp_binary}/include") + target_include_directories(eve_engine_includes INTERFACE "${_eve_tp_binary}/include") # SDL2 and FreeType install their public headers one level below include/. # Their sources use and , so expose those roots too. if(IS_DIRECTORY "${_eve_tp_binary}/include/SDL2") - include_directories("${_eve_tp_binary}/include/SDL2") + target_include_directories(eve_engine_includes INTERFACE + "${_eve_tp_binary}/include/SDL2") endif() if(IS_DIRECTORY "${_eve_tp_binary}/include/freetype2") - include_directories("${_eve_tp_binary}/include/freetype2") + target_include_directories(eve_engine_includes INTERFACE + "${_eve_tp_binary}/include/freetype2") endif() - link_directories("${_eve_tp_binary}/lib") + target_link_directories(eve_engine_includes INTERFACE "${_eve_tp_binary}/lib") message(STATUS "third-party: using read-only prebuilt tree ${_eve_tp_binary}") return() endif() @@ -681,14 +707,23 @@ function(check_third_party_project name repo) # --config is required for multi-config generators (VS); ignored by Ninja. set(_eve_tp_build_cmd ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_SOURCE_DIR}/build/${name}/${TP_BUILD_PATH}/ --target install --config ${_eve_tp_build_type} --parallel 8) + # Tracked patch applied at build time (never by mutating the checkout at + # configure time). Paths in the patch are relative to the medialoader + # submodule root. See cmake/patch_third_party.cmake. + set(_eve_tp_patch_cmd ${CMAKE_COMMAND} + -DPATCH=${CMAKE_SOURCE_DIR}/cmake/patches/medialoader-smooth-normals.patch + -DPATCH_DIR=${CMAKE_CURRENT_SOURCE_DIR}/${name}/medialoader + -P ${CMAKE_SOURCE_DIR}/cmake/patch_third_party.cmake) + if(_eve_tp_use_ninja) ExternalProject_Add(${name} GIT_REPOSITORY ${GIT_HTTPS_LINK} - GIT_TAG origin/main + GIT_TAG 566e4073d2ec1c723b581daa50334a4b05956091 SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${name} BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/build/${name}/${TP_BUILD_PATH} CMAKE_GENERATOR "Ninja" CMAKE_ARGS ${_eve_tp_cmake_args} + PATCH_COMMAND ${_eve_tp_patch_cmd} BUILD_COMMAND ${_eve_tp_build_cmd} BUILD_ALWAYS 0 EXCLUDE_FROM_ALL 1 @@ -696,29 +731,33 @@ function(check_third_party_project name repo) else() ExternalProject_Add(${name} GIT_REPOSITORY ${GIT_HTTPS_LINK} - GIT_TAG origin/main + GIT_TAG 566e4073d2ec1c723b581daa50334a4b05956091 SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${name} BINARY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/build/${name}/${TP_BUILD_PATH} CMAKE_ARGS ${_eve_tp_cmake_args} + PATCH_COMMAND ${_eve_tp_patch_cmd} BUILD_COMMAND ${_eve_tp_build_cmd} BUILD_ALWAYS 0 EXCLUDE_FROM_ALL 1 ) endif() - include_directories(${CMAKE_CURRENT_SOURCE_DIR}/build/${name}-binary/${TP_BUILD_PATH}/include) - link_directories(${CMAKE_CURRENT_SOURCE_DIR}/build/${name}-binary/${TP_BUILD_PATH}/lib) + target_include_directories(eve_engine_includes INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/build/${name}-binary/${TP_BUILD_PATH}/include) + target_link_directories(eve_engine_includes INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/build/${name}-binary/${TP_BUILD_PATH}/lib) endfunction() # Load third party project from github function(load_third_party) - if(CMAKE_BUILD_TYPE STREQUAL "Release") - message("Build Type: Release") - elseif(CMAKE_BUILD_TYPE STREQUAL "Debug") - message("Build Type: Debug") + # Debug gets its own third-party install tree (-debug); the other + # supported single-config build types (Release, RelWithDebInfo, MinSizeRel) + # share the release-style tree and the md library postfixes on Windows. + if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(DEBUG_APPENDIX "-debug") else() - message(FATAL_ERROR "Unsupported Build Type: ${CMAKE_BUILD_TYPE}") + set(DEBUG_APPENDIX "") endif() + message("Build Type: ${CMAKE_BUILD_TYPE} (third-party tree suffix '${DEBUG_APPENDIX}')") set(TP_BUILD_PATH ${BUILD_PLATFORM}${DEBUG_APPENDIX}) message("Third Party Build Path: ${TP_BUILD_PATH}") @@ -732,8 +771,8 @@ function(load_third_party) "EVENGINE_THIRD_PARTY_BINARY_DIR must contain include/ and lib/: ${_eve_prebuilt_tp}") endif() message(STATUS "third-party: using read-only prebuilt install ${_eve_prebuilt_tp}") - include_directories("${_eve_prebuilt_tp}/include") - link_directories("${_eve_prebuilt_tp}/lib") + target_include_directories(eve_engine_includes INTERFACE "${_eve_prebuilt_tp}/include") + target_link_directories(eve_engine_includes INTERFACE "${_eve_prebuilt_tp}/lib") set(EVENGINE_THIRD_PARTY_BINARY_ONLY ON CACHE INTERNAL "Use installed third-party prefix" FORCE) # Preserve the dependency target expected by create_module(), but give it no @@ -744,30 +783,17 @@ function(load_third_party) endif() ensure_third_party_checkout() + # EVEngine's database module uses POCO Data + SQLite. The third-party + # aggregate already defaults ENABLE_DATA / ENABLE_DATA_SQLITE to ON (pinned + # by GIT_TAG below), so no configure-time edits are needed for it. + # # Normal generation for assets without authored normals: medialoader maps # generateNormalsIfMissing to aiProcess_GenNormals (per-face, flat shading). - # Patch the cloned tree (idempotent, same pattern as the ENABLE_DATA edit - # below) to request aiProcess_GenSmoothNormals, so sphere-like models shade - # smoothly while hard edges (default smoothing angle 80 degrees) keep seams. - set(_eve_ml_loader "${CMAKE_SOURCE_DIR}/third-party/medialoader/model/ModelLoader.cpp") - if(EXISTS "${_eve_ml_loader}") - file(READ "${_eve_ml_loader}" _eve_ml_loader_text) - string(REPLACE "aiProcess_GenNormals" "aiProcess_GenSmoothNormals" - _eve_ml_loader_text "${_eve_ml_loader_text}") - file(WRITE "${_eve_ml_loader}" "${_eve_ml_loader_text}") - endif() - # EVEngine's database module uses POCO Data and its bundled SQLite connector. - # The third-party aggregate disables these by default to reduce its footprint; - # enable the two targets in the checked-out aggregate before configuring it. - if(NOT EVENGINE_THIRD_PARTY_BINARY_DIR) - set(_eve_tp_cmake "${CMAKE_CURRENT_SOURCE_DIR}/third-party/CMakeLists.txt") - file(READ "${_eve_tp_cmake}" _eve_tp_contents) - string(REPLACE "set(ENABLE_DATA OFF CACHE BOOL \"\" FORCE)" - "set(ENABLE_DATA ON CACHE BOOL \"\" FORCE)" _eve_tp_contents "${_eve_tp_contents}") - string(REPLACE "set(ENABLE_DATA_SQLITE OFF CACHE BOOL \"\" FORCE)" - "set(ENABLE_DATA_SQLITE ON CACHE BOOL \"\" FORCE)" _eve_tp_contents "${_eve_tp_contents}") - file(WRITE "${_eve_tp_cmake}" "${_eve_tp_contents}") - endif() + # A tracked patch (cmake/patches/medialoader-smooth-normals.patch) switches + # it to aiProcess_GenSmoothNormals, so sphere-like models shade smoothly + # while hard edges (default smoothing angle 80 degrees) keep seams. The + # patch is applied by the third-party ExternalProject's PATCH_COMMAND at + # build time — never by mutating the checkout during configure. check_third_party_project(third-party "https://github.com/EVEngine/third-party") ADD_CUSTOM_TARGET(deps DEPENDS third-party) @@ -801,7 +827,9 @@ function(load_webgpu_backend) set(DAWN_ENABLE_OPENGLES OFF CACHE BOOL "" FORCE) set(DAWN_USE_XCODE OFF CACHE BOOL "" FORCE) - set(EVENGINE_DAWN_TAG "main" CACHE STRING + # Pinned Dawn commit (2026-08-19 main HEAD) so native WebGPU builds stay + # reproducible; override with -DEVENGINE_DAWN_TAG= if needed. + set(EVENGINE_DAWN_TAG "c3845d0d962656a2ea03063bddc85dc1ef1a0baa" CACHE STRING "Dawn commit/tag to fetch for the native WebGPU backend (pin for reproducible builds)") FetchContent_Declare(dawn GIT_REPOSITORY https://github.com/google/dawn.git @@ -816,7 +844,7 @@ function(load_webgpu_backend) endif() # Dawn's webgpu.h / webgpu_cpp.h live under src/ in the checkout; expose # them to every module target (engine, eve_imgui, gpgpu/webgpu, ...). - include_directories("${dawn_SOURCE_DIR}/src") + target_include_directories(eve_engine_includes INTERFACE "${dawn_SOURCE_DIR}/src") set(EVENGINE_WEBGPU_LIB dawn::dawn CACHE INTERNAL "WebGPU link target") message(STATUS "WebGPU (native): Dawn ready, link target = ${EVENGINE_WEBGPU_LIB}") endfunction() diff --git a/Makefile b/Makefile index bb0b2c1d..bb070354 100644 --- a/Makefile +++ b/Makefile @@ -114,6 +114,7 @@ GAME ?= reinstall/third-party/android reinstall/third-party/android-debug \ reinstall/third-party/ios reinstall/third-party/ios-debug \ link-compile-commands download-classic-scenes download-skinned-character \ + check/test-manifest check/module-layers \ docs # Default: every debug target this machine can build (host + optional ios/android/wsl). @@ -132,6 +133,14 @@ show-targets: @echo "sdk -> sdk/$(PLATFORM) (Release) or sdk/$(PLATFORM)-debug" @echo "run -> run/$(PLATFORM)-debug (GAME=$(GAME), empty = embedded demo)" +# Verify every test/*.cpp on disk is registered in test/CMakeLists.txt. +check/test-manifest: + python3 scripts/check_test_manifest.py + +# Verify module includes never climb above the declared manifest LAYER. +check/module-layers: + python3 scripts/module_depgraph.py --check-layers + # clangd: build/compile_commands.json -> host platform debug CDB link-compile-commands: @mkdir -p build @@ -513,7 +522,7 @@ docs: CTEST_FILTER = $(if $(FILTER),-R '^$(subst .,\.,$(FILTER))') # Default: run tests per case (process-isolated; this is the fast path on CI — -# main runs 1551 cases in ~2-11 min). "bundle/" entries stay registered +# main runs 1526 cases in ~2-11 min). "bundle/" entries stay registered # by cmake/ZeroErrDiscoverTestsImpl.cmake as an opt-in: GPU/window tests were # ~70x slower when several shared one process on CI, so bundles are excluded # unless requested (FILTER=bundle/ or ctest -L bundle). diff --git a/cmake/install_sdk.cmake b/cmake/install_sdk.cmake index 40cf4a74..83678182 100644 --- a/cmake/install_sdk.cmake +++ b/cmake/install_sdk.cmake @@ -57,10 +57,19 @@ install(FILES "${CMAKE_BINARY_DIR}/src/engine/common/config.h" DESTINATION include/eve/common ) -set(_eve_module_dirs - animation audio data event filesystem graphics image ik joystick keyboard mouse - model3d network particles plugins sound spatial timer touch ui window -) +# Module header dirs are derived from the module manifest (single source of +# truth) so the SDK always ships every module's public headers. CORE modules +# (common / cmdline / devtools) live under src/engine and are installed above. +set(_eve_module_dirs "") +foreach(_eve_mod IN LISTS EVE_ALL_MODULES) + if(EVE_MODULE_${_eve_mod}_CORE) + continue() + endif() + list(APPEND _eve_module_dirs "${_eve_mod}") +endforeach() +if(NOT _eve_module_dirs) + message(WARNING "install_sdk.cmake: EVE_ALL_MODULES is empty; no module headers will be installed") +endif() foreach(_eve_mod IN LISTS _eve_module_dirs) if(EXISTS "${CMAKE_SOURCE_DIR}/src/modules/${_eve_mod}") install(DIRECTORY "${CMAKE_SOURCE_DIR}/src/modules/${_eve_mod}/" diff --git a/cmake/module_manifest.cmake b/cmake/module_manifest.cmake index 9a0123c9..048f906b 100644 --- a/cmake/module_manifest.cmake +++ b/cmake/module_manifest.cmake @@ -80,8 +80,9 @@ eve_declare_module(NAME spatial LAYER 0 SCRIPT Spatial SLOT spatial GROUP 2d 3d web) eve_declare_module(NAME ik LIB EVIK LAYER 0 SCRIPT IK GROUP 2d 3d web) -eve_declare_module(NAME editor LAYER 0 SCRIPT Editor SLOT editor - GROUP 3d web) +eve_declare_module(NAME editor LAYER 6 SCRIPT Editor SLOT editor + GROUP 3d web + OPTIONAL_DEPS procgen) eve_declare_module(NAME plugins LAYER 0 SCRIPT Plugins GROUP 3d) eve_declare_module(NAME database LAYER 0 SCRIPT Database @@ -92,7 +93,7 @@ eve_declare_module(NAME inventory LAYER 0 SCRIPT Inventory) # THIRDPARTY poco is required so MSVC compiles those TUs with # POCO_NO_AUTOMATIC_LIBS; otherwise the obj records a link of # PocoFoundationd.lib instead of the *mdd archive the third-party build emits. -eve_declare_module(NAME building LAYER 0 SCRIPT Building +eve_declare_module(NAME building LAYER 4 SCRIPT Building DEPS grid data THIRDPARTY poco) @@ -117,7 +118,7 @@ eve_declare_module(NAME joystick LAYER 1 SCRIPT Joystick DEPS event THIRDPARTY sdl2 GROUP minimal 2d 3d web) -eve_declare_module(NAME model3d LIB EVModel3D LAYER 1 SCRIPT Model3D SLOT model3d +eve_declare_module(NAME model3d LIB EVModel3D LAYER 4 SCRIPT Model3D SLOT model3d DEPS filesystem THIRDPARTY medialoader_model assimp GROUP 3d) @@ -204,8 +205,8 @@ eve_declare_module(NAME weather LAYER 4 SCRIPT Weather SLOT weather eve_declare_module(NAME stylize LAYER 4 SCRIPT Stylize SLOT stylize DEPS graphics image GROUP 3d) -eve_declare_module(NAME voxel LAYER 4 SCRIPT Voxel - DEPS graphics +eve_declare_module(NAME voxel LAYER 5 SCRIPT Voxel + DEPS graphics procgen GROUP 3d) eve_declare_module(NAME spritestack LIB EVSpriteStack LAYER 4 SCRIPT SpriteStack SLOT spritestack DEPS graphics image model3d diff --git a/cmake/patch_third_party.cmake b/cmake/patch_third_party.cmake new file mode 100644 index 00000000..c931e8ca --- /dev/null +++ b/cmake/patch_third_party.cmake @@ -0,0 +1,61 @@ +# Idempotent git-apply wrapper used by ExternalProject PATCH_COMMAND. +# +# Usage: +# cmake -DPATCH= +# -DPATCH_DIR= +# -P cmake/patch_third_party.cmake +# +# The patch is applied with `git apply` inside PATCH_DIR. If it is already +# applied (reverse-apply succeeds) the step is a no-op so rebuilds do not fail. +# If the working tree drifted so the patch no longer applies, configure/build +# fails loudly instead of silently compiling different third-party code. + +if(NOT DEFINED PATCH OR NOT DEFINED PATCH_DIR) + message(FATAL_ERROR "patch_third_party.cmake requires -DPATCH=... and -DPATCH_DIR=...") +endif() + +get_filename_component(PATCH_ABS "${PATCH}" ABSOLUTE) +get_filename_component(PATCH_DIR_ABS "${PATCH_DIR}" ABSOLUTE) +if(NOT EXISTS "${PATCH_ABS}") + message(FATAL_ERROR "Third-party patch file not found: ${PATCH_ABS}") +endif() +if(NOT IS_DIRECTORY "${PATCH_DIR_ABS}") + message(FATAL_ERROR "Third-party patch target dir not found: ${PATCH_DIR_ABS}") +endif() + +find_package(Git QUIET) +if(NOT GIT_EXECUTABLE) + message(FATAL_ERROR "git is required to apply third-party patches") +endif() + +# Already applied? `git apply --reverse --check` succeeds only then. +execute_process( + COMMAND "${GIT_EXECUTABLE}" apply --reverse --check "${PATCH_ABS}" + WORKING_DIRECTORY "${PATCH_DIR_ABS}" + RESULT_VARIABLE _eve_patch_reverse + OUTPUT_QUIET ERROR_QUIET) +if(_eve_patch_reverse EQUAL 0) + message(STATUS "Third-party patch already applied: ${PATCH_ABS}") + return() +endif() + +# Apply cleanly, or fail loudly so a stale patch cannot silently bit-rot. +execute_process( + COMMAND "${GIT_EXECUTABLE}" apply --check "${PATCH_ABS}" + WORKING_DIRECTORY "${PATCH_DIR_ABS}" + RESULT_VARIABLE _eve_patch_check + OUTPUT_QUIET ERROR_QUIET) +if(NOT _eve_patch_check EQUAL 0) + message(FATAL_ERROR + "Third-party patch no longer applies cleanly: ${PATCH_ABS}\n" + "The vendored source changed upstream; update or drop the patch.") +endif() + +execute_process( + COMMAND "${GIT_EXECUTABLE}" apply "${PATCH_ABS}" + WORKING_DIRECTORY "${PATCH_DIR_ABS}" + RESULT_VARIABLE _eve_patch_result) +if(NOT _eve_patch_result EQUAL 0) + message(FATAL_ERROR "git apply failed for ${PATCH_ABS} (exit ${_eve_patch_result})") +endif() +message(STATUS "Applied third-party patch: ${PATCH_ABS}") diff --git a/cmake/patches/medialoader-smooth-normals.patch b/cmake/patches/medialoader-smooth-normals.patch new file mode 100644 index 00000000..b102e633 --- /dev/null +++ b/cmake/patches/medialoader-smooth-normals.patch @@ -0,0 +1,14 @@ +diff --git a/model/ModelLoader.cpp b/model/ModelLoader.cpp +index beccd4e..604deb8 100644 +--- a/model/ModelLoader.cpp ++++ b/model/ModelLoader.cpp +@@ -17,7 +17,7 @@ unsigned int ModelLoader::flagsFrom(const LoadOptions &opt) const { + unsigned int flags = 0; + if (opt.triangulate) + flags |= aiProcess_Triangulate; + if (opt.generateNormalsIfMissing) +- flags |= aiProcess_GenNormals; ++ flags |= aiProcess_GenSmoothNormals; + if (opt.joinIdenticalVertices) + flags |= aiProcess_JoinIdenticalVertices; + if (opt.flipUVs) diff --git a/cmake/system_libraries.cmake b/cmake/system_libraries.cmake new file mode 100644 index 00000000..7c75717d --- /dev/null +++ b/cmake/system_libraries.cmake @@ -0,0 +1,80 @@ +# Platform system libraries, appended to the list variable named by . +# +# Shared by src/engine/CMakeLists.txt (host binary) and test/CMakeLists.txt +# (unit-test runner) so the per-platform link lists cannot drift apart. +function(eve_append_system_libraries out_var) + if(WIN32) + list(APPEND ${out_var} winmm version imm32 Setupapi ws2_32 iphlpapi shlwapi) + elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) + find_library(ANDROID_LOG_LIB log) + find_library(ANDROID_LIB android) + find_library(ANDROID_OPENSLES_LIB OpenSLES) + list(APPEND ${out_var} + ${ANDROID_LOG_LIB} + ${ANDROID_LIB} + ${ANDROID_OPENSLES_LIB} + dl + m + c++_shared + ) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + # Emscripten exposes pthread/dl/GL as virtual libraries; nothing to link. + # Game-side threading is enabled through -sPROXY_TO_PTHREAD (platform/webgpu). + elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) + list(APPEND ${out_var} + "-framework UIKit" + "-framework Foundation" + "-framework CoreFoundation" + "-framework Metal" + "-framework QuartzCore" + "-framework CoreGraphics" + "-framework AudioToolbox" + "-framework CoreAudio" + "-framework AVFoundation" + "-framework CoreMedia" + "-framework GameController" + "-framework CoreHaptics" + "-framework CoreMotion" + "-framework OpenGLES" + "-framework CoreBluetooth" + # Static MoltenVK references IOSurface APIs. + "-framework IOSurface" + iconv + ) + elseif(APPLE) + list(APPEND ${out_var} + dl pthread iconv + "-framework Cocoa" + "-framework IOKit" + "-framework ForceFeedback" + "-framework Carbon" + "-framework CoreVideo" + "-framework Metal" + "-framework QuartzCore" + "-framework AudioToolbox" + "-framework CoreAudio" + "-framework GameController" + "-framework CoreHaptics" + "-framework AVFoundation" + "-framework CoreMedia" + "-framework CoreFoundation" + "-framework Foundation" + "-framework OpenGL" + ) + elseif(UNIX) + # Static SDL2 pulls Wayland/X11 extras; FreeType needs bz2/png. + list(APPEND ${out_var} + dl pthread m rt bz2 png + X11 Xext Xcursor Xrandr Xxf86vm Xi Xss Xinerama + wayland-client wayland-cursor wayland-egl xkbcommon + asound + ) + # OpenAL Soft may compile the sndio backend when libsndio-dev is present. + # GitHub Actions runners typically lack libsndio, so only link when found. + find_library(EVENGINE_SNDIO_LIBRARY sndio) + if(EVENGINE_SNDIO_LIBRARY) + list(APPEND ${out_var} sndio) + endif() + endif() + set(${out_var} "${${out_var}}" PARENT_SCOPE) +endfunction() diff --git a/scripts/check_test_manifest.py b/scripts/check_test_manifest.py new file mode 100644 index 00000000..e4020c27 --- /dev/null +++ b/scripts/check_test_manifest.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Verify every test/*.cpp on disk is registered in test/CMakeLists.txt. + +The unit-test suite is listed explicitly in test/CMakeLists.txt (file(GLOB +all_test_cpp ...) plus conditionals). A source file that lands in test/ but is +missing from that list silently never compiles or runs, which is how +test/hair.cpp and test/runtime.cpp rotted before this check existed. + +Usage: + python3 scripts/check_test_manifest.py # exit 1 on drift +""" + +import pathlib +import re +import sys + +REPO = pathlib.Path(__file__).resolve().parent.parent +TEST_DIR = REPO / "test" +CMAKE_LIST = TEST_DIR / "CMakeLists.txt" + +ENTRY_RE = re.compile(r"^\s+([\w.-]+\.cpp)\s*$") +APPEND_RE = re.compile(r"list\(APPEND\s+all_test_cpp\s+([\w./-]+\.cpp)\)") + +# Test sources appended conditionally inside test/CMakeLists.txt. +CONDITIONAL = { + "demo.cpp", # only when EVENGINE_BUILD_DEMO=ON +} + + +def listed_sources() -> set[str]: + text = CMAKE_LIST.read_text(encoding="utf-8") + listed = set() + in_glob = False + for line in text.splitlines(): + if "file(GLOB all_test_cpp" in line: + in_glob = True + continue + if in_glob: + if line.rstrip().endswith(")"): + in_glob = False + continue + m = ENTRY_RE.match(line) + if m: + listed.add(m.group(1)) + m = APPEND_RE.search(line) + if m: + listed.add(pathlib.PurePosixPath(m.group(1)).name) + return listed + + +def main() -> int: + if not CMAKE_LIST.exists(): + print(f"error: {CMAKE_LIST} not found", file=sys.stderr) + return 1 + + on_disk = {p.name for p in TEST_DIR.glob("*.cpp")} + listed = listed_sources() + missing = sorted((on_disk - listed) - CONDITIONAL) + stale = sorted(listed - on_disk - CONDITIONAL) + + if not missing and not stale: + print(f"test manifest OK: {len(listed)} sources listed, {len(on_disk)} on disk") + return 0 + + if missing: + print("error: test source files missing from test/CMakeLists.txt:", file=sys.stderr) + for name in missing: + print(f" {name}", file=sys.stderr) + if stale: + print("error: entries in test/CMakeLists.txt with no source file:", file=sys.stderr) + for name in stale: + print(f" {name}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/module_depgraph.py b/scripts/module_depgraph.py index fb06e1f8..e53bab96 100755 --- a/scripts/module_depgraph.py +++ b/scripts/module_depgraph.py @@ -8,6 +8,11 @@ docs/dev/模块编排与裁剪架构.md for how the output is interpreted. python3 scripts/module_depgraph.py # graph + layers python3 scripts/module_depgraph.py --cycles # cycles only python3 scripts/module_depgraph.py --check # exit 1 on unlisted back-edge + python3 scripts/module_depgraph.py --check-layers + # exit 1 when an include edge + # climbs above the LAYER the + # module declares in the + # module manifest Only sub-path includes ("map/Map.h") count as a cross-module dependency; a bare / / is a standard library header. @@ -21,6 +26,7 @@ from collections import defaultdict REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) MODULES_DIR = os.path.join(REPO, "src", "modules") +MANIFEST_FILE = os.path.join(REPO, "cmake", "module_manifest.cmake") INCLUDE_RE = re.compile(r'^\s*#\s*include\s*[<"]([^">]+)[">]', re.M) SOURCE_EXT = (".cpp", ".cc", ".h", ".hpp") @@ -32,7 +38,6 @@ KNOWN_BACK_EDGES = { # window/Window.cpp, window/sdl/Window.cpp: the window owns the Graphics # pointer and decodes its own icon image. ("window", "graphics"), - ("window", "image"), # scene/Scene.cpp: pickScreenAt and collectFrustumIdsAt take a # graphics::Camera3D and read its projection. Everything else about scene is # now renderer-agnostic (graphics registers its own link kinds), so these two @@ -40,6 +45,29 @@ KNOWN_BACK_EDGES = { ("scene", "graphics"), } +# eve_declare_module(NAME ... LAYER ...) blocks in the module manifest. +DECLARE_RE = re.compile(r"eve_declare_module\((.*?)\)", re.S) +NAME_RE = re.compile(r"NAME\s+(\S+)") +LAYER_RE = re.compile(r"LAYER\s+(-?\d+)") + + +def declared_layers(): + """Return {module: declared LAYER} parsed from cmake/module_manifest.cmake. + + The manifest is the single source of truth for the *declared* module + layers. The computed layers printed by the default report are derived from + the include graph instead; this check exists to keep the two in step. + """ + with open(MANIFEST_FILE, encoding="utf-8") as handle: + text = handle.read() + result = {} + for block in DECLARE_RE.findall(text): + name = NAME_RE.search(block) + layer = LAYER_RE.search(block) + if name and layer: + result[name.group(1)] = int(layer.group(1)) + return result + def scan(): """Return (modules, edges, header_edges). @@ -112,12 +140,61 @@ def cycles(edges, ignore=frozenset()): return found +def check_declared_layers(edges, header_edges): + """Exit 1 when an include edge climbs above the declared module LAYER. + + The manifest declares each module's layer (cmake/module_manifest.cmake). + A module may only include headers from modules declared at the same or a + lower layer; an edge toward a *strictly higher* layer is a layering + violation and is reported (KNOWN_BACK_EDGES whitelists accepted debt). + Same-layer edges (e.g. window -> image) are allowed, matching the DEPS the + manifest itself declares. + """ + layers_map = declared_layers() + missing = sorted(m for m in set(edges) | {b for deps in edges.values() for b in deps} + if m not in layers_map) + if missing: + print("warning: modules missing a LAYER declaration in the manifest: " + + ", ".join(missing), file=sys.stderr) + + actual = {(a, b) for a, deps in edges.items() for b in deps} + offenders = sorted( + (a, b) for (a, b) in actual + if a in layers_map and b in layers_map + and layers_map[b] > layers_map[a] + and (a, b) not in KNOWN_BACK_EDGES + ) + stale = sorted( + (a, b) for (a, b) in KNOWN_BACK_EDGES + if (a, b) in actual + and (a not in layers_map or b not in layers_map + or layers_map[b] <= layers_map[a]) + ) + + for a, b in offenders: + marker = "*" if (a, b) in header_edges else " " + print(f"error: layer violation {a}(L{layers_map[a]}) -> {b}(L{layers_map[b]}){marker}", + file=sys.stderr) + for a, b in stale: + print(f"note: {a} -> {b} no longer violates the declared layers; " + "drop it from KNOWN_BACK_EDGES", file=sys.stderr) + + if offenders: + return 1 + print(f"ok: no declared-layer violations " + f"({len(KNOWN_BACK_EDGES)} known back-edges, {len(stale)} now stale)") + return 0 + + def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("--cycles", action="store_true", help="report dependency cycles only") parser.add_argument("--check", action="store_true", help="fail when a back-edge outside KNOWN_BACK_EDGES appears") + parser.add_argument("--check-layers", action="store_true", + help="fail when an include edge climbs above the module's " + "declared manifest LAYER") args = parser.parse_args() modules, edges, header_edges = scan() @@ -150,6 +227,9 @@ def main(): print(f"ok: no new back-edges ({len(KNOWN_BACK_EDGES)} known, {len(stale)} now stale)") return 0 + if args.check_layers: + return check_declared_layers(edges, header_edges) + by_layer = defaultdict(list) for module in modules: by_layer[depth[module]].append(module) diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 5ac6c4f0..1f83f52f 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -40,6 +40,9 @@ endif() # symbolized stack trace on crash; it is a host-only tool, not a game module. if(WIN32) add_library(EVBacktrace INTERFACE) + # backward.hpp lives in the third-party install; get its include dirs + # through the shared include target instead of global include_directories. + target_link_libraries(EVBacktrace INTERFACE eve_engine_includes) if(EVENGINE_THIRD_PARTY_BINARY_DIR) # Prebuilt-install mode: point at the install tree directly. target_include_directories(EVBacktrace INTERFACE ${EVENGINE_THIRD_PARTY_BINARY_DIR}/include) @@ -53,81 +56,18 @@ if(WIN32) endif() endif() -if(WIN32) - set(SystemLibraries ${SystemLibraries} winmm version imm32 Setupapi ws2_32 iphlpapi shlwapi) -elseif(ANDROID OR (CMAKE_SYSTEM_NAME STREQUAL "Android")) - find_library(ANDROID_LOG_LIB log) - find_library(ANDROID_LIB android) - find_library(ANDROID_OPENSLES_LIB OpenSLES) - set(SystemLibraries ${SystemLibraries} - ${ANDROID_LOG_LIB} - ${ANDROID_LIB} - ${ANDROID_OPENSLES_LIB} - dl - m - c++_shared - ) -elseif(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") - # Emscripten exposes pthread/dl/GL as virtual libraries; nothing to link. - # Game-side threading is enabled through -sPROXY_TO_PTHREAD (platform/webgpu). - set(SystemLibraries ${SystemLibraries}) -elseif(CMAKE_SYSTEM_NAME STREQUAL "iOS" OR EVENGINE_IOS) - set(SystemLibraries ${SystemLibraries} - "-framework UIKit" - "-framework Foundation" - "-framework CoreFoundation" - "-framework Metal" - "-framework QuartzCore" - "-framework CoreGraphics" - "-framework AudioToolbox" - "-framework CoreAudio" - "-framework AVFoundation" - "-framework CoreMedia" - "-framework GameController" - "-framework CoreHaptics" - "-framework CoreMotion" - "-framework OpenGLES" - "-framework CoreBluetooth" - # Static MoltenVK references IOSurface APIs. - "-framework IOSurface" - iconv - ) -elseif(APPLE) - set(SystemLibraries ${SystemLibraries} - dl pthread iconv - "-framework Cocoa" - "-framework IOKit" - "-framework ForceFeedback" - "-framework Carbon" - "-framework CoreVideo" - "-framework Metal" - "-framework QuartzCore" - "-framework AudioToolbox" - "-framework CoreAudio" - "-framework GameController" - "-framework CoreHaptics" - "-framework AVFoundation" - "-framework CoreMedia" - "-framework CoreFoundation" - "-framework Foundation" - "-framework OpenGL" - ) -elseif(UNIX) - # Static SDL2 pulls Wayland/X11 extras; FreeType needs bz2/png. - set(SystemLibraries ${SystemLibraries} - dl pthread m rt bz2 png - X11 Xext Xcursor Xrandr Xxf86vm Xi Xss Xinerama - wayland-client wayland-cursor wayland-egl xkbcommon - asound - ) - # OpenAL Soft may compile the sndio backend when libsndio-dev is present. - # GitHub Actions runners typically lack libsndio, so only link when found. - find_library(EVENGINE_SNDIO_LIBRARY sndio) - if(EVENGINE_SNDIO_LIBRARY) - list(APPEND SystemLibraries sndio) - endif() +# Engine entry-point sources include common/config.h / cmdline/*.h directly; +# link the shared include set explicitly (also propagates the third-party +# link directories needed by the bare-name ${ThirdParty} list). +if(TARGET ${EVENGINE_NATIVE_TARGET}) + # Plain signature: the target's other links use the plain form. + target_link_libraries(${EVENGINE_NATIVE_TARGET} eve_engine_includes) endif() +# Platform system libraries, shared with the unit-test runner +# (see cmake/system_libraries.cmake). +eve_append_system_libraries(SystemLibraries) + # Third-party link list, derived from the module manifest: only the libraries # the enabled modules actually need, in the canonical order GNU ld requires. # See cmake/thirdparty_libs.cmake for the group -> link-name mapping. diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index ca0e039f..5cf4a2ac 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -225,6 +225,9 @@ if(_has_ui) target_compile_definitions(eve_imgui PUBLIC IMGUI_DISABLE_DEMO_WINDOWS) endif() + # eve_imgui may be INTERFACE (prebuilt mode) or STATIC; only the usage + # requirements of the shared include set are needed either way. + target_link_libraries(eve_imgui INTERFACE eve_engine_includes) target_link_libraries(EVUI PUBLIC eve_imgui) target_include_directories(EVUI PUBLIC ${CMAKE_SOURCE_DIR}/third-party/imgui) target_include_directories(EVUI BEFORE PUBLIC