diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30d7c485..fa31c8fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,79 @@ jobs: key: ccache-${{ runner.os }}-${{ github.ref }}-${{ github.sha }} prefix: ccache-${{ runner.os }}-${{ github.ref }}- + macos-arm64: + name: macOS arm64 CPU SDK & Runtime + runs-on: macos-15 + timeout-minutes: 60 + permissions: + contents: read + env: + LLM_EDGEFLOW_BUILD_PRESET: macos-arm64-cpu + LLM_EDGEFLOW_LINKER: system + LLM_EDGEFLOW_JOBS: "3" + CLANG_FORMAT: clang-format + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - name: Set up native Apple Silicon tools + run: | + test "$(uname -m)" = arm64 + python -m pip install cmake==3.28.4 ninja==1.11.1.1 clang-format==18.1.8 jsonschema==4.23.0 + xcrun --show-sdk-path + - name: Cache verified macOS dependencies + uses: actions/cache@v4 + with: + path: | + 3rdparty/googletest + 3rdparty/pcre2 + 3rdparty/llama_cpp + 3rdparty/whisper_cpp + 3rdparty/onnxruntime + 3rdparty/nlohmann_json + 3rdparty/stb_image + key: 3rdparty-macos-arm64-${{ hashFiles('cmake_ext/**/*.cmake', 'CMakePresets.json') }} + - name: Run the canonical gate including dylib exports + run: ./scripts/run_all_tests.sh + - name: Cache pinned GGUF + uses: actions/cache@v4 + with: + path: configs/qwen2.5-0.5b-instruct-q4_k_m.gguf + key: qwen2.5-0.5b-q4-k-m-configs-v1-74a4da8c9fdb + - name: Run real GGUF through the public Operator on arm64 + run: | + ./scripts/fetch_real_test_models.sh --gguf-only + cmake --preset=macos-arm64-cpu -B build -DENABLE_REAL_MODEL_TESTS=ON + cmake --build build --target test_real_models_e2e --parallel 3 + ./build/test_real_models_e2e --gtest_filter=RealModelExecutionTest.GgufTranslationTraversesPublicOperatorWithActualWeights + + linux-arm64-cross: + name: Linux arm64 Cross Compilation + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-build-env + with: + packages: build-essential cmake ninja-build:ninja gcc-aarch64-linux-gnu:aarch64-linux-gnu-gcc g++-aarch64-linux-gnu:aarch64-linux-gnu-g++ + - name: Build and inspect the arm64 SDK without host execution + run: | + cmake --preset=linux-arm64-cross + cmake --build build/variants/linux-arm64-cross --target alg_sdk --parallel 4 + aarch64-linux-gnu-readelf -h build/variants/linux-arm64-cross/libllm_edgeflow_sdk.so | grep 'Machine:.*AArch64' + - name: Verify the pinned Inferlet target package compiles and links + run: | + cmake --preset=linux-arm64-cross -B build/variants/linux-arm64-inferlet -DENABLE_INFERLET=ON + cmake --build build/variants/linux-arm64-inferlet --target alg_sdk --parallel 4 + aarch64-linux-gnu-readelf -h build/variants/linux-arm64-inferlet/libllm_edgeflow_sdk.so | grep 'Machine:.*AArch64' + sanitizer: name: Production Backends ASan & UBSan runs-on: ubuntu-latest @@ -450,7 +523,7 @@ jobs: acceptance-evidence: name: Exact-SHA Acceptance Evidence if: always() - needs: [build-and-test, sanitizer, real-c-abi-and-profile, whisper-asr, kite-llm] + needs: [build-and-test, macos-arm64, linux-arm64-cross, sanitizer, real-c-abi-and-profile, whisper-asr, kite-llm] runs-on: ubuntu-latest timeout-minutes: 10 @@ -462,6 +535,8 @@ jobs: env: WHISPER_GATE_RESULT: ${{ needs.whisper-asr.result }} KITELLM_GATE_RESULT: ${{ needs.kite-llm.result }} + MACOS_ARM64_GATE_RESULT: ${{ needs.macos-arm64.result }} + LINUX_ARM64_CROSS_GATE_RESULT: ${{ needs.linux-arm64-cross.result }} run: | git fetch origin main --quiet ./scripts/generate_acceptance_evidence.sh \ @@ -480,6 +555,8 @@ jobs: - name: Enforce Required Job Results if: >- needs.build-and-test.result != 'success' || + needs.macos-arm64.result != 'success' || + needs.linux-arm64-cross.result != 'success' || needs.sanitizer.result != 'success' || needs.real-c-abi-and-profile.result != 'success' || needs.whisper-asr.result != 'success' || diff --git a/3rdparty/README.md b/3rdparty/README.md index 3f7018fc..35c69557 100644 --- a/3rdparty/README.md +++ b/3rdparty/README.md @@ -1,6 +1,7 @@ # 3rdparty 依赖库目录规范 -本项目采用统一的 `3rdparty/` 本地持久化归档体系,用于缓存第三方开源依赖(包括预编译静态库 `.a`、动态库 `.so` 及对应的头文件)。 +本项目使用 `3rdparty/` 持久缓存固定版本、经过 SHA-256 校验的依赖。原始发布归档可以共享, +本地编译产物按目标平台和构建身份分目录,避免 Linux/macOS、CPU 架构及 Debug/Release 相互覆盖。 用户指定的私有 GitHub kiteLLM 依赖使用 `kite_llm///` 缓存已校验 的 Release 归档;启用方式、读取授权和平台限制见 [kiteLLM 接入说明](../doc/kitellm.md)。 @@ -13,24 +14,22 @@ ```text 3rdparty/ -├── llama_cpp/ # llama.cpp 编译产物 (静态库与头文件) -│ ├── include/ # llama.h, ggml.h, gguf.h 等 -│ └── lib/ # libllama.a, libggml.a, libggml-base.a, libggml-cpu.a -│ -├── onnxruntime/ # ONNX Runtime 官方预编译发行包 -│ ├── include/ # onnxruntime_c_api.h, onnxruntime_cxx_api.h 等 -│ └── lib/ # libonnxruntime.so (Linux) 或 libonnxruntime.dylib (macOS) -│ -├── googletest/ # Google Test 单元测试框架 -│ ├── include/ # gtest/gtest.h 等 -│ └── lib/ # libgtest.a, libgtest_main.a -│ -└── nlohmann_json/ # nlohmann/json 现代化 C++ JSON 库 (Header-only) - └── include/ # nlohmann/json.hpp +├── llama_cpp/// # 静态库与头文件 +├── whisper_cpp/// # 身份还包含对应 GGML provider +├── pcre2/// +├── googletest/// +├── onnxruntime/// # 固定平台发行包 +├── inferlet/// # 已校验下载归档,在各 build/_deps 解压 +├── kite_llm/// +├── nlohmann_json/include/ # 与平台无关的头文件 +└── stb_image/ # 与平台无关的头文件 ``` -每个依赖目录还包含自动生成的 `.edgeflow-cache-fingerprint`。该文件记录固定版本、 -源码 SHA-256、缓存类型,以及二进制缓存所需的平台、工具链和 ABI 选项。 +库缓存目录包含 `.edgeflow-cache-fingerprint`,记录版本、来源散列和目标 OS/架构。 +源码编译的静态库另记录编译器、工具链、SDK/sysroot、当前构建类型的编译参数和 sanitizer 设置; +官方预编译包不因本机 SDK 更新而改变身份。`target` 例如 `linux-arm64`、`macos-arm64`。 +项目使用 Ninja 等单配置生成器;`cmake_ext/third_party/ThirdPartyCacheMetadata.cmake` +拥有身份计算规则,各依赖模块消费其返回的路径。 --- @@ -39,13 +38,16 @@ 1. **自动生成与归档**: - 首次执行 CMake 构建(如 `cmake -B build`)且本地 `3rdparty/` 为空时,构建系统会自动拉取依赖并完成编译/规整,将产物保存到 `3rdparty/` 对应子目录中。 2. **零编译 / 零下载秒级复用**: - - 当 `3rdparty//` 存在有效库、头文件及匹配的缓存指纹时,后续清理或新建构建目录,CMake 均会直接以 `IMPORTED` 静态库/动态库秒级导入。 + - 单配置构建在所选身份目录存在完整库、头文件和匹配指纹时,使用 `IMPORTED` 目标复用。 - 指纹缺失或与当前固定源码、平台、编译器、ABI 选项不匹配时,CMake 会 fail-closed 地忽略旧缓存,并从已校验的固定来源重新生成和归档。 + - llama.cpp 只缓存项目提前确定的 GGML 选项组合。出现未纳入身份的 GGML/LLAMA 构建变量时,llama/Whisper 均使用源码增量构建且不读写共享归档;同一源码构建目录中上游新增的默认选项也按此处理,身份目录保持稳定。配置日志会列出禁用共享归档的变量名称,或需要保留源码目标依赖的 Metal/BLAS 组合。 3. **强制重新编译 / 刷新**: - - 如需更新或重新编译 llama.cpp,可在 CMake 中指定 `-DLLAMA_CPP_FORCE_REBUILD=ON`,或直接删除 `3rdparty/llama_cpp` 目录。 + - 如需重新编译 llama.cpp,可指定 `-DLLAMA_CPP_FORCE_REBUILD=ON`。不同身份目录可以并存,无需删除其他平台的缓存。 --- ## 3. 离线与边缘硬件手动放置说明 -在无法连接公网的离线服务器或嵌入式边缘设备(如 ARM / NPU 板卡)上,可复制由同一项目配置在兼容工具链中生成的完整 `3rdparty//` 目录(包括缓存指纹)。只复制来源不明的库和头文件会被拒绝,避免静默加载 ABI 不兼容产物。 +离线构建可复制由同一项目配置和工具链生成的完整缓存目录,包括指纹及目录层级。 +指纹或编译器/sysroot 路径不同会重新选择源码构建;此时还需预置固定版本源码或相同发布归档。 +设备上实际使用哪些库,以当前配置输出及目标产物为准。 diff --git a/CMakeLists.txt b/CMakeLists.txt index 224600a8..13ab6683 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ endif() project(LLMEdgeFlow VERSION 11.0.0 LANGUAGES C CXX) +include(cmake_ext/PlatformOptions.cmake) include(cmake_ext/BackendOptions.cmake) include(cmake_ext/ProjectSetup.cmake) include(cmake_ext/Dependencies.cmake) diff --git a/CMakePresets.json b/CMakePresets.json index 4c4d5ede..6ae9d51a 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -8,6 +8,7 @@ "binaryDir": "${sourceDir}/build/variants/${presetName}", "cacheVariables": { "BUILD_TESTING": "ON", + "LLM_EDGEFLOW_EXPECTED_PLATFORM": "", "ENABLE_ONNXRUNTIME": "OFF", "ENABLE_LLAMACPP": "OFF", "ENABLE_WHISPERCPP": "OFF", @@ -16,6 +17,7 @@ "CMAKE_BUILD_TYPE": "Release", "ENABLE_REAL_MODEL_TESTS": "OFF", "ENABLE_SANITIZERS": "OFF", + "LLM_EDGEFLOW_LLAMACPP_METAL": "OFF", "LLM_EDGEFLOW_TEST_PCH": "OFF" } }, @@ -44,6 +46,34 @@ } } }, + { + "name": "macos-arm64-cpu", + "displayName": "macOS Apple Silicon CPU (M3 and arm64)", + "inherits": "full-cpu", + "cacheVariables": { + "LLM_EDGEFLOW_EXPECTED_PLATFORM": "macos-arm64", + "CMAKE_OSX_ARCHITECTURES": "arm64", + "CMAKE_OSX_SYSROOT": "macosx", + "CMAKE_OSX_DEPLOYMENT_TARGET": "14.0", + "LLM_EDGEFLOW_LINKER": "system" + }, + "vendor": { + "llm-edgeflow/selection": { + "backends": ["onnxruntime", "llama_cpp", "whisper_cpp"] + } + } + }, + { + "name": "linux-arm64-cross", + "displayName": "GNU/Linux arm64 SDK cross compilation", + "inherits": "minimal", + "cacheVariables": { + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake_ext/toolchains/linux-arm64.cmake", + "LLM_EDGEFLOW_EXPECTED_PLATFORM": "linux-arm64", + "BUILD_TESTING": "OFF", + "LLM_EDGEFLOW_LINKER": "system" + } + }, { "name": "kite-cpu", "inherits": "base", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a71bb963..3019a343 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -168,6 +168,11 @@ Finish the intended changes and durable documentation, then choose the gate entr ./scripts/run_all_tests.sh ``` +On macOS Apple Silicon, select the native CPU preset through the same entrypoint: +`LLM_EDGEFLOW_BUILD_PRESET=macos-arm64-cpu ./scripts/run_all_tests.sh`. +The gate always enables tests; a cross-compilation preset cannot substitute +for native execution. See [platform builds](doc/VERIFIABLE_SELECTION.md#跨平台构建). + - When the user has authorized PR delivery or merge, use the delivery script in section 7. It runs this same gate before committing and pushing; do not run a separate full gate first. diff --git a/README.md b/README.md index 2c182f55..b2df3c6a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ LLM-EdgeFlow 用算法节点处理规则、检索、文本、图像和语音。P ## 快速开始 本例使用 Linux CPU 环境。在仓库根目录执行所有命令。 +macOS Apple Silicon(含 M3)使用 `macos-arm64-cpu` 预设,见[跨平台构建](doc/VERIFIABLE_SELECTION.md#跨平台构建)。 构建需要 C++17 编译器、CMake 3.19+、Ninja、Python 3 和 Git。 首次构建需要联网下载固定版本的依赖。模型权重需单独准备。 @@ -44,7 +45,7 @@ cmake --preset=inferlet-cpu -B build cmake --build build --target alg_sdk alg_demo alg_pipeline_tool alg_show --parallel 4 ``` -默认构建只启用 Inferlet,也可运行无需模型的示例。其他 Backend 须按[构建变体](doc/VERIFIABLE_SELECTION.md#构建变体) +Linux 默认构建只启用 Inferlet,也可运行无需模型的示例。其他 Backend 须按[构建变体](doc/VERIFIABLE_SELECTION.md#构建变体) 显式启用;使用 Inferlet 在 CPU 上验证时见 [Inferlet 接入](doc/VERIFIABLE_SELECTION.md#inferlet-cpu-验证)。 主要产物如下: diff --git a/cmake_ext/BackendOptions.cmake b/cmake_ext/BackendOptions.cmake index 99fd9fb5..b2889ab5 100644 --- a/cmake_ext/BackendOptions.cmake +++ b/cmake_ext/BackendOptions.cmake @@ -1,7 +1,15 @@ # 在下载或加载厂商包前统一确定 Backend 选择;模块只负责选中依赖的接入。 -option(ENABLE_INFERLET "Enable the pinned Inferlet CPU release (Linux x86_64/aarch64)" ON) +# 现有 Inferlet/Kite 包只覆盖 Linux,macOS 默认使用 llama.cpp CPU。 +if(LLM_EDGEFLOW_TARGET_OS STREQUAL "macos") + set(_edgeflow_default_inferlet OFF) + set(_edgeflow_default_llamacpp ON) +else() + set(_edgeflow_default_inferlet ON) + set(_edgeflow_default_llamacpp OFF) +endif() +option(ENABLE_INFERLET "Enable the pinned Inferlet CPU release (Linux x86_64/aarch64)" ${_edgeflow_default_inferlet}) option(ENABLE_ONNXRUNTIME "Enable ONNX Runtime" OFF) -option(ENABLE_LLAMACPP "Enable llama.cpp" OFF) +option(ENABLE_LLAMACPP "Enable llama.cpp" ${_edgeflow_default_llamacpp}) option(ENABLE_WHISPERCPP "Enable whisper.cpp (requires llama.cpp)" OFF) option(ENABLE_KITELLM "Enable the pinned Kite CPU release" OFF) @@ -15,3 +23,8 @@ endif() if(ENABLE_WHISPERCPP AND NOT ENABLE_LLAMACPP) message(FATAL_ERROR "ENABLE_WHISPERCPP=ON requires ENABLE_LLAMACPP=ON for shared GGML") endif() + +if((ENABLE_INFERLET OR ENABLE_KITELLM) AND NOT LLM_EDGEFLOW_TARGET_OS STREQUAL "linux") + message(FATAL_ERROR + "Inferlet and Kite packages support Linux x86_64/arm64 only; use macos-arm64-cpu on Apple Silicon") +endif() diff --git a/cmake_ext/PlatformOptions.cmake b/cmake_ext/PlatformOptions.cmake new file mode 100644 index 00000000..2ace0631 --- /dev/null +++ b/cmake_ext/PlatformOptions.cmake @@ -0,0 +1,71 @@ +# Toolchain / preset 在 project() 之前确定编译器、SDK 和目标架构。 +# 本入口只读取 CMake 的目标信息并校验;Backend 与依赖模块据此决定各自的默认策略。 +if(CMAKE_CONFIGURATION_TYPES) + message(FATAL_ERROR "Use a single-config generator such as Ninja; multi-config builds are not supported") +endif() +set(_edgeflow_processor "${CMAKE_SYSTEM_PROCESSOR}") +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_OSX_ARCHITECTURES) + list(LENGTH CMAKE_OSX_ARCHITECTURES _edgeflow_arch_count) + if(NOT _edgeflow_arch_count EQUAL 1) + message(FATAL_ERROR "Build one macOS architecture per directory; universal dependency packages are not provided") + endif() + set(_edgeflow_processor "${CMAKE_OSX_ARCHITECTURES}") +endif() +string(TOLOWER "${_edgeflow_processor}" _edgeflow_processor) +if(_edgeflow_processor MATCHES "^(aarch64|arm64)$") + set(LLM_EDGEFLOW_TARGET_ARCH arm64) +elseif(_edgeflow_processor MATCHES "^(x86_64|amd64)$") + set(LLM_EDGEFLOW_TARGET_ARCH x86_64) +else() + message(FATAL_ERROR "Unsupported target architecture: ${_edgeflow_processor}") +endif() + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(LLM_EDGEFLOW_TARGET_OS linux) + set(LLM_EDGEFLOW_BINARY_FORMAT ELF) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(LLM_EDGEFLOW_TARGET_OS macos) + set(LLM_EDGEFLOW_BINARY_FORMAT MachO) +else() + message(FATAL_ERROR "Unsupported target system: ${CMAKE_SYSTEM_NAME}; use Linux or macOS") +endif() +set(LLM_EDGEFLOW_TARGET_PLATFORM "${LLM_EDGEFLOW_TARGET_OS}-${LLM_EDGEFLOW_TARGET_ARCH}") +set(LLM_EDGEFLOW_EXPECTED_PLATFORM "" CACHE STRING "Optional preset assertion for the resolved target platform") +if(LLM_EDGEFLOW_EXPECTED_PLATFORM AND + NOT LLM_EDGEFLOW_EXPECTED_PLATFORM STREQUAL LLM_EDGEFLOW_TARGET_PLATFORM) + message(FATAL_ERROR + "Preset requires ${LLM_EDGEFLOW_EXPECTED_PLATFORM}, but the toolchain targets ${LLM_EDGEFLOW_TARGET_PLATFORM}. Use a fresh build directory and the matching toolchain.") +endif() +if(CMAKE_SIZEOF_VOID_P AND NOT CMAKE_SIZEOF_VOID_P EQUAL 8) + message(FATAL_ERROR "The selected target requires a 64-bit ABI") +endif() + +# 禁止在已有构建目录里静默切换目标,依赖查找前就给出明确诊断。 +if(DEFINED LLM_EDGEFLOW_CONFIGURED_PLATFORM AND + NOT LLM_EDGEFLOW_CONFIGURED_PLATFORM STREQUAL LLM_EDGEFLOW_TARGET_PLATFORM) + message(FATAL_ERROR "Target platform changed; configure in a fresh build directory") +endif() +set(LLM_EDGEFLOW_CONFIGURED_PLATFORM "${LLM_EDGEFLOW_TARGET_PLATFORM}" CACHE INTERNAL "Configured target platform") + +set(LLM_EDGEFLOW_NATIVE_EXECUTION ON) +if(CMAKE_CROSSCOMPILING) + set(LLM_EDGEFLOW_NATIVE_EXECUTION OFF) +endif() +if(LLM_EDGEFLOW_TARGET_OS STREQUAL "macos") + string(TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" _edgeflow_host_processor) + if(_edgeflow_host_processor MATCHES "^(aarch64|arm64)$") + set(_edgeflow_host_processor arm64) + elseif(_edgeflow_host_processor MATCHES "^(x86_64|amd64)$") + set(_edgeflow_host_processor x86_64) + endif() + if(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin" OR + NOT _edgeflow_host_processor STREQUAL LLM_EDGEFLOW_TARGET_ARCH) + set(LLM_EDGEFLOW_NATIVE_EXECUTION OFF) + endif() +endif() +option(BUILD_TESTING "Build native host and runtime tests" ${LLM_EDGEFLOW_NATIVE_EXECUTION}) +if(BUILD_TESTING AND NOT LLM_EDGEFLOW_NATIVE_EXECUTION) + message(FATAL_ERROR + "Cross builds compile target artifacts only. Set BUILD_TESTING=OFF; run the native gate separately on the target (on Apple Silicon use a native arm64 terminal). Python/tooling tests execute target programs.") +endif() +message(STATUS "LLM-EdgeFlow target: ${LLM_EDGEFLOW_TARGET_PLATFORM}; cross compiling: ${CMAKE_CROSSCOMPILING}") diff --git a/cmake_ext/ProductTargets.cmake b/cmake_ext/ProductTargets.cmake index e5984e94..1c87391c 100644 --- a/cmake_ext/ProductTargets.cmake +++ b/cmake_ext/ProductTargets.cmake @@ -8,27 +8,30 @@ if(NOT LLM_EDGEFLOW_SDK_OUTPUT_NAME MATCHES "^[A-Za-z0-9_][A-Za-z0-9_.-]*$") message(FATAL_ERROR "LLM_EDGEFLOW_SDK_OUTPUT_NAME must be a nonempty library base name, not a path") endif() -add_library(alg_sdk SHARED - $ - $ - $ - $ - $ - $) +add_library(alg_sdk SHARED ${edgeflow_runtime_object_files}) add_library(llm_edgeflow::sdk ALIAS alg_sdk) target_link_libraries(alg_sdk PUBLIC edgeflow_public_headers PRIVATE edgeflow_runtime_dependencies) -if(UNIX AND NOT APPLE) - target_link_options(alg_sdk PRIVATE - "LINKER:--version-script=${LLM_EDGEFLOW_SDK_VERSION_SCRIPT}") - set_target_properties(alg_sdk PROPERTIES - LINK_DEPENDS "${LLM_EDGEFLOW_SDK_VERSION_SCRIPT}") -endif() +target_link_options(alg_sdk PRIVATE ${LLM_EDGEFLOW_SDK_LINK_OPTIONS}) set_target_properties(alg_sdk PROPERTIES OUTPUT_NAME "${LLM_EDGEFLOW_SDK_OUTPUT_NAME}" + LINK_DEPENDS "${LLM_EDGEFLOW_SDK_EXPORT_FILE}" ) +# Demo、CLI 和测试共享的 Profile 字段头。 +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${PROJECT_SOURCE_DIR}/demo/common/profile_fields.json") +file(READ "${PROJECT_SOURCE_DIR}/demo/common/profile_fields.json" + LLM_EDGEFLOW_DEMO_PROFILE_FIELDS) +configure_file( + "${PROJECT_SOURCE_DIR}/cmake_ext/templates/demo_profile_fields.h.in" + "${PROJECT_BINARY_DIR}/generated/internal/demo/common/demo_profile_fields.h" + @ONLY) +add_library(edgeflow_demo_profile_fields INTERFACE) +target_include_directories(edgeflow_demo_profile_fields INTERFACE + "${PROJECT_BINARY_DIR}/generated/internal") + add_subdirectory(dev_support) add_subdirectory(demo) add_subdirectory(src/cli) diff --git a/cmake_ext/ProjectSetup.cmake b/cmake_ext/ProjectSetup.cmake index 50eda0bc..c11f262c 100644 --- a/cmake_ext/ProjectSetup.cmake +++ b/cmake_ext/ProjectSetup.cmake @@ -1,17 +1,31 @@ -# 编译策略、生成文件与公共/扩展/内部头文件目标。 +# 编译策略集中在依赖与业务目标创建之前。 set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_POSITION_INDEPENDENT_CODE ON) +# 第三方源码依赖统一构建为静态归档,与缓存导入规则一致;alg_sdk 显式声明 SHARED。 +# 使用缓存项,旧 CMake 策略下上游的 option(BUILD_SHARED_LIBS) 也不会改回共享库。 +if(BUILD_SHARED_LIBS) + message(FATAL_ERROR "BUILD_SHARED_LIBS=ON is unsupported; third-party dependencies are linked statically") +endif() +set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build third-party source dependencies as shared libraries") set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) set(CMAKE_BUILD_RPATH_USE_ORIGIN TRUE) -# 编译与链接加速配置 + +if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$") + message(FATAL_ERROR "Unsupported compiler: ${CMAKE_CXX_COMPILER_ID}") +endif() include(cmake_ext/support/BuildAcceleration.cmake) add_compile_options(-Wall -Wextra) +# 与固定 GGML 上游的默认值一致,在任何依赖计算缓存身份前确定。 +if(ENABLE_LLAMACPP AND NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() + option(ENABLE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF) set(LLM_EDGEFLOW_SANITIZERS "address,undefined" CACHE STRING "Comma-separated Clang/GCC sanitizer set used when ENABLE_SANITIZERS=ON") @@ -31,12 +45,55 @@ if(ENABLE_SANITIZERS) "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=${LLM_EDGEFLOW_SANITIZERS}") endif() -set(LLM_EDGEFLOW_SDK_VERSION_SCRIPT - "${PROJECT_BINARY_DIR}/edgeflow_sdk.map") -configure_file( - "${PROJECT_SOURCE_DIR}/cmake_ext/templates/edgeflow_sdk.map.in" - "${LLM_EDGEFLOW_SDK_VERSION_SCRIPT}" - @ONLY) +# 直接调用编译器的分层契约探针也使用相同的 Apple SDK/架构。 +set(edgeflow_cxx_platform_flags "") +if(LLM_EDGEFLOW_TARGET_OS STREQUAL "macos") + if(CMAKE_OSX_SYSROOT) + set(_edgeflow_sdk_path "${CMAKE_OSX_SYSROOT}") + if(NOT IS_ABSOLUTE "${_edgeflow_sdk_path}") + execute_process(COMMAND xcrun --sdk "${CMAKE_OSX_SYSROOT}" --show-sdk-path + OUTPUT_VARIABLE _edgeflow_sdk_path OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _edgeflow_sdk_result) + if(NOT _edgeflow_sdk_result EQUAL 0) + message(FATAL_ERROR "Cannot resolve the selected macOS SDK: ${CMAKE_OSX_SYSROOT}") + endif() + endif() + set(LLM_EDGEFLOW_APPLE_SDK_PATH "${_edgeflow_sdk_path}") + execute_process(COMMAND xcrun --sdk "${_edgeflow_sdk_path}" --show-sdk-version + OUTPUT_VARIABLE LLM_EDGEFLOW_APPLE_SDK_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _edgeflow_sdk_result) + if(NOT _edgeflow_sdk_result EQUAL 0) + message(FATAL_ERROR "Cannot identify the selected macOS SDK: ${_edgeflow_sdk_path}") + endif() + list(APPEND edgeflow_cxx_platform_flags -isysroot "${_edgeflow_sdk_path}") + endif() + list(APPEND edgeflow_cxx_platform_flags -arch "${LLM_EDGEFLOW_TARGET_ARCH}") + if(CMAKE_OSX_DEPLOYMENT_TARGET) + list(APPEND edgeflow_cxx_platform_flags "-mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() +endif() + +# 同一份公开符号清单生成 ELF / Mach-O 的链接限制。 +set(_edgeflow_exports "${PROJECT_SOURCE_DIR}/cmake_ext/templates/edgeflow_sdk.exports") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${_edgeflow_exports}") +file(STRINGS "${_edgeflow_exports}" _edgeflow_sdk_symbols) +set(LLM_EDGEFLOW_SDK_EXPORT_FILE "${PROJECT_BINARY_DIR}/edgeflow_sdk.exports") +if(LLM_EDGEFLOW_BINARY_FORMAT STREQUAL "MachO") + set(_edgeflow_export_content "") + foreach(symbol IN LISTS _edgeflow_sdk_symbols) + string(APPEND _edgeflow_export_content "_${symbol}\n") + endforeach() + set(LLM_EDGEFLOW_SDK_LINK_OPTIONS "LINKER:-exported_symbols_list,${LLM_EDGEFLOW_SDK_EXPORT_FILE}") +else() + set(_edgeflow_export_content "{\n global:\n") + foreach(symbol IN LISTS _edgeflow_sdk_symbols) + string(APPEND _edgeflow_export_content " ${symbol};\n") + endforeach() + string(APPEND _edgeflow_export_content " local: *;\n};\n") + set(LLM_EDGEFLOW_SDK_LINK_OPTIONS "LINKER:--version-script=${LLM_EDGEFLOW_SDK_EXPORT_FILE}") +endif() +# 内容不变时保留时间戳,避免 LINK_DEPENDS 在每次配置后触发 SDK 重链接。 +file(GENERATE OUTPUT "${LLM_EDGEFLOW_SDK_EXPORT_FILE}" CONTENT "${_edgeflow_export_content}") include(cmake_ext/support/OperatorShell.cmake) include(cmake_ext/support/LayerHeaderViews.cmake) @@ -53,25 +110,8 @@ target_link_libraries(edgeflow_extension_headers INTERFACE # 仓库内工具和测试可访问源码;公开 SDK 目标只传播明确列出的调用头。 add_library(edgeflow_internal_headers INTERFACE) -set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS - "${PROJECT_SOURCE_DIR}/demo/common/profile_fields.json") -file(READ "${PROJECT_SOURCE_DIR}/demo/common/profile_fields.json" - LLM_EDGEFLOW_DEMO_PROFILE_FIELDS) -configure_file( - "${PROJECT_SOURCE_DIR}/cmake_ext/templates/demo_profile_fields.h.in" - "${PROJECT_BINARY_DIR}/generated/internal/demo/common/demo_profile_fields.h" - @ONLY) target_include_directories(edgeflow_internal_headers INTERFACE ${PROJECT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/src) target_link_libraries(edgeflow_internal_headers INTERFACE edgeflow_extension_headers) - -set(edgeflow_cxx_system_include_dirs "") -if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") - if(EXISTS "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1") - list(APPEND edgeflow_cxx_system_include_dirs - "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1") - include_directories(SYSTEM ${edgeflow_cxx_system_include_dirs}) - endif() -endif() diff --git a/cmake_ext/RuntimeGraph.cmake b/cmake_ext/RuntimeGraph.cmake index 36cf5875..8288aca7 100644 --- a/cmake_ext/RuntimeGraph.cmake +++ b/cmake_ext/RuntimeGraph.cmake @@ -31,8 +31,6 @@ function(edgeflow_add_runtime_object_target target_name dependency_target) target_link_libraries(${target_name} PUBLIC ${dependency_target}) target_compile_definitions(${target_name} PRIVATE EDGEFLOW_BUILDING_SDK=1) - target_compile_definitions(${target_name} INTERFACE - EDGEFLOW_STATIC_LINK=1) set_target_properties(${target_name} PROPERTIES C_VISIBILITY_PRESET hidden CXX_VISIBILITY_PRESET hidden @@ -64,6 +62,15 @@ target_include_directories(edgeflow_composition_objects PRIVATE "${edgeflow_integration_include_dir}") edgeflow_generate_layer_compile_manifest() +# SDK 与仓库工具共用同一组层对象。 +set(edgeflow_runtime_object_files + $ + $ + $ + $ + $ + $) + # 仓库工具和测试继续链接一个聚合 INTERFACE target,不依赖公共共享库的 # 内部 C++ 动态符号。该 target 自身不拥有层内源码。 add_library(edgeflow_runtime_objects INTERFACE) @@ -73,43 +80,19 @@ target_link_libraries(edgeflow_runtime_objects INTERFACE target_compile_definitions(edgeflow_runtime_objects INTERFACE EDGEFLOW_STATIC_LINK=1) -if(LLM_EDGEFLOW_HAS_ONNXRUNTIME) - target_include_directories(edgeflow_model_execution_backends_objects SYSTEM PRIVATE - "${ONNXRUNTIME_INCLUDE_DIR}") - target_compile_definitions(edgeflow_model_execution_backends_objects PRIVATE - HAVE_ONNXRUNTIME=1) - target_link_libraries(edgeflow_runtime_dependencies INTERFACE - "$") -endif() -if(LLM_EDGEFLOW_HAS_LLAMACPP) - target_include_directories(edgeflow_model_execution_backends_objects SYSTEM PRIVATE - ${LLAMACPP_INCLUDE_DIRS}) - target_compile_definitions(edgeflow_model_execution_backends_objects PRIVATE - HAVE_LLAMACPP=1) -endif() -if(LLM_EDGEFLOW_HAS_WHISPERCPP) - target_include_directories(edgeflow_model_execution_backends_objects SYSTEM PRIVATE - ${WHISPERCPP_INCLUDE_DIRS}) - target_compile_definitions(edgeflow_model_execution_backends_objects PRIVATE - HAVE_WHISPERCPP=1) -endif() +# 引擎模块只追加 Backend 的 SYSTEM 头目录、编译定义和链接目标; +# 新增 Backend 不修改此处,厂商编译要求只进入 Backend 实现。 +target_include_directories(edgeflow_model_execution_backends_objects SYSTEM PRIVATE + ${LLM_EDGEFLOW_BACKEND_INCLUDE_DIRS}) target_compile_definitions(edgeflow_model_execution_backends_objects PRIVATE ${LLM_EDGEFLOW_BACKEND_COMPILE_DEFINITIONS}) -if(THIRD_PARTY_ENGINE_LIBS) +foreach(backend_library IN LISTS LLM_EDGEFLOW_BACKEND_LIBRARIES) target_link_libraries(edgeflow_model_execution_backends_objects PRIVATE - ${THIRD_PARTY_ENGINE_LIBS}) - foreach(runtime_library IN LISTS THIRD_PARTY_ENGINE_LIBS) - target_link_libraries(edgeflow_runtime_dependencies INTERFACE - "$") - endforeach() -endif() + ${backend_library}) + target_link_libraries(edgeflow_runtime_dependencies INTERFACE + "$") +endforeach() add_subdirectory(src) -target_sources(edgeflow_runtime_objects INTERFACE - $ - $ - $ - $ - $ - $) +target_sources(edgeflow_runtime_objects INTERFACE ${edgeflow_runtime_object_files}) diff --git a/cmake_ext/engines/Inferlet.cmake b/cmake_ext/engines/Inferlet.cmake index 66ec6831..e8712ce1 100644 --- a/cmake_ext/engines/Inferlet.cmake +++ b/cmake_ext/engines/Inferlet.cmake @@ -2,69 +2,24 @@ set(LLM_EDGEFLOW_HAS_INFERLET OFF) if(NOT ENABLE_INFERLET) return() endif() -if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND - CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") +if(LLM_EDGEFLOW_TARGET_ARCH STREQUAL "x86_64") set(_inferlet_platform "x64") set(_inferlet_sha256 "3c5e09b43053cc0bddc9ff4c9353be59e810ecf0a41ccd0bb0fed1ddfa77d807") -elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND - CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") +else() set(_inferlet_platform "arm") set(_inferlet_sha256 "de7aa0078191e5c4450c057de89206dcb164f5d01892f3b13e6aac83633bf841") -else() - message(FATAL_ERROR "Inferlet integration supports Linux x86_64/aarch64 only") endif() include(cmake_ext/third_party/ThirdPartyCacheMetadata.cmake) set(_inferlet_version "v0.1.1") set(_inferlet_asset "inferlet-${_inferlet_platform}.tar.gz") -set(_inferlet_cache "${CMAKE_SOURCE_DIR}/3rdparty/inferlet/${_inferlet_version}/${_inferlet_platform}") -set(_inferlet_archive "${_inferlet_cache}/${_inferlet_asset}") -file(MAKE_DIRECTORY "${_inferlet_cache}") -file(LOCK "${_inferlet_cache}/.download.lock" GUARD PROCESS TIMEOUT 60) -if(NOT EXISTS "${_inferlet_archive}") - file(DOWNLOAD - "https://github.com/chamsechan/Inferlet/releases/download/${_inferlet_version}/${_inferlet_asset}" - "${_inferlet_archive}.download" - TLS_VERIFY ON STATUS _inferlet_download_status TIMEOUT 120) - list(GET _inferlet_download_status 0 _inferlet_download_code) - if(NOT _inferlet_download_code EQUAL 0) - message(FATAL_ERROR "Cannot download Inferlet: ${_inferlet_download_status}") - endif() - file(SHA256 "${_inferlet_archive}.download" _inferlet_actual_sha256) - if(NOT _inferlet_actual_sha256 STREQUAL _inferlet_sha256) - message(FATAL_ERROR "Downloaded Inferlet SHA-256 mismatch") - endif() - file(RENAME "${_inferlet_archive}.download" "${_inferlet_archive}") -endif() -file(SHA256 "${_inferlet_archive}" _inferlet_actual_sha256) -if(NOT _inferlet_actual_sha256 STREQUAL _inferlet_sha256) - message(FATAL_ERROR "Cached Inferlet SHA-256 mismatch: ${_inferlet_archive}") -endif() - -# 从已校验归档重建当前构建的包;内网可预置同一归档以离线配置。 -set(_inferlet_extract "${CMAKE_BINARY_DIR}/_deps/inferlet_release") -file(REMOVE_RECURSE "${_inferlet_extract}") -file(MAKE_DIRECTORY "${_inferlet_extract}") -execute_process(COMMAND "${CMAKE_COMMAND}" -E tar xzf "${_inferlet_archive}" - WORKING_DIRECTORY "${_inferlet_extract}" RESULT_VARIABLE _inferlet_extract_result) -if(NOT _inferlet_extract_result EQUAL 0) - message(FATAL_ERROR "Cannot extract verified Inferlet release") -endif() -edgeflow_prepare_third_party_cache( - NAME inferlet VERSION "${_inferlet_version}" SOURCE_SHA256 "${_inferlet_sha256}" - CACHE_DIR "${_inferlet_cache}" KIND PREBUILT - OUT_VALID _inferlet_cache_valid OUT_MARKER _inferlet_marker) -configure_file("${_inferlet_marker}" "${_inferlet_cache}/.edgeflow-cache-fingerprint" COPYONLY) - -set(_inferlet_root "${_inferlet_extract}/inferlet-${_inferlet_platform}") -unset(Inferlet_DIR CACHE) -set(Inferlet_DIR "${_inferlet_root}/lib/cmake/Inferlet") -find_package(Inferlet 0.1.1 EXACT CONFIG REQUIRED - PATHS "${Inferlet_DIR}" NO_DEFAULT_PATH) -get_target_property(_inferlet_library Inferlet::Inferlet IMPORTED_LOCATION_RELEASE) -if(NOT _inferlet_library STREQUAL "${_inferlet_root}/lib/libinferlet.a") - message(FATAL_ERROR "Inferlet must resolve to the verified release archive") -endif() +# 内网可预置同一归档以离线配置。 +set(_inferlet_archive + "${CMAKE_SOURCE_DIR}/3rdparty/inferlet/${_inferlet_version}/${_inferlet_platform}/${_inferlet_asset}") +edgeflow_download_verified_file("${_inferlet_archive}" "${_inferlet_sha256}" + URL "https://github.com/chamsechan/Inferlet/releases/download/${_inferlet_version}/${_inferlet_asset}") +edgeflow_import_release_package(Inferlet 0.1.1 "${_inferlet_archive}" + "${CMAKE_BINARY_DIR}/_deps/inferlet_release" "inferlet-${_inferlet_platform}" libinferlet.a) include(CheckCXXSourceCompiles) include(CMakePushCheckState) @@ -89,5 +44,4 @@ if(NOT INFERLET_API_COMPATIBLE) endif() set(LLM_EDGEFLOW_HAS_INFERLET ON) list(APPEND LLM_EDGEFLOW_BACKEND_COMPILE_DEFINITIONS HAVE_INFERLET=1) -list(APPEND THIRD_PARTY_ENGINE_LIBS Inferlet::Inferlet) -message(STATUS "[Inferlet] Verified ${_inferlet_version}: ${_inferlet_library}") +list(APPEND LLM_EDGEFLOW_BACKEND_LIBRARIES Inferlet::Inferlet) diff --git a/cmake_ext/engines/KiteLlm.cmake b/cmake_ext/engines/KiteLlm.cmake index 93818fc6..3b33f086 100644 --- a/cmake_ext/engines/KiteLlm.cmake +++ b/cmake_ext/engines/KiteLlm.cmake @@ -8,84 +8,30 @@ endif() include(cmake_ext/third_party/ThirdPartyCacheMetadata.cmake) set(_kite_release "v0.1.0") -if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND - CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") +if(LLM_EDGEFLOW_TARGET_ARCH STREQUAL "x86_64") set(_kite_platform "x64") set(_kite_sha256 "bddaa55abe37cfc87f481139c41aadf79ad579b7cc20fe2b12c01978a2d3b1a0") -elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND - CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$") +else() set(_kite_platform "arm") set(_kite_sha256 "1388da99c8fd4a45d0f85a489d29b2a12189e4955302ed75fd8bbddd1ebfc70d") -else() - message(FATAL_ERROR - "Pinned kiteLLM integration supports Linux x86_64/aarch64 only; got " - "${CMAKE_SYSTEM_NAME}/${CMAKE_SYSTEM_PROCESSOR}") endif() set(_kite_asset "kiteLLM-${_kite_platform}.tar.gz") set(_kite_cache "${CMAKE_SOURCE_DIR}/3rdparty/kite_llm/${_kite_release}/${_kite_platform}") set(_kite_archive "${_kite_cache}/${_kite_asset}") -file(MAKE_DIRECTORY "${_kite_cache}") -# 不同构建目录共享此归档缓存。 -file(LOCK "${_kite_cache}/.download.lock" GUARD PROCESS TIMEOUT 60) -if(NOT EXISTS "${_kite_archive}") - find_program(KITELLM_GH_EXECUTABLE NAMES gh) - if(NOT KITELLM_GH_EXECUTABLE) - message(FATAL_ERROR - "kiteLLM is a private GitHub dependency. Install gh and run gh auth login " - "with repository read access (CI: set GH_TOKEN).") - endif() - message(STATUS "[kiteLLM] Downloading chamsechan/kiteLLM ${_kite_release}/${_kite_asset}") - # 先下载到暂存目录,避免中断的下载变成可复用的归档。 - # 即使 tag 被移动,也以下方校验和为准。 - set(_kite_download "${_kite_cache}/download") - file(MAKE_DIRECTORY "${_kite_download}") - execute_process( - COMMAND "${KITELLM_GH_EXECUTABLE}" release download "${_kite_release}" - --repo chamsechan/kiteLLM --pattern "${_kite_asset}" - --dir "${_kite_download}" --clobber - RESULT_VARIABLE _kite_download_result - OUTPUT_QUIET ERROR_QUIET) - if(NOT _kite_download_result EQUAL 0) - message(FATAL_ERROR - "Cannot download private kiteLLM release. Check network and gh auth status; " - "the account (or CI GH_TOKEN) must have read access to chamsechan/kiteLLM.") - endif() - file(SHA256 "${_kite_download}/${_kite_asset}" _kite_actual_sha256) - if(NOT _kite_actual_sha256 STREQUAL _kite_sha256) - message(FATAL_ERROR "Downloaded kiteLLM release SHA-256 mismatch") - endif() - file(RENAME "${_kite_download}/${_kite_asset}" "${_kite_archive}") -endif() -file(SHA256 "${_kite_archive}" _kite_actual_sha256) -if(NOT _kite_actual_sha256 STREQUAL _kite_sha256) - message(FATAL_ERROR - "Cached kiteLLM SHA-256 mismatch. Remove ${_kite_archive} and configure again.") -endif() - -# 只把已校验的字节重新解压到本构建,避免残留或被篡改的头文件/库, -# 以及旧环境遗留的机器相关绝对路径。 -set(_kite_extract "${CMAKE_BINARY_DIR}/_deps/kite_llm_release") -file(MAKE_DIRECTORY "${_kite_extract}") -execute_process(COMMAND "${CMAKE_COMMAND}" -E tar xzf "${_kite_archive}" - WORKING_DIRECTORY "${_kite_extract}" RESULT_VARIABLE _kite_extract_result) -if(NOT _kite_extract_result EQUAL 0) - message(FATAL_ERROR "Cannot extract verified kiteLLM release") -endif() -set(_kite_root "${_kite_extract}/kiteLLM-${_kite_platform}") -edgeflow_prepare_third_party_cache( - NAME kite_llm VERSION "${_kite_release}" SOURCE_SHA256 "${_kite_sha256}" - CACHE_DIR "${_kite_cache}" KIND PREBUILT - OUT_VALID _kite_cache_valid OUT_MARKER _kite_marker) -configure_file("${_kite_marker}" "${_kite_cache}/.edgeflow-cache-fingerprint" COPYONLY) - -unset(kiteLLM_DIR CACHE) -set(kiteLLM_DIR "${_kite_root}/lib/cmake/kiteLLM") -find_package(kiteLLM 0.1.0 EXACT CONFIG REQUIRED - PATHS "${_kite_root}/lib/cmake/kiteLLM" NO_DEFAULT_PATH) -get_target_property(_kite_imported_library kiteLLM::kiteLLM IMPORTED_LOCATION_RELEASE) -if(NOT _kite_imported_library STREQUAL "${_kite_root}/lib/libkiteLLM.a") - message(FATAL_ERROR "kiteLLM package must resolve to the verified release archive") -endif() +# 不同构建目录共享此归档缓存;只有缺少归档时才调用 gh。 +# 即使 tag 被移动,也以固定校验和为准。 +find_program(KITELLM_GH_EXECUTABLE NAMES gh) +file(MAKE_DIRECTORY "${_kite_cache}/download") +edgeflow_download_verified_file("${_kite_archive}" "${_kite_sha256}" + COMMAND "${KITELLM_GH_EXECUTABLE}" release download "${_kite_release}" + --repo chamsechan/kiteLLM --pattern "${_kite_asset}" + --dir "${_kite_cache}/download" --clobber + DOWNLOADED "${_kite_cache}/download/${_kite_asset}" + FAILURE_MESSAGE + "Cannot download private kiteLLM release. Install gh and run gh auth login; " + "the account (or CI GH_TOKEN) must have read access to chamsechan/kiteLLM.") +edgeflow_import_release_package(kiteLLM 0.1.0 "${_kite_archive}" + "${CMAKE_BINARY_DIR}/_deps/kite_llm_release" "kiteLLM-${_kite_platform}" libkiteLLM.a) include(CheckCXXSourceCompiles) include(CMakePushCheckState) @@ -137,5 +83,4 @@ if(NOT KITELLM_DIRECT_API_COMPATIBLE) endif() set(LLM_EDGEFLOW_HAS_KITELLM ON) list(APPEND LLM_EDGEFLOW_BACKEND_COMPILE_DEFINITIONS HAVE_KITELLM=1) -list(APPEND THIRD_PARTY_ENGINE_LIBS kiteLLM::kiteLLM) -message(STATUS "[kiteLLM] Verified ${_kite_release}: ${_kite_imported_library}") +list(APPEND LLM_EDGEFLOW_BACKEND_LIBRARIES kiteLLM::kiteLLM) diff --git a/cmake_ext/engines/LlamaCpp.cmake b/cmake_ext/engines/LlamaCpp.cmake index 05c484bf..48bed9b7 100644 --- a/cmake_ext/engines/LlamaCpp.cmake +++ b/cmake_ext/engines/LlamaCpp.cmake @@ -5,6 +5,7 @@ option(LLAMA_CPP_FORCE_REBUILD "Force rebuilding llama.cpp from source even if c option(LLM_EDGEFLOW_LLAMACPP_METAL "Enable llama.cpp Metal backend (requires a usable Metal device)" OFF) set(LLM_EDGEFLOW_HAS_LLAMACPP OFF) +set(LLM_EDGEFLOW_LLAMACPP_CACHEABLE OFF) if(ENABLE_LLAMACPP) message(STATUS "[Model Execution] Enabling llama.cpp Backend support...") @@ -14,130 +15,131 @@ if(ENABLE_LLAMACPP) "70adb1b4cea5ee39f867792c78dc59320921eda7") set(_LLAMA_SOURCE_SHA256 "94d215f1fd85ded40f4674eccdbd3caf4a9b0daa00b6d72255efec922c6d94a4") - # 先解析固定上游版本的 BLAS 默认值,再检查缓存。 - # 这些选项须同时描述源码构建和导入的归档。 - if(APPLE) + # 先解析平台对应的 GGML 默认值,再检查缓存。 + # 这些选项须同时描述源码构建和导入的归档:macOS 使用 Apple Accelerate 并关闭 OpenMP。 + if(LLM_EDGEFLOW_TARGET_OS STREQUAL "macos") set(_llama_blas_default ON) set(_llama_blas_vendor_default Apple) + set(_llama_openmp_default OFF) else() set(_llama_blas_default OFF) set(_llama_blas_vendor_default Generic) + set(_llama_openmp_default ON) endif() option(GGML_BLAS "ggml: use BLAS" ${_llama_blas_default}) set(GGML_BLAS_VENDOR "${_llama_blas_vendor_default}" CACHE STRING "ggml: BLAS library vendor") + # 在缓存查询和源码子工程之前确定同一组选项,不让宿主探测改变目标 CPU。 + set(GGML_NATIVE OFF CACHE BOOL "Build with native CPU flags" FORCE) + set(LLAMA_NATIVE OFF CACHE BOOL "Build with native CPU flags" FORCE) + set(GGML_METAL ${LLM_EDGEFLOW_LLAMACPP_METAL} CACHE BOOL "Enable Metal" FORCE) + set(GGML_CCACHE OFF CACHE BOOL "Use ccache for ggml" FORCE) + foreach(feature AVX512 AVX512_VBMI AVX512_VNNI AVX512_BF16) + set(GGML_${feature} OFF CACHE BOOL "Enable ${feature}" FORCE) + endforeach() + option(GGML_OPENMP "ggml: use OpenMP" ${_llama_openmp_default}) + option(GGML_ACCELERATE "ggml: use Accelerate" ${_llama_blas_default}) + set(GGML_CPU_ARM_ARCH "" CACHE STRING "ggml: target ARM architecture") + # 身份只使用进入上游前已确定的选项,避免上游新增默认值使重新配置漂移。 + set(_llama_cache_options GGML_BLAS GGML_BLAS_VENDOR GGML_NATIVE GGML_METAL + GGML_CCACHE GGML_AVX512 GGML_AVX512_VBMI GGML_AVX512_VNNI GGML_AVX512_BF16 + GGML_OPENMP GGML_ACCELERATE GGML_CPU_ARM_ARCH) + set(_llama_abi_options BUILD_SHARED_LIBS=OFF) + foreach(variable IN LISTS _llama_cache_options) + list(APPEND _llama_abi_options "${variable}=${${variable}}") + endforeach() + # 未纳入身份的调优使用源码增量构建,既不导入也不覆盖共享归档。 + # VARIABLES 同时覆盖普通变量及上游会转换的 LLAMA_* 旧选项。 + set(_llama_cache_controls LLAMA_CPP_FORCE_REBUILD LLAMA_NATIVE LLAMA_3RDPARTY_DIR) + set(LLM_EDGEFLOW_LLAMACPP_CACHEABLE ON) + set(_llama_uncached_options) + get_cmake_property(_llama_variables VARIABLES) + foreach(variable IN LISTS _llama_variables) + get_property(_type CACHE "${variable}" PROPERTY TYPE) + get_property(_cached_value CACHE "${variable}" PROPERTY VALUE) + if(variable MATCHES "^(GGML|LLAMA)_" AND + NOT variable IN_LIST _llama_cache_options AND NOT variable IN_LIST _llama_cache_controls AND + NOT (_type STREQUAL "INTERNAL" AND "${${variable}}" STREQUAL "${_cached_value}")) + set(LLM_EDGEFLOW_LLAMACPP_CACHEABLE OFF) + list(APPEND _llama_uncached_options "${variable}") + endif() + endforeach() + if(_llama_uncached_options) + string(JOIN ", " _llama_uncached_options_text ${_llama_uncached_options}) + message(STATUS + "[3rdparty] llama.cpp/Whisper shared archive cache disabled: untracked build options: ${_llama_uncached_options_text}. Using incremental source builds.") + endif() + # 归档导入只覆盖完整的 CPU/Apple BLAS 链。其他组合保留源码目标的传递依赖。 + if(GGML_METAL OR (GGML_BLAS AND NOT GGML_BLAS_VENDOR STREQUAL "Apple")) + set(LLM_EDGEFLOW_LLAMACPP_CACHEABLE OFF) + message(STATUS + "[3rdparty] llama.cpp/Whisper shared archive cache disabled: GGML_METAL=${GGML_METAL}, GGML_BLAS=${GGML_BLAS}, GGML_BLAS_VENDOR=${GGML_BLAS_VENDOR} require source target dependencies.") + endif() + set(_llama_archives llama ggml ggml-cpu ggml-base) + if(GGML_BLAS) + list(APPEND _llama_archives ggml-blas) + endif() + set(_llama_required_files include/llama.h) + foreach(_archive IN LISTS _llama_archives) + list(APPEND _llama_required_files "lib/lib${_archive}.a") + endforeach() edgeflow_prepare_third_party_cache( NAME llama_cpp VERSION "${LLM_EDGEFLOW_LLAMACPP_COMMIT}" SOURCE_SHA256 "${_LLAMA_SOURCE_SHA256}" CACHE_DIR "${LLAMA_3RDPARTY_DIR}" KIND STATIC - ABI_OPTIONS GGML_NATIVE=OFF GGML_METAL=${LLM_EDGEFLOW_LLAMACPP_METAL} - GGML_BLAS=${GGML_BLAS} GGML_BLAS_VENDOR=${GGML_BLAS_VENDOR} - BUILD_SHARED_LIBS=OFF + ABI_OPTIONS ${_llama_abi_options} + REQUIRED_FILES ${_llama_required_files} + OUT_CACHE_DIR LLAMA_3RDPARTY_DIR OUT_VALID _LLAMA_CACHE_VALID OUT_MARKER _LLAMA_CACHE_MARKER) - set(_llama_required_sublibs ggml ggml-cpu ggml-base) - if(GGML_BLAS) - list(APPEND _llama_required_sublibs ggml-blas) - endif() - if(_LLAMA_CACHE_VALID) - foreach(_sublib IN LISTS _llama_required_sublibs) - if(NOT EXISTS "${LLAMA_3RDPARTY_DIR}/lib/lib${_sublib}.a") - message(STATUS "[3rdparty] Ignoring incomplete llama_cpp cache: missing lib${_sublib}.a") - set(_LLAMA_CACHE_VALID OFF) - endif() - endforeach() - endif() - - if(_LLAMA_CACHE_VALID AND - EXISTS "${LLAMA_3RDPARTY_DIR}/lib/libllama.a" AND - EXISTS "${LLAMA_3RDPARTY_DIR}/include/llama.h" AND - NOT LLAMA_CPP_FORCE_REBUILD) + if(LLM_EDGEFLOW_LLAMACPP_CACHEABLE AND _LLAMA_CACHE_VALID AND NOT LLAMA_CPP_FORCE_REBUILD) message(STATUS "[Model Execution] Using prebuilt llama.cpp from 3rdparty: ${LLAMA_3RDPARTY_DIR} (skipping compilation)") find_package(Threads REQUIRED) - find_package(OpenMP) set(_llama_imported_deps Threads::Threads) - if(OpenMP_FOUND) - if(TARGET OpenMP::OpenMP_CXX) - list(APPEND _llama_imported_deps OpenMP::OpenMP_CXX) - endif() - if(TARGET OpenMP::OpenMP_C) - list(APPEND _llama_imported_deps OpenMP::OpenMP_C) + if(GGML_OPENMP) + find_package(OpenMP) + if(OpenMP_FOUND) + list(APPEND _llama_imported_deps OpenMP::OpenMP_CXX OpenMP::OpenMP_C) + elseif(LLM_EDGEFLOW_TARGET_OS STREQUAL "linux") + list(APPEND _llama_imported_deps gomp) endif() - elseif(UNIX AND NOT APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - list(APPEND _llama_imported_deps gomp) endif() - if(CMAKE_DL_LIBS) - list(APPEND _llama_imported_deps ${CMAKE_DL_LIBS}) - endif() - if(UNIX AND NOT APPLE) + list(APPEND _llama_imported_deps ${CMAKE_DL_LIBS}) + if(LLM_EDGEFLOW_TARGET_OS STREQUAL "linux") list(APPEND _llama_imported_deps m) - endif() - if(APPLE) - find_library(ACCELERATE_FRAMEWORK Accelerate) - if(ACCELERATE_FRAMEWORK) - list(APPEND _llama_imported_deps ${ACCELERATE_FRAMEWORK}) - endif() + else() + find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED) + list(APPEND _llama_imported_deps ${ACCELERATE_FRAMEWORK}) endif() - set(_llama_sublibs "") - foreach(_sublib IN LISTS _llama_required_sublibs) - if(EXISTS "${LLAMA_3RDPARTY_DIR}/lib/lib${_sublib}.a") - if(NOT TARGET ${_sublib}) - add_library(${_sublib} STATIC IMPORTED GLOBAL) - set_target_properties(${_sublib} PROPERTIES - IMPORTED_LOCATION "${LLAMA_3RDPARTY_DIR}/lib/lib${_sublib}.a" - INTERFACE_INCLUDE_DIRECTORIES "${LLAMA_3RDPARTY_DIR}/include" - INTERFACE_LINK_LIBRARIES "${_llama_imported_deps}" - ) - endif() - list(APPEND _llama_sublibs ${_sublib}) - endif() + foreach(_archive IN LISTS _llama_archives) + add_library(${_archive} STATIC IMPORTED GLOBAL) + set_target_properties(${_archive} PROPERTIES + IMPORTED_LOCATION "${LLAMA_3RDPARTY_DIR}/lib/lib${_archive}.a" + INTERFACE_INCLUDE_DIRECTORIES "${LLAMA_3RDPARTY_DIR}/include") endforeach() - - if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - set(_llama_link_group "-Wl,--start-group" ${_llama_sublibs} "-Wl,--end-group") + # GGML 静态库相互引用;Linux 链接器需要分组解析,Whisper 也通过 ggml 获得整组依赖。 + list(REMOVE_ITEM _llama_archives llama) + if(LLM_EDGEFLOW_TARGET_OS STREQUAL "linux") + set(_llama_link_group "-Wl,--start-group" ${_llama_archives} "-Wl,--end-group") else() - set(_llama_link_group ${_llama_sublibs}) - endif() - - if(TARGET ggml) - set_target_properties(ggml PROPERTIES - INTERFACE_LINK_LIBRARIES "${_llama_link_group};${_llama_imported_deps}") - endif() - - if(NOT TARGET llama) - add_library(llama STATIC IMPORTED GLOBAL) - set_target_properties(llama PROPERTIES - IMPORTED_LOCATION "${LLAMA_3RDPARTY_DIR}/lib/libllama.a" - INTERFACE_INCLUDE_DIRECTORIES "${LLAMA_3RDPARTY_DIR}/include" - INTERFACE_LINK_LIBRARIES "${_llama_link_group};${_llama_imported_deps}" - ) + set(_llama_link_group ${_llama_archives}) endif() - - set(THIRD_PARTY_ENGINE_LIBS ${THIRD_PARTY_ENGINE_LIBS} llama) - set(LLM_EDGEFLOW_HAS_LLAMACPP ON) - set(LLAMACPP_INCLUDE_DIRS "${LLAMA_3RDPARTY_DIR}/include") - message(STATUS "[Model Execution] Prebuilt llama.cpp targets imported successfully.") + foreach(_archive IN LISTS _llama_archives) + set_target_properties(${_archive} PROPERTIES INTERFACE_LINK_LIBRARIES "${_llama_imported_deps}") + endforeach() + set_target_properties(ggml llama PROPERTIES + INTERFACE_LINK_LIBRARIES "${_llama_link_group};${_llama_imported_deps}") + set(_llama_include_dirs "${LLAMA_3RDPARTY_DIR}/include") else() - # 配置 llama.cpp 极简构建选项,强制开启 -fPIC 以便链接到 .so 中 - set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "Position independent code" FORCE) + # 配置 llama.cpp 极简构建选项;静态与 PIC 策略由 ProjectSetup 统一确定。 set(LLAMA_BUILD_TESTS OFF CACHE BOOL "Build tests" FORCE) set(LLAMA_BUILD_EXAMPLES OFF CACHE BOOL "Build examples" FORCE) set(LLAMA_BUILD_SERVER OFF CACHE BOOL "Build server" FORCE) - set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries" FORCE) - set(GGML_NATIVE OFF CACHE BOOL "Build with native CPU flags" FORCE) - set(LLAMA_NATIVE OFF CACHE BOOL "Build with native CPU flags" FORCE) - set(GGML_AVX512 OFF CACHE BOOL "Enable AVX512" FORCE) - set(GGML_AVX512_VBMI OFF CACHE BOOL "Enable AVX512_VBMI" FORCE) - set(GGML_AVX512_VNNI OFF CACHE BOOL "Enable AVX512_VNNI" FORCE) - set(GGML_AVX512_BF16 OFF CACHE BOOL "Enable AVX512_BF16" FORCE) - set(GGML_METAL ${LLM_EDGEFLOW_LLAMACPP_METAL} CACHE BOOL - "Enable Metal backend for llama.cpp" FORCE) - set(GGML_CCACHE OFF CACHE BOOL "Use ccache for ggml" FORCE) set(LLAMA_BUILD_COMMIT "${LLM_EDGEFLOW_LLAMACPP_COMMIT}" CACHE STRING "Pinned llama.cpp source commit" FORCE) @@ -148,44 +150,32 @@ if(ENABLE_LLAMACPP) ${EDGEFLOW_FETCHCONTENT_TIMESTAMP_ARGS} ) FetchContent_MakeAvailable(llama_cpp_source) + if(NOT TARGET llama) + message(FATAL_ERROR "Pinned llama.cpp source did not define target 'llama'") + endif() + set(_llama_include_dirs + "${llama_cpp_source_SOURCE_DIR}/include" + "${llama_cpp_source_SOURCE_DIR}/ggml/include") - # 将 llama.cpp 作为三方子工程引入 - if(EXISTS "${llama_cpp_source_SOURCE_DIR}/CMakeLists.txt") - if(TARGET llama) - set(THIRD_PARTY_ENGINE_LIBS ${THIRD_PARTY_ENGINE_LIBS} llama) - set(LLM_EDGEFLOW_HAS_LLAMACPP ON) - set(LLAMACPP_INCLUDE_DIRS - ${llama_cpp_source_SOURCE_DIR}/include - ${llama_cpp_source_SOURCE_DIR}/ggml/include) - message(STATUS "[Model Execution] llama.cpp target configured successfully.") - - set(_llama_archive_deps llama ggml ggml-base ggml-cpu) - set(_llama_archive_commands - COMMAND ${CMAKE_COMMAND} -E make_directory "${LLAMA_3RDPARTY_DIR}/lib" "${LLAMA_3RDPARTY_DIR}/include" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${LLAMA_3RDPARTY_DIR}/lib/" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${LLAMA_3RDPARTY_DIR}/lib/" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${LLAMA_3RDPARTY_DIR}/lib/" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${LLAMA_3RDPARTY_DIR}/lib/" - ) - if(TARGET ggml-blas) - list(APPEND _llama_archive_deps ggml-blas) - list(APPEND _llama_archive_commands - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${LLAMA_3RDPARTY_DIR}/lib/" - ) + if(LLM_EDGEFLOW_LLAMACPP_CACHEABLE) + # 上游未生成的可选库(如未找到 BLAS)不发布,缓存因缺文件而不会被导入。 + set(_llama_built_archives) + set(_llama_archive_files) + foreach(_archive IN LISTS _llama_archives) + if(TARGET ${_archive}) + list(APPEND _llama_built_archives ${_archive}) + list(APPEND _llama_archive_files "$") endif() - list(APPEND _llama_archive_commands - COMMAND ${CMAKE_COMMAND} -E copy_directory "${llama_cpp_source_SOURCE_DIR}/include" "${LLAMA_3RDPARTY_DIR}/include" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${llama_cpp_source_SOURCE_DIR}/ggml/include" "${LLAMA_3RDPARTY_DIR}/include" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_LLAMA_CACHE_MARKER}" "${LLAMA_3RDPARTY_DIR}/.edgeflow-cache-fingerprint" - ) - add_custom_target(archive_llama_cpp_to_3rdparty ALL - ${_llama_archive_commands} - DEPENDS ${_llama_archive_deps} - COMMENT "[3rdparty] Archiving llama.cpp static libraries and headers to ${LLAMA_3RDPARTY_DIR}" - ) - endif() - else() - message(WARNING "[Model Execution] llama.cpp CMakeLists.txt not found, falling back to stub.") + endforeach() + edgeflow_publish_third_party_cache(llama_cpp + CACHE_DIR "${LLAMA_3RDPARTY_DIR}" MARKER "${_LLAMA_CACHE_MARKER}" + DEPENDS ${_llama_built_archives} LIBRARIES ${_llama_archive_files} + HEADER_DIRS ${_llama_include_dirs}) endif() endif() + + set(LLM_EDGEFLOW_HAS_LLAMACPP ON) + list(APPEND LLM_EDGEFLOW_BACKEND_INCLUDE_DIRS ${_llama_include_dirs}) + list(APPEND LLM_EDGEFLOW_BACKEND_COMPILE_DEFINITIONS HAVE_LLAMACPP=1) + list(APPEND LLM_EDGEFLOW_BACKEND_LIBRARIES llama) endif() diff --git a/cmake_ext/engines/OnnxRuntime.cmake b/cmake_ext/engines/OnnxRuntime.cmake index 871f3d65..0c44768a 100644 --- a/cmake_ext/engines/OnnxRuntime.cmake +++ b/cmake_ext/engines/OnnxRuntime.cmake @@ -2,84 +2,57 @@ include(FetchContent) include(cmake_ext/third_party/ThirdPartyCacheMetadata.cmake) set(LLM_EDGEFLOW_HAS_ONNXRUNTIME OFF) - -if(ENABLE_ONNXRUNTIME) - message(STATUS "[Model Execution] Enabling ONNX Runtime Backend support...") - - set(ORT_3RDPARTY_DIR "${CMAKE_SOURCE_DIR}/3rdparty/onnxruntime") - if(APPLE) - set(ORT_LIB_NAME "libonnxruntime.dylib") - else() - set(ORT_LIB_NAME "libonnxruntime.so") - endif() - - # 先选定固定的官方包,再检查持久缓存,使缓存标记与具体平台归档对应。 - if(APPLE) - if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") - set(ORT_URL "https://github.com/microsoft/onnxruntime/releases/download/v1.17.3/onnxruntime-osx-arm64-1.17.3.tgz") - set(ORT_SHA256 "236c49c9065213b0ec9dec874e3619da3d01cbc8b984bb24291247293454d0f4") - else() - set(ORT_URL "https://github.com/microsoft/onnxruntime/releases/download/v1.17.3/onnxruntime-osx-x86_64-1.17.3.tgz") - set(ORT_SHA256 "6292ad3d2e095b54b012a9fce7361f39fbac0b75fb6e9b1d9c320874515182e8") - endif() - elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") - set(ORT_URL "https://github.com/microsoft/onnxruntime/releases/download/v1.17.3/onnxruntime-linux-aarch64-1.17.3.tgz") - set(ORT_SHA256 "9f801577bd99676d1d821022e52b1f4554f56339ae3606c7b5ff3155f443c921") - else() - set(ORT_URL "https://github.com/microsoft/onnxruntime/releases/download/v1.17.3/onnxruntime-linux-x64-1.17.3.tgz") - set(ORT_SHA256 "f2f11f9da1e3e19b22a8b378b9af57a58433f40e3db6a803e75c0ec0eba97a20") - endif() - edgeflow_prepare_third_party_cache( - NAME onnxruntime - VERSION 1.17.3 - SOURCE_SHA256 "${ORT_SHA256}" - CACHE_DIR "${ORT_3RDPARTY_DIR}" - KIND PREBUILT - OUT_VALID _ORT_CACHE_VALID - OUT_MARKER _ORT_CACHE_MARKER) - - set(_ORT_FOUND OFF) - if(_ORT_CACHE_VALID AND - EXISTS "${ORT_3RDPARTY_DIR}/lib/${ORT_LIB_NAME}" AND - EXISTS "${ORT_3RDPARTY_DIR}/include/onnxruntime_c_api.h") - set(_ORT_FOUND ON) - set(ONNXRUNTIME_INCLUDE_DIR "${ORT_3RDPARTY_DIR}/include") - set(ONNXRUNTIME_LIB "${ORT_3RDPARTY_DIR}/lib/${ORT_LIB_NAME}") - message(STATUS "[Model Execution] Using prebuilt ONNX Runtime from 3rdparty: ${ORT_3RDPARTY_DIR}") - endif() - - if(NOT _ORT_FOUND) - FetchContent_Declare( - onnxruntime_prebuilt - URL ${ORT_URL} - URL_HASH SHA256=${ORT_SHA256} - ${EDGEFLOW_FETCHCONTENT_TIMESTAMP_ARGS} - ) - FetchContent_MakeAvailable(onnxruntime_prebuilt) - - # 自动规整并同步至 3rdparty/onnxruntime - file(MAKE_DIRECTORY "${ORT_3RDPARTY_DIR}/include" "${ORT_3RDPARTY_DIR}/lib") - file(COPY "${onnxruntime_prebuilt_SOURCE_DIR}/include/" DESTINATION "${ORT_3RDPARTY_DIR}/include") - file(GLOB _ort_downloaded_libs "${onnxruntime_prebuilt_SOURCE_DIR}/lib/*") - file(COPY ${_ort_downloaded_libs} DESTINATION "${ORT_3RDPARTY_DIR}/lib") - configure_file("${_ORT_CACHE_MARKER}" - "${ORT_3RDPARTY_DIR}/.edgeflow-cache-fingerprint" - COPYONLY) - - set(ONNXRUNTIME_INCLUDE_DIR "${ORT_3RDPARTY_DIR}/include") - if(EXISTS "${ORT_3RDPARTY_DIR}/lib/${ORT_LIB_NAME}") - set(ONNXRUNTIME_LIB "${ORT_3RDPARTY_DIR}/lib/${ORT_LIB_NAME}") - elseif(EXISTS "${onnxruntime_prebuilt_SOURCE_DIR}/lib/${ORT_LIB_NAME}") - set(ONNXRUNTIME_LIB "${onnxruntime_prebuilt_SOURCE_DIR}/lib/${ORT_LIB_NAME}") - endif() - endif() - - if(EXISTS "${ONNXRUNTIME_INCLUDE_DIR}" AND EXISTS "${ONNXRUNTIME_LIB}") - message(STATUS "[Model Execution] ONNX Runtime successfully loaded from: ${ONNXRUNTIME_INCLUDE_DIR}") - set(LLM_EDGEFLOW_HAS_ONNXRUNTIME ON) - set(CMAKE_BUILD_RPATH "${CMAKE_BUILD_RPATH};${ORT_3RDPARTY_DIR}/lib") - set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_RPATH};${ORT_3RDPARTY_DIR}/lib") - else() - message(WARNING "[Model Execution] ONNX Runtime library or headers not found.") - endif() +if(NOT ENABLE_ONNXRUNTIME) + return() +endif() +message(STATUS "[Model Execution] Enabling ONNX Runtime Backend support...") + +# 先选定固定的官方包,再检查持久缓存,使缓存标记与具体平台归档对应。 +set(_ort_version 1.17.3) +if(LLM_EDGEFLOW_TARGET_PLATFORM STREQUAL "macos-arm64") + set(_ort_package osx-arm64) + set(_ort_sha256 "236c49c9065213b0ec9dec874e3619da3d01cbc8b984bb24291247293454d0f4") +elseif(LLM_EDGEFLOW_TARGET_PLATFORM STREQUAL "macos-x86_64") + set(_ort_package osx-x86_64) + set(_ort_sha256 "6292ad3d2e095b54b012a9fce7361f39fbac0b75fb6e9b1d9c320874515182e8") +elseif(LLM_EDGEFLOW_TARGET_PLATFORM STREQUAL "linux-arm64") + set(_ort_package linux-aarch64) + set(_ort_sha256 "9f801577bd99676d1d821022e52b1f4554f56339ae3606c7b5ff3155f443c921") +else() + set(_ort_package linux-x64) + set(_ort_sha256 "f2f11f9da1e3e19b22a8b378b9af57a58433f40e3db6a803e75c0ec0eba97a20") endif() +set(_ort_library "lib/libonnxruntime${CMAKE_SHARED_LIBRARY_SUFFIX}") +edgeflow_prepare_third_party_cache( + NAME onnxruntime + VERSION ${_ort_version} + SOURCE_SHA256 "${_ort_sha256}" + CACHE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/onnxruntime" + KIND PREBUILT + REQUIRED_FILES "${_ort_library}" include/onnxruntime_c_api.h + OUT_CACHE_DIR _ort_dir + OUT_VALID _ort_cache_valid + OUT_MARKER _ort_marker) + +if(_ort_cache_valid) + message(STATUS "[Model Execution] Using prebuilt ONNX Runtime from 3rdparty: ${_ort_dir}") +else() + FetchContent_Declare( + onnxruntime_prebuilt + URL "https://github.com/microsoft/onnxruntime/releases/download/v${_ort_version}/onnxruntime-${_ort_package}-${_ort_version}.tgz" + URL_HASH SHA256=${_ort_sha256} + ${EDGEFLOW_FETCHCONTENT_TIMESTAMP_ARGS} + ) + FetchContent_MakeAvailable(onnxruntime_prebuilt) + file(COPY "${onnxruntime_prebuilt_SOURCE_DIR}/include" "${onnxruntime_prebuilt_SOURCE_DIR}/lib" + DESTINATION "${_ort_dir}") + configure_file("${_ort_marker}" "${_ort_dir}/.edgeflow-cache-fingerprint" COPYONLY) +endif() + +add_library(edgeflow_onnxruntime SHARED IMPORTED GLOBAL) +set_target_properties(edgeflow_onnxruntime PROPERTIES + IMPORTED_LOCATION "${_ort_dir}/${_ort_library}") +set(LLM_EDGEFLOW_HAS_ONNXRUNTIME ON) +list(APPEND LLM_EDGEFLOW_BACKEND_INCLUDE_DIRS "${_ort_dir}/include") +list(APPEND LLM_EDGEFLOW_BACKEND_COMPILE_DEFINITIONS HAVE_ONNXRUNTIME=1) +list(APPEND LLM_EDGEFLOW_BACKEND_LIBRARIES edgeflow_onnxruntime) diff --git a/cmake_ext/engines/WhisperCpp.cmake b/cmake_ext/engines/WhisperCpp.cmake index bd73b1cc..4bb9784e 100644 --- a/cmake_ext/engines/WhisperCpp.cmake +++ b/cmake_ext/engines/WhisperCpp.cmake @@ -22,32 +22,22 @@ edgeflow_prepare_third_party_cache( SOURCE_SHA256 "${_WHISPER_SOURCE_SHA256}" CACHE_DIR "${WHISPER_3RDPARTY_DIR}" KIND STATIC - ABI_OPTIONS GGML_PROVIDER=llama_cpp BUILD_SHARED_LIBS=OFF + ABI_OPTIONS GGML_PROVIDER=llama_cpp GGML_SOURCE=${LLM_EDGEFLOW_LLAMACPP_COMMIT} + GGML_CACHE=${LLAMA_3RDPARTY_DIR} BUILD_SHARED_LIBS=OFF + REQUIRED_FILES lib/libwhisper.a include/whisper.h + OUT_CACHE_DIR WHISPER_3RDPARTY_DIR OUT_VALID _WHISPER_CACHE_VALID OUT_MARKER _WHISPER_CACHE_MARKER) -if(_WHISPER_CACHE_VALID AND - EXISTS "${WHISPER_3RDPARTY_DIR}/lib/libwhisper.a" AND - EXISTS "${WHISPER_3RDPARTY_DIR}/include/whisper.h" AND - NOT WHISPER_CPP_FORCE_REBUILD) +if(LLM_EDGEFLOW_LLAMACPP_CACHEABLE AND _WHISPER_CACHE_VALID AND NOT WHISPER_CPP_FORCE_REBUILD) message(STATUS "[Model Execution] Using prebuilt whisper.cpp from 3rdparty: ${WHISPER_3RDPARTY_DIR} (skipping compilation)") - - if(NOT TARGET whisper) - add_library(whisper STATIC IMPORTED GLOBAL) - set_target_properties(whisper PROPERTIES - IMPORTED_LOCATION "${WHISPER_3RDPARTY_DIR}/lib/libwhisper.a" - INTERFACE_INCLUDE_DIRECTORIES "${WHISPER_3RDPARTY_DIR}/include" - INTERFACE_LINK_LIBRARIES ggml - ) - endif() - - set(THIRD_PARTY_ENGINE_LIBS ${THIRD_PARTY_ENGINE_LIBS} whisper) - set(LLM_EDGEFLOW_HAS_WHISPERCPP ON) - set(WHISPERCPP_INCLUDE_DIRS "${WHISPER_3RDPARTY_DIR}/include") - message(STATUS "[Model Execution] Prebuilt whisper.cpp target imported successfully.") + add_library(whisper STATIC IMPORTED GLOBAL) + set_target_properties(whisper PROPERTIES + IMPORTED_LOCATION "${WHISPER_3RDPARTY_DIR}/lib/libwhisper.a" + INTERFACE_INCLUDE_DIRECTORIES "${WHISPER_3RDPARTY_DIR}/include" + INTERFACE_LINK_LIBRARIES ggml) + set(_whisper_include_dir "${WHISPER_3RDPARTY_DIR}/include") else() - set(CMAKE_POSITION_INDEPENDENT_CODE ON CACHE BOOL "Position independent code" FORCE) - set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared libraries" FORCE) set(WHISPER_BUILD_EXAMPLES OFF CACHE BOOL "Build examples" FORCE) set(WHISPER_BUILD_TESTS OFF CACHE BOOL "Build tests" FORCE) set(WHISPER_BUILD_SERVER OFF CACHE BOOL "Build server" FORCE) @@ -61,28 +51,26 @@ else() URL_HASH SHA256=${_WHISPER_SOURCE_SHA256} ${EDGEFLOW_FETCHCONTENT_TIMESTAMP_ARGS} ) + # MakeAvailable 在 CMake 3.28 前不支持 EXCLUDE_FROM_ALL,此处只构建被链接的 whisper。 FetchContent_GetProperties(whisper_cpp_source) if(NOT whisper_cpp_source_POPULATED) FetchContent_Populate(whisper_cpp_source) add_subdirectory(${whisper_cpp_source_SOURCE_DIR} ${whisper_cpp_source_BINARY_DIR} EXCLUDE_FROM_ALL) endif() + if(NOT TARGET whisper) + message(FATAL_ERROR "Pinned whisper.cpp source did not define target 'whisper'") + endif() + set(_whisper_include_dir "${whisper_cpp_source_SOURCE_DIR}/include") - if(TARGET whisper) - set(THIRD_PARTY_ENGINE_LIBS ${THIRD_PARTY_ENGINE_LIBS} whisper) - set(LLM_EDGEFLOW_HAS_WHISPERCPP ON) - set(WHISPERCPP_INCLUDE_DIRS - ${whisper_cpp_source_SOURCE_DIR}/include) - message(STATUS "[Model Execution] whisper.cpp target configured successfully.") - - add_custom_target(archive_whisper_cpp_to_3rdparty ALL - COMMAND ${CMAKE_COMMAND} -E make_directory "${WHISPER_3RDPARTY_DIR}/lib" "${WHISPER_3RDPARTY_DIR}/include" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${WHISPER_3RDPARTY_DIR}/lib/" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${whisper_cpp_source_SOURCE_DIR}/include" "${WHISPER_3RDPARTY_DIR}/include" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_WHISPER_CACHE_MARKER}" "${WHISPER_3RDPARTY_DIR}/.edgeflow-cache-fingerprint" - DEPENDS whisper - COMMENT "[3rdparty] Archiving whisper.cpp static library and headers to ${WHISPER_3RDPARTY_DIR}" - ) - else() - message(WARNING "[Model Execution] whisper.cpp target 'whisper' was not created.") + if(LLM_EDGEFLOW_LLAMACPP_CACHEABLE) + edgeflow_publish_third_party_cache(whisper_cpp + CACHE_DIR "${WHISPER_3RDPARTY_DIR}" MARKER "${_WHISPER_CACHE_MARKER}" + DEPENDS whisper LIBRARIES "$" + HEADER_DIRS "${_whisper_include_dir}") endif() endif() + +set(LLM_EDGEFLOW_HAS_WHISPERCPP ON) +list(APPEND LLM_EDGEFLOW_BACKEND_INCLUDE_DIRS "${_whisper_include_dir}") +list(APPEND LLM_EDGEFLOW_BACKEND_COMPILE_DEFINITIONS HAVE_WHISPERCPP=1) +list(APPEND LLM_EDGEFLOW_BACKEND_LIBRARIES whisper) diff --git a/cmake_ext/support/BuildAcceleration.cmake b/cmake_ext/support/BuildAcceleration.cmake index 17e16fc2..d9001846 100644 --- a/cmake_ext/support/BuildAcceleration.cmake +++ b/cmake_ext/support/BuildAcceleration.cmake @@ -23,7 +23,7 @@ endfunction() set(_edgeflow_selected_linker system) set(_edgeflow_linker_flag "") -if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") +if(LLM_EDGEFLOW_TARGET_OS STREQUAL "linux" AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") find_program(LLM_EDGEFLOW_MOLD_EXECUTABLE mold) find_program(LLM_EDGEFLOW_LLD_EXECUTABLE ld.lld) @@ -49,8 +49,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") elseif(NOT LLM_EDGEFLOW_LINKER STREQUAL "auto" AND NOT LLM_EDGEFLOW_LINKER STREQUAL "system") message(FATAL_ERROR - "LLM_EDGEFLOW_LINKER=${LLM_EDGEFLOW_LINKER} requires a GNU- or " - "Clang-compatible compiler driver.") + "LLM_EDGEFLOW_LINKER=${LLM_EDGEFLOW_LINKER} requires a Linux GNU/Clang toolchain; use system on macOS.") endif() if(_edgeflow_linker_flag) diff --git a/cmake_ext/support/LayerHeaderViews.cmake b/cmake_ext/support/LayerHeaderViews.cmake index a3b7ceda..167fc830 100644 --- a/cmake_ext/support/LayerHeaderViews.cmake +++ b/cmake_ext/support/LayerHeaderViews.cmake @@ -87,10 +87,7 @@ edgeflow_header_view(integration ${integration_headers} ${core_headers} function(edgeflow_generate_layer_compile_manifest) set(content "set(layer_cxx [==[${CMAKE_CXX_COMPILER}]==])\n") string(APPEND content "set(layer_has_inferlet [==[${LLM_EDGEFLOW_HAS_INFERLET}]==])\n") - set(layer_cxx_flags "") - foreach(directory IN LISTS edgeflow_cxx_system_include_dirs) - list(APPEND layer_cxx_flags -isystem "${directory}") - endforeach() + set(layer_cxx_flags "${edgeflow_cxx_platform_flags}") string(APPEND content "set(layer_cxx_flags [==[${layer_cxx_flags}]==])\n") string(APPEND content "set(layer_source_dir [==[${PROJECT_SOURCE_DIR}]==])\n") string(APPEND content "set(layer_generator [==[${CMAKE_GENERATOR}]==])\n") diff --git a/cmake_ext/templates/edgeflow_sdk.exports b/cmake_ext/templates/edgeflow_sdk.exports new file mode 100644 index 00000000..1ea3c2d1 --- /dev/null +++ b/cmake_ext/templates/edgeflow_sdk.exports @@ -0,0 +1,5 @@ +AlgBase_setLogLevelByName +AlgBase_getLogLevelByName +_ZN12llm_edgeflow12operator_api30Get_LLM_EDGEFLOW_OperatorTableEv +_ZN12llm_edgeflow12operator_api20GetOperatorLastErrorEv +_ZN12llm_edgeflow12operator_api23ResolveOperatorConfigIoEPKcS2_PNS0_18OperatorIoContractEPcm diff --git a/cmake_ext/templates/edgeflow_sdk.map.in b/cmake_ext/templates/edgeflow_sdk.map.in deleted file mode 100644 index 6196edd3..00000000 --- a/cmake_ext/templates/edgeflow_sdk.map.in +++ /dev/null @@ -1,10 +0,0 @@ -{ - global: - AlgBase_setLogLevelByName; - AlgBase_getLogLevelByName; - _ZN12llm_edgeflow12operator_api30Get_LLM_EDGEFLOW_OperatorTableEv; - _ZN12llm_edgeflow12operator_api20GetOperatorLastErrorEv; - _ZN12llm_edgeflow12operator_api23ResolveOperatorConfigIoEPKcS2_PNS0_18OperatorIoContractEPcm; - local: - *; -}; diff --git a/cmake_ext/third_party/GoogleTest.cmake b/cmake_ext/third_party/GoogleTest.cmake index 92b954a5..07caa213 100644 --- a/cmake_ext/third_party/GoogleTest.cmake +++ b/cmake_ext/third_party/GoogleTest.cmake @@ -1,49 +1,32 @@ -# cmake_ext/third_party/GoogleTest.cmake # Google Test (GTest) 智能缓存与预编译复用配置 -set(GTEST_3RDPARTY_DIR "${CMAKE_SOURCE_DIR}/3rdparty/googletest") option(GTEST_FORCE_REBUILD "Force rebuilding googletest even if cached in 3rdparty" OFF) include(cmake_ext/third_party/ThirdPartyCacheMetadata.cmake) edgeflow_prepare_third_party_cache( NAME googletest VERSION 1.14.0 SOURCE_SHA256 8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7 - CACHE_DIR "${GTEST_3RDPARTY_DIR}" + CACHE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/googletest" KIND STATIC - ABI_OPTIONS BUILD_GMOCK=OFF GTEST_FORCE_SHARED_CRT=ON + ABI_OPTIONS BUILD_GMOCK=OFF + REQUIRED_FILES lib/libgtest.a lib/libgtest_main.a include/gtest/gtest.h + OUT_CACHE_DIR GTEST_3RDPARTY_DIR OUT_VALID _GTEST_CACHE_VALID OUT_MARKER _GTEST_CACHE_MARKER) -if(_GTEST_CACHE_VALID AND - EXISTS "${GTEST_3RDPARTY_DIR}/lib/libgtest.a" AND - EXISTS "${GTEST_3RDPARTY_DIR}/lib/libgtest_main.a" AND - EXISTS "${GTEST_3RDPARTY_DIR}/include/gtest/gtest.h" AND - NOT GTEST_FORCE_REBUILD) +if(_GTEST_CACHE_VALID AND NOT GTEST_FORCE_REBUILD) message(STATUS "[3rdparty] Using cached GoogleTest from ${GTEST_3RDPARTY_DIR}") find_package(Threads REQUIRED) - - if(NOT TARGET GTest::gtest) - add_library(GTest::gtest STATIC IMPORTED GLOBAL) - set_target_properties(GTest::gtest PROPERTIES - IMPORTED_LOCATION "${GTEST_3RDPARTY_DIR}/lib/libgtest.a" - INTERFACE_INCLUDE_DIRECTORIES "${GTEST_3RDPARTY_DIR}/include" - INTERFACE_LINK_LIBRARIES "Threads::Threads" - ) - endif() - if(NOT TARGET GTest::gtest_main) - add_library(GTest::gtest_main STATIC IMPORTED GLOBAL) - set_target_properties(GTest::gtest_main PROPERTIES - IMPORTED_LOCATION "${GTEST_3RDPARTY_DIR}/lib/libgtest_main.a" - INTERFACE_INCLUDE_DIRECTORIES "${GTEST_3RDPARTY_DIR}/include" - INTERFACE_LINK_LIBRARIES "GTest::gtest" - ) - endif() - if(NOT TARGET gtest) - add_library(gtest ALIAS GTest::gtest) - endif() - if(NOT TARGET gtest_main) - add_library(gtest_main ALIAS GTest::gtest_main) - endif() + add_library(GTest::gtest STATIC IMPORTED GLOBAL) + set_target_properties(GTest::gtest PROPERTIES + IMPORTED_LOCATION "${GTEST_3RDPARTY_DIR}/lib/libgtest.a" + INTERFACE_INCLUDE_DIRECTORIES "${GTEST_3RDPARTY_DIR}/include" + INTERFACE_LINK_LIBRARIES Threads::Threads) + add_library(GTest::gtest_main STATIC IMPORTED GLOBAL) + set_target_properties(GTest::gtest_main PROPERTIES + IMPORTED_LOCATION "${GTEST_3RDPARTY_DIR}/lib/libgtest_main.a" + INTERFACE_INCLUDE_DIRECTORIES "${GTEST_3RDPARTY_DIR}/include" + INTERFACE_LINK_LIBRARIES GTest::gtest) else() include(FetchContent) FetchContent_Declare( @@ -52,24 +35,15 @@ else() URL_HASH SHA256=8ad598c73ad796e0d8280b082cebd82a630d73e73cd3c70057938a6501bba5d7 ${EDGEFLOW_FETCHCONTENT_TIMESTAMP_ARGS} ) - - # 防止覆盖父项目的编译器/运行时设置 - set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) - FetchContent_MakeAvailable(googletest) - # 自动归档构建出的 libgtest.a / libgtest_main.a 和头文件到 3rdparty/googletest - add_custom_target(archive_googletest_to_3rdparty ALL - COMMAND ${CMAKE_COMMAND} -E make_directory "${GTEST_3RDPARTY_DIR}/lib" "${GTEST_3RDPARTY_DIR}/include" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${GTEST_3RDPARTY_DIR}/lib/" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${GTEST_3RDPARTY_DIR}/lib/" - COMMAND ${CMAKE_COMMAND} -E copy_directory "${googletest_SOURCE_DIR}/googletest/include/gtest" "${GTEST_3RDPARTY_DIR}/include/gtest" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_GTEST_CACHE_MARKER}" "${GTEST_3RDPARTY_DIR}/.edgeflow-cache-fingerprint" + edgeflow_publish_third_party_cache(googletest + CACHE_DIR "${GTEST_3RDPARTY_DIR}" MARKER "${_GTEST_CACHE_MARKER}" DEPENDS gtest gtest_main - COMMENT "[3rdparty] Archiving GoogleTest static libraries to ${GTEST_3RDPARTY_DIR}" - ) + LIBRARIES "$" "$" + HEADER_DIRS "${googletest_SOURCE_DIR}/googletest/include") endif() include(GoogleTest) diff --git a/cmake_ext/third_party/NlohmannJson.cmake b/cmake_ext/third_party/NlohmannJson.cmake index 111dd84f..ffc0d007 100644 --- a/cmake_ext/third_party/NlohmannJson.cmake +++ b/cmake_ext/third_party/NlohmannJson.cmake @@ -1,4 +1,3 @@ -# cmake_ext/third_party/NlohmannJson.cmake # nlohmann/json 现代化 C++ JSON 库智能缓存配置 set(JSON_3RDPARTY_DIR "${CMAKE_SOURCE_DIR}/3rdparty/nlohmann_json") @@ -9,17 +8,15 @@ edgeflow_prepare_third_party_cache( SOURCE_SHA256 0d8ef5af7f9794e3263480193c491549b2ba6cc74bb018906202ada498a79406 CACHE_DIR "${JSON_3RDPARTY_DIR}" KIND HEADER_ONLY + REQUIRED_FILES include/nlohmann/json.hpp OUT_VALID _JSON_CACHE_VALID OUT_MARKER _JSON_CACHE_MARKER) -if(_JSON_CACHE_VALID AND - EXISTS "${JSON_3RDPARTY_DIR}/include/nlohmann/json.hpp") +if(_JSON_CACHE_VALID) message(STATUS "[3rdparty] Using cached nlohmann/json from ${JSON_3RDPARTY_DIR}") - if(NOT TARGET nlohmann_json::nlohmann_json) - add_library(nlohmann_json INTERFACE) - add_library(nlohmann_json::nlohmann_json ALIAS nlohmann_json) - target_include_directories(nlohmann_json INTERFACE "${JSON_3RDPARTY_DIR}/include") - endif() + add_library(nlohmann_json INTERFACE) + add_library(nlohmann_json::nlohmann_json ALIAS nlohmann_json) + target_include_directories(nlohmann_json INTERFACE "${JSON_3RDPARTY_DIR}/include") else() include(FetchContent) diff --git a/cmake_ext/third_party/Pcre2.cmake b/cmake_ext/third_party/Pcre2.cmake index 6013be96..da3b9f56 100644 --- a/cmake_ext/third_party/Pcre2.cmake +++ b/cmake_ext/third_party/Pcre2.cmake @@ -1,41 +1,34 @@ -# cmake_ext/third_party/Pcre2.cmake # PCRE2 智能缓存与预编译复用配置 -set(PCRE2_3RDPARTY_DIR "${CMAKE_SOURCE_DIR}/3rdparty/pcre2") option(PCRE2_FORCE_REBUILD "Force rebuilding pcre2 even if cached in 3rdparty" OFF) include(cmake_ext/third_party/ThirdPartyCacheMetadata.cmake) edgeflow_prepare_third_party_cache( NAME pcre2 VERSION 10.47 SOURCE_SHA256 c08ae2388ef333e8403e670ad70c0a11f1eed021fd88308d7e02f596fcd9dc16 - CACHE_DIR "${PCRE2_3RDPARTY_DIR}" + CACHE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/pcre2" KIND STATIC ABI_OPTIONS PCRE2_BUILD_PCRE2_8=ON PCRE2_STATIC_PIC=ON + REQUIRED_FILES lib/libpcre2-8.a include/pcre2.h + OUT_CACHE_DIR PCRE2_3RDPARTY_DIR OUT_VALID _PCRE2_CACHE_VALID OUT_MARKER _PCRE2_CACHE_MARKER) +# PCRE2 是 alg_sdk 的实现细节,静态链接时不得从公开共享库导出其 C 符号。 add_library(edgeflow_pcre2 INTERFACE) target_compile_definitions(edgeflow_pcre2 INTERFACE PCRE2_STATIC) -if(APPLE) +if(LLM_EDGEFLOW_TARGET_OS STREQUAL "macos") # 按名称隐藏整个静态归档的符号;两条路径分别提供其库目录。 target_link_libraries(edgeflow_pcre2 INTERFACE "-Wl,-hidden-lpcre2-8") -elseif(UNIX AND CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") +else() target_link_options(edgeflow_pcre2 INTERFACE "LINKER:--exclude-libs,libpcre2-8.a") endif() -if(_PCRE2_CACHE_VALID AND - EXISTS "${PCRE2_3RDPARTY_DIR}/lib/libpcre2-8.a" AND - EXISTS "${PCRE2_3RDPARTY_DIR}/include/pcre2.h" AND - NOT PCRE2_FORCE_REBUILD) +if(_PCRE2_CACHE_VALID AND NOT PCRE2_FORCE_REBUILD) message(STATUS "[3rdparty] Using cached PCRE2 from ${PCRE2_3RDPARTY_DIR}") - - target_include_directories( - edgeflow_pcre2 SYSTEM INTERFACE "${PCRE2_3RDPARTY_DIR}/include") - if(APPLE) - target_link_directories(edgeflow_pcre2 INTERFACE "${PCRE2_3RDPARTY_DIR}/lib") - else() - target_link_libraries(edgeflow_pcre2 INTERFACE "${PCRE2_3RDPARTY_DIR}/lib/libpcre2-8.a") - endif() + set(_pcre2_include_dir "${PCRE2_3RDPARTY_DIR}/include") + set(_pcre2_library_dir "${PCRE2_3RDPARTY_DIR}/lib") + set(_pcre2_library "${PCRE2_3RDPARTY_DIR}/lib/libpcre2-8.a") else() include(FetchContent) @@ -49,7 +42,6 @@ else() set(PCRE2_SHOW_REPORT OFF CACHE BOOL "Disable PCRE2 configuration report" FORCE) set(PCRE2_STATIC_PIC ON CACHE BOOL "Build position-independent PCRE2" FORCE) set(BUILD_STATIC_LIBS ON CACHE BOOL "Build static third-party libraries" FORCE) - set(BUILD_SHARED_LIBS OFF CACHE BOOL "Disable shared third-party libraries" FORCE) FetchContent_Declare( pcre2 @@ -57,31 +49,23 @@ else() URL_HASH SHA256=c08ae2388ef333e8403e670ad70c0a11f1eed021fd88308d7e02f596fcd9dc16 ${EDGEFLOW_FETCHCONTENT_TIMESTAMP_ARGS} ) - FetchContent_MakeAvailable(pcre2) + target_compile_options(pcre2-8-static PRIVATE -fvisibility=hidden) + add_dependencies(edgeflow_pcre2 pcre2-8-static) + set(_pcre2_include_dir "${pcre2_BINARY_DIR}/interface") + set(_pcre2_library_dir "$") + set(_pcre2_library pcre2-8) - # PCRE2 是 alg_sdk 的实现细节。在 ELF/Mach-O 上静态链接时, - # 不得从公开共享库导出其 C 符号。 - if(TARGET pcre2-8-static AND CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(pcre2-8-static PRIVATE -fvisibility=hidden) - endif() - - if(APPLE) - add_dependencies(edgeflow_pcre2 pcre2-8-static) - target_include_directories( - edgeflow_pcre2 SYSTEM INTERFACE "${pcre2_BINARY_DIR}/interface") - target_link_directories(edgeflow_pcre2 INTERFACE "${pcre2_BINARY_DIR}") - else() - target_link_libraries(edgeflow_pcre2 INTERFACE pcre2-8) - endif() - - # 自动归档 PCRE2 静态库与头文件到 3rdparty/pcre2 - add_custom_target(archive_pcre2_to_3rdparty ALL - COMMAND ${CMAKE_COMMAND} -E make_directory "${PCRE2_3RDPARTY_DIR}/lib" "${PCRE2_3RDPARTY_DIR}/include" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "$" "${PCRE2_3RDPARTY_DIR}/lib/" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${pcre2_BINARY_DIR}/interface/pcre2.h" "${PCRE2_3RDPARTY_DIR}/include/" - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_PCRE2_CACHE_MARKER}" "${PCRE2_3RDPARTY_DIR}/.edgeflow-cache-fingerprint" + edgeflow_publish_third_party_cache(pcre2 + CACHE_DIR "${PCRE2_3RDPARTY_DIR}" MARKER "${_PCRE2_CACHE_MARKER}" DEPENDS pcre2-8-static - COMMENT "[3rdparty] Archiving PCRE2 static library to ${PCRE2_3RDPARTY_DIR}" - ) + LIBRARIES "$" + HEADERS "${pcre2_BINARY_DIR}/interface/pcre2.h") +endif() + +target_include_directories(edgeflow_pcre2 SYSTEM INTERFACE "${_pcre2_include_dir}") +if(LLM_EDGEFLOW_TARGET_OS STREQUAL "macos") + target_link_directories(edgeflow_pcre2 INTERFACE "${_pcre2_library_dir}") +else() + target_link_libraries(edgeflow_pcre2 INTERFACE "${_pcre2_library}") endif() diff --git a/cmake_ext/third_party/StbImage.cmake b/cmake_ext/third_party/StbImage.cmake index d8d02de9..cb78f945 100644 --- a/cmake_ext/third_party/StbImage.cmake +++ b/cmake_ext/third_party/StbImage.cmake @@ -1,19 +1,9 @@ # 版本固定且经校验的图像解码器。不提交任何第三方源码。 +include(cmake_ext/third_party/ThirdPartyCacheMetadata.cmake) set(_stb_commit "013ac3beddff3dbffafd5177e7972067cd2b5083") -set(_stb_sha "594c2fe35d49488b4382dbfaec8f98366defca819d916ac95becf3e75f4200b3") set(_stb_dir "${CMAKE_SOURCE_DIR}/3rdparty/stb_image/${_stb_commit}") -file(MAKE_DIRECTORY "${_stb_dir}") -file(LOCK "${_stb_dir}/.download.lock" GUARD FILE TIMEOUT 60) -if(NOT EXISTS "${_stb_dir}/stb_image.h") - file(DOWNLOAD - "https://raw.githubusercontent.com/nothings/stb/${_stb_commit}/stb_image.h" - "${_stb_dir}/stb_image.h.download" - EXPECTED_HASH "SHA256=${_stb_sha}" TLS_VERIFY ON) - file(RENAME "${_stb_dir}/stb_image.h.download" "${_stb_dir}/stb_image.h") -endif() -file(SHA256 "${_stb_dir}/stb_image.h" _stb_actual) -if(NOT _stb_actual STREQUAL _stb_sha) - message(FATAL_ERROR "Cached stb_image SHA-256 mismatch: ${_stb_dir}/stb_image.h") -endif() +edgeflow_download_verified_file("${_stb_dir}/stb_image.h" + "594c2fe35d49488b4382dbfaec8f98366defca819d916ac95becf3e75f4200b3" + URL "https://raw.githubusercontent.com/nothings/stb/${_stb_commit}/stb_image.h") add_library(edgeflow_stb_image INTERFACE) target_include_directories(edgeflow_stb_image SYSTEM INTERFACE "${_stb_dir}") diff --git a/cmake_ext/third_party/ThirdPartyCacheMetadata.cmake b/cmake_ext/third_party/ThirdPartyCacheMetadata.cmake index 3d9eebee..b7c5a731 100644 --- a/cmake_ext/third_party/ThirdPartyCacheMetadata.cmake +++ b/cmake_ext/third_party/ThirdPartyCacheMetadata.cmake @@ -10,11 +10,11 @@ if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24") endif() # 准备可检查的缓存标记,并报告现有缓存是否由相同的固定源码 -# 和 ABI 相关构建环境生成。 +# 和 ABI 相关构建环境生成;REQUIRED_FILES 缺失时同样不可复用。 function(edgeflow_prepare_third_party_cache) set(_one_value_args - NAME VERSION SOURCE_SHA256 CACHE_DIR KIND MARKER_ROOT OUT_VALID OUT_MARKER) - set(_multi_value_args ABI_OPTIONS) + NAME VERSION SOURCE_SHA256 CACHE_DIR KIND MARKER_ROOT OUT_VALID OUT_MARKER OUT_CACHE_DIR) + set(_multi_value_args ABI_OPTIONS REQUIRED_FILES) cmake_parse_arguments(ARG "" "${_one_value_args}" "${_multi_value_args}" ${ARGN}) @@ -32,14 +32,42 @@ function(edgeflow_prepare_third_party_cache) set(_basis "schema=1\nname=${ARG_NAME}\nversion=${ARG_VERSION}\nsource_sha256=${ARG_SOURCE_SHA256}\nkind=${ARG_KIND}\n") if(ARG_KIND STREQUAL "PREBUILT" OR ARG_KIND STREQUAL "STATIC") + set(_target_arch "${CMAKE_SYSTEM_PROCESSOR}") + if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" AND CMAKE_OSX_ARCHITECTURES) + set(_target_arch "${CMAKE_OSX_ARCHITECTURES}") + endif() string(APPEND _basis - "system_name=${CMAKE_SYSTEM_NAME}\nsystem_processor=${CMAKE_SYSTEM_PROCESSOR}\npointer_size=${CMAKE_SIZEOF_VOID_P}\n") + "system_name=${CMAKE_SYSTEM_NAME}\nsystem_processor=${_target_arch}\npointer_size=${CMAKE_SIZEOF_VOID_P}\n") endif() if(ARG_KIND STREQUAL "STATIC") + string(APPEND _basis + "osx_architectures=${CMAKE_OSX_ARCHITECTURES}\nosx_sysroot=${CMAKE_OSX_SYSROOT}\n" + "osx_deployment_target=${CMAKE_OSX_DEPLOYMENT_TARGET}\nsysroot=${CMAKE_SYSROOT}\n" + "apple_sdk_path=${LLM_EDGEFLOW_APPLE_SDK_PATH}\napple_sdk_version=${LLM_EDGEFLOW_APPLE_SDK_VERSION}\n") string(APPEND _basis "c_compiler=${CMAKE_C_COMPILER_ID}-${CMAKE_C_COMPILER_VERSION}\n" "cxx_compiler=${CMAKE_CXX_COMPILER_ID}-${CMAKE_CXX_COMPILER_VERSION}\n" - "cxx_standard=${CMAKE_CXX_STANDARD}\n") + "cxx_standard=${CMAKE_CXX_STANDARD}\n" + "c_compiler_path=${CMAKE_C_COMPILER}\ncxx_compiler_path=${CMAKE_CXX_COMPILER}\n" + "c_target=${CMAKE_C_COMPILER_TARGET}\ncxx_target=${CMAKE_CXX_COMPILER_TARGET}\n" + "build_type=${CMAKE_BUILD_TYPE}\npic=${CMAKE_POSITION_INDEPENDENT_CODE}\n" + "sanitizers_enabled=${ENABLE_SANITIZERS}\nsanitizers=${LLM_EDGEFLOW_SANITIZERS}\n") + if(CMAKE_TOOLCHAIN_FILE AND EXISTS "${CMAKE_TOOLCHAIN_FILE}") + file(SHA256 "${CMAKE_TOOLCHAIN_FILE}" _toolchain_hash) + string(APPEND _basis "toolchain_sha256=${_toolchain_hash}\n") + endif() + foreach(variable CMAKE_C_FLAGS CMAKE_CXX_FLAGS CMAKE_EXE_LINKER_FLAGS + CMAKE_SHARED_LINKER_FLAGS CMAKE_STATIC_LINKER_FLAGS + CMAKE_SYSROOT_COMPILE CMAKE_SYSROOT_LINK CMAKE_C_COMPILER_EXTERNAL_TOOLCHAIN + CMAKE_CXX_COMPILER_EXTERNAL_TOOLCHAIN) + string(APPEND _basis "${variable}=${${variable}}\n") + endforeach() + if(CMAKE_BUILD_TYPE) + string(TOUPPER "${CMAKE_BUILD_TYPE}" config) + foreach(prefix CMAKE_C_FLAGS CMAKE_CXX_FLAGS CMAKE_EXE_LINKER_FLAGS CMAKE_SHARED_LINKER_FLAGS CMAKE_STATIC_LINKER_FLAGS) + string(APPEND _basis "${prefix}_${config}=${${prefix}_${config}}\n") + endforeach() + endif() endif() if(ARG_ABI_OPTIONS) list(SORT ARG_ABI_OPTIONS) @@ -51,6 +79,16 @@ function(edgeflow_prepare_third_party_cache) string(SHA256 _fingerprint "${_basis}") set(_metadata "${_basis}fingerprint=${_fingerprint}\n") + # 原始下载包可以共享;构建出的库按完整身份分目录,平台/配置切换不会覆盖。 + set(_cache_dir "${ARG_CACHE_DIR}") + if(ARG_OUT_CACHE_DIR AND NOT ARG_KIND STREQUAL "HEADER_ONLY") + set(_cache_platform "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}") + if(LLM_EDGEFLOW_TARGET_PLATFORM) + set(_cache_platform "${LLM_EDGEFLOW_TARGET_PLATFORM}") + endif() + set(_cache_dir "${ARG_CACHE_DIR}/${_cache_platform}/${_fingerprint}") + endif() + if(ARG_MARKER_ROOT) set(_marker_dir "${ARG_MARKER_ROOT}") else() @@ -60,7 +98,7 @@ function(edgeflow_prepare_third_party_cache) set(_expected_marker "${_marker_dir}/${ARG_NAME}.txt") file(WRITE "${_expected_marker}" "${_metadata}") - set(_cache_marker "${ARG_CACHE_DIR}/.edgeflow-cache-fingerprint") + set(_cache_marker "${_cache_dir}/.edgeflow-cache-fingerprint") set(_valid OFF) if(EXISTS "${_cache_marker}") file(READ "${_cache_marker}" _cached_metadata) @@ -68,13 +106,111 @@ function(edgeflow_prepare_third_party_cache) set(_valid ON) else() message(STATUS - "[3rdparty] Ignoring incompatible ${ARG_NAME} cache: ${ARG_CACHE_DIR}") + "[3rdparty] Ignoring incompatible ${ARG_NAME} cache: ${_cache_dir}") endif() - elseif(EXISTS "${ARG_CACHE_DIR}") + elseif(EXISTS "${_cache_dir}") message(STATUS - "[3rdparty] Ignoring unverified ${ARG_NAME} cache: ${ARG_CACHE_DIR}") + "[3rdparty] Ignoring unverified ${ARG_NAME} cache: ${_cache_dir}") endif() + # 指纹只证明身份,归档仍须完整才能导入。 + foreach(_required IN LISTS ARG_REQUIRED_FILES) + if(_valid AND NOT EXISTS "${_cache_dir}/${_required}") + message(STATUS + "[3rdparty] Ignoring incomplete ${ARG_NAME} cache: missing ${_required}") + set(_valid OFF) + endif() + endforeach() set(${ARG_OUT_VALID} "${_valid}" PARENT_SCOPE) set(${ARG_OUT_MARKER} "${_expected_marker}" PARENT_SCOPE) + if(ARG_OUT_CACHE_DIR) + set(${ARG_OUT_CACHE_DIR} "${_cache_dir}" PARENT_SCOPE) + endif() +endfunction() + +# 源码构建完成后把库和头文件发布到缓存身份目录;指纹最后写入, +# 中断的发布不会成为可导入缓存。 +function(edgeflow_publish_third_party_cache name) + cmake_parse_arguments(ARG "" "CACHE_DIR;MARKER" "DEPENDS;LIBRARIES;HEADERS;HEADER_DIRS" + ${ARGN}) + set(_commands + COMMAND "${CMAKE_COMMAND}" -E make_directory "${ARG_CACHE_DIR}/lib" "${ARG_CACHE_DIR}/include") + foreach(_library IN LISTS ARG_LIBRARIES) + list(APPEND _commands + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_library}" "${ARG_CACHE_DIR}/lib/") + endforeach() + foreach(_header IN LISTS ARG_HEADERS) + list(APPEND _commands + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_header}" "${ARG_CACHE_DIR}/include/") + endforeach() + foreach(_directory IN LISTS ARG_HEADER_DIRS) + list(APPEND _commands + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${_directory}" "${ARG_CACHE_DIR}/include") + endforeach() + list(APPEND _commands + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${ARG_MARKER}" "${ARG_CACHE_DIR}/.edgeflow-cache-fingerprint") + add_custom_target(archive_${name}_to_3rdparty ALL ${_commands} + DEPENDS ${ARG_DEPENDS} + COMMENT "[3rdparty] Archiving ${name} to ${ARG_CACHE_DIR}" + VERBATIM) +endfunction() + +# 先下载到暂存文件并校验 SHA-256 再改名,共享缓存只出现完整文件; +# 已有文件每次配置都重新校验。URL 直接下载,COMMAND 用于需要认证的下载工具。 +function(edgeflow_download_verified_file path sha256) + cmake_parse_arguments(ARG "" "URL;DOWNLOADED;FAILURE_MESSAGE" "COMMAND" ${ARGN}) + get_filename_component(_directory "${path}" DIRECTORY) + file(MAKE_DIRECTORY "${_directory}") + file(LOCK "${_directory}/.download.lock" GUARD FUNCTION TIMEOUT 60) + if(NOT EXISTS "${path}") + message(STATUS "[3rdparty] Downloading ${path}") + set(_downloaded "${path}.download") + if(ARG_URL) + file(DOWNLOAD "${ARG_URL}" "${_downloaded}" TLS_VERIFY ON STATUS _status TIMEOUT 120) + list(GET _status 0 _result) + else() + if(ARG_DOWNLOADED) + set(_downloaded "${ARG_DOWNLOADED}") + endif() + execute_process(COMMAND ${ARG_COMMAND} RESULT_VARIABLE _result OUTPUT_QUIET ERROR_QUIET) + set(_status "${_result}") + endif() + if(NOT _result EQUAL 0) + if(ARG_FAILURE_MESSAGE) + message(FATAL_ERROR "${ARG_FAILURE_MESSAGE}") + endif() + message(FATAL_ERROR "Cannot download ${path}: ${_status}") + endif() + file(SHA256 "${_downloaded}" _actual) + if(NOT _actual STREQUAL sha256) + message(FATAL_ERROR "Downloaded SHA-256 mismatch: ${path}") + endif() + file(RENAME "${_downloaded}" "${path}") + endif() + file(SHA256 "${path}" _actual) + if(NOT _actual STREQUAL sha256) + message(FATAL_ERROR "Cached SHA-256 mismatch. Remove ${path} and configure again.") + endif() +endfunction() + +# 每次配置把已校验归档重新解压到当前构建,避免残留文件和旧环境的绝对路径; +# 只接受解压目录内与固定版本完全一致的 CMake 包及其静态库。 +function(edgeflow_import_release_package package version archive extract_dir package_root library) + file(REMOVE_RECURSE "${extract_dir}") + file(MAKE_DIRECTORY "${extract_dir}") + execute_process(COMMAND "${CMAKE_COMMAND}" -E tar xzf "${archive}" + WORKING_DIRECTORY "${extract_dir}" RESULT_VARIABLE _result) + if(NOT _result EQUAL 0) + message(FATAL_ERROR "Cannot extract verified ${package} release: ${archive}") + endif() + set(_root "${extract_dir}/${package_root}") + unset(${package}_DIR CACHE) + find_package(${package} ${version} EXACT CONFIG REQUIRED + PATHS "${_root}/lib/cmake/${package}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) + get_target_property(_library ${package}::${package} IMPORTED_LOCATION_RELEASE) + if(NOT _library STREQUAL "${_root}/lib/${library}") + message(FATAL_ERROR "${package} must resolve to the verified release archive") + endif() + message(STATUS "[3rdparty] Verified ${package} ${version}: ${_library}") endfunction() diff --git a/cmake_ext/toolchains/linux-arm64.cmake b/cmake_ext/toolchains/linux-arm64.cmake new file mode 100644 index 00000000..964c4281 --- /dev/null +++ b/cmake_ext/toolchains/linux-arm64.cmake @@ -0,0 +1,10 @@ +# 可公开复用的 GNU/Linux arm64 工具链;设备 SDK 可提供自己的 toolchain。 +# 用 -DCMAKE_SYSROOT=/path/to/target-sysroot 选择目标根文件系统。 +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR aarch64) +set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc CACHE FILEPATH "Target C compiler") +set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++ CACHE FILEPATH "Target C++ compiler") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt index e1054b2c..09aedb47 100644 --- a/demo/CMakeLists.txt +++ b/demo/CMakeLists.txt @@ -8,9 +8,8 @@ file(GLOB_RECURSE EDGEFLOW_DEMO_SHELL_SOURCES CONFIGURE_DEPENDS list(APPEND EDGEFLOW_DEMO_SOURCES ${EDGEFLOW_DEMO_SHELL_SOURCES}) add_library(edgeflow_demo_objects OBJECT ${EDGEFLOW_DEMO_SOURCES}) target_link_libraries(edgeflow_demo_objects PRIVATE - edgeflow_internal_headers llm_edgeflow::internal_runtime edgeflow_stb_image) -target_include_directories(edgeflow_demo_objects PRIVATE - "${PROJECT_BINARY_DIR}/generated/internal") + edgeflow_internal_headers llm_edgeflow::internal_runtime edgeflow_stb_image + edgeflow_demo_profile_fields) add_executable(alg_demo main.cpp diff --git a/doc/VERIFIABLE_SELECTION.md b/doc/VERIFIABLE_SELECTION.md index 4a0462a5..ecbc41a9 100644 --- a/doc/VERIFIABLE_SELECTION.md +++ b/doc/VERIFIABLE_SELECTION.md @@ -90,8 +90,10 @@ schema 1。编译使用 `cmake --build <目录>`,不使用 3.20 才支持的 b | `kite-cpu` | Kite | Kite 文本、视觉及生成向量能力 | | `inferlet-cpu` | Inferlet | Linux CPU 上的文本、视觉及生成向量验证 | | `minimal` | 无推理 Backend | 规则、模板及纯数据处理方案 | +| `macos-arm64-cpu` | ONNX Runtime、llama.cpp、whisper.cpp | Apple Silicon(含 M3)CPU 构建与原生测试 | +| `linux-arm64-cross` | 无推理 Backend,可显式添加 | GNU/Linux arm64 SDK 编译,不在宿主执行目标程序 | -普通 `cmake -S . -B build` 默认只启用 Inferlet;所有 `ENABLE_*` Backend 开关及互斥检查 +普通 `cmake -S . -B build` 在 Linux 默认只启用 Inferlet,在 macOS 默认只启用 llama.cpp CPU;所有 `ENABLE_*` Backend 开关及互斥检查 集中在 [BackendOptions.cmake](../cmake_ext/BackendOptions.cmake)。其他引擎仅在显式启用后加载依赖。 例如额外启用 ONNX Runtime,或切换为独立 llama.cpp: @@ -126,6 +128,56 @@ sanitizer 的 `--fast` 复用 `minimal`,`--full` / `--ci-runtime` 复用 `dev- CMake 缓存包含绝对路径。如果更换构建目录,在新目录重新配置。 保留已校验的 `3rdparty/` 缓存可减少重复下载。 +### 跨平台构建 + +[`PlatformOptions.cmake`](../cmake_ext/PlatformOptions.cmake) 在依赖获取前统一校验目标。 +配置覆盖 Linux 与 macOS 的 x86_64/arm64,使用单配置生成器,每个构建目录只对应一种架构;实际支持证据 +以对应平台 CI 和设备执行结果为准。Windows、Universal Binary 和 Metal GPU 不在本次范围内。 + +M3 使用原生 arm64 终端,安装 Xcode Command Line Tools、CMake、Ninja、Python 3 后执行: + +```bash +cmake --preset=macos-arm64-cpu +cmake --build build/variants/macos-arm64-cpu --parallel 3 +(cd build/variants/macos-arm64-cpu && ctest --output-on-failure --no-tests=error) +``` + +该预设在 `project()` 前选定 arm64、macosx SDK 和 macOS 14.0 最低部署版本,启用 +ONNX Runtime、llama.cpp、Whisper CPU。Mac 默认使用 Apple Accelerate、关闭 OpenMP 和 Metal; +Inferlet/Kite 的现有包仅用于 Linux。`GGML_CPU_ARM_ARCH` 默认留空,使用目标编译器默认指令集, +设备预设可显式覆盖;不使用宿主 `-march=native`。 +三方缓存按目标平台、工具链和编译参数隔离,Linux arm64 与 macOS arm64 不共享库产物。 +最低部署版本可用 `-DCMAKE_OSX_DEPLOYMENT_TARGET=<版本>` 覆盖,仍须满足所选依赖要求。 + +完整开发门禁使用同一入口(还需 clang-format 18、Python `jsonschema`、Java 和下载程序): + +```bash +LLM_EDGEFLOW_BUILD_PRESET=macos-arm64-cpu ./scripts/run_all_tests.sh +``` + +该变量也用于 sanitizer 的 `--full` / `--ci-runtime`;`--fast` 仍使用 `minimal`。 +macOS CI 执行 CPU 门禁、dylib 导出检查和真实 GGUF Operator 推理;托管 Apple Silicon +runner 的结果不代替 M3 的性能或 Metal 验收。 + +Linux arm64 交叉编译由外部 GNU 工具链提供编译器及可选 sysroot: + +```bash +cmake --preset=linux-arm64-cross +# 有设备 sysroot 时,配置命令附加 -DCMAKE_SYSROOT=/path/to/target-rootfs +cmake --build build/variants/linux-arm64-cross --target alg_sdk --parallel 4 +``` + +[`linux-arm64.cmake`](../cmake_ext/toolchains/linux-arm64.cmake) 选择 `aarch64-linux-gnu-gcc/g++`, +宿主程序按宿主路径查找,目标头/库/包使用 CMake 的 root-only 搜索规则。 +该隔离需要配置目标 `CMAKE_SYSROOT` 或 `CMAKE_FIND_ROOT_PATH`;两者为空时, +仅设置 `CMAKE_FIND_ROOT_PATH_MODE_*=ONLY` 不会阻止搜索宿主前缀。 +设备 SDK 可用 `-DCMAKE_TOOLCHAIN_FILE=...` 替换工具链并提供目标搜索根。 +构建 SDK、Demo 或 CLI 直接选择已有的 `alg_sdk`、`alg_demo`、`alg_pipeline_tool` 目标。 +交叉预设关闭 `BUILD_TESTING`,目标程序在目标环境验证,不能用交叉编译替代原生测试门禁。 +CI 检查中性 SDK 和 Inferlet SDK 的交叉编译及 ELF 架构,编译成功不代表设备推理通过。 + +无推理 Backend 的本机构建仍使用 `minimal`: + ```bash cmake --preset=minimal cmake --build build/variants/minimal --target alg_pipeline_tool diff --git a/doc/dev_guide/source_layout.md b/doc/dev_guide/source_layout.md index a43d45fe..d0aa892d 100644 --- a/doc/dev_guide/source_layout.md +++ b/doc/dev_guide/source_layout.md @@ -38,21 +38,33 @@ | 模块 | 职责 | | :--- | :--- | -| `BackendOptions.cmake` | Backend 开关与互斥检查;默认 Inferlet,其他引擎显式启用,在依赖获取前校验 | -| `ProjectSetup.cmake` | 编译标准、警告、PIC、sanitizer、内部生成头与导出符号脚本,Operator 壳选择,以及 public/extension/internal 头目标 | +| `PlatformOptions.cmake` | 解析目标 OS/架构,校验 preset 和交叉编译执行边界;在依赖获取之前拒绝不支持的组合 | +| `BackendOptions.cmake` | Backend 开关、平台限制与互斥检查;Linux 默认 Inferlet,macOS 默认 llama.cpp CPU | +| `ProjectSetup.cmake` | 集中编译选项、第三方静态依赖策略与 SDK 导出规则,选择 Operator 壳,创建分层头视图及 public/extension/internal 头目标 | | `Dependencies.cmake` | 聚合项目依赖;每个依赖独立维护 | | `RuntimeGraph.cmake` | 各层依赖与 OBJECT 目标、Backend 能力接线,加载 `src/` | -| `ProductTargets.cmake` | SDK 产物与版本脚本、输出路径、Demo、CLI 和测试入口 | +| `ProductTargets.cmake` | SDK 产物、输出路径、Demo Profile 字段头、Demo、CLI 和测试入口 | -`cmake_ext/` 根目录只保留上述五个入口,其余文件按职责归类: +`cmake_ext/` 根目录只保留上述六个入口,其余文件按职责归类: | 子目录 | 内容 | | :--- | :--- | | `engines/` | 推理运行时的依赖接入模块 | -| `third_party/` | JSON、PCRE2、stb_image、GoogleTest 等通用依赖,以及引擎和通用依赖共用的缓存元数据工具 | -| `support/` | 构建加速、Operator 壳选择、分层头文件视图,以及与视图模块一起维护的 Node 契约清单 | +| `third_party/` | JSON、PCRE2、stb_image、GoogleTest 等通用依赖,以及引擎和通用依赖共用的校验下载、缓存身份与归档发布工具 | +| `support/` | 构建加速、Operator 壳选择和分层头文件视图 | +| `toolchains/` | 在 `project()` 之前生效的编译器、sysroot 和搜索规则;当前提供中性的 GNU/Linux arm64 示例 | | `templates/` | SDK 导出符号脚本、Demo Profile 字段头等生成文件的输入模板 | +平台配置顺序是 **Preset/Toolchain → `project()` → 平台校验 → Backend 选择 → 编译策略 → +依赖 → 组件目标**。Preset 选择场景,Toolchain 定义目标环境;`PlatformOptions.cmake` 只读取 +CMake 的目标信息,不根据宿主 CPU 猜测目标包。Apple 的显式 `CMAKE_OSX_ARCHITECTURES` +优先于 `CMAKE_SYSTEM_PROCESSOR`,每个目录只构建一种架构。组件 `src/**/CMakeLists.txt` +继续负责源码归属和依赖,新增平台应优先修改入口策略,避免向组件编译规则散布平台判断。 + +ELF 和 Mach-O 的导出限制从同一份 +[`edgeflow_sdk.exports`](../../cmake_ext/templates/edgeflow_sdk.exports) 生成;导出检查保留独立的公开 ABI 预期。 +具体平台入口及验收范围见[跨平台构建](../VERIFIABLE_SELECTION.md#跨平台构建)。 + SDK 产物基名统一由 `LLM_EDGEFLOW_SDK_OUTPUT_NAME` 指定,默认 `llm_edgeflow_sdk`。 迁移到宿主构建时可传入 `-DLLM_EDGEFLOW_SDK_OUTPUT_NAME=host_algorithm`;填写基名, 不包含目录或平台的 `lib` 前缀、`.so` / `.dylib` / `.dll` 后缀。 @@ -60,8 +72,11 @@ CMake 链接方继续使用 `llm_edgeflow::sdk`,脚本和测试通过目标路 修改名称后应重新构建并更新宿主的加载路径;旧构建目录中遗留的旧名库不会自动删除。 推理引擎的依赖接入统一放在 `engines/`,包括 ONNX Runtime、llama.cpp、Whisper、Inferlet 和 Kite。 -目录按运行时职责划分,不按作者、仓库公开性或接口来源划分;各模块维护包获取、固定版本、 -校验及编译链接探针,缓存指纹由 `third_party/ThirdPartyCacheMetadata.cmake` 共用。 +目录按运行时职责划分,不按作者、仓库公开性或接口来源划分;各模块只维护固定版本、上游选项 +和编译链接探针。校验下载、发布包导入、缓存身份与归档发布由 +`third_party/ThirdPartyCacheMetadata.cmake` 共用。引擎模块把 Backend 的 SYSTEM 头目录、编译定义和 +链接目标分别追加到 `LLM_EDGEFLOW_BACKEND_INCLUDE_DIRS`、`LLM_EDGEFLOW_BACKEND_COMPILE_DEFINITIONS`、 +`LLM_EDGEFLOW_BACKEND_LIBRARIES`,由 `RuntimeGraph.cmake` 统一接入 Backend 实现;新增 Backend 不修改它。 Whisper 复用 llama.cpp 的 GGML,因此依赖聚合中先加载 llama.cpp。 测试依赖 GoogleTest 在测试入口按需加载。根文件负责装配。 diff --git a/include/contracts/parameters.h b/include/contracts/parameters.h index b0a7d24c..98370502 100644 --- a/include/contracts/parameters.h +++ b/include/contracts/parameters.h @@ -290,14 +290,17 @@ template nlohmann::json SerializeParameterValue( const T& value, const std::shared_ptr>& item_spec) { if constexpr (ParameterContainer::kArray || ParameterContainer::kMap) { + using Value = typename ParameterContainer::Value; auto result = ParameterContainer::kArray ? nlohmann::json::array() : nlohmann::json::object(); if constexpr (ParameterContainer::kArray) { + // libc++ 的 vector const iterator + // 也可能返回代理,按声明的元素类型序列化。 for (const auto& item : value) - result.push_back(SerializeParameterValue(item, item_spec)); + result.push_back(SerializeParameterValue(item, item_spec)); } else { for (const auto& [key, item] : value) - result[key] = SerializeParameterValue(item, item_spec); + result[key] = SerializeParameterValue(item, item_spec); } return result; } else if constexpr (FieldTypeTraits::kKind == ConfigValueKind::kObject) { diff --git a/scripts/check_sdk_exports.sh b/scripts/check_sdk_exports.sh index 60f8c81f..5d41cd1e 100755 --- a/scripts/check_sdk_exports.sh +++ b/scripts/check_sdk_exports.sh @@ -1,17 +1,20 @@ #!/usr/bin/env bash set -euo pipefail -if [[ $# -ne 1 || ! -f "$1" ]]; then - echo "Usage: $0 " >&2 +if [[ $# -ne 3 || ! -f "$1" ]]; then + echo "Usage: $0 " >&2 exit 2 fi SDK_LIBRARY="$1" -if ! command -v nm >/dev/null 2>&1; then +FORMAT="$2" +NM="$3" +if ! command -v "$NM" >/dev/null 2>&1; then echo "nm is required to validate the SDK export surface" >&2 exit 2 fi +# 独立的公开 ABI 基线,不能从生成链接限制的清单读取预期。 EXPECTED_SYMBOLS=$( printf '%s\n' \ AlgBase_getLogLevelByName \ @@ -22,19 +25,22 @@ EXPECTED_SYMBOLS=$( sort -u ) -ACTUAL_SYMBOLS=$( - nm -D --defined-only "${SDK_LIBRARY}" | - awk '$2 != "A" {print $3}' | - sed 's/@.*//' | - sed '/^$/d' | - sort -u -) +case "$FORMAT" in + ELF) + ACTUAL_SYMBOLS=$("$NM" -D --defined-only "${SDK_LIBRARY}" | + awk '$2 != "A" {print $3}' | sed 's/@.*//' | sed '/^$/d' | sort -u) + ;; + MachO) + ACTUAL_SYMBOLS=$("$NM" -gjU "${SDK_LIBRARY}" | sed 's/^_//' | sed '/^$/d' | sort -u) + ;; + *) echo "Unsupported binary format: $FORMAT" >&2; exit 2 ;; +esac if [[ "${ACTUAL_SYMBOLS}" != "${EXPECTED_SYMBOLS}" ]]; then - echo "SDK export surface differs from the 5-symbol allowlist:" >&2 + echo "SDK export surface differs from the supported Operator ABI:" >&2 diff -u <(printf '%s\n' "${EXPECTED_SYMBOLS}") \ <(printf '%s\n' "${ACTUAL_SYMBOLS}") >&2 || true exit 1 fi -echo "SDK export surface is restricted to 5 supported symbols." +echo "SDK export surface matches the supported Operator ABI." diff --git a/scripts/generate_acceptance_evidence.sh b/scripts/generate_acceptance_evidence.sh index 3b9fc330..9dbaf03f 100755 --- a/scripts/generate_acceptance_evidence.sh +++ b/scripts/generate_acceptance_evidence.sh @@ -73,6 +73,11 @@ evidence["gates"]["whisper_asr_backend_and_real_profile"] = os.environ.get( if "KITELLM_GATE_RESULT" in os.environ: evidence["gates"]["kitellm_private_release_and_real_gguf"] = os.environ[ "KITELLM_GATE_RESULT"] +for variable, name in ( + ("MACOS_ARM64_GATE_RESULT", "macos_arm64_cpu_sdk_and_runtime"), + ("LINUX_ARM64_CROSS_GATE_RESULT", "linux_arm64_cross_compilation")): + if variable in os.environ: + evidence["gates"][name] = os.environ[variable] with open(os.environ["OUTPUT_JSON"], "w", encoding="utf-8") as stream: json.dump(evidence, stream, ensure_ascii=False, indent=2, sort_keys=True) stream.write("\n") diff --git a/scripts/run_all_tests.sh b/scripts/run_all_tests.sh index a8b77bab..bfcae248 100755 --- a/scripts/run_all_tests.sh +++ b/scripts/run_all_tests.sh @@ -13,6 +13,7 @@ if [[ $# -ne 0 ]]; then exit 2 fi BUILD_DIR="$ROOT_DIR/build" +PRESET="${LLM_EDGEFLOW_BUILD_PRESET:-dev-gate}" SELECTED_LINKER="${LLM_EDGEFLOW_LINKER:-auto}" JOBS="${LLM_EDGEFLOW_JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)}" @@ -26,7 +27,7 @@ NC='\033[0m' echo -e "${BOLD}${CYAN}==================================================================${NC}" echo -e "${BOLD}${CYAN} LLM-EdgeFlow Unified Quality Gate ${NC}" -echo -e "${BOLD}${CYAN} Build: $BUILD_DIR | Linker: $SELECTED_LINKER${NC}" +echo -e "${BOLD}${CYAN} Build: $BUILD_DIR | Preset: $PRESET | Linker: $SELECTED_LINKER${NC}" echo -e "${BOLD}${CYAN}==================================================================${NC}\n" echo -e "${BOLD}[ Step 1/6: Shell syntax, Google C++ format and Git diff gates ]${NC}" @@ -38,8 +39,9 @@ git -C "$ROOT_DIR" diff --check echo -e "${GREEN}✓ Static source gates passed.${NC}\n" echo -e "${BOLD}[ Step 2/6: Configure and build ]${NC}" -"$SCRIPT_DIR/configure_build.sh" "$ROOT_DIR" "$BUILD_DIR" dev-gate \ - -DLLM_EDGEFLOW_LINKER="$SELECTED_LINKER" +"$SCRIPT_DIR/configure_build.sh" "$ROOT_DIR" "$BUILD_DIR" "$PRESET" \ + -DLLM_EDGEFLOW_LINKER="$SELECTED_LINKER" \ + -DBUILD_TESTING=ON cmake --build "$BUILD_DIR" -j"$JOBS" echo -e "${GREEN}✓ Build completed.${NC}\n" diff --git a/scripts/run_sanitizers.sh b/scripts/run_sanitizers.sh index 3d9839a0..8f182fd5 100755 --- a/scripts/run_sanitizers.sh +++ b/scripts/run_sanitizers.sh @@ -69,7 +69,7 @@ if [[ -d "${PROJECT_ROOT}/build/_deps/googletest-src" ]]; then -DFETCHCONTENT_SOURCE_DIR_GOOGLETEST="${PROJECT_ROOT}/build/_deps/googletest-src" ) fi -PRESET=dev-gate +PRESET="${LLM_EDGEFLOW_BUILD_PRESET:-dev-gate}" if [[ "${MODE}" == "fast" ]]; then PRESET=minimal fi @@ -133,8 +133,7 @@ else fi fi -export LD_LIBRARY_PATH="${PROJECT_ROOT}/3rdparty/onnxruntime/lib:${BUILD_DIR}:${BUILD_DIR}/_deps/onnxruntime_prebuilt-src/lib:${LD_LIBRARY_PATH:-}" -export DYLD_LIBRARY_PATH="${PROJECT_ROOT}/3rdparty/onnxruntime/lib:${BUILD_DIR}:${BUILD_DIR}/_deps/onnxruntime_prebuilt-src/lib:${DYLD_LIBRARY_PATH:-}" +# 目标链接携带实际选中依赖的 build RPATH,无需猜测缓存布局。 export LLM_EDGEFLOW_PIPELINE_TOOL="${BUILD_DIR}/alg_pipeline_tool" export LLM_EDGEFLOW_DEMO_BINARY="${BUILD_DIR}/alg_demo" diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index a8ee0ce7..b135d832 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -14,6 +14,4 @@ target_link_libraries(alg_show PRIVATE add_executable(alg_pipeline_tool alg_pipeline_tool.cpp) target_link_libraries(alg_pipeline_tool PRIVATE - edgeflow_pipeline_tooling) -target_include_directories(alg_pipeline_tool PRIVATE - "${PROJECT_BINARY_DIR}/generated/internal") + edgeflow_pipeline_tooling edgeflow_demo_profile_fields) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 09220dd2..ce378591 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,26 +12,22 @@ add_library(edgeflow_test_allocation_failure OBJECT target_include_directories(edgeflow_test_allocation_failure PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/support") -if(UNIX AND NOT APPLE) - add_test( - NAME SdkExportSurfaceTest - COMMAND ${PROJECT_SOURCE_DIR}/scripts/check_sdk_exports.sh - $) - set_tests_properties(SdkExportSurfaceTest PROPERTIES - WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" - LABELS "tier2;static-gate;dev-fast;sanitizer-compatible") -endif() +add_test( + NAME SdkExportSurfaceTest + COMMAND ${PROJECT_SOURCE_DIR}/scripts/check_sdk_exports.sh + $ ${LLM_EDGEFLOW_BINARY_FORMAT} ${CMAKE_NM}) +set_tests_properties(SdkExportSurfaceTest PROPERTIES + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + LABELS "tier2;static-gate;dev-fast;sanitizer-compatible") # 校验一致性测试使用确定性注册,不污染生产 CLI 或共享 SDK。 add_executable(alg_pipeline_tool_test ${PROJECT_SOURCE_DIR}/src/cli/alg_pipeline_tool.cpp $) target_link_libraries(alg_pipeline_tool_test PRIVATE llm_edgeflow::internal_runtime - edgeflow_pipeline_tooling) + edgeflow_pipeline_tooling edgeflow_demo_profile_fields) target_compile_definitions(alg_pipeline_tool_test PRIVATE LLM_EDGEFLOW_TOOL_HAS_TEST_REGISTRATIONS=1) -target_include_directories(alg_pipeline_tool_test PRIVATE - "${PROJECT_BINARY_DIR}/generated/internal") # GNU 兼容的链接包装仅限于现有的模型测试 runner。 function(edgeflow_enable_aligned_allocation_failure target_name) @@ -44,6 +40,10 @@ endfunction() find_package(Python3 COMPONENTS Interpreter REQUIRED) +set(_edgeflow_guard_platform_flags "") +foreach(flag IN LISTS edgeflow_cxx_platform_flags) + list(APPEND _edgeflow_guard_platform_flags "--cxx-flag=${flag}") +endforeach() set(_edgeflow_guard_sanitizers "") if(ENABLE_SANITIZERS) list(APPEND _edgeflow_guard_sanitizers --sanitizers "${LLM_EDGEFLOW_SANITIZERS}") @@ -52,6 +52,7 @@ add_test(NAME QualityGateScriptsContractTest COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/tests/contract/architecture/test_quality_gate_contract.py --cxx ${CMAKE_CXX_COMPILER} + ${_edgeflow_guard_platform_flags} ${_edgeflow_guard_sanitizers} --gtest-include $ --gtest-library $ diff --git a/tests/README.md b/tests/README.md index b52f7cda..5b406881 100644 --- a/tests/README.md +++ b/tests/README.md @@ -79,6 +79,11 @@ ELF 的 `posix_memalign` 故障包装仅在 Tensor 测试链接时启用。 默认开发门禁使用 `dev-gate`(Inferlet)。ONNX Runtime、llama.cpp 和 Whisper 由显式 `full-cpu` 构建覆盖;构建选择见[构建变体](../doc/VERIFIABLE_SELECTION.md#构建变体)。 + +macOS Apple Silicon 使用 `LLM_EDGEFLOW_BUILD_PRESET=macos-arm64-cpu ./scripts/run_all_tests.sh`。 +同一门禁覆盖 Mach-O 导出清单。平台选择和缓存身份的契约测试 +可以在宿主用模拟 CMake 目标事实执行,但不代表 macOS 编译成功;实际编译和运行由对应平台 CI 验证。 +交叉编译只产生目标产物,目标上的原生测试独立执行。 没有所需权重时,可选物理生成用例明确跳过。`EDGEFLOW_TEST_LLAMA_GGUF` 和 `EDGEFLOW_TEST_WHISPER_MODEL` 可提供相应 Backend 资产。`EDGEFLOW_TEST_BPE_TOKENIZER_DIR` 指向含 `.eftok` 文件的目录(见[配置说明](../configs/README.md#分词器资产onnx-llm))时, BPE 分词器测试额外对照 Hugging Face 的样例 ID。可选 E2E 用 `ENABLE_REAL_MODEL_TESTS=ON` 开启,并使用仓库固定权重和 PCM。 diff --git a/tests/contract/architecture/test_cmake_structure.cmake b/tests/contract/architecture/test_cmake_structure.cmake index 1a6d246e..b5518039 100644 --- a/tests/contract/architecture/test_cmake_structure.cmake +++ b/tests/contract/architecture/test_cmake_structure.cmake @@ -18,6 +18,7 @@ endforeach() # 检查模块装配顺序,不绑定目标定义的行号或具体格式。 string(REGEX MATCHALL "cmake_ext/[A-Za-z0-9_./]+\\.cmake" modules "${root}") set(expected_modules + cmake_ext/PlatformOptions.cmake cmake_ext/BackendOptions.cmake cmake_ext/ProjectSetup.cmake cmake_ext/Dependencies.cmake @@ -34,6 +35,11 @@ foreach(module IN LISTS modules) if(content MATCHES "DH_[A-Za-z_]+|\\$\\{(platform|sdk)\\}|cmake/(utils|common)\\.cmake") message(FATAL_ERROR "Project modules must not infer internal host build context: ${module}") endif() + # 静态依赖策略只在 ProjectSetup 确定,不能随前一个依赖是否命中缓存而变化。 + if(NOT module MATCHES "/ProjectSetup\\.cmake$" AND + content MATCHES "set[ \t\n]*\\([ \t\n]*BUILD_SHARED_LIBS[ \t\n)]") + message(FATAL_ERROR "Only ProjectSetup.cmake may set BUILD_SHARED_LIBS: ${module}") + endif() endforeach() # 包括源码子目录中的项目模块引用,重命名后不能遗留失效 include。 diff --git a/tests/contract/architecture/test_llama_cache.cmake b/tests/contract/architecture/test_llama_cache.cmake index 62eabb0e..43e68b72 100644 --- a/tests/contract/architecture/test_llama_cache.cmake +++ b/tests/contract/architecture/test_llama_cache.cmake @@ -4,7 +4,7 @@ if(NOT PROJECT_SOURCE_DIR OR NOT TEST_ROOT) endif() set(fixture "${TEST_ROOT}/llama") file(MAKE_DIRECTORY "${fixture}/cmake_ext/engines" "${fixture}/neutral/include" "${fixture}/neutral/ggml/include") -foreach(module third_party/ThirdPartyCacheMetadata engines/LlamaCpp) +foreach(module PlatformOptions third_party/ThirdPartyCacheMetadata engines/LlamaCpp engines/WhisperCpp) configure_file("${PROJECT_SOURCE_DIR}/cmake_ext/${module}.cmake" "${fixture}/cmake_ext/${module}.cmake" COPYONLY) endforeach() @@ -13,27 +13,74 @@ file(WRITE "${fixture}/neutral/include/llama.h" "int neutral_llama();\n") file(WRITE "${fixture}/neutral/ggml/include/ggml.h" "int neutral_ggml();\n") file(WRITE "${fixture}/neutral/llama.cpp" "int neutral_ggml(); int neutral_llama(){return neutral_ggml();}\n") file(WRITE "${fixture}/neutral/ggml.cpp" "int neutral_cpu(); int neutral_base(); int neutral_ggml(){return neutral_cpu()+neutral_base();}\n") -file(WRITE "${fixture}/neutral/cpu.cpp" "int neutral_cpu(){return 2;}\n") +file(WRITE "${fixture}/neutral/cpu.cpp" "int neutral_cpu(){return 2;} int neutral_variant(){return NEUTRAL_VARIANT + NEUTRAL_AVX2;}\n") file(WRITE "${fixture}/neutral/base.cpp" "int neutral_base(){return 3;}\n") file(WRITE "${fixture}/neutral/CMakeLists.txt" [=[ cmake_minimum_required(VERSION 3.19) project(NeutralCache LANGUAGES CXX) +option(GGML_CUDA "Neutral upstream GPU option" OFF) +# Older upstream aliases translate only after the parent cache decision. +if(LLAMA_CUDA) + set(GGML_CUDA ON CACHE BOOL "Neutral upstream GPU option" FORCE) +endif() +option(GGML_AVX2 "Neutral upstream CPU option" ON) add_library(llama STATIC llama.cpp) add_library(ggml STATIC ggml.cpp) add_library(ggml-cpu STATIC cpu.cpp) add_library(ggml-base STATIC base.cpp) +target_compile_definitions(ggml-cpu PRIVATE NEUTRAL_VARIANT=$ NEUTRAL_AVX2=$) target_link_libraries(llama PUBLIC ggml) target_link_libraries(ggml PUBLIC ggml-cpu ggml-base) ]=]) +file(MAKE_DIRECTORY "${fixture}/neutral-whisper/include") +file(WRITE "${fixture}/neutral-whisper/include/whisper.h" "int neutral_whisper();\n") +file(WRITE "${fixture}/neutral-whisper/whisper.cpp" + "int neutral_ggml(); int neutral_whisper(){return neutral_ggml();} int whisper_variant(){return NEUTRAL_VARIANT;}\n") +file(WRITE "${fixture}/neutral-whisper/CMakeLists.txt" [=[ +cmake_minimum_required(VERSION 3.19) +project(NeutralWhisper LANGUAGES CXX) +add_library(whisper STATIC whisper.cpp) +target_link_libraries(whisper PUBLIC ggml) +target_compile_definitions(whisper PRIVATE NEUTRAL_VARIANT=$) +]=]) file(WRITE "${fixture}/consumer.cpp" "#include \nint main(){return neutral_llama()==5?0:1;}\n") file(WRITE "${fixture}/CMakeLists.txt" [=[ cmake_minimum_required(VERSION 3.19) project(CacheConsumer LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) +set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE) +if(TEST_INTERNAL_GGML) + set(GGML_DETECTED_TEST ON CACHE INTERNAL "Neutral upstream detection result") +endif() +if(TEST_NORMAL_GGML) + set(GGML_AVX2 OFF) +endif() set(ENABLE_LLAMACPP ON CACHE BOOL "" FORCE) +set(ENABLE_WHISPERCPP ON CACHE BOOL "" FORCE) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(FETCHCONTENT_SOURCE_DIR_WHISPER_CPP_SOURCE "${CMAKE_SOURCE_DIR}/neutral-whisper" CACHE PATH "" FORCE) set(GGML_BLAS OFF CACHE BOOL "" FORCE) set(FETCHCONTENT_SOURCE_DIR_LLAMA_CPP_SOURCE "${CMAKE_SOURCE_DIR}/neutral" CACHE PATH "" FORCE) +include(cmake_ext/PlatformOptions.cmake) include(cmake_ext/engines/LlamaCpp.cmake) +include(cmake_ext/engines/WhisperCpp.cmake) +file(WRITE "${CMAKE_BINARY_DIR}/whisper-cache-path.txt" "${WHISPER_3RDPARTY_DIR}") +if(NOT DEFINED GGML_CPU_ARM_ARCH OR (EXPECT_DEFAULT_ARM AND NOT GGML_CPU_ARM_ARCH STREQUAL "")) + message(FATAL_ERROR "Default ARM architecture must leave upstream selection empty") +endif() +if(EXPECT_UNCACHEABLE AND (TARGET archive_llama_cpp_to_3rdparty OR TARGET archive_whisper_cpp_to_3rdparty)) + message(FATAL_ERROR "Uncacheable configuration must not publish shared archives") +endif() +if(EXPECT_UNCACHEABLE AND LLM_EDGEFLOW_LLAMACPP_CACHEABLE) + message(FATAL_ERROR "Untracked GGML options must disable shared archive reads and writes") +endif() +get_target_property(whisper_imported whisper IMPORTED) +if(EXPECT_IMPORTED AND NOT whisper_imported) + message(FATAL_ERROR "Expected verified Whisper cache import") +elseif(EXPECT_UNCACHEABLE AND whisper_imported) + message(FATAL_ERROR "Source GGML policy must also select source Whisper") +endif() +file(WRITE "${CMAKE_BINARY_DIR}/cache-path.txt" "${LLAMA_3RDPARTY_DIR}") if(NOT LLM_EDGEFLOW_HAS_LLAMACPP) message(FATAL_ERROR "Neutral source must configure the llama cache path") endif() @@ -44,8 +91,8 @@ elseif(EXPECT_SOURCE AND imported) message(FATAL_ERROR "Incomplete/incompatible cache must select source") endif() add_executable(consumer consumer.cpp) -target_include_directories(consumer PRIVATE ${LLAMACPP_INCLUDE_DIRS}) -target_link_libraries(consumer PRIVATE llama) +target_include_directories(consumer PRIVATE ${LLM_EDGEFLOW_BACKEND_INCLUDE_DIRS}) +target_link_libraries(consumer PRIVATE llama whisper) ]=]) function(run_checked) execute_process(COMMAND ${ARGN} RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error) @@ -53,10 +100,10 @@ function(run_checked) message(FATAL_ERROR "Cache consumer failed (${result}): ${ARGN}\n${output}\n${error}") endif() endfunction() -run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/source-build" -DEXPECT_SOURCE=ON) +run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/source-build" -DEXPECT_SOURCE=ON -DEXPECT_DEFAULT_ARM=ON) run_checked("${CMAKE_COMMAND}" --build "${fixture}/source-build" --parallel 2) run_checked("${fixture}/source-build/consumer") -set(cache "${fixture}/3rdparty/llama_cpp") +file(READ "${fixture}/source-build/cache-path.txt" cache) foreach(archive llama ggml ggml-cpu ggml-base) if(NOT EXISTS "${cache}/lib/lib${archive}.a") message(FATAL_ERROR "Actual neutral build did not publish ${archive} cache archive") @@ -71,9 +118,71 @@ if(NOT metadata MATCHES "version=70adb1b4cea5ee39f867792c78dc59320921eda7" OR NOT metadata MATCHES "BUILD_SHARED_LIBS=OFF") message(FATAL_ERROR "Published llama cache must identify the current source pin and ABI") endif() +file(READ "${fixture}/source-build/whisper-cache-path.txt" whisper_cache) +set(shared_archives "${whisper_cache}/lib/libwhisper.a") +foreach(archive llama ggml ggml-cpu ggml-base) + list(APPEND shared_archives "${cache}/lib/lib${archive}.a") +endforeach() +set(original_hashes) +foreach(archive IN LISTS shared_archives) + file(SHA256 "${archive}" digest) + list(APPEND original_hashes "${digest}") +endforeach() +function(assert_original_archives) + set(current_hashes) + foreach(archive IN LISTS shared_archives) + file(SHA256 "${archive}" digest) + list(APPEND current_hashes "${digest}") + endforeach() + if(NOT current_hashes STREQUAL original_hashes) + message(FATAL_ERROR "Uncacheable configuration polluted default llama/Whisper archives") + endif() +endfunction() +# Upstream options first appear during add_subdirectory. Later configurations +# retain the same identity and source targets, without publishing shared archives. +foreach(iteration RANGE 2 3) + run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/source-build" + -DEXPECT_SOURCE=ON -DEXPECT_UNCACHEABLE=ON) + file(READ "${fixture}/source-build/cache-path.txt" repeated_cache) + file(READ "${fixture}/source-build/whisper-cache-path.txt" repeated_whisper_cache) + if(NOT repeated_cache STREQUAL cache OR NOT repeated_whisper_cache STREQUAL whisper_cache) + message(FATAL_ERROR "Upstream option discovery changed the default cache path") + endif() + run_checked("${CMAKE_COMMAND}" --build "${fixture}/source-build" --parallel 2) + assert_original_archives() +endforeach() +foreach(setting GGML_CUDA GGML_AVX2 LLAMA_CUDA) + set(value ON) + if(setting STREQUAL "GGML_AVX2") + set(value OFF) + endif() + run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/${setting}-build" + -DEXPECT_SOURCE=ON -DEXPECT_UNCACHEABLE=ON "-D${setting}=${value}") + run_checked("${CMAKE_COMMAND}" --build "${fixture}/${setting}-build" --parallel 2) + assert_original_archives() +endforeach() +run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/normal-variable-build" + -DEXPECT_SOURCE=ON -DEXPECT_UNCACHEABLE=ON -DTEST_NORMAL_GGML=ON) +run_checked("${CMAKE_COMMAND}" --build "${fixture}/normal-variable-build" --parallel 2) +assert_original_archives() run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/import-build" -DEXPECT_IMPORTED=ON) run_checked("${CMAKE_COMMAND}" --build "${fixture}/import-build" --parallel 2) run_checked("${fixture}/import-build/consumer") +# A caller-selected CPU feature policy must not import the previous archives. +run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/cpu-policy-build" + -DEXPECT_SOURCE=ON -DGGML_CPU_ARM_ARCH=armv8.2-a) +file(READ "${fixture}/cpu-policy-build/cache-path.txt" cpu_policy_cache) +if(cpu_policy_cache STREQUAL cache) + message(FATAL_ERROR "Changed GGML_CPU_ARM_ARCH must use a distinct physical cache") +endif() +foreach(archive llama ggml ggml-cpu ggml-base) + if(NOT EXISTS "${cache}/lib/lib${archive}.a") + message(FATAL_ERROR "CPU policy change removed the original ${archive} cache archive") + endif() +endforeach() +run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/original-policy-build" -DEXPECT_IMPORTED=ON) +run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/internal-detection-build" + -DEXPECT_IMPORTED=ON -DTEST_INTERNAL_GGML=ON) # Metadata alone cannot turn an incomplete archive set into a usable import. file(REMOVE "${cache}/lib/libggml-cpu.a") run_checked("${CMAKE_COMMAND}" -S "${fixture}" -B "${fixture}/missing-build" -DEXPECT_SOURCE=ON) diff --git a/tests/contract/architecture/test_quality_gate_contract.py b/tests/contract/architecture/test_quality_gate_contract.py index 60c51c01..41dae1ad 100644 --- a/tests/contract/architecture/test_quality_gate_contract.py +++ b/tests/contract/architecture/test_quality_gate_contract.py @@ -22,13 +22,21 @@ def setUp(self): self.root = Path(temporary.name) shutil.copy2(ROOT / 'CMakePresets.json', self.root) shutil.copy2(ROOT / 'cmake_ext/BackendOptions.cmake', self.root) + shutil.copy2(ROOT / 'cmake_ext/PlatformOptions.cmake', self.root) (self.root / 'CMakeLists.txt').write_text( 'cmake_minimum_required(VERSION 3.19)\nproject(BackendSelection NONE)\n' - 'include(BackendOptions.cmake)\n') + 'set(CMAKE_SYSTEM_NAME ${TEST_SYSTEM})\n' + 'set(CMAKE_SYSTEM_PROCESSOR ${TEST_PROCESSOR})\n' + 'set(CMAKE_HOST_SYSTEM_NAME ${TEST_HOST_SYSTEM})\n' + 'set(CMAKE_HOST_SYSTEM_PROCESSOR ${TEST_HOST_PROCESSOR})\n' + 'if(TEST_CROSS)\nset(CMAKE_CROSSCOMPILING TRUE)\nendif()\n' + 'include(PlatformOptions.cmake)\ninclude(BackendOptions.cmake)\n') def configure(self, *arguments, expected=None): result = subprocess.run( - ['cmake', '-S', str(self.root), '-B', str(self.root / 'build'), *arguments], + ['cmake', '-S', str(self.root), '-B', str(self.root / 'build'), + '-DTEST_SYSTEM=Linux', '-DTEST_PROCESSOR=x86_64', + '-DTEST_HOST_SYSTEM=Linux', '-DTEST_HOST_PROCESSOR=x86_64', *arguments], cwd=self.root, capture_output=True, text=True, timeout=30) if expected is None: self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr) @@ -57,9 +65,50 @@ def test_presets_reset_cached_engines_and_match_selection_metadata(self): selection = preset.get('vendor', {}).get('llm-edgeflow/selection') if selection is not None: with self.subTest(preset=preset['name']): + shutil.rmtree(self.root / 'build', ignore_errors=True) self.configure('--preset=' + preset['name'], + *(['-DTEST_SYSTEM=Darwin', '-DTEST_HOST_SYSTEM=Darwin', + '-DTEST_HOST_PROCESSOR=arm64'] + if preset['name'] == 'macos-arm64-cpu' else []), expected={'ENABLE_' + backends[name] for name in selection['backends']}) + def test_target_platform_validation_and_apple_architecture_override(self): + self.configure('-DTEST_SYSTEM=Darwin', '-DCMAKE_OSX_ARCHITECTURES=arm64', + '-DLLM_EDGEFLOW_EXPECTED_PLATFORM=macos-arm64', expected={'ENABLE_LLAMACPP'}) + output = self.configure('-DTEST_SYSTEM=Darwin', '-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64') + self.assertIn('one macOS architecture', output) + output = self.configure('-DTEST_SYSTEM=Darwin', '-DCMAKE_OSX_ARCHITECTURES=arm64', + '-DENABLE_INFERLET=ON', '-DENABLE_LLAMACPP=OFF') + self.assertIn('support Linux', output) + + def test_apple_host_architecture_mismatch_disables_native_tests(self): + for host_arch, target_arch in (('x86_64', 'arm64'), ('arm64', 'x86_64')): + with self.subTest(host=host_arch, target=target_arch): + shutil.rmtree(self.root / 'build', ignore_errors=True) + arguments = ['-DTEST_SYSTEM=Darwin', '-DTEST_HOST_SYSTEM=Darwin', + '-DTEST_HOST_PROCESSOR=' + host_arch, + '-DCMAKE_OSX_ARCHITECTURES=' + target_arch] + self.configure(*arguments, expected={'ENABLE_LLAMACPP'}) + cache = (self.root / 'build/CMakeCache.txt').read_text() + self.assertIn('BUILD_TESTING:BOOL=OFF', cache) + output = self.configure(*arguments, '-DBUILD_TESTING=ON') + self.assertIn('BUILD_TESTING=OFF', output) + shutil.rmtree(self.root / 'build', ignore_errors=True) + self.configure('-DTEST_SYSTEM=Darwin', '-DTEST_HOST_SYSTEM=Darwin', + '-DTEST_HOST_PROCESSOR=aarch64', '-DCMAKE_OSX_ARCHITECTURES=arm64', + '-DBUILD_TESTING=ON', expected={'ENABLE_LLAMACPP'}) + + def test_unsupported_and_mismatched_platforms_fail_early(self): + for arguments, diagnostic in ( + (['-DCMAKE_CONFIGURATION_TYPES=Debug;Release'], 'single-config'), + (['-DTEST_SYSTEM=Windows'], 'Unsupported target system'), + (['-DTEST_PROCESSOR=riscv64'], 'Unsupported target architecture'), + (['-DLLM_EDGEFLOW_EXPECTED_PLATFORM=macos-arm64'], 'Preset requires'), + (['-DTEST_CROSS=ON', '-DBUILD_TESTING=ON'], 'Cross builds compile target artifacts only')): + with self.subTest(arguments=arguments): + shutil.rmtree(self.root / 'build', ignore_errors=True) + self.assertIn(diagnostic, self.configure(*arguments)) + def test_conflicts_fail_before_dependency_acquisition(self): for first, second in [('INFERLET', 'KITELLM'), ('INFERLET', 'LLAMACPP'), ('INFERLET', 'WHISPERCPP'), ('KITELLM', 'LLAMACPP'), @@ -98,10 +147,10 @@ def setUp(self): target.write_text(stub) target.chmod(0o755) - def gate(self, failure=''): + def gate(self, failure='', preset='dev-gate'): return subprocess.run(['bash', str(self.root / 'scripts/run_all_tests.sh')], cwd=self.root, env={**os.environ, 'PATH': str(self.root / 'bin') + os.pathsep + os.environ['PATH'], - 'CALL_TRACE': str(self.trace), 'FAIL_AT': failure, 'LLM_EDGEFLOW_JOBS': '2'}, + 'CALL_TRACE': str(self.trace), 'FAIL_AT': failure, 'LLM_EDGEFLOW_BUILD_PRESET': preset, 'LLM_EDGEFLOW_JOBS': '2'}, capture_output=True, text=True, timeout=20) def test_canonical_gate_selects_one_build_and_all_ctest_cases(self): @@ -116,6 +165,13 @@ def test_canonical_gate_selects_one_build_and_all_ctest_cases(self): self.assertNotIn(' -R ', calls[4]) self.assertIn('All required development gates passed', result.stdout) + def test_selected_preset_is_forwarded_without_disabling_gate_coverage(self): + result = self.gate(preset='macos-arm64-cpu') + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + configure = self.trace.read_text().splitlines()[2] + self.assertIn('macos-arm64-cpu', configure) + self.assertIn('-DBUILD_TESTING=ON', configure) + def test_each_failed_stage_stops_later_commands_and_success_reporting(self): for stage, final_command in (('format', 'format.sh'), ('git', 'git'), ('configure', 'configure_build.sh'), ('build', 'cmake'), ('ctest', 'ctest')): @@ -132,6 +188,41 @@ def test_shell_syntax_failure_stops_before_any_build_transport(self): self.assertNotEqual(result.returncode, 0) self.assertFalse(self.trace.exists()) + def test_sdk_export_guard_rejects_expanded_or_replaced_public_symbols(self): + self.assertIsNotNone(HARNESS, 'CTest must supply the compiler') + symbols = [ + 'AlgBase_setLogLevelByName', 'AlgBase_getLogLevelByName', + '_ZN12llm_edgeflow12operator_api30Get_LLM_EDGEFLOW_OperatorTableEv', + '_ZN12llm_edgeflow12operator_api20GetOperatorLastErrorEv', + '_ZN12llm_edgeflow12operator_api23ResolveOperatorConfigIoEPKcS2_PNS0_18OperatorIoContractEPcm', + ] + script = self.root / 'scripts/check_sdk_exports.sh' + shutil.copy2(ROOT / 'scripts/check_sdk_exports.sh', script) + manifest = self.root / 'cmake_ext/templates/edgeflow_sdk.exports' + manifest.parent.mkdir(parents=True) + binary_format = 'MachO' if sys.platform == 'darwin' else 'ELF' + nm = shutil.which('nm') + self.assertIsNotNone(nm) + for name, exports, expected in ( + ('supported', symbols, 0), + ('expanded', symbols + ['UnexpectedSdkExport'], 1), + ('replaced', symbols[:-1] + ['UnexpectedSdkExport'], 1)): + with self.subTest(case=name): + # Also change the linker manifest: the independent public ABI + # guard must still reject a coordinated manifest/library drift. + manifest.write_text('\n'.join(exports) + '\n') + source = self.root / (name + '.cpp') + source.write_text('\n'.join('extern "C" void ' + symbol + '() {}' for symbol in exports)) + library = self.root / (name + ('.dylib' if sys.platform == 'darwin' else '.so')) + compiled = subprocess.run( + [HARNESS.cxx, *HARNESS.cxx_flag, '-dynamiclib' if sys.platform == 'darwin' else '-shared', + '-fPIC', str(source), '-o', str(library)], + capture_output=True, text=True, timeout=60) + self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr) + checked = subprocess.run(['bash', str(script), str(library), binary_format, nm], + capture_output=True, text=True, timeout=30) + self.assertEqual(checked.returncode, expected, checked.stdout + checked.stderr) + def test_real_gtest_main_rejects_failures_required_skips_and_zero_selected_cases(self): self.assertIsNotNone(HARNESS, 'CTest must supply the real compiler/GTest/main arguments') source = self.root / 'guard.cpp' @@ -161,7 +252,7 @@ class OptionalTeardownFailure : public ::testing::Test { } ''') executable = self.root / 'guard' - command = [HARNESS.cxx, '-std=c++17', '-pthread'] + command = [HARNESS.cxx, *HARNESS.cxx_flag, '-std=c++17', '-pthread'] if HARNESS.sanitizers: command += [f'-fsanitize={HARNESS.sanitizers}', '-fno-omit-frame-pointer'] for directory in HARNESS.gtest_include: @@ -226,6 +317,7 @@ class OptionalTeardownFailure : public ::testing::Test { if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--cxx', required=True) + parser.add_argument('--cxx-flag', action='append', default=[]) parser.add_argument('--sanitizers', default='') parser.add_argument('--gtest-include', action='append', required=True) parser.add_argument('--gtest-library', type=Path, required=True) diff --git a/tests/contract/architecture/test_third_party_cache_metadata.cmake b/tests/contract/architecture/test_third_party_cache_metadata.cmake index 21bf1e99..b965eb95 100644 --- a/tests/contract/architecture/test_third_party_cache_metadata.cmake +++ b/tests/contract/architecture/test_third_party_cache_metadata.cmake @@ -45,6 +45,57 @@ probe(STATIC a=1 z=3) if(valid) message(FATAL_ERROR "Changed ABI option reused stale cache") endif() +# Identity directories retain the original cache when any ABI input changes. +function(identity_probe) + edgeflow_prepare_third_party_cache( + NAME identity VERSION pin SOURCE_SHA256 abc CACHE_DIR "${TEST_ROOT}/identities" + KIND STATIC MARKER_ROOT "${TEST_ROOT}/markers" + OUT_VALID identity_valid OUT_MARKER identity_marker OUT_CACHE_DIR identity_dir) + set(identity_valid "${identity_valid}" PARENT_SCOPE) + set(identity_marker "${identity_marker}" PARENT_SCOPE) + set(identity_dir "${identity_dir}" PARENT_SCOPE) +endfunction() +set(CMAKE_BUILD_TYPE Release) +identity_probe() +set(original_dir "${identity_dir}") +file(MAKE_DIRECTORY "${original_dir}") +configure_file("${identity_marker}" "${original_dir}/.edgeflow-cache-fingerprint" COPYONLY) +foreach(variable CMAKE_C_COMPILER CMAKE_CXX_COMPILER CMAKE_CXX_FLAGS CMAKE_C_FLAGS_RELEASE CMAKE_EXE_LINKER_FLAGS + CMAKE_SHARED_LINKER_FLAGS CMAKE_STATIC_LINKER_FLAGS CMAKE_SYSROOT + CMAKE_SYSROOT_COMPILE CMAKE_SYSROOT_LINK CMAKE_OSX_ARCHITECTURES + CMAKE_OSX_SYSROOT LLM_EDGEFLOW_APPLE_SDK_PATH LLM_EDGEFLOW_APPLE_SDK_VERSION + CMAKE_OSX_DEPLOYMENT_TARGET CMAKE_CXX_COMPILER_TARGET + CMAKE_BUILD_TYPE ENABLE_SANITIZERS LLM_EDGEFLOW_SANITIZERS) + set(saved "${${variable}}") + set(${variable} "changed") + identity_probe() + if(identity_valid OR identity_dir STREQUAL original_dir) + message(FATAL_ERROR "${variable} must select a distinct untrusted cache") + endif() + set(${variable} "${saved}") + identity_probe() + if(NOT identity_valid OR NOT identity_dir STREQUAL original_dir) + message(FATAL_ERROR "Changing ${variable} must preserve the original reusable cache") + endif() +endforeach() +set(CMAKE_TOOLCHAIN_FILE "${TEST_ROOT}/toolchain.cmake") +file(WRITE "${CMAKE_TOOLCHAIN_FILE}" "# original toolchain\n") +identity_probe() +set(toolchain_dir "${identity_dir}") +file(WRITE "${CMAKE_TOOLCHAIN_FILE}" "# changed toolchain at the same path\n") +identity_probe() +if(identity_dir STREQUAL toolchain_dir) + message(FATAL_ERROR "Editing a toolchain in place must change cache identity") +endif() +unset(CMAKE_TOOLCHAIN_FILE) +set(CMAKE_C_FLAGS_DEBUG "unused debug flags") +set(CMAKE_CXX_FLAGS_MINSIZEREL "unused size flags") +identity_probe() +if(NOT identity_valid OR NOT identity_dir STREQUAL original_dir) + message(FATAL_ERROR "Unselected configuration flags must not split the static cache") +endif() +unset(CMAKE_C_FLAGS_DEBUG) +unset(CMAKE_CXX_FLAGS_MINSIZEREL) probe(HEADER_ONLY) configure_file("${marker}" "${TEST_ROOT}/cache/.edgeflow-cache-fingerprint" COPYONLY) set(CMAKE_SYSTEM_PROCESSOR arm64) @@ -55,11 +106,94 @@ if(NOT valid) endif() probe(PREBUILT) configure_file("${marker}" "${TEST_ROOT}/cache/.edgeflow-cache-fingerprint" COPYONLY) +foreach(variable CMAKE_OSX_SYSROOT LLM_EDGEFLOW_APPLE_SDK_PATH LLM_EDGEFLOW_APPLE_SDK_VERSION + CMAKE_SYSROOT CMAKE_SYSROOT_COMPILE CMAKE_SYSROOT_LINK CMAKE_OSX_DEPLOYMENT_TARGET + CMAKE_C_COMPILER CMAKE_CXX_COMPILER CMAKE_CXX_COMPILER_VERSION) + set(saved "${${variable}}") + set(${variable} "changed locally") + probe(PREBUILT) + if(NOT valid) + message(FATAL_ERROR "Pinned prebuilt package must not depend on local ${variable}") + endif() + set(${variable} "${saved}") +endforeach() set(CMAKE_SYSTEM_PROCESSOR x86_64) probe(PREBUILT) if(valid) message(FATAL_ERROR "Prebuilt cache must depend on platform ABI") endif() +set(CMAKE_SYSTEM_NAME Darwin) +set(CMAKE_SYSTEM_PROCESSOR x86_64) +set(CMAKE_OSX_ARCHITECTURES arm64) +probe(PREBUILT) +configure_file("${marker}" "${TEST_ROOT}/cache/.edgeflow-cache-fingerprint" COPYONLY) +set(CMAKE_SYSTEM_PROCESSOR arm64) +probe(PREBUILT) +if(NOT valid) + message(FATAL_ERROR "Explicit macOS target architecture must override the host processor") +endif() +set(CMAKE_OSX_ARCHITECTURES x86_64) +probe(PREBUILT) +if(valid) + message(FATAL_ERROR "Changing the actual macOS target architecture must invalidate prebuilt cache") +endif() +# A matching marker cannot make an incomplete cache importable. +function(required_probe) + edgeflow_prepare_third_party_cache( + NAME required VERSION pin SOURCE_SHA256 abc CACHE_DIR "${TEST_ROOT}/required" + KIND HEADER_ONLY MARKER_ROOT "${TEST_ROOT}/markers" REQUIRED_FILES include/required.h + OUT_VALID required_valid OUT_MARKER required_marker) + set(required_valid "${required_valid}" PARENT_SCOPE) + set(required_marker "${required_marker}" PARENT_SCOPE) +endfunction() +required_probe() +configure_file("${required_marker}" "${TEST_ROOT}/required/.edgeflow-cache-fingerprint" COPYONLY) +required_probe() +if(required_valid) + message(FATAL_ERROR "Cache missing a required file must not be reused") +endif() +file(WRITE "${TEST_ROOT}/required/include/required.h" "") +required_probe() +if(NOT required_valid) + message(FATAL_ERROR "Complete cache with a matching marker must be reusable") +endif() + +# Verified downloads publish only complete files and recheck cached bytes. +file(WRITE "${TEST_ROOT}/source/payload.txt" "verified payload\n") +file(SHA256 "${TEST_ROOT}/source/payload.txt" payload_sha) +set(url_download "${TEST_ROOT}/downloads/url/payload.txt") +edgeflow_download_verified_file("${url_download}" "${payload_sha}" + URL "file://${TEST_ROOT}/source/payload.txt") +set(command_download "${TEST_ROOT}/downloads/command/payload.txt") +edgeflow_download_verified_file("${command_download}" "${payload_sha}" + COMMAND "${CMAKE_COMMAND}" -E copy "${TEST_ROOT}/source/payload.txt" + "${TEST_ROOT}/downloads/command/staged.txt" + DOWNLOADED "${TEST_ROOT}/downloads/command/staged.txt") +foreach(download IN ITEMS "${url_download}" "${command_download}") + if(NOT EXISTS "${download}") + message(FATAL_ERROR "Verified download was not published: ${download}") + endif() +endforeach() +function(expect_download_failure name script diagnostic) + file(WRITE "${TEST_ROOT}/${name}.cmake" + "include(\"${PROJECT_SOURCE_DIR}/cmake_ext/third_party/ThirdPartyCacheMetadata.cmake\")\n${script}\n") + execute_process(COMMAND "${CMAKE_COMMAND}" -P "${TEST_ROOT}/${name}.cmake" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error) + if(result EQUAL 0 OR NOT "${output}${error}" MATCHES "${diagnostic}") + message(FATAL_ERROR "${name} must fail with '${diagnostic}':\n${output}${error}") + endif() +endfunction() +expect_download_failure(wrong-hash + "edgeflow_download_verified_file(\"${TEST_ROOT}/downloads/wrong/payload.txt\" 0000 URL \"file://${TEST_ROOT}/source/payload.txt\")" + "Downloaded SHA-256 mismatch") +if(EXISTS "${TEST_ROOT}/downloads/wrong/payload.txt") + message(FATAL_ERROR "Unverified download must not become a cached file") +endif() +file(WRITE "${url_download}" "tampered\n") +expect_download_failure(tampered-cache + "edgeflow_download_verified_file(\"${url_download}\" ${payload_sha} URL \"file://${TEST_ROOT}/source/payload.txt\")" + "Cached SHA-256 mismatch") + file(WRITE "${TEST_ROOT}/invalid.cmake" "include(\"${PROJECT_SOURCE_DIR}/cmake_ext/third_party/ThirdPartyCacheMetadata.cmake\")\nedgeflow_prepare_third_party_cache(NAME invalid KIND UNKNOWN)\n") execute_process(COMMAND "${CMAKE_COMMAND}" -P "${TEST_ROOT}/invalid.cmake" diff --git a/tests/integration/operator/operator_shell/mock/test_business_responses.cpp b/tests/integration/operator/operator_shell/mock/test_business_responses.cpp index 3af3bb8e..bbcd8f48 100644 --- a/tests/integration/operator/operator_shell/mock/test_business_responses.cpp +++ b/tests/integration/operator/operator_shell/mock/test_business_responses.cpp @@ -272,8 +272,8 @@ Json Generate(const std::string& input) { "llm_generate", "generate", {{"input", input}}, {{"bind_model", "llm"}, {"endpoints", {{"answer", Json::object()}}}}); } -Json Pipeline(const char* business, Json inputs, Json outputs, Json nodes, - Json models) { +Json Pipeline(const char* business, Json inputs, Json outputs, + std::vector nodes, std::vector models) { Json inbound = Json::array(); for (const auto& type : inputs) inbound.push_back({{"type", type}, {"name", business}}); @@ -384,7 +384,7 @@ TEST_F(BusinessResponsesTest, Pipeline("translate", {"entity_in"}, {{"translation", "prompt.text"}}, {Node("text_template", "prompt", {{"primary", "input.query"}}, {{"template", "{{primary}}"}})}, - Json::array()); + {}); // {"translated":"xx"} 为 19 字节;第 3 行的结果超过容量。 pipeline["io"]["output"][0]["params"] = {{"result_json_max_bytes", 20}}; Create(std::move(pipeline), 3); @@ -430,7 +430,7 @@ TEST_F(BusinessResponsesTest, Create(Pipeline("translate", {"entity_in"}, {{"translation", "prompt.text"}}, {Node("text_template", "prompt", {{"primary", "input.query"}}, {{"template", "old={{primary}}"}})}, - Json::array())); + {})); std::array raw{R"({"query":"hello"})", R"({"query":"goodbye"})"}; test_support::MockTextCarrier requests[]{ diff --git a/tests/integration/operator/test_io_parameters.cpp b/tests/integration/operator/test_io_parameters.cpp index 0540b1db..a18b5841 100644 --- a/tests/integration/operator/test_io_parameters.cpp +++ b/tests/integration/operator/test_io_parameters.cpp @@ -41,7 +41,7 @@ class IoParametersTest : public ::testing::Test { void SetUp() override { char directory[] = "/tmp/edgeflow-deployment-XXXXXX"; ASSERT_NE(mkdtemp(directory), nullptr); - directory_ = directory; + directory_ = std::filesystem::canonical(directory); root_ = directory_ / "root"; config_ = root_ / "conf"; std::filesystem::create_directories(config_ / "nested"); diff --git a/tests/tooling/test_dev_recipe.py b/tests/tooling/test_dev_recipe.py index 9f6e42d4..10991d5e 100644 --- a/tests/tooling/test_dev_recipe.py +++ b/tests/tooling/test_dev_recipe.py @@ -25,7 +25,7 @@ def setUp(self): self.assertTrue(DEMO.is_file(), f'Set LLM_EDGEFLOW_DEMO_BINARY: {DEMO}') self.temporary = tempfile.TemporaryDirectory(prefix='recipe-consumer-') self.addCleanup(self.temporary.cleanup) - self.root = Path(self.temporary.name) + self.root = Path(self.temporary.name).resolve() self.build = self.root / 'build' self.build.mkdir() # Exact production scripts plus actual built binaries; no inferred Catalog. diff --git a/tests/tooling/test_pipeline_cli.py b/tests/tooling/test_pipeline_cli.py index bd6779c9..d31a5bb4 100644 --- a/tests/tooling/test_pipeline_cli.py +++ b/tests/tooling/test_pipeline_cli.py @@ -132,7 +132,7 @@ def test_patch_uses_native_semantics_and_keeps_batch_failure_atomic(self): def test_deployment_resolution_reports_the_selected_file_and_effective_io(self): with tempfile.TemporaryDirectory() as directory: - directory = Path(directory) + directory = Path(directory).resolve() (directory / "pipeline.json").write_text(json.dumps(self.base), encoding="utf-8") conf = directory / "pipeline.conf" conf.write_text('{"pipe_path":"pipeline.json"}', encoding="utf-8") diff --git a/tests/tooling/test_pipeline_studio.py b/tests/tooling/test_pipeline_studio.py index 8831ac87..8d49706a 100644 --- a/tests/tooling/test_pipeline_studio.py +++ b/tests/tooling/test_pipeline_studio.py @@ -62,7 +62,7 @@ def setUpClass(cls): def setUp(self): self.temporary = tempfile.TemporaryDirectory(prefix="studio-contract-") self.addCleanup(self.temporary.cleanup) - self.root = Path(self.temporary.name) + self.root = Path(self.temporary.name).resolve() self.configs = self.root / "configs" self.configs.mkdir() (self.root / "demo").mkdir() diff --git a/tests/tooling/test_prepare_bpe_tokenizer.py b/tests/tooling/test_prepare_bpe_tokenizer.py index 55083b81..4c0c93e6 100644 --- a/tests/tooling/test_prepare_bpe_tokenizer.py +++ b/tests/tooling/test_prepare_bpe_tokenizer.py @@ -293,7 +293,7 @@ def test_missing_extra_and_error_lines_fail(self): tool.run_driver(self.driver(body), "unused.eftok", requests) self.assertIn(message, str(raised.exception)) with self.assertRaises(tool.ToolError): - tool.run_driver("/bin/true", "/nonexistent.eftok", requests) + tool.run_driver(self.driver("exit 0\n"), "/nonexistent.eftok", requests) class ByteFallbackVocabularyTest(ToolTestCase): diff --git a/tests/unit/adapter/test_file_parameters.cpp b/tests/unit/adapter/test_file_parameters.cpp index 434f4958..7f9406b4 100644 --- a/tests/unit/adapter/test_file_parameters.cpp +++ b/tests/unit/adapter/test_file_parameters.cpp @@ -74,7 +74,7 @@ class FileParametersTest : public ::testing::Test { ASSERT_TRUE(RegisterFileDeclarations()); char directory[] = "/tmp/edgeflow-file-resolution-XXXXXX"; ASSERT_NE(mkdtemp(directory), nullptr); - directory_ = directory; + directory_ = std::filesystem::canonical(directory); root_ = directory_ / "pipeline"; std::filesystem::create_directory(root_); } diff --git a/tests/unit/contracts/test_nested_parameters.cpp b/tests/unit/contracts/test_nested_parameters.cpp index b868d790..d841c71d 100644 --- a/tests/unit/contracts/test_nested_parameters.cpp +++ b/tests/unit/contracts/test_nested_parameters.cpp @@ -96,6 +96,36 @@ TEST(NestedParametersTest, NestedDefaultsAndHooksProduceOwnedTypedValues) { EXPECT_EQ(default_items->items.at(0).prepared_units, 9); } +TEST(NestedParametersTest, PackedBooleanContainersRoundTripDefaultsAndValues) { + struct Flags { + std::vector values; + std::vector> rows; + std::map> groups; + }; + const Parameters spec{ + Field("values", &Flags::values).Default(std::vector{true, false}), + Field("rows", &Flags::rows) + .Default(std::vector>{{false, true}, {}}), + Field("groups", &Flags::groups) + .Default(std::map>{{"enabled", {true}}, + {"empty", {}}})}; + const Json expected = { + {"values", Json::array({true, false})}, + {"rows", Json::array({Json::array({false, true}), Json::array()})}, + {"groups", {{"enabled", Json::array({true})}, {"empty", Json::array()}}}}; + std::string diagnostic; + const auto defaults = spec.Parse(Json::object(), &diagnostic); + ASSERT_TRUE(defaults) << diagnostic; + EXPECT_EQ(spec.Read(*defaults), expected); + Json raw = { + {"values", Json::array({false})}, + {"rows", Json::array({Json::array({true}), Json::array({false})})}, + {"groups", {{"mixed", Json::array({false, true, false})}}}}; + const auto parsed = spec.Parse(raw, &diagnostic); + ASSERT_TRUE(parsed) << diagnostic; + EXPECT_EQ(spec.Read(*parsed), raw); +} + TEST(NestedParametersTest, NestedErrorsReportEscapedPointersAndLeafKinds) { using Kind = ConfigFieldErrorKind; struct Case {