Ship portable CPU dispatch and verified shared runtime dependencies
This commit is contained in:
1 parent
278b09f82c
commit
0bba1a0aa7
19 files changed
+675
-17
No files matched your search
+1
-1
@@ -3,4 +3,4 @@
|
|||||||
*.jpg binary
|
*.jpg binary
|
||||||
*.ico binary
|
*.ico binary
|
||||||
# Unified-diff context lines intentionally contain a single leading space.
|
# Unified-diff context lines intentionally contain a single leading space.
|
||||||
tools/embeddinggemma2/no-logits.patch -whitespace
|
tools/embeddinggemma2/*.patch -whitespace
|
||||||
@@ -9,7 +9,10 @@ __pycache__/
|
|||||||
/apps/desktop/src-tauri/gen/
|
/apps/desktop/src-tauri/gen/
|
||||||
/apps/desktop/src-tauri/resources/searchi-gemma-worker*
|
/apps/desktop/src-tauri/resources/searchi-gemma-worker*
|
||||||
/apps/desktop/src-tauri/resources/vulkan-1.dll
|
/apps/desktop/src-tauri/resources/vulkan-1.dll
|
||||||
|
/apps/desktop/src-tauri/resources/*.dll
|
||||||
|
/apps/desktop/src-tauri/resources/runtime-libraries.json
|
||||||
/apps/desktop/src-tauri/resources/VULKAN-LICENSE.txt
|
/apps/desktop/src-tauri/resources/VULKAN-LICENSE.txt
|
||||||
|
/apps/desktop/src-tauri/resources/MSVC-REDISTRIBUTION.txt
|
||||||
/apps/desktop/src-tauri/resources/embeddinggemma2/
|
/apps/desktop/src-tauri/resources/embeddinggemma2/
|
||||||
/apps/desktop/src-tauri/resources/media-runtime/bin/
|
/apps/desktop/src-tauri/resources/media-runtime/bin/
|
||||||
/apps/desktop/src-tauri/icons/android/
|
/apps/desktop/src-tauri/icons/android/
|
||||||
|
|||||||
@@ -4,10 +4,28 @@ if(NOT DEFINED LLAMA_CPP_DIR OR NOT EXISTS "${LLAMA_CPP_DIR}/include/llama.h")
|
|||||||
message(FATAL_ERROR "Supply -DLLAMA_CPP_DIR=<pinned patched llama.cpp b11461 source>")
|
message(FATAL_ERROR "Supply -DLLAMA_CPP_DIR=<pinned patched llama.cpp b11461 source>")
|
||||||
endif()
|
endif()
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
if(MSVC)
|
# CPU-only builds remain small internal VM tools. Normal Windows x64 builds
|
||||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>" CACHE STRING "Standalone worker static MSVC runtime" FORCE)
|
# ship baseline and optimized CPU modules in the same GPU-capable package.
|
||||||
|
set(SEARCHI_ACCELERATION "auto" CACHE STRING "auto; explicit developer overrides: cpu, metal, vulkan, cuda")
|
||||||
|
set(SEARCHI_CPU_DISPATCH OFF)
|
||||||
|
if(WIN32 AND CMAKE_SIZEOF_VOID_P EQUAL 8 AND
|
||||||
|
(CMAKE_SYSTEM_PROCESSOR MATCHES "^(AMD64|amd64|x86_64|X86_64)$" OR CMAKE_GENERATOR_PLATFORM STREQUAL "x64") AND
|
||||||
|
NOT SEARCHI_ACCELERATION STREQUAL "cpu")
|
||||||
|
set(SEARCHI_CPU_DISPATCH ON)
|
||||||
endif()
|
endif()
|
||||||
set(BUILD_SHARED_LIBS OFF CACHE BOOL "Static local worker runtime" FORCE)
|
if(MSVC)
|
||||||
|
# llama and ggml exchange FILE* and other CRT-owned state across DLLs.
|
||||||
|
# Every shared-runtime component must use the same dynamic CRT.
|
||||||
|
if(SEARCHI_CPU_DISPATCH)
|
||||||
|
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL" CACHE STRING "Shared worker CRT across runtime DLLs" FORCE)
|
||||||
|
else()
|
||||||
|
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>" CACHE STRING "Internal static worker CRT" FORCE)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
set(BUILD_SHARED_LIBS ${SEARCHI_CPU_DISPATCH} CACHE BOOL "Shared runtime for Windows CPU dispatch" FORCE)
|
||||||
|
set(GGML_BACKEND_DL ${SEARCHI_CPU_DISPATCH} CACHE BOOL "Load compatible packaged CPU backend" FORCE)
|
||||||
|
set(GGML_CPU_ALL_VARIANTS ${SEARCHI_CPU_DISPATCH} CACHE BOOL "Baseline and optimized CPU variants" FORCE)
|
||||||
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||||
set(GGML_OPENMP OFF CACHE BOOL "Use bounded native llama thread pools without external OpenMP runtime" FORCE)
|
set(GGML_OPENMP OFF CACHE BOOL "Use bounded native llama thread pools without external OpenMP runtime" FORCE)
|
||||||
set(GGML_NATIVE OFF CACHE BOOL "Portable CPU kernels")
|
set(GGML_NATIVE OFF CACHE BOOL "Portable CPU kernels")
|
||||||
# Normal distribution builds include GPU support and CPU fallback. CPU-only
|
# Normal distribution builds include GPU support and CPU fallback. CPU-only
|
||||||
@@ -15,7 +33,6 @@ set(GGML_NATIVE OFF CACHE BOOL "Portable CPU kernels")
|
|||||||
set(GGML_CUDA OFF CACHE BOOL "Production CUDA backend")
|
set(GGML_CUDA OFF CACHE BOOL "Production CUDA backend")
|
||||||
set(GGML_VULKAN OFF CACHE BOOL "Production Vulkan backend")
|
set(GGML_VULKAN OFF CACHE BOOL "Production Vulkan backend")
|
||||||
set(GGML_METAL OFF CACHE BOOL "Production Metal backend")
|
set(GGML_METAL OFF CACHE BOOL "Production Metal backend")
|
||||||
set(SEARCHI_ACCELERATION "auto" CACHE STRING "auto; explicit developer overrides: cpu, metal, vulkan, cuda")
|
|
||||||
if(SEARCHI_ACCELERATION STREQUAL "cpu")
|
if(SEARCHI_ACCELERATION STREQUAL "cpu")
|
||||||
set(GGML_CUDA OFF CACHE BOOL "CPU test build" FORCE)
|
set(GGML_CUDA OFF CACHE BOOL "CPU test build" FORCE)
|
||||||
set(GGML_VULKAN OFF CACHE BOOL "CPU test build" FORCE)
|
set(GGML_VULKAN OFF CACHE BOOL "CPU test build" FORCE)
|
||||||
@@ -46,5 +63,8 @@ add_subdirectory("${LLAMA_CPP_DIR}" llama-build)
|
|||||||
add_executable(searchi-gemma-worker worker.cpp)
|
add_executable(searchi-gemma-worker worker.cpp)
|
||||||
target_include_directories(searchi-gemma-worker PRIVATE "${LLAMA_CPP_DIR}/vendor")
|
target_include_directories(searchi-gemma-worker PRIVATE "${LLAMA_CPP_DIR}/vendor")
|
||||||
target_link_libraries(searchi-gemma-worker PRIVATE llama mtmd)
|
target_link_libraries(searchi-gemma-worker PRIVATE llama mtmd)
|
||||||
|
if(SEARCHI_CPU_DISPATCH)
|
||||||
|
target_compile_definitions(searchi-gemma-worker PRIVATE SEARCHI_CPU_DISPATCH=1)
|
||||||
|
endif()
|
||||||
target_compile_definitions(searchi-gemma-worker PRIVATE SEARCHI_LLAMA_TAG="b11461-no-logits-v1")
|
target_compile_definitions(searchi-gemma-worker PRIVATE SEARCHI_LLAMA_TAG="b11461-no-logits-v1")
|
||||||
install(TARGETS searchi-gemma-worker RUNTIME DESTINATION bin)
|
install(TARGETS searchi-gemma-worker RUNTIME DESTINATION bin)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Local NDJSON helper around the pinned llama.cpp encoder. stdout is exclusively
|
// Local NDJSON helper around the pinned llama.cpp encoder. stdout is exclusively
|
||||||
// protocol output; llama diagnostics go to stderr. No model download or HTTP.
|
// protocol output; llama diagnostics go to stderr. No model download or HTTP.
|
||||||
#include "llama.h"
|
#include "llama.h"
|
||||||
|
#include "ggml-backend.h"
|
||||||
#include "mtmd.h"
|
#include "mtmd.h"
|
||||||
#include "nlohmann/json.hpp"
|
#include "nlohmann/json.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -14,6 +15,12 @@
|
|||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#ifdef SEARCHI_CPU_DISPATCH
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#define NOMINMAX
|
||||||
|
#include <windows.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#endif
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
constexpr size_t max_request_bytes = 16 * 1024 * 1024;
|
constexpr size_t max_request_bytes = 16 * 1024 * 1024;
|
||||||
@@ -71,6 +78,8 @@ class Encoder {
|
|||||||
}
|
}
|
||||||
public:
|
public:
|
||||||
std::string device;
|
std::string device;
|
||||||
|
std::string cpu_backend_module;
|
||||||
|
bool vulkan_module_loaded = false;
|
||||||
int threads;
|
int threads;
|
||||||
Encoder(const std::string & path, int cpu_threads, bool want_gpu) : model_path(path),threads(cpu_threads) {
|
Encoder(const std::string & path, int cpu_threads, bool want_gpu) : model_path(path),threads(cpu_threads) {
|
||||||
llama_log_set([](ggml_log_level level, const char * text, void *) {
|
llama_log_set([](ggml_log_level level, const char * text, void *) {
|
||||||
@@ -79,6 +88,31 @@ public:
|
|||||||
mtmd_log_set([](ggml_log_level level, const char * text, void *) {
|
mtmd_log_set([](ggml_log_level level, const char * text, void *) {
|
||||||
if (level >= GGML_LOG_LEVEL_WARN) std::fputs(text,stderr);
|
if (level >= GGML_LOG_LEVEL_WARN) std::fputs(text,stderr);
|
||||||
},nullptr);
|
},nullptr);
|
||||||
|
#ifdef SEARCHI_CPU_DISPATCH
|
||||||
|
// Only load our adjacent modules: no current-directory or environment
|
||||||
|
// plugin discovery. CPU-only startup never loads the Vulkan module.
|
||||||
|
std::vector<wchar_t> executable(32768);
|
||||||
|
DWORD length = GetModuleFileNameW(nullptr, executable.data(), DWORD(executable.size()));
|
||||||
|
require(length > 0 && length < executable.size(), "Cannot locate packaged worker backends");
|
||||||
|
const auto directory = std::filesystem::path(std::wstring(executable.data(), length)).parent_path().u8string();
|
||||||
|
if (want_gpu) {
|
||||||
|
ggml_backend_load_all_from_path(directory.c_str());
|
||||||
|
} else {
|
||||||
|
ggml_backend_load_cpu_only_from_path(directory.c_str());
|
||||||
|
}
|
||||||
|
require(ggml_backend_reg_by_name("CPU") != nullptr, "Cannot load compatible packaged CPU backend");
|
||||||
|
const auto cpu_reg = ggml_backend_reg_by_name("CPU");
|
||||||
|
const auto feature_proc = ggml_backend_reg_get_proc_address(cpu_reg, "ggml_backend_get_features");
|
||||||
|
HMODULE cpu_module = nullptr;
|
||||||
|
if (feature_proc && GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||||
|
reinterpret_cast<LPCWSTR>(feature_proc), &cpu_module)) {
|
||||||
|
DWORD module_length = GetModuleFileNameW(cpu_module, executable.data(), DWORD(executable.size()));
|
||||||
|
if (module_length > 0 && module_length < executable.size()) {
|
||||||
|
cpu_backend_module = std::filesystem::path(std::wstring(executable.data(), module_length)).filename().u8string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vulkan_module_loaded = GetModuleHandleW(L"ggml-vulkan.dll") != nullptr;
|
||||||
|
#endif
|
||||||
llama_backend_init();
|
llama_backend_init();
|
||||||
bool use_gpu = want_gpu && llama_supports_gpu_offload();
|
bool use_gpu = want_gpu && llama_supports_gpu_offload();
|
||||||
require(open(use_gpu) || (use_gpu && open(false)),"Cannot initialize local EmbeddingGemma 2 model/context");
|
require(open(use_gpu) || (use_gpu && open(false)),"Cannot initialize local EmbeddingGemma 2 model/context");
|
||||||
@@ -335,14 +369,17 @@ int main(int argc, char ** argv) {
|
|||||||
// probing/enabling the Vulkan provider in the Windows VM.
|
// probing/enabling the Vulkan provider in the Windows VM.
|
||||||
if (force_cpu || device == "cpu") {
|
if (force_cpu || device == "cpu") {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
require(_putenv_s("GGML_CPU_ONLY", "1") == 0, "Cannot restrict CPU startup");
|
||||||
require(_putenv_s("GGML_DISABLE_VULKAN", "1") == 0, "Cannot disable Vulkan for CPU startup");
|
require(_putenv_s("GGML_DISABLE_VULKAN", "1") == 0, "Cannot disable Vulkan for CPU startup");
|
||||||
#else
|
#else
|
||||||
|
require(setenv("GGML_CPU_ONLY", "1", 1) == 0, "Cannot restrict CPU startup");
|
||||||
require(setenv("GGML_DISABLE_VULKAN", "1", 1) == 0, "Cannot disable Vulkan for CPU startup");
|
require(setenv("GGML_DISABLE_VULKAN", "1", 1) == 0, "Cannot disable Vulkan for CPU startup");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
Encoder encoder(path,threads,device == "auto" && !force_cpu);
|
Encoder encoder(path,threads,device == "auto" && !force_cpu);
|
||||||
std::cout << json({{"ready",true},{"model","google/embeddinggemma-2"},{"dimensions",dimensions},
|
std::cout << json({{"ready",true},{"model","google/embeddinggemma-2"},{"dimensions",dimensions},
|
||||||
{"device",encoder.device},{"threads",threads},{"runtime",SEARCHI_LLAMA_TAG},{"media_supported",true},{"code_query_supported",true}}).dump() << '\n' << std::flush;
|
{"device",encoder.device},{"threads",threads},{"runtime",SEARCHI_LLAMA_TAG},{"media_supported",true},{"code_query_supported",true},
|
||||||
|
{"cpu_backend_module",encoder.cpu_backend_module},{"vulkan_module_loaded",encoder.vulkan_module_loaded}}).dump() << '\n' << std::flush;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
std::string line; char c;
|
std::string line; char c;
|
||||||
while (std::cin.get(c) && c != '\n') {
|
while (std::cin.get(c) && c != '\n') {
|
||||||
|
|||||||
+9
-2
@@ -135,10 +135,17 @@ The generated `.searchi/packages/searchi-windows.zip` contains the desktop,
|
|||||||
matching-architecture Google worker, FFmpeg/FFprobe and notices. It excludes
|
matching-architecture Google worker, FFmpeg/FFprobe and notices. It excludes
|
||||||
model caches, local settings and indexes. `PACKAGE.json` records SHA-256 for
|
model caches, local settings and indexes. `PACKAGE.json` records SHA-256 for
|
||||||
each included file. Extract the entire archive before launching `searchi.exe`.
|
each included file. Extract the entire archive before launching `searchi.exe`.
|
||||||
First launch uses normal verified model setup. WebView2 and the applicable
|
First launch uses normal verified model setup. WebView2 remains a Windows
|
||||||
Microsoft C++ runtime remain Windows prerequisites. The Windows archive includes
|
prerequisite. The normal x64 archive ships unchanged Microsoft release CRT
|
||||||
|
DLLs with their redistribution notice; Visual Studio is not required to run it.
|
||||||
|
The Windows archive includes baseline and optimized CPU modules, selected
|
||||||
|
using CPU and OS capabilities, and
|
||||||
the verified Vulkan loader DLL and its license; GPU availability is detected at
|
the verified Vulkan loader DLL and its license; GPU availability is detected at
|
||||||
runtime, with CPU fallback. The SDK is used only to compile the app.
|
runtime, with CPU fallback. The SDK is used only to compile the app.
|
||||||
|
All runtime DLLs are listed by size and SHA-256 in `runtime-libraries.json`.
|
||||||
|
Staging and packaging reject missing or changed listed dependencies before
|
||||||
|
replacing output. CPU-only VM smoke tests can also omit optimized modules to
|
||||||
|
exercise the packaged baseline; this does not certify older physical hardware.
|
||||||
|
|
||||||
This is an unsigned development archive, not a production installer. Native
|
This is an unsigned development archive, not a production installer. Native
|
||||||
launch, clean-machine dependencies, upgrades and signatures must still be
|
launch, clean-machine dependencies, upgrades and signatures must still be
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ Status: implementation started; no production release yet. Windows VM tests use
|
|||||||
provenance; universal CPU/Vulkan worker and embedded-frontend desktop built.
|
provenance; universal CPU/Vulkan worker and embedded-frontend desktop built.
|
||||||
- [x] Real Windows archive integrity, packaged decoder startup and auto-mode
|
- [x] Real Windows archive integrity, packaged decoder startup and auto-mode
|
||||||
CPU inference with a controlled no-GPU-provider registry; Google CPU parity.
|
CPU inference with a controlled no-GPU-provider registry; Google CPU parity.
|
||||||
|
- [x] Normal Windows x64 archive includes baseline/optimized CPU dispatch,
|
||||||
|
OS AVX-state guards, shared app-local MSVC CRT and hashed DLL inventory;
|
||||||
|
real optimized and baseline-only package inference passes in the CPU VM.
|
||||||
|
- [ ] Verify CPU dispatch on older physical CPUs and a clean Windows machine;
|
||||||
|
dependency inspection and baseline execution on a modern VM are insufficient.
|
||||||
- [ ] Actual missing/incompatible GPU drivers, precision, device loss and memory
|
- [ ] Actual missing/incompatible GPU drivers, precision, device loss and memory
|
||||||
pressure on release hardware; native GUI and clean-machine archive launch.
|
pressure on release hardware; native GUI and clean-machine archive launch.
|
||||||
- [x] Windows desktop archive packager includes explicit runtime/helper files
|
- [x] Windows desktop archive packager includes explicit runtime/helper files
|
||||||
|
|||||||
+7
-2
@@ -28,8 +28,13 @@ retry of that request. WorkerInfo and responses report the actual device.
|
|||||||
**Windows VM tests remain CPU only**, via SEARCHI_FORCE_CPU=1 and Device::Cpu.
|
**Windows VM tests remain CPU only**, via SEARCHI_FORCE_CPU=1 and Device::Cpu.
|
||||||
GPU execution/failure recovery has not been exercised here.
|
GPU execution/failure recovery has not been exercised here.
|
||||||
|
|
||||||
The Windows worker links a static MSVC CRT and static llama libraries, without
|
The normal Windows x64 worker ships shared llama libraries and baseline plus
|
||||||
an external OpenMP runtime. Production CMake supports cpu, auto, metal, vulkan,
|
optimized CPU modules. Selection checks CPU features and OS support for AVX
|
||||||
|
register state; it loads packaged backends from the executable directory.
|
||||||
|
Shared libraries use a common MSVC CRT because model file handles cross DLL
|
||||||
|
boundaries. The unchanged release CRT files are packaged alongside the worker.
|
||||||
|
Internal static CPU builds retain a static CRT. Neither profile requires an
|
||||||
|
external OpenMP runtime. Production CMake supports cpu, auto, metal, vulkan,
|
||||||
and cuda; CPU builds force GPU flags off. Runtime/provider parity and packaging
|
and cuda; CPU builds force GPU flags off. Runtime/provider parity and packaging
|
||||||
on other operating systems remain independent release checks.
|
on other operating systems remain independent release checks.
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,35 @@ and `SEARCHI_FORCE_CPU=1`.
|
|||||||
|
|
||||||
## Current integration checkpoint (2026-10-12)
|
## Current integration checkpoint (2026-10-12)
|
||||||
|
|
||||||
|
The normal Windows x64 package now includes baseline and optimized CPU DLLs
|
||||||
|
with CPU/OS feature checks, plus the Vulkan backend. Shared DLLs use a common
|
||||||
|
MSVC CRT: a first `/MT` prototype crashed because model file handles crossed
|
||||||
|
separate CRT instances; the corrected `/MD` package passes real-model startup.
|
||||||
|
All 25 runtime DLLs and the worker have their non-Windows PE imports satisfied
|
||||||
|
by packaged files. Windows system UCRT/WinRT and WebView2 remain platform
|
||||||
|
requirements. This is dependency inspection, not clean-machine certification.
|
||||||
|
|
||||||
|
Both extracted-archive tests passed: automatic CPU dispatch selected
|
||||||
|
`ggml-cpu-cascadelake.dll`, and removing optimized modules from the temporary
|
||||||
|
extraction selected `ggml-cpu-x64.dll`. Both produced genuine normalized 768d
|
||||||
|
Google code vectors, started FFprobe, verified all 39 listed files, and confirmed
|
||||||
|
the Vulkan module was not loaded. The unsigned archive is 145,196,878 bytes;
|
||||||
|
its digest is recorded in [dispatch evidence](evidence/windows-package-cpu-dispatch.json)
|
||||||
|
and [baseline evidence](evidence/windows-package-cpu-baseline.json).
|
||||||
|
Recorded startup/query/exit times are diagnostic smoke samples, not comparable
|
||||||
|
benchmarks: cache state and concurrent checks differ. Baseline inference on
|
||||||
|
this modern VM does not certify older physical CPUs or operating systems.
|
||||||
|
Three builder, five staging and eleven archive regression tests passed.
|
||||||
|
Two real Google code tests and two text-reference/tail/debug parity tests also
|
||||||
|
passed using the new shared worker in forced-CPU mode. Text reference cosine
|
||||||
|
similarities range from 0.99983106 to 0.99993036; debug/normal query cosine is
|
||||||
|
1.00000000. These verify runtime equivalence, not broad retrieval quality.
|
||||||
|
The genuine Google media parity test also passed on this shared CPU runtime:
|
||||||
|
image budgets 280/140/70 and the speech fixture all meet the 0.999 reference
|
||||||
|
cosine threshold. Image/audio quality limitations and GPU gates remain open.
|
||||||
|
|
||||||
|
The following evidence describes the earlier static worker checkpoint:
|
||||||
|
|
||||||
The Windows-x64 universal worker built with CPU and Vulkan enabled, using the
|
The Windows-x64 universal worker built with CPU and Vulkan enabled, using the
|
||||||
pinned official LunarG 1.4.363.0 SDK (download hash and publisher signature
|
pinned official LunarG 1.4.363.0 SDK (download hash and publisher signature
|
||||||
verified). Copy-only installation and later provenance/swap validation ran in
|
verified). Copy-only installation and later provenance/swap validation ran in
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"architecture": "x86_64",
|
||||||
|
"verified_files": 39,
|
||||||
|
"cpu_backend_module": "ggml-cpu-x64.dll",
|
||||||
|
"vulkan_module_loaded": false,
|
||||||
|
"baseline_only": true,
|
||||||
|
"archive_sha256": "0e35c10ec48d4f3380915528a16529eb694418fb72717acd5cb81e2d9da46332",
|
||||||
|
"worker_requested_device": "auto",
|
||||||
|
"worker_actual_device": "cpu",
|
||||||
|
"no_gpu_condition": "GGML_DISABLE_VULKAN=1 only in child; SEARCHI_FORCE_CPU absent",
|
||||||
|
"worker_startup_query_and_exit_ms": 7931.76289999974,
|
||||||
|
"query_dimensions": 768,
|
||||||
|
"decoder": "ffprobe version n9.0.2-27-g7e93014c5b-20261012 Copyright (c) 2007-2026 the FFmpeg developers",
|
||||||
|
"scope": "Real packaged worker default-mode CPU fallback with disabled provider; no GPU, native UI or clean-machine certification"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"architecture": "x86_64",
|
||||||
|
"verified_files": 39,
|
||||||
|
"cpu_backend_module": "ggml-cpu-cascadelake.dll",
|
||||||
|
"vulkan_module_loaded": false,
|
||||||
|
"baseline_only": false,
|
||||||
|
"archive_sha256": "0e35c10ec48d4f3380915528a16529eb694418fb72717acd5cb81e2d9da46332",
|
||||||
|
"worker_requested_device": "auto",
|
||||||
|
"worker_actual_device": "cpu",
|
||||||
|
"no_gpu_condition": "GGML_DISABLE_VULKAN=1 only in child; SEARCHI_FORCE_CPU absent",
|
||||||
|
"worker_startup_query_and_exit_ms": 19626.68719998328,
|
||||||
|
"query_dimensions": 768,
|
||||||
|
"decoder": "ffprobe version n9.0.2-27-g7e93014c5b-20261012 Copyright (c) 2007-2026 the FFmpeg developers",
|
||||||
|
"scope": "Real packaged worker default-mode CPU fallback with disabled provider; no GPU, native UI or clean-machine certification"
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
"""Build a pinned isolated native worker; development VM builds use --backend cpu."""
|
"""Build a pinned isolated native worker; development VM builds use --backend cpu."""
|
||||||
import argparse
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -9,6 +12,49 @@ import sys
|
|||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
TAG = "b11461"
|
TAG = "b11461"
|
||||||
COMMIT = "4d756bc72bf00a4aacf410ae15a2d315f3db400d"
|
COMMIT = "4d756bc72bf00a4aacf410ae15a2d315f3db400d"
|
||||||
|
WINDOWS_CRT_FILES = (
|
||||||
|
"concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll",
|
||||||
|
"msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll",
|
||||||
|
"vcruntime140.dll", "vcruntime140_1.dll", "vcruntime140_threads.dll",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def bundle_windows_crt(build):
|
||||||
|
"""Copy unchanged release CRT from the selected Visual Studio installation."""
|
||||||
|
cache = (build / "CMakeCache.txt").read_text(encoding="utf-8")
|
||||||
|
match = re.search(r"^CMAKE_GENERATOR_INSTANCE:[^=]+=([^\r\n]+)$", cache, re.MULTILINE)
|
||||||
|
if not match:
|
||||||
|
raise RuntimeError("Selected Visual Studio installation is missing from CMake cache")
|
||||||
|
installation = Path(match.group(1))
|
||||||
|
auxiliary = installation / "VC/Auxiliary/Build"
|
||||||
|
version = (auxiliary / "Microsoft.VCRedistVersion.default.txt").read_text().strip()
|
||||||
|
tools_version = (auxiliary / "Microsoft.VCToolsVersion.default.txt").read_text().strip()
|
||||||
|
if not re.fullmatch(r"\d+\.\d+\.\d+", version) or version.split(".")[:2] != tools_version.split(".")[:2]:
|
||||||
|
raise RuntimeError("Visual Studio CRT redistributable does not match selected build tools")
|
||||||
|
cmake_version = [re.search(rf"^CMAKE_CACHE_{part}_VERSION:[^=]+=(\d+)$", cache, re.MULTILINE)
|
||||||
|
for part in ("MAJOR", "MINOR", "PATCH")]
|
||||||
|
if all(cmake_version):
|
||||||
|
compiler_info = build / "CMakeFiles" / ".".join(match.group(1) for match in cmake_version) / "CMakeCXXCompiler.cmake"
|
||||||
|
if compiler_info.is_file():
|
||||||
|
compiler = compiler_info.read_text(encoding="utf-8")
|
||||||
|
actual = re.search(r'set\(CMAKE_CXX_COMPILER_VERSION "(\d+)\.(\d+)\.[^"]+"\)', compiler)
|
||||||
|
if not actual or (int(actual.group(1)), int(actual.group(2))) != (19, int(version.split(".")[1])):
|
||||||
|
raise RuntimeError("Visual Studio CRT redistributable does not match actual selected compiler")
|
||||||
|
source = installation / "VC/Redist/MSVC" / version / "x64/Microsoft.VC143.CRT"
|
||||||
|
files = [source / name for name in WINDOWS_CRT_FILES]
|
||||||
|
if any(not path.is_file() or path.is_symlink() for path in files):
|
||||||
|
raise RuntimeError("Required release Visual Studio CRT redistributable is missing")
|
||||||
|
destination = build / "Release"
|
||||||
|
for path in files:
|
||||||
|
shutil.copy2(path, destination / path.name)
|
||||||
|
(destination / "MSVC-REDISTRIBUTION.txt").write_text(
|
||||||
|
"Microsoft Visual C++ release runtime — copyright Microsoft Corporation.\n"
|
||||||
|
f"Unmodified x64 redistributable files from Visual Studio VC/Redist/MSVC/{version}/x64/Microsoft.VC143.CRT.\n"
|
||||||
|
"These proprietary files are distributed under Microsoft's applicable Visual Studio license terms, not searchi's license.\n"
|
||||||
|
"Terms: https://visualstudio.microsoft.com/license-terms/vs2022-ga-diagnosticbuildtools/\n"
|
||||||
|
"Redistributable list: https://learn.microsoft.com/en-us/visualstudio/releases/2022/redistribution\n"
|
||||||
|
"Deployment guidance: https://learn.microsoft.com/en-us/cpp/windows/redistributing-visual-cpp-files\n",
|
||||||
|
encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def run(arguments, cwd=None):
|
def run(arguments, cwd=None):
|
||||||
@@ -44,6 +90,14 @@ def main():
|
|||||||
reverse = subprocess.run(["git", "apply", "--reverse", "--check", str(patch)], cwd=source, capture_output=True)
|
reverse = subprocess.run(["git", "apply", "--reverse", "--check", str(patch)], cwd=source, capture_output=True)
|
||||||
if reverse.returncode:
|
if reverse.returncode:
|
||||||
raise RuntimeError("Runtime memory patch cannot be applied or verified")
|
raise RuntimeError("Runtime memory patch cannot be applied or verified")
|
||||||
|
if os.name == "nt" and args.backend != "cpu":
|
||||||
|
for patch_name in ["cpu-dispatch.patch", "cpu-only-loader.patch"]:
|
||||||
|
dispatch_patch = Path(__file__).with_name(patch_name)
|
||||||
|
check = subprocess.run(["git", "apply", "--check", str(dispatch_patch)], cwd=source, capture_output=True)
|
||||||
|
if check.returncode == 0:
|
||||||
|
run(["git", "apply", dispatch_patch], cwd=source)
|
||||||
|
elif subprocess.run(["git", "apply", "--reverse", "--check", str(dispatch_patch)], cwd=source, capture_output=True).returncode:
|
||||||
|
raise RuntimeError(f"Runtime patch cannot be applied or verified: {patch_name}")
|
||||||
cmake_exe = shutil.which("cmake")
|
cmake_exe = shutil.which("cmake")
|
||||||
if not cmake_exe:
|
if not cmake_exe:
|
||||||
try:
|
try:
|
||||||
@@ -59,6 +113,18 @@ def main():
|
|||||||
if vulkan_loader:
|
if vulkan_loader:
|
||||||
for name in ["vulkan-1.dll", "VULKAN-LICENSE.txt"]:
|
for name in ["vulkan-1.dll", "VULKAN-LICENSE.txt"]:
|
||||||
shutil.copy2(vulkan_loader / name, build / "Release" / name)
|
shutil.copy2(vulkan_loader / name, build / "Release" / name)
|
||||||
|
if os.name == "nt" and args.backend != "cpu":
|
||||||
|
directory = build / "Release"
|
||||||
|
bundle_windows_crt(build)
|
||||||
|
def checksum(path):
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||||
|
inventory = {"worker_sha256": checksum(directory / "searchi-gemma-worker.exe"),
|
||||||
|
"libraries": [{"path": path.name, "bytes": path.stat().st_size, "sha256": checksum(path)}
|
||||||
|
for path in sorted(directory.glob("*.dll"))]}
|
||||||
|
temporary = directory / "runtime-libraries.json.partial"
|
||||||
|
temporary.write_text(json.dumps(inventory, indent=2), encoding="utf-8")
|
||||||
|
os.replace(temporary, directory / "runtime-libraries.json")
|
||||||
print(f"Runtime build complete: {build}")
|
print(f"Runtime build complete: {build}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
--- a/ggml/src/ggml-cpu/arch/x86/cpu-feats.cpp
|
||||||
|
+++ b/ggml/src/ggml-cpu/arch/x86/cpu-feats.cpp
|
||||||
|
@@ -261,10 +261,30 @@
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static int ggml_backend_cpu_x86_score() {
|
||||||
|
- // FIXME: this does not check for OS support
|
||||||
|
-
|
||||||
|
int score = 1;
|
||||||
|
cpuid_x86 is;
|
||||||
|
+ // CPUID describes hardware; AVX also requires OS-managed XMM/YMM state.
|
||||||
|
+ // Keep this selector compiled without architecture flags (upstream design).
|
||||||
|
+ unsigned long long xcr0 = 0;
|
||||||
|
+ if (is.XSAVE() && is.OSXSAVE()) {
|
||||||
|
+#ifdef _MSC_VER
|
||||||
|
+ xcr0 = _xgetbv(0);
|
||||||
|
+#else
|
||||||
|
+ unsigned int eax, edx;
|
||||||
|
+ __asm__ __volatile__("xgetbv" : "=a"(eax), "=d"(edx) : "c"(0));
|
||||||
|
+ xcr0 = (static_cast<unsigned long long>(edx) << 32) | eax;
|
||||||
|
+#endif
|
||||||
|
+ }
|
||||||
|
+#if defined(GGML_AVX) || defined(GGML_AVX2) || defined(GGML_FMA) || defined(GGML_F16C) || defined(GGML_AVX_VNNI) || defined(GGML_AVX512) || defined(GGML_AVX512_VBMI) || defined(GGML_AVX512_BF16) || defined(GGML_AVX512_VNNI)
|
||||||
|
+ if (!is.AVX() || (xcr0 & 0x6) != 0x6) { return 0; }
|
||||||
|
+#endif
|
||||||
|
+#if defined(GGML_AVX512) || defined(GGML_AVX512_VBMI) || defined(GGML_AVX512_BF16) || defined(GGML_AVX512_VNNI)
|
||||||
|
+ if ((xcr0 & 0xe6) != 0xe6) { return 0; }
|
||||||
|
+#endif
|
||||||
|
+#ifdef GGML_AMX_INT8
|
||||||
|
+ // Tile state also needs OS permission. Searchi does not request it.
|
||||||
|
+ return 0;
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
#ifdef GGML_FMA
|
||||||
|
if (!is.FMA()) { return 0; }
|
||||||
|
--- a/ggml/src/ggml-backend-reg.cpp
|
||||||
|
+++ b/ggml/src/ggml-backend-reg.cpp
|
||||||
|
@@ -582,6 +582,11 @@
|
||||||
|
bool silent = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+ const char * cpu_only = std::getenv("GGML_CPU_ONLY");
|
||||||
|
+ if (cpu_only && std::strcmp(cpu_only, "1") == 0) {
|
||||||
|
+ ggml_backend_load_best("cpu", silent, dir_path);
|
||||||
|
+ return;
|
||||||
|
+ }
|
||||||
|
ggml_backend_load_best("blas", silent, dir_path);
|
||||||
|
ggml_backend_load_best("zendnn", silent, dir_path);
|
||||||
|
ggml_backend_load_best("cann", silent, dir_path);
|
||||||
|
@@ -590,7 +595,10 @@
|
||||||
|
ggml_backend_load_best("metal", silent, dir_path);
|
||||||
|
ggml_backend_load_best("rpc", silent, dir_path);
|
||||||
|
ggml_backend_load_best("sycl", silent, dir_path);
|
||||||
|
- ggml_backend_load_best("vulkan", silent, dir_path);
|
||||||
|
+ const char * disable_vulkan = std::getenv("GGML_DISABLE_VULKAN");
|
||||||
|
+ if (!disable_vulkan || std::strcmp(disable_vulkan, "1") != 0) {
|
||||||
|
+ ggml_backend_load_best("vulkan", silent, dir_path);
|
||||||
|
+ }
|
||||||
|
ggml_backend_load_best("virtgpu", silent, dir_path);
|
||||||
|
ggml_backend_load_best("opencl", silent, dir_path);
|
||||||
|
ggml_backend_load_best("hexagon", silent, dir_path);
|
||||||
|
@@ -599,7 +607,7 @@
|
||||||
|
ggml_backend_load_best("cpu", silent, dir_path);
|
||||||
|
// check the environment variable GGML_BACKEND_PATH to load an out-of-tree backend
|
||||||
|
const char * backend_path = std::getenv("GGML_BACKEND_PATH");
|
||||||
|
- if (backend_path) {
|
||||||
|
+ if (backend_path && dir_path == nullptr) {
|
||||||
|
ggml_backend_load(backend_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
--- a/CMakeLists.txt
|
||||||
|
+++ b/CMakeLists.txt
|
||||||
|
@@ -37,7 +37,9 @@
|
||||||
|
# Add path to modules
|
||||||
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/")
|
||||||
|
|
||||||
|
-set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
|
+if (NOT DEFINED CMAKE_RUNTIME_OUTPUT_DIRECTORY)
|
||||||
|
+ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
|
+endif()
|
||||||
|
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
|
|
||||||
|
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||||
|
--- a/ggml/src/ggml-cpu/CMakeLists.txt
|
||||||
|
+++ b/ggml/src/ggml-cpu/CMakeLists.txt
|
||||||
|
@@ -13,7 +13,11 @@
|
||||||
|
# from inlining architecture-specific instructions into the score function.
|
||||||
|
# Without this, LTO can cause SIGILL when loading backends on older CPUs
|
||||||
|
# (e.g., loading power10 backend on power9 crashes before feature check runs).
|
||||||
|
- target_compile_options(${GGML_CPU_FEATS_NAME} PRIVATE -fno-lto)
|
||||||
|
+ if (MSVC)
|
||||||
|
+ target_compile_options(${GGML_CPU_FEATS_NAME} PRIVATE /GL-)
|
||||||
|
+ else()
|
||||||
|
+ target_compile_options(${GGML_CPU_FEATS_NAME} PRIVATE -fno-lto)
|
||||||
|
+ endif()
|
||||||
|
target_link_libraries(${cpu_name} PRIVATE ${GGML_CPU_FEATS_NAME})
|
||||||
|
endfunction()
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
--- a/ggml/include/ggml-backend.h
|
||||||
|
+++ b/ggml/include/ggml-backend.h
|
||||||
|
@@ -260,6 +260,8 @@
|
||||||
|
// Load all known backends from dynamic libraries
|
||||||
|
GGML_API void ggml_backend_load_all(void);
|
||||||
|
GGML_API void ggml_backend_load_all_from_path(const char * dir_path);
|
||||||
|
+ // Searchi: explicit CPU-only startup, independent of per-DLL CRT environment state.
|
||||||
|
+ GGML_API void ggml_backend_load_cpu_only_from_path(const char * dir_path);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Backend scheduler
|
||||||
|
--- a/ggml/src/ggml-backend-reg.cpp
|
||||||
|
+++ b/ggml/src/ggml-backend-reg.cpp
|
||||||
|
@@ -575,6 +575,17 @@
|
||||||
|
ggml_backend_load_all_from_path(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
+void ggml_backend_load_cpu_only_from_path(const char * dir_path) {
|
||||||
|
+#ifdef NDEBUG
|
||||||
|
+ const bool silent = true;
|
||||||
|
+#else
|
||||||
|
+ const bool silent = false;
|
||||||
|
+#endif
|
||||||
|
+ // Do not inspect GPU backends or GGML_BACKEND_PATH. The caller supplies
|
||||||
|
+ // the executable directory, so working-directory discovery is excluded.
|
||||||
|
+ ggml_backend_load_best("cpu", silent, dir_path);
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
void ggml_backend_load_all_from_path(const char * dir_path) {
|
||||||
|
#ifdef NDEBUG
|
||||||
|
bool silent = true;
|
||||||
@@ -22,6 +22,7 @@ def main():
|
|||||||
parser.add_argument("--archive", type=Path, default=ROOT / ".searchi/packages/searchi-windows.zip")
|
parser.add_argument("--archive", type=Path, default=ROOT / ".searchi/packages/searchi-windows.zip")
|
||||||
parser.add_argument("--models", type=Path, default=ROOT / ".searchi/models/embeddinggemma2")
|
parser.add_argument("--models", type=Path, default=ROOT / ".searchi/models/embeddinggemma2")
|
||||||
parser.add_argument("--output", type=Path, default=ROOT / ".searchi/packages/smoke.json")
|
parser.add_argument("--output", type=Path, default=ROOT / ".searchi/packages/smoke.json")
|
||||||
|
parser.add_argument("--baseline", action="store_true", help="Extract only the baseline CPU module to test the packaged fallback")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||||
model = args.models / manifest["files"][0]["path"]
|
model = args.models / manifest["files"][0]["path"]
|
||||||
@@ -40,6 +41,8 @@ def main():
|
|||||||
data = archive.read(record["path"])
|
data = archive.read(record["path"])
|
||||||
assert len(data) == record["bytes"]
|
assert len(data) == record["bytes"]
|
||||||
assert hashlib.sha256(data).hexdigest() == record["sha256"]
|
assert hashlib.sha256(data).hexdigest() == record["sha256"]
|
||||||
|
if args.baseline and record["path"].startswith("ggml-cpu-") and record["path"] != "ggml-cpu-x64.dll":
|
||||||
|
continue
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_bytes(data)
|
path.write_bytes(data)
|
||||||
environment = dict(os.environ, GGML_DISABLE_VULKAN="1")
|
environment = dict(os.environ, GGML_DISABLE_VULKAN="1")
|
||||||
@@ -56,10 +59,13 @@ def main():
|
|||||||
worker.kill()
|
worker.kill()
|
||||||
worker.wait(timeout=10)
|
worker.wait(timeout=10)
|
||||||
raise
|
raise
|
||||||
assert worker.returncode == 0, stderr
|
assert worker.returncode == 0, f"Worker exit {worker.returncode:#x}: {stderr}"
|
||||||
worker_duration_ms = (time.perf_counter()-started)*1000
|
worker_duration_ms = (time.perf_counter()-started)*1000
|
||||||
hello, result = [json.loads(line) for line in stdout.splitlines()]
|
hello, result = [json.loads(line) for line in stdout.splitlines()]
|
||||||
assert hello["ready"] and hello["device"] == "cpu" and result["device"] == "cpu" and result["ok"]
|
assert hello["ready"] and hello["device"] == "cpu" and result["device"] == "cpu" and result["ok"]
|
||||||
|
assert hello.get("vulkan_module_loaded") is False
|
||||||
|
if args.baseline:
|
||||||
|
assert hello.get("cpu_backend_module") == "ggml-cpu-x64.dll"
|
||||||
vector = result["vectors"][0]
|
vector = result["vectors"][0]
|
||||||
assert len(vector) == 768 and all(math.isfinite(x) for x in vector)
|
assert len(vector) == 768 and all(math.isfinite(x) for x in vector)
|
||||||
assert abs(sum(x*x for x in vector) - 1) < 0.0001
|
assert abs(sum(x*x for x in vector) - 1) < 0.0001
|
||||||
@@ -69,6 +75,9 @@ def main():
|
|||||||
with args.archive.open("rb") as stream:
|
with args.archive.open("rb") as stream:
|
||||||
archive_checksum = hashlib.file_digest(stream, "sha256").hexdigest()
|
archive_checksum = hashlib.file_digest(stream, "sha256").hexdigest()
|
||||||
report = {"architecture": package["architecture"], "verified_files": len(package["files"]),
|
report = {"architecture": package["architecture"], "verified_files": len(package["files"]),
|
||||||
|
"cpu_backend_module": hello.get("cpu_backend_module"),
|
||||||
|
"vulkan_module_loaded": hello.get("vulkan_module_loaded"),
|
||||||
|
"baseline_only": args.baseline,
|
||||||
"archive_sha256": archive_checksum,
|
"archive_sha256": archive_checksum,
|
||||||
"worker_requested_device": "auto", "worker_actual_device": hello["device"],
|
"worker_requested_device": "auto", "worker_actual_device": hello["device"],
|
||||||
"no_gpu_condition": "GGML_DISABLE_VULKAN=1 only in child; SEARCHI_FORCE_CPU absent",
|
"no_gpu_condition": "GGML_DISABLE_VULKAN=1 only in child; SEARCHI_FORCE_CPU absent",
|
||||||
|
|||||||
@@ -3,10 +3,56 @@ import argparse
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
CRT_LIBRARIES = {"concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll", "msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll", "vcruntime140.dll", "vcruntime140_1.dll", "vcruntime140_threads.dll"}
|
||||||
|
|
||||||
|
|
||||||
|
def verify_runtime(source, required=False):
|
||||||
|
"""Preflight an explicit, bounded dependency inventory, never a DLL glob."""
|
||||||
|
inventory = source.parent / "runtime-libraries.json"
|
||||||
|
if not inventory.is_file():
|
||||||
|
if required:
|
||||||
|
raise RuntimeError("Runtime dependency inventory is missing")
|
||||||
|
return []
|
||||||
|
if inventory.stat().st_size > 65536:
|
||||||
|
raise RuntimeError("Runtime dependency inventory is oversized")
|
||||||
|
manifest = json.loads(inventory.read_text(encoding="utf-8"))
|
||||||
|
if not isinstance(manifest, dict):
|
||||||
|
raise RuntimeError("Invalid runtime dependency inventory")
|
||||||
|
with source.open("rb") as stream:
|
||||||
|
if hashlib.file_digest(stream, "sha256").hexdigest() != manifest.get("worker_sha256"):
|
||||||
|
raise RuntimeError("Runtime worker checksum mismatch")
|
||||||
|
records = manifest.get("libraries")
|
||||||
|
if not isinstance(records, list) or not 1 <= len(records) <= 64:
|
||||||
|
raise RuntimeError("Invalid runtime dependency inventory")
|
||||||
|
paths, names = [], set()
|
||||||
|
for record in records:
|
||||||
|
if not isinstance(record, dict):
|
||||||
|
raise RuntimeError("Invalid runtime library metadata")
|
||||||
|
name = record.get("path", "")
|
||||||
|
if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+\.dll", name) or name.lower() in names:
|
||||||
|
raise RuntimeError("Invalid or duplicate runtime library path")
|
||||||
|
names.add(name.lower())
|
||||||
|
artifact = source.parent / name
|
||||||
|
if artifact.is_symlink() or not artifact.resolve().is_relative_to(source.parent.resolve()):
|
||||||
|
raise RuntimeError("Runtime library resolves outside build directory")
|
||||||
|
size = record.get("bytes")
|
||||||
|
checksum = record.get("sha256", "")
|
||||||
|
if type(size) is not int or size <= 0 or not isinstance(checksum, str) or not re.fullmatch(r"[0-9a-f]{64}", checksum):
|
||||||
|
raise RuntimeError("Invalid runtime library metadata")
|
||||||
|
if not artifact.is_file() or artifact.stat().st_size != size:
|
||||||
|
raise RuntimeError(f"Runtime library missing or size mismatch: {name}")
|
||||||
|
with artifact.open("rb") as stream:
|
||||||
|
if hashlib.file_digest(stream, "sha256").hexdigest() != checksum:
|
||||||
|
raise RuntimeError(f"Runtime library checksum mismatch: {name}")
|
||||||
|
paths.append(artifact)
|
||||||
|
if required and not ({"ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll"} | CRT_LIBRARIES).issubset(names):
|
||||||
|
raise RuntimeError("Runtime inventory lacks required CPU/GPU libraries")
|
||||||
|
return [inventory, *paths]
|
||||||
|
|
||||||
|
|
||||||
def verify_bundle(root, manifest):
|
def verify_bundle(root, manifest):
|
||||||
@@ -38,6 +84,12 @@ def main():
|
|||||||
license_path = ROOT / ".searchi/references/llama.cpp/LICENSE"
|
license_path = ROOT / ".searchi/references/llama.cpp/LICENSE"
|
||||||
if not license_path.is_file():
|
if not license_path.is_file():
|
||||||
raise RuntimeError("Native worker license is missing")
|
raise RuntimeError("Native worker license is missing")
|
||||||
|
dependencies = verify_runtime(source, os.name == "nt" and args.backend in {"auto", "vulkan"})
|
||||||
|
crt_notice = source.parent / "MSVC-REDISTRIBUTION.txt"
|
||||||
|
if os.name == "nt" and args.backend != "cpu":
|
||||||
|
if not crt_notice.is_file():
|
||||||
|
raise RuntimeError("Microsoft CRT redistribution notice is missing")
|
||||||
|
dependencies.append(crt_notice)
|
||||||
artifacts = []
|
artifacts = []
|
||||||
if args.bundle_model:
|
if args.bundle_model:
|
||||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||||
@@ -49,11 +101,14 @@ def main():
|
|||||||
destination = ROOT / "apps/desktop/src-tauri/resources"
|
destination = ROOT / "apps/desktop/src-tauri/resources"
|
||||||
destination.mkdir(parents=True, exist_ok=True)
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy2(source, destination / name)
|
shutil.copy2(source, destination / name)
|
||||||
|
for dependency in dependencies:
|
||||||
|
shutil.copy2(dependency, destination / dependency.name)
|
||||||
# Include the upstream runtime license with the statically linked helper.
|
# Include the upstream runtime license with the statically linked helper.
|
||||||
shutil.copy2(license_path, destination / "LLAMA-LICENSE.txt")
|
shutil.copy2(license_path, destination / "LLAMA-LICENSE.txt")
|
||||||
if loader:
|
if loader:
|
||||||
for name in ["vulkan-1.dll", "VULKAN-LICENSE.txt"]:
|
for name in ["vulkan-1.dll", "VULKAN-LICENSE.txt"]:
|
||||||
shutil.copy2(loader / name, destination / name)
|
if name not in {path.name for path in dependencies}:
|
||||||
|
shutil.copy2(loader / name, destination / name)
|
||||||
if args.bundle_model:
|
if args.bundle_model:
|
||||||
model_destination = destination / "embeddinggemma2"
|
model_destination = destination / "embeddinggemma2"
|
||||||
model_destination.mkdir(exist_ok=True)
|
model_destination.mkdir(exist_ok=True)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""Redistributable preparation tests; no native builds or GPU execution."""
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from build_runtime import bundle_windows_crt, WINDOWS_CRT_FILES
|
||||||
|
|
||||||
|
|
||||||
|
class CrtTests(unittest.TestCase):
|
||||||
|
def fixture(self, directory):
|
||||||
|
root = Path(directory)
|
||||||
|
installation = root / "Visual Studio"
|
||||||
|
auxiliary = installation / "VC/Auxiliary/Build"
|
||||||
|
auxiliary.mkdir(parents=True)
|
||||||
|
(auxiliary / "Microsoft.VCRedistVersion.default.txt").write_text("14.44.35112")
|
||||||
|
(auxiliary / "Microsoft.VCToolsVersion.default.txt").write_text("14.44.35207")
|
||||||
|
source = installation / "VC/Redist/MSVC/14.44.35112/x64/Microsoft.VC143.CRT"
|
||||||
|
source.mkdir(parents=True)
|
||||||
|
for name in WINDOWS_CRT_FILES:
|
||||||
|
(source / name).write_bytes(name.encode())
|
||||||
|
(source / "unlisted-debug.dll").write_bytes(b"do not distribute")
|
||||||
|
build = root / "build"
|
||||||
|
(build / "Release").mkdir(parents=True)
|
||||||
|
(build / "CMakeCache.txt").write_text(f"CMAKE_GENERATOR_INSTANCE:INTERNAL={installation}\n")
|
||||||
|
return build, source, auxiliary
|
||||||
|
|
||||||
|
def test_copies_only_fixed_unmodified_release_files_and_public_provenance(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
build, source, _ = self.fixture(directory)
|
||||||
|
bundle_windows_crt(build)
|
||||||
|
for name in WINDOWS_CRT_FILES:
|
||||||
|
self.assertEqual((build / "Release" / name).read_bytes(), (source / name).read_bytes())
|
||||||
|
self.assertFalse((build / "Release/unlisted-debug.dll").exists())
|
||||||
|
notice = (build / "Release/MSVC-REDISTRIBUTION.txt").read_text(encoding="utf-8")
|
||||||
|
self.assertIn("14.44.35112", notice)
|
||||||
|
self.assertIn("https://learn.microsoft.com/", notice)
|
||||||
|
self.assertNotIn(directory, notice)
|
||||||
|
|
||||||
|
def test_missing_or_mismatched_crt_preserves_existing_files(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
build, source, auxiliary = self.fixture(directory)
|
||||||
|
existing = build / "Release/msvcp140.dll"
|
||||||
|
existing.write_bytes(b"previous CRT")
|
||||||
|
(source / "vcruntime140.dll").unlink()
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "redistributable is missing"):
|
||||||
|
bundle_windows_crt(build)
|
||||||
|
self.assertEqual(existing.read_bytes(), b"previous CRT")
|
||||||
|
(auxiliary / "Microsoft.VCRedistVersion.default.txt").write_text("14.43.12345")
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "does not match"):
|
||||||
|
bundle_windows_crt(build)
|
||||||
|
self.assertEqual(existing.read_bytes(), b"previous CRT")
|
||||||
|
|
||||||
|
def test_custom_toolset_compiler_mismatch_rejects_before_copy(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
build, _, _ = self.fixture(directory)
|
||||||
|
with (build / "CMakeCache.txt").open("a") as stream:
|
||||||
|
stream.write("CMAKE_CACHE_MAJOR_VERSION:INTERNAL=4\nCMAKE_CACHE_MINOR_VERSION:INTERNAL=2\nCMAKE_CACHE_PATCH_VERSION:INTERNAL=0\n")
|
||||||
|
compiler = build / "CMakeFiles/4.2.0/CMakeCXXCompiler.cmake"
|
||||||
|
compiler.parent.mkdir(parents=True)
|
||||||
|
compiler.write_text('set(CMAKE_CXX_COMPILER_VERSION "19.45.12345")\n')
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "actual selected compiler"):
|
||||||
|
bundle_windows_crt(build)
|
||||||
|
self.assertFalse((build / "Release/msvcp140.dll").exists())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Offline installation regressions; no model downloads or inference."""
|
"""Offline installation regressions; no model downloads or inference."""
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -10,6 +11,70 @@ import stage_runtime
|
|||||||
|
|
||||||
|
|
||||||
class StagingTests(unittest.TestCase):
|
class StagingTests(unittest.TestCase):
|
||||||
|
@unittest.skipUnless(os.name == "nt", "Windows app-local CRT staging")
|
||||||
|
def test_missing_crt_notice_preserves_existing_staged_worker(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
runtime = root / ".searchi/runtime/auto/Release"
|
||||||
|
runtime.mkdir(parents=True)
|
||||||
|
worker = runtime / "searchi-gemma-worker.exe"
|
||||||
|
worker.write_bytes(b"new worker")
|
||||||
|
names = {"ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll"} | stage_runtime.CRT_LIBRARIES
|
||||||
|
records = []
|
||||||
|
for name in sorted(names):
|
||||||
|
data = name.encode()
|
||||||
|
(runtime / name).write_bytes(data)
|
||||||
|
records.append({"path": name, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()})
|
||||||
|
(runtime / "runtime-libraries.json").write_text(json.dumps({"worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "libraries": records}))
|
||||||
|
license_path = root / ".searchi/references/llama.cpp/LICENSE"
|
||||||
|
license_path.parent.mkdir(parents=True)
|
||||||
|
license_path.write_text("license")
|
||||||
|
destination = root / "apps/desktop/src-tauri/resources"
|
||||||
|
destination.mkdir(parents=True)
|
||||||
|
(destination / worker.name).write_bytes(b"previous worker")
|
||||||
|
with patch.object(stage_runtime, "ROOT", root), patch("sys.argv", ["stage_runtime"]):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "redistribution notice"):
|
||||||
|
stage_runtime.main()
|
||||||
|
self.assertEqual((destination / worker.name).read_bytes(), b"previous worker")
|
||||||
|
|
||||||
|
def test_missing_dependency_preserves_staged_worker(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
runtime = root / ".searchi/runtime/cpu"
|
||||||
|
if os.name == "nt":
|
||||||
|
runtime /= "Release"
|
||||||
|
runtime.mkdir(parents=True)
|
||||||
|
name = "searchi-gemma-worker.exe" if os.name == "nt" else "searchi-gemma-worker"
|
||||||
|
(runtime / name).write_bytes(b"new worker")
|
||||||
|
(runtime / "runtime-libraries.json").write_text(json.dumps({"worker_sha256": hashlib.sha256(b"new worker").hexdigest(), "libraries": [{"path": "ggml-cpu-x64.dll", "bytes": 4, "sha256": hashlib.sha256(b"test").hexdigest()}]}))
|
||||||
|
license_path = root / ".searchi/references/llama.cpp/LICENSE"
|
||||||
|
license_path.parent.mkdir(parents=True)
|
||||||
|
license_path.write_text("license")
|
||||||
|
destination = root / "apps/desktop/src-tauri/resources"
|
||||||
|
destination.mkdir(parents=True)
|
||||||
|
(destination / name).write_bytes(b"previous worker")
|
||||||
|
with patch.object(stage_runtime, "ROOT", root), patch("sys.argv", ["stage_runtime", "--backend", "cpu"]):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "missing or size mismatch"):
|
||||||
|
stage_runtime.main()
|
||||||
|
self.assertEqual((destination / name).read_bytes(), b"previous worker")
|
||||||
|
|
||||||
|
def test_inventory_rejects_escape_and_omits_unlisted_libraries(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
worker = root / "worker.exe"
|
||||||
|
worker.write_bytes(b"worker")
|
||||||
|
(root / "good.dll").write_bytes(b"good")
|
||||||
|
(root / "private.dll").write_bytes(b"private")
|
||||||
|
record = {"path": "good.dll", "bytes": 4, "sha256": hashlib.sha256(b"good").hexdigest()}
|
||||||
|
manifest = {"worker_sha256": hashlib.sha256(b"worker").hexdigest(), "libraries": [record]}
|
||||||
|
path = root / "runtime-libraries.json"
|
||||||
|
path.write_text(json.dumps(manifest))
|
||||||
|
self.assertEqual([p.name for p in stage_runtime.verify_runtime(worker)], ["runtime-libraries.json", "good.dll"])
|
||||||
|
record["path"] = "../escape.dll"
|
||||||
|
path.write_text(json.dumps(manifest))
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "library path"):
|
||||||
|
stage_runtime.verify_runtime(worker)
|
||||||
|
|
||||||
def test_corrupt_second_model_preserves_existing_staged_worker(self):
|
def test_corrupt_second_model_preserves_existing_staged_worker(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import argparse
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import struct
|
import struct
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
CRT_LIBRARIES = {"concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll", "msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll", "vcruntime140.dll", "vcruntime140_1.dll", "vcruntime140_threads.dll"}
|
||||||
|
|
||||||
|
|
||||||
def pe_machine(path):
|
def pe_machine(path):
|
||||||
@@ -32,11 +34,49 @@ def pe_machine(path):
|
|||||||
|
|
||||||
def package(root, executable, output):
|
def package(root, executable, output):
|
||||||
resources = root / "apps/desktop/src-tauri/resources"
|
resources = root / "apps/desktop/src-tauri/resources"
|
||||||
|
inventory = resources / "runtime-libraries.json"
|
||||||
|
if not inventory.is_file() or inventory.stat().st_size > 65536:
|
||||||
|
raise ValueError("Required runtime dependency inventory is missing or oversized")
|
||||||
|
inventory_bytes = inventory.read_bytes()
|
||||||
|
manifest = json.loads(inventory_bytes)
|
||||||
|
if not isinstance(manifest, dict):
|
||||||
|
raise ValueError("Invalid runtime dependency inventory")
|
||||||
|
libraries = manifest.get("libraries")
|
||||||
|
if not isinstance(libraries, list) or not 1 <= len(libraries) <= 64:
|
||||||
|
raise ValueError("Invalid runtime dependency inventory")
|
||||||
|
expected = {"searchi-gemma-worker.exe": {"sha256": manifest.get("worker_sha256")}}
|
||||||
|
for record in libraries:
|
||||||
|
if not isinstance(record, dict):
|
||||||
|
raise ValueError("Invalid runtime library metadata")
|
||||||
|
name = record.get("path", "")
|
||||||
|
if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+\.dll", name) or name.lower() in {key.lower() for key in expected}:
|
||||||
|
raise ValueError("Invalid or duplicate runtime library path")
|
||||||
|
if type(record.get("bytes")) is not int or record["bytes"] <= 0:
|
||||||
|
raise ValueError("Invalid runtime library size")
|
||||||
|
expected[name] = record
|
||||||
|
if not ({"ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll"} | CRT_LIBRARIES).issubset(expected):
|
||||||
|
raise ValueError("Runtime inventory lacks required CPU/GPU libraries")
|
||||||
|
expected[inventory.name] = {"bytes": len(inventory_bytes), "sha256": hashlib.sha256(inventory_bytes).hexdigest()}
|
||||||
|
for name, record in expected.items():
|
||||||
|
path = resources / name
|
||||||
|
if path.is_symlink() or not path.resolve().is_relative_to(resources.resolve()) or not path.is_file():
|
||||||
|
raise ValueError(f"Required runtime library is missing or outside staging: {name}")
|
||||||
|
checksum = record.get("sha256", "")
|
||||||
|
if not isinstance(checksum, str) or not re.fullmatch(r"[0-9a-f]{64}", checksum):
|
||||||
|
raise ValueError("Invalid runtime checksum")
|
||||||
|
with path.open("rb") as stream:
|
||||||
|
if hashlib.file_digest(stream, "sha256").hexdigest() != checksum:
|
||||||
|
raise ValueError(f"Runtime checksum mismatch: {name}")
|
||||||
|
if "bytes" in record and path.stat().st_size != record["bytes"]:
|
||||||
|
raise ValueError(f"Runtime size mismatch: {name}")
|
||||||
selected = [(executable, "searchi.exe"), (root / "LICENSE", "LICENSE.txt")]
|
selected = [(executable, "searchi.exe"), (root / "LICENSE", "LICENSE.txt")]
|
||||||
for name in ["searchi-gemma-worker.exe", "LLAMA-LICENSE.txt", "vulkan-1.dll", "VULKAN-LICENSE.txt", "media-runtime/manifest.json",
|
for name in ["searchi-gemma-worker.exe", "LLAMA-LICENSE.txt", "MSVC-REDISTRIBUTION.txt", "vulkan-1.dll", "VULKAN-LICENSE.txt", "media-runtime/manifest.json",
|
||||||
"media-runtime/bin/ffmpeg.exe", "media-runtime/bin/ffprobe.exe",
|
"media-runtime/bin/ffmpeg.exe", "media-runtime/bin/ffprobe.exe",
|
||||||
"licenses/Inter-OFL.txt", "licenses/Fluent-MIT.txt", "media-runtime/licenses/LICENSE.txt"]:
|
"licenses/Inter-OFL.txt", "licenses/Fluent-MIT.txt", "media-runtime/licenses/LICENSE.txt"]:
|
||||||
selected.append((resources / name, name))
|
selected.append((resources / name, name))
|
||||||
|
selected.append((inventory, inventory.name))
|
||||||
|
selected_names = {name for _, name in selected}
|
||||||
|
selected.extend((resources / name, name) for name in expected if name not in selected_names)
|
||||||
for directory in ["licenses", "media-runtime/licenses"]:
|
for directory in ["licenses", "media-runtime/licenses"]:
|
||||||
notices = list((resources / directory).rglob("*.txt"))
|
notices = list((resources / directory).rglob("*.txt"))
|
||||||
if not notices:
|
if not notices:
|
||||||
@@ -73,11 +113,13 @@ def package(root, executable, output):
|
|||||||
checksum.update(block)
|
checksum.update(block)
|
||||||
count += len(block)
|
count += len(block)
|
||||||
records.append({"path": name, "bytes": count, "sha256": checksum.hexdigest()})
|
records.append({"path": name, "bytes": count, "sha256": checksum.hexdigest()})
|
||||||
|
if name in expected and (checksum.hexdigest() != expected[name]["sha256"] or ("bytes" in expected[name] and count != expected[name]["bytes"])):
|
||||||
|
raise ValueError(f"Runtime changed while packaging: {name}")
|
||||||
archive.writestr("PACKAGE.json", json.dumps({"platform": "windows", "architecture": architecture,
|
archive.writestr("PACKAGE.json", json.dumps({"platform": "windows", "architecture": architecture,
|
||||||
"status": "unsigned development package; clean-machine verification pending", "files": records}, indent=2))
|
"status": "unsigned development package; clean-machine verification pending", "files": records}, indent=2))
|
||||||
archive.writestr("START-HERE.txt", "searchi — Find what matters. Locally.\n\n"
|
archive.writestr("START-HERE.txt", "searchi — Find what matters. Locally.\n\n"
|
||||||
"Extract the entire folder, then open searchi.exe. Keep its helpers alongside it.\n"
|
"Extract the entire folder, then open searchi.exe. Keep its helpers alongside it.\n"
|
||||||
"Windows needs WebView2 and the applicable Microsoft C++ runtime.\n"
|
"Windows needs WebView2. The Microsoft C++ runtime ships with these helpers; Visual Studio is not required.\n"
|
||||||
"First launch downloads approximately 865 MB of verified Google model files.\n"
|
"First launch downloads approximately 865 MB of verified Google model files.\n"
|
||||||
"Choose Add folder to index your own files; ordinary search stays local.\n"
|
"Choose Add folder to index your own files; ordinary search stays local.\n"
|
||||||
"This is an unsigned development build, not a certified production release.\n")
|
"This is an unsigned development build, not a certified production release.\n")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import unittest
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from package_windows import package
|
from package_windows import package, CRT_LIBRARIES
|
||||||
|
|
||||||
|
|
||||||
def executable(path, machine=0x8664):
|
def executable(path, machine=0x8664):
|
||||||
@@ -20,6 +20,15 @@ def executable(path, machine=0x8664):
|
|||||||
path.write_bytes(data)
|
path.write_bytes(data)
|
||||||
|
|
||||||
|
|
||||||
|
def inventory(resources):
|
||||||
|
worker = resources / "searchi-gemma-worker.exe"
|
||||||
|
records = []
|
||||||
|
for name in ["ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll", *sorted(CRT_LIBRARIES)]:
|
||||||
|
data = (resources / name).read_bytes()
|
||||||
|
records.append({"path": name, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()})
|
||||||
|
(resources / "runtime-libraries.json").write_text(json.dumps({"worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "libraries": records}))
|
||||||
|
|
||||||
|
|
||||||
class PackageTests(unittest.TestCase):
|
class PackageTests(unittest.TestCase):
|
||||||
def fixture(self, directory):
|
def fixture(self, directory):
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
@@ -27,13 +36,16 @@ class PackageTests(unittest.TestCase):
|
|||||||
app = root / "build/app.exe"
|
app = root / "build/app.exe"
|
||||||
executable(app)
|
executable(app)
|
||||||
(root / "LICENSE").write_text("application notice")
|
(root / "LICENSE").write_text("application notice")
|
||||||
for name in ["searchi-gemma-worker.exe", "vulkan-1.dll", "media-runtime/bin/ffmpeg.exe", "media-runtime/bin/ffprobe.exe"]:
|
for name in ["searchi-gemma-worker.exe", "vulkan-1.dll", "ggml-cpu-x64.dll", "ggml-vulkan.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll", "media-runtime/bin/ffmpeg.exe", "media-runtime/bin/ffprobe.exe"]:
|
||||||
executable(resources / name)
|
executable(resources / name)
|
||||||
for name in ["LLAMA-LICENSE.txt", "VULKAN-LICENSE.txt", "licenses/Inter-OFL.txt", "licenses/Fluent-MIT.txt", "media-runtime/licenses/LICENSE.txt", "media-runtime/manifest.json"]:
|
for name in sorted(CRT_LIBRARIES):
|
||||||
|
executable(resources / name)
|
||||||
|
for name in ["LLAMA-LICENSE.txt", "MSVC-REDISTRIBUTION.txt", "VULKAN-LICENSE.txt", "licenses/Inter-OFL.txt", "licenses/Fluent-MIT.txt", "media-runtime/licenses/LICENSE.txt", "media-runtime/manifest.json"]:
|
||||||
path = resources / name
|
path = resources / name
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text("notice")
|
path.write_text("notice")
|
||||||
(resources / "settings.json").write_text("private settings")
|
(resources / "settings.json").write_text("private settings")
|
||||||
|
inventory(resources)
|
||||||
return root, resources, app, root / "archive.zip"
|
return root, resources, app, root / "archive.zip"
|
||||||
|
|
||||||
def test_complete_archive_excludes_settings_and_verifies_recorded_bytes(self):
|
def test_complete_archive_excludes_settings_and_verifies_recorded_bytes(self):
|
||||||
@@ -53,6 +65,7 @@ class PackageTests(unittest.TestCase):
|
|||||||
root, resources, app, output = self.fixture(directory)
|
root, resources, app, output = self.fixture(directory)
|
||||||
output.write_bytes(b"previous package")
|
output.write_bytes(b"previous package")
|
||||||
executable(resources / "searchi-gemma-worker.exe", 0xAA64)
|
executable(resources / "searchi-gemma-worker.exe", 0xAA64)
|
||||||
|
inventory(resources)
|
||||||
with self.assertRaisesRegex(ValueError, "architecture differs"):
|
with self.assertRaisesRegex(ValueError, "architecture differs"):
|
||||||
package(root, app, output)
|
package(root, app, output)
|
||||||
self.assertEqual(output.read_bytes(), b"previous package")
|
self.assertEqual(output.read_bytes(), b"previous package")
|
||||||
@@ -109,10 +122,86 @@ class PackageTests(unittest.TestCase):
|
|||||||
root, resources, app, output = self.fixture(directory)
|
root, resources, app, output = self.fixture(directory)
|
||||||
output.write_bytes(b"previous package")
|
output.write_bytes(b"previous package")
|
||||||
executable(resources / "vulkan-1.dll", 0xAA64)
|
executable(resources / "vulkan-1.dll", 0xAA64)
|
||||||
|
inventory(resources)
|
||||||
with self.assertRaisesRegex(ValueError, "architecture differs: vulkan-1.dll"):
|
with self.assertRaisesRegex(ValueError, "architecture differs: vulkan-1.dll"):
|
||||||
package(root, app, output)
|
package(root, app, output)
|
||||||
self.assertEqual(output.read_bytes(), b"previous package")
|
self.assertEqual(output.read_bytes(), b"previous package")
|
||||||
|
|
||||||
|
def test_inventory_includes_dispatch_libraries_but_not_unlisted_dll(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, resources, app, output = self.fixture(directory)
|
||||||
|
executable(resources / "private-plugin.dll")
|
||||||
|
package(root, app, output)
|
||||||
|
with zipfile.ZipFile(output) as archive:
|
||||||
|
self.assertIn("ggml-cpu-x64.dll", archive.namelist())
|
||||||
|
self.assertIn("ggml-vulkan.dll", archive.namelist())
|
||||||
|
self.assertIn("runtime-libraries.json", archive.namelist())
|
||||||
|
self.assertIn("msvcp140.dll", archive.namelist())
|
||||||
|
self.assertIn("MSVC-REDISTRIBUTION.txt", archive.namelist())
|
||||||
|
self.assertNotIn("private-plugin.dll", archive.namelist())
|
||||||
|
|
||||||
|
def test_missing_inventory_or_corrupt_library_preserves_archive(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, resources, app, output = self.fixture(directory)
|
||||||
|
output.write_bytes(b"previous package")
|
||||||
|
(resources / "ggml-cpu-x64.dll").write_bytes(b"corrupt")
|
||||||
|
with self.assertRaisesRegex(ValueError, "checksum mismatch"):
|
||||||
|
package(root, app, output)
|
||||||
|
self.assertEqual(output.read_bytes(), b"previous package")
|
||||||
|
(resources / "runtime-libraries.json").unlink()
|
||||||
|
with self.assertRaisesRegex(ValueError, "inventory"):
|
||||||
|
package(root, app, output)
|
||||||
|
self.assertEqual(output.read_bytes(), b"previous package")
|
||||||
|
|
||||||
|
def test_dependency_change_during_copy_preserves_archive(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, resources, app, output = self.fixture(directory)
|
||||||
|
output.write_bytes(b"previous package")
|
||||||
|
original_open = zipfile.ZipFile.open
|
||||||
|
|
||||||
|
def mutate_library(archive, name, mode="r", *args, **kwargs):
|
||||||
|
if name == "ggml-cpu-x64.dll" and mode == "w":
|
||||||
|
library = resources / name
|
||||||
|
library.write_bytes(library.read_bytes() + b"changed")
|
||||||
|
return original_open(archive, name, mode, *args, **kwargs)
|
||||||
|
|
||||||
|
with mock.patch.object(zipfile.ZipFile, "open", mutate_library):
|
||||||
|
with self.assertRaisesRegex(ValueError, "changed while packaging"):
|
||||||
|
package(root, app, output)
|
||||||
|
self.assertEqual(output.read_bytes(), b"previous package")
|
||||||
|
self.assertEqual(list(output.parent.glob("*.partial")), [])
|
||||||
|
|
||||||
|
def test_missing_crt_or_notice_preserves_previous_archive(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, resources, app, output = self.fixture(directory)
|
||||||
|
output.write_bytes(b"previous package")
|
||||||
|
(resources / "msvcp140.dll").unlink()
|
||||||
|
with self.assertRaisesRegex(ValueError, "msvcp140"):
|
||||||
|
package(root, app, output)
|
||||||
|
self.assertEqual(output.read_bytes(), b"previous package")
|
||||||
|
executable(resources / "msvcp140.dll")
|
||||||
|
(resources / "MSVC-REDISTRIBUTION.txt").unlink()
|
||||||
|
with self.assertRaisesRegex(ValueError, "MSVC-REDISTRIBUTION"):
|
||||||
|
package(root, app, output)
|
||||||
|
self.assertEqual(output.read_bytes(), b"previous package")
|
||||||
|
|
||||||
|
def test_inventory_change_during_copy_preserves_previous_archive(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root, resources, app, output = self.fixture(directory)
|
||||||
|
output.write_bytes(b"previous package")
|
||||||
|
original_open = zipfile.ZipFile.open
|
||||||
|
|
||||||
|
def mutate_inventory(archive, name, mode="r", *args, **kwargs):
|
||||||
|
if name == "runtime-libraries.json" and mode == "w":
|
||||||
|
manifest = resources / name
|
||||||
|
manifest.write_bytes(manifest.read_bytes() + b" ")
|
||||||
|
return original_open(archive, name, mode, *args, **kwargs)
|
||||||
|
|
||||||
|
with mock.patch.object(zipfile.ZipFile, "open", mutate_inventory):
|
||||||
|
with self.assertRaisesRegex(ValueError, "changed while packaging"):
|
||||||
|
package(root, app, output)
|
||||||
|
self.assertEqual(output.read_bytes(), b"previous package")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user