Ship portable CPU dispatch and verified shared runtime dependencies

This commit is contained in:
karl committed 2026-10-12 22:35:34 +02:00
1 parent 278b09f82c
commit 0bba1a0aa7
19 files changed
+675 -17

No files matched your search

+1 -1
View File
@@ -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
+3
View File
@@ -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/
+24 -4
View File
@@ -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)
+38 -1
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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
View File
@@ -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.
+29
View File
@@ -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"
}
+66
View File
@@ -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}")
+98
View File
@@ -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;
+10 -1
View File
@@ -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",
+56 -1
View File
@@ -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)
+44 -2
View File
@@ -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")
+92 -3
View File
@@ -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()