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,6 +1,9 @@
|
||||
"""Build a pinned isolated native worker; development VM builds use --backend cpu."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -9,6 +12,49 @@ import sys
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TAG = "b11461"
|
||||
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):
|
||||
@@ -44,6 +90,14 @@ def main():
|
||||
reverse = subprocess.run(["git", "apply", "--reverse", "--check", str(patch)], cwd=source, capture_output=True)
|
||||
if reverse.returncode:
|
||||
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")
|
||||
if not cmake_exe:
|
||||
try:
|
||||
@@ -59,6 +113,18 @@ def main():
|
||||
if vulkan_loader:
|
||||
for name in ["vulkan-1.dll", "VULKAN-LICENSE.txt"]:
|
||||
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}")
|
||||
|
||||
|
||||
|
||||
@@ -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("--models", type=Path, default=ROOT / ".searchi/models/embeddinggemma2")
|
||||
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()
|
||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||
model = args.models / manifest["files"][0]["path"]
|
||||
@@ -40,6 +41,8 @@ def main():
|
||||
data = archive.read(record["path"])
|
||||
assert len(data) == record["bytes"]
|
||||
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.write_bytes(data)
|
||||
environment = dict(os.environ, GGML_DISABLE_VULKAN="1")
|
||||
@@ -56,10 +59,13 @@ def main():
|
||||
worker.kill()
|
||||
worker.wait(timeout=10)
|
||||
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
|
||||
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.get("vulkan_module_loaded") is False
|
||||
if args.baseline:
|
||||
assert hello.get("cpu_backend_module") == "ggml-cpu-x64.dll"
|
||||
vector = result["vectors"][0]
|
||||
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
|
||||
@@ -69,6 +75,9 @@ def main():
|
||||
with args.archive.open("rb") as stream:
|
||||
archive_checksum = hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
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,
|
||||
"worker_requested_device": "auto", "worker_actual_device": hello["device"],
|
||||
"no_gpu_condition": "GGML_DISABLE_VULKAN=1 only in child; SEARCHI_FORCE_CPU absent",
|
||||
|
||||
@@ -3,10 +3,56 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
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):
|
||||
@@ -38,6 +84,12 @@ def main():
|
||||
license_path = ROOT / ".searchi/references/llama.cpp/LICENSE"
|
||||
if not license_path.is_file():
|
||||
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 = []
|
||||
if args.bundle_model:
|
||||
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.mkdir(parents=True, exist_ok=True)
|
||||
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.
|
||||
shutil.copy2(license_path, destination / "LLAMA-LICENSE.txt")
|
||||
if loader:
|
||||
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:
|
||||
model_destination = destination / "embeddinggemma2"
|
||||
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."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
@@ -10,6 +11,70 @@ import stage_runtime
|
||||
|
||||
|
||||
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):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
||||
Reference in new issue
Block a user