Package portable Linux runtime and verify Google CPU search
This commit is contained in:
1 parent
e3558a0104
commit
0b56ab553c
22 files changed
+1585
-16
No files matched your search
@@ -3,6 +3,7 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
@@ -17,6 +18,87 @@ WINDOWS_CRT_FILES = (
|
||||
"msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll",
|
||||
"vcruntime140.dll", "vcruntime140_1.dll", "vcruntime140_threads.dll",
|
||||
)
|
||||
LINUX_CPU_VARIANTS = {"x64", "sse42", "sandybridge", "ivybridge", "piledriver", "haswell",
|
||||
"skylakex", "cannonlake", "cascadelake", "icelake", "cooperlake", "zen4",
|
||||
"alderlake", "sapphirerapids"}
|
||||
LINUX_SYSTEM_LIBRARIES = {"libc.so.6", "libm.so.6", "libdl.so.2", "libpthread.so.0",
|
||||
"librt.so.1", "libstdc++.so.6", "libgcc_s.so.1", "ld-linux-x86-64.so.2"}
|
||||
|
||||
|
||||
def linux_dispatch(backend):
|
||||
"""Only normal Linux Vulkan modes use the packaged shared-module layout."""
|
||||
return (sys.platform.startswith("linux") and platform.machine().lower() in {"amd64", "x86_64"}
|
||||
and backend in {"auto", "vulkan"})
|
||||
|
||||
|
||||
def elf_dynamic(path):
|
||||
"""Read ELF metadata without executing the artifact or following ldd hooks."""
|
||||
output = subprocess.check_output(["readelf", "--dynamic", str(path)], text=True)
|
||||
values = {}
|
||||
for key, value in re.findall(r"\((NEEDED|SONAME|RUNPATH|RPATH)\).*?\[([^\]]+)\]", output):
|
||||
values.setdefault(key, []).append(value)
|
||||
return values
|
||||
|
||||
|
||||
def bundle_linux_libraries(build):
|
||||
"""Flatten approved SONAME files beside the worker and record dependency closure.
|
||||
|
||||
System libc/C++ ABI remains a declared distro compatibility gate. Vulkan's
|
||||
loader is optional: only the dynamically loaded GPU module may depend on it.
|
||||
Preflight precedes flattening; the preceding CMake build itself is not a
|
||||
transactional release replacement. Staging performs that separate swap.
|
||||
"""
|
||||
build = Path(build)
|
||||
worker = build / "searchi-gemma-worker"
|
||||
candidates = [build / "bin" / f"lib{name}.so" for name in ("ggml", "ggml-base", "llama", "mtmd")]
|
||||
candidates += [build / f"libggml-cpu-{variant}.so" for variant in sorted(LINUX_CPU_VARIANTS)]
|
||||
candidates.append(build / "libggml-vulkan.so")
|
||||
records = []
|
||||
names = set()
|
||||
for link in candidates:
|
||||
if not link.is_file():
|
||||
raise RuntimeError(f"Required Linux runtime library is missing: {link.name}")
|
||||
source = link.resolve()
|
||||
if not source.is_relative_to(build.resolve()) or not source.is_file():
|
||||
raise RuntimeError("Linux runtime library escapes its build directory")
|
||||
metadata = elf_dynamic(source)
|
||||
sonames = metadata.get("SONAME", [])
|
||||
module = link.parent == build
|
||||
if ((not module and len(sonames) != 1) or len(sonames) > 1
|
||||
or any(not re.fullmatch(r"lib[A-Za-z0-9_-]+\.so(?:\.\d+)*", name) for name in sonames)):
|
||||
raise RuntimeError("Linux runtime has invalid or missing SONAME")
|
||||
# Backend discovery uses the unversioned module filename; core linkage
|
||||
# uses its SONAME. Both destinations are actual files, never symlinks.
|
||||
name = link.name if module else sonames[0]
|
||||
if name in names:
|
||||
raise RuntimeError("Duplicate Linux runtime library")
|
||||
names.add(name)
|
||||
destination = build / name
|
||||
records.append((source, destination, metadata))
|
||||
all_names = names
|
||||
for path, metadata in [(worker, elf_dynamic(worker)), *[(destination, metadata) for _, destination, metadata in records]]:
|
||||
if metadata.get("RPATH") or metadata.get("RUNPATH") != ["$ORIGIN"]:
|
||||
raise RuntimeError(f"Linux runtime requires origin-only RUNPATH: {path.name}")
|
||||
needed = set(metadata.get("NEEDED", []))
|
||||
allowed = all_names | LINUX_SYSTEM_LIBRARIES
|
||||
if path.name == "libggml-vulkan.so":
|
||||
allowed = allowed | {"libvulkan.so.1"}
|
||||
if needed - allowed:
|
||||
raise RuntimeError(f"Unpackaged Linux runtime dependency for {path.name}: {sorted(needed - allowed)}")
|
||||
for source, destination, _ in records:
|
||||
if destination != source:
|
||||
if destination.is_symlink():
|
||||
destination.unlink()
|
||||
shutil.copy2(source, destination)
|
||||
def checksum(path):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
inventory = {"platform": "linux", "worker_sha256": checksum(worker),
|
||||
"libraries": [{"path": path.name, "bytes": path.stat().st_size, "sha256": checksum(path)}
|
||||
for _, path, _ in sorted(records)]}
|
||||
temporary = build / "runtime-libraries.json.partial"
|
||||
temporary.write_text(json.dumps(inventory, indent=2), encoding="utf-8")
|
||||
os.replace(temporary, build / "runtime-libraries.json")
|
||||
|
||||
|
||||
def bundle_windows_crt(build):
|
||||
@@ -90,7 +172,7 @@ 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":
|
||||
if (os.name == "nt" and args.backend != "cpu") or linux_dispatch(args.backend):
|
||||
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)
|
||||
@@ -125,6 +207,8 @@ def main():
|
||||
temporary = directory / "runtime-libraries.json.partial"
|
||||
temporary.write_text(json.dumps(inventory, indent=2), encoding="utf-8")
|
||||
os.replace(temporary, directory / "runtime-libraries.json")
|
||||
if linux_dispatch(args.backend):
|
||||
bundle_linux_libraries(build)
|
||||
print(f"Runtime build complete: {build}")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Prove Linux auto CPU fallback with the Vulkan loader physically absent.
|
||||
|
||||
Run as root on x86_64 Linux with util-linux unshare/mount/umount and chroot.
|
||||
This tool never escalates privileges. It launches itself inside a private mount
|
||||
namespace and verifies that namespace before changing mount propagation. The
|
||||
sealed chroot includes approved runtime files, standard C/C++ ABI libraries and
|
||||
a read-only Google model, without Vulkan loaders, ICDs, drivers or GPU devices.
|
||||
This tests missing-loader fallback, not GPU execution or distro compatibility.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ABI = ("libc.so.6", "libm.so.6", "libdl.so.2", "libpthread.so.0",
|
||||
"librt.so.1", "libstdc++.so.6", "libgcc_s.so.1")
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
def checksum(path):
|
||||
with Path(path).open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
|
||||
|
||||
def namespace(pid="self"):
|
||||
return Path(f"/proc/{pid}/ns/mnt").stat().st_ino
|
||||
|
||||
|
||||
def isolated_check(args, commands):
|
||||
# Check the still-live outer process as well as its recorded namespace.
|
||||
# A direct child invocation in the outer namespace fails before any mount.
|
||||
outer = namespace(args.outer_pid)
|
||||
require(outer == args.outer_namespace and namespace() != outer,
|
||||
"Private mount namespace was not established; refusing mount changes")
|
||||
subprocess.run([commands["mount"], "--make-rprivate", "/"], check=True)
|
||||
from stage_runtime import verify_runtime
|
||||
worker = args.worker.resolve()
|
||||
dependencies = verify_runtime(worker, True)
|
||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||
artifact = manifest["files"][0]
|
||||
model = args.models.resolve() / artifact["path"]
|
||||
require(model.is_file() and model.stat().st_size == artifact["bytes"]
|
||||
and checksum(model) == artifact["sha256"], "Google model verification failed")
|
||||
base = Path(tempfile.gettempdir()).resolve()
|
||||
root = Path(tempfile.mkdtemp(prefix="searchi-loader-chroot-", dir=base)).resolve()
|
||||
require(root.parent == base and root.stat().st_uid == 0,
|
||||
"Invalid owned chroot directory")
|
||||
mounted = []
|
||||
try:
|
||||
app = root / "app"
|
||||
app.mkdir()
|
||||
for source in [worker, *dependencies]:
|
||||
destination = app / source.name
|
||||
shutil.copy2(source, destination)
|
||||
require(checksum(source) == checksum(destination), "Copied runtime checksum mismatch")
|
||||
abi_dir = root / "lib/x86_64-linux-gnu"
|
||||
abi_dir.mkdir(parents=True)
|
||||
for name in ABI:
|
||||
shutil.copy2(Path("/lib/x86_64-linux-gnu") / name, abi_dir / name)
|
||||
(root / "lib64").mkdir()
|
||||
shutil.copy2("/lib64/ld-linux-x86-64.so.2", root / "lib64/ld-linux-x86-64.so.2")
|
||||
(root / "models").mkdir()
|
||||
mounted_model = root / "models/model.gguf"
|
||||
mounted_model.touch()
|
||||
proc = root / "proc"
|
||||
proc.mkdir()
|
||||
subprocess.run([commands["mount"], "--bind", str(model), str(mounted_model)], check=True)
|
||||
mounted.append(mounted_model)
|
||||
subprocess.run([commands["mount"], "-o", "remount,bind,ro", str(mounted_model)], check=True)
|
||||
subprocess.run([commands["mount"], "-t", "proc", "-o", "ro,nosuid,nodev,noexec", "proc", str(proc)], check=True)
|
||||
mounted.append(proc)
|
||||
require(not (root / "dev").exists() and not (root / "etc").exists(),
|
||||
"Unexpected devices or driver configuration in chroot")
|
||||
require({path.name for path in abi_dir.iterdir()} == set(ABI), "Unexpected system library")
|
||||
require({path.name for path in app.iterdir()} == {worker.name, *(p.name for p in dependencies)},
|
||||
"Unexpected runtime file")
|
||||
# The worker runs with no inherited loader/ggml variables and no CPU
|
||||
# override: the absence of libvulkan must cause its actual auto fallback.
|
||||
environment = {"PATH": "/usr/bin:/bin", "LANG": "C.UTF-8"}
|
||||
process = subprocess.Popen([commands["chroot"], str(root), "/app/searchi-gemma-worker",
|
||||
"--model", "/models/model.gguf", "--threads", "1", "--device", "auto"],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, encoding="utf-8", env=environment)
|
||||
try:
|
||||
stdout, stderr = process.communicate(json.dumps({"op": "code_query",
|
||||
"text": "retry failed requests with exponential backoff"}) + "\n", timeout=180)
|
||||
except BaseException:
|
||||
process.kill()
|
||||
process.wait(timeout=10)
|
||||
raise
|
||||
require(process.returncode == 0, f"Worker failed: {stderr[-4096:]}")
|
||||
lines = [json.loads(line) for line in stdout.splitlines()]
|
||||
require(len(lines) == 2, "Unexpected protocol output")
|
||||
hello, result = lines
|
||||
require(hello.get("ready") and hello.get("device") == "cpu"
|
||||
and not hello.get("vulkan_module_loaded") and result.get("ok")
|
||||
and result.get("device") == "cpu", "Expected auto fallback to CPU without Vulkan loading")
|
||||
vectors = result.get("vectors", [])
|
||||
require(len(vectors) == 1 and len(vectors[0]) == 768
|
||||
and all(isinstance(v, (float, int)) and math.isfinite(v) for v in vectors[0]),
|
||||
"Invalid real Google vector")
|
||||
norm = math.sqrt(sum(value * value for value in vectors[0]))
|
||||
require(abs(norm - 1) < .001, "Google vector is not normalized")
|
||||
evidence = {"platform": "linux", "device_requested": "auto", "forced_cpu": False,
|
||||
"gpu_execution": False, "vulkan_loader_present": False,
|
||||
"seal": {"system_abi": list(ABI), "proc_read_only": True, "model_read_only": True,
|
||||
"private_mount_namespace_verified": True, "gpu_devices": False,
|
||||
"icd_manifests": False, "environment": environment},
|
||||
"worker_sha256": checksum(worker), "model_sha256": checksum(model),
|
||||
"hello": hello, "dimensions": len(vectors[0]), "norm": norm,
|
||||
"limitations": ["Actual GPU and driver recovery untested", "Distribution ABI compatibility untested"]}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(evidence, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"auto_without_loader": "cpu", "cpu_module": hello["cpu_backend_module"],
|
||||
"vulkan_loaded": False}))
|
||||
finally:
|
||||
failures = []
|
||||
for path in reversed(mounted):
|
||||
result = subprocess.run([commands["umount"], str(path)], check=False)
|
||||
if result.returncode:
|
||||
failures.append(str(path))
|
||||
# Never recursively remove a directory containing a live bind mount.
|
||||
require(not failures, f"Mount cleanup failed; preserved owned directory {root}")
|
||||
require(root.parent == base and root.name.startswith("searchi-loader-chroot-"),
|
||||
"Refusing cleanup outside owned temporary directory")
|
||||
shutil.rmtree(root)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--worker", type=Path, required=True)
|
||||
parser.add_argument("--models", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--outer-pid", type=int, help=argparse.SUPPRESS)
|
||||
parser.add_argument("--outer-namespace", type=int, help=argparse.SUPPRESS)
|
||||
args = parser.parse_args()
|
||||
require(sys.platform.startswith("linux") and platform.machine().lower() in {"amd64", "x86_64"},
|
||||
"Run this check on x86_64 Linux")
|
||||
require(os.geteuid() == 0, "Run explicitly as root; this tool does not escalate privileges")
|
||||
commands = {name: shutil.which(name) for name in ("unshare", "mount", "umount", "chroot")}
|
||||
require(all(commands.values()), "Install util-linux mount tools and chroot")
|
||||
if args.outer_pid is not None or args.outer_namespace is not None:
|
||||
require(args.outer_pid is not None and args.outer_namespace is not None,
|
||||
"Incomplete namespace handoff")
|
||||
isolated_check(args, commands)
|
||||
else:
|
||||
subprocess.run([commands["unshare"], "--mount", "--fork", sys.executable, str(Path(__file__).resolve()),
|
||||
"--worker", str(args.worker.resolve()), "--models", str(args.models.resolve()),
|
||||
"--output", str(args.output.resolve()), "--outer-pid", str(os.getpid()),
|
||||
"--outer-namespace", str(namespace())], check=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Exercise a real Linux distribution worker on CPU, outside its build directory.
|
||||
|
||||
This checks genuine Google inference and origin-relative runtime packaging.
|
||||
It does not certify GPU operation, older processors or a Linux desktop.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def sha256(path):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--worker", type=Path, required=True)
|
||||
parser.add_argument("--models", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--baseline", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if os.name != "posix" or not Path("/proc/self/exe").exists():
|
||||
parser.error("Run this check natively on Linux")
|
||||
# Reuse exactly the staging verifier: hashes and bounded inventory names
|
||||
# must agree with what enters a distribution.
|
||||
from stage_runtime import verify_runtime
|
||||
dependencies = verify_runtime(args.worker.resolve(), True)
|
||||
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
|
||||
model = args.models.resolve() / manifest["files"][0]["path"]
|
||||
if model.stat().st_size != manifest["files"][0]["bytes"] or sha256(model) != manifest["files"][0]["sha256"]:
|
||||
raise RuntimeError("Google model checksum mismatch")
|
||||
with tempfile.TemporaryDirectory(prefix="searchi-linux-smoke-") as temporary:
|
||||
target = Path(temporary)
|
||||
copied = []
|
||||
for source in [args.worker.resolve(), *dependencies]:
|
||||
# Only exact approved CPU module names are omitted. Core libraries
|
||||
# and GPU support remain present in the baseline extraction.
|
||||
if args.baseline and source.name.startswith("libggml-cpu-") and source.name != "libggml-cpu-x64.so":
|
||||
continue
|
||||
destination = target / source.name
|
||||
shutil.copy2(source, destination)
|
||||
if sha256(destination) != sha256(source):
|
||||
raise RuntimeError("Extracted runtime checksum mismatch")
|
||||
copied.append(source.name)
|
||||
worker = target / args.worker.name
|
||||
dynamic = subprocess.check_output(["readelf", "-d", str(worker)], text=True)
|
||||
if "libvulkan" in dynamic or "libggml-vulkan" in dynamic:
|
||||
raise RuntimeError("Worker requires Vulkan at process startup; CPU fallback cannot work without the loader")
|
||||
if "$ORIGIN" not in dynamic:
|
||||
raise RuntimeError("Worker lacks origin-relative runtime lookup")
|
||||
environment = dict(os.environ, SEARCHI_FORCE_CPU="1", GGML_DISABLE_VULKAN="1")
|
||||
environment.pop("LD_LIBRARY_PATH", None)
|
||||
environment.pop("GGML_BACKEND_PATH", None)
|
||||
started = time.perf_counter()
|
||||
process = subprocess.Popen([str(worker), "--model", str(model), "--threads", "1", "--device", "cpu"],
|
||||
cwd=target, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=True, encoding="utf-8", env=environment)
|
||||
try:
|
||||
stdout, stderr = process.communicate(json.dumps({"op": "code_query", "text": "retry failed requests with exponential backoff"}) + "\n", timeout=180)
|
||||
except BaseException:
|
||||
process.kill()
|
||||
process.wait(timeout=10)
|
||||
raise
|
||||
elapsed_ms = (time.perf_counter() - started) * 1000
|
||||
if process.returncode:
|
||||
raise RuntimeError(f"Worker failed ({process.returncode}): {stderr[-4096:]}")
|
||||
lines = [json.loads(line) for line in stdout.splitlines()]
|
||||
if len(lines) != 2:
|
||||
raise RuntimeError("Unexpected worker protocol output")
|
||||
hello, result = lines
|
||||
if not hello.get("ready") or hello.get("device") != "cpu" or not result.get("ok") or result.get("device") != "cpu":
|
||||
raise RuntimeError("Expected genuine ready CPU inference")
|
||||
if hello.get("vulkan_module_loaded"):
|
||||
raise RuntimeError("CPU check loaded a Vulkan provider")
|
||||
vectors = result.get("vectors")
|
||||
vector = vectors[0] if isinstance(vectors, list) and len(vectors) == 1 else None
|
||||
if not isinstance(vector, list) or len(vector) != 768 or not all(isinstance(value, (int, float)) and math.isfinite(value) for value in vector):
|
||||
raise RuntimeError("Invalid Google embedding vector")
|
||||
norm = math.sqrt(sum(value * value for value in vector))
|
||||
if abs(norm - 1) > 0.001:
|
||||
raise RuntimeError("Google vector is not normalized")
|
||||
module = hello.get("cpu_backend_module", "")
|
||||
if args.baseline and module != "libggml-cpu-x64.so":
|
||||
raise RuntimeError(f"Baseline extraction selected unexpected CPU module: {module}")
|
||||
evidence = {"platform": "linux", "cpu_only": True, "baseline_only_extraction": args.baseline,
|
||||
"worker_sha256": sha256(args.worker), "model_sha256": sha256(model),
|
||||
"files": sorted(copied), "hello": hello, "dimensions": len(vector), "norm": norm,
|
||||
"startup_query_exit_ms": elapsed_ms,
|
||||
"timing_scope": "single diagnostic smoke sample; not a performance benchmark",
|
||||
"worker_dynamic_section": dynamic,
|
||||
"limitations": ["GPU untested", "older physical CPUs untested", "desktop and installer untested", "system-loader absence not physically exercised"]}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(evidence, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"ready": True, "cpu_module": module, "dimensions": 768, "baseline": args.baseline}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -15,6 +15,11 @@ 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 library_name(name):
|
||||
return isinstance(name, str) and bool(re.fullmatch(
|
||||
r"(?:[A-Za-z0-9_-]+\.dll|lib[A-Za-z0-9_-]+\.so(?:\.\d+)*)", name))
|
||||
|
||||
|
||||
def _is_link(path):
|
||||
"""Reject Windows reparse points, including junctions, on Python 3.11+."""
|
||||
path = Path(path)
|
||||
@@ -208,7 +213,7 @@ def _previous_owned(destination):
|
||||
raise RuntimeError("Invalid previous runtime inventory")
|
||||
for record in records:
|
||||
name = record.get("path") if isinstance(record, dict) else None
|
||||
if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+\.dll", name):
|
||||
if not library_name(name):
|
||||
raise RuntimeError("Invalid previous runtime library path")
|
||||
names.add(name)
|
||||
return names
|
||||
@@ -345,7 +350,7 @@ def verify_runtime(source, required=False):
|
||||
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:
|
||||
if not library_name(name) or name.lower() in names:
|
||||
raise RuntimeError("Invalid or duplicate runtime library path")
|
||||
names.add(name.lower())
|
||||
artifact = source.parent / name
|
||||
@@ -361,8 +366,17 @@ def verify_runtime(source, required=False):
|
||||
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")
|
||||
if required:
|
||||
if (source.suffix == ".exe") == (manifest.get("platform") == "linux"):
|
||||
raise RuntimeError("Runtime inventory platform does not match worker")
|
||||
if manifest.get("platform") == "linux":
|
||||
core = {"ggml", "ggml-base", "llama", "mtmd"}
|
||||
present = {name for name in core if any(re.fullmatch(rf"lib{re.escape(name)}\.so(?:\.\d+)*", file) for file in names)}
|
||||
complete = present == core and {"libggml-cpu-x64.so", "libggml-vulkan.so"}.issubset(names)
|
||||
else:
|
||||
complete = ({"ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll"} | CRT_LIBRARIES).issubset(names)
|
||||
if not complete:
|
||||
raise RuntimeError("Runtime inventory lacks required CPU/GPU libraries")
|
||||
return [inventory, *paths]
|
||||
|
||||
|
||||
@@ -401,7 +415,8 @@ 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"})
|
||||
required_inventory = (os.name == "nt" or os.sys.platform.startswith("linux")) and args.backend in {"auto", "vulkan"}
|
||||
dependencies = verify_runtime(source, required_inventory)
|
||||
crt_notice = source.parent / "MSVC-REDISTRIBUTION.txt"
|
||||
if os.name == "nt" and args.backend != "cpu":
|
||||
if not crt_notice.is_file():
|
||||
@@ -425,7 +440,7 @@ def main():
|
||||
sources.extend((path, f"embeddinggemma2/{path.name}") for path in artifacts)
|
||||
|
||||
def validate(candidate):
|
||||
verify_runtime(candidate / source.name, os.name == "nt" and args.backend in {"auto", "vulkan"})
|
||||
verify_runtime(candidate / source.name, required_inventory)
|
||||
if model_manifest:
|
||||
for artifact, spec in zip(artifacts, model_manifest["files"], strict=True):
|
||||
if _record(candidate / "embeddinggemma2" / artifact.name) != (spec["bytes"], spec["sha256"]):
|
||||
|
||||
@@ -1,11 +1,87 @@
|
||||
"""Redistributable preparation tests; no native builds or GPU execution."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import build_runtime
|
||||
from build_runtime import bundle_windows_crt, WINDOWS_CRT_FILES
|
||||
|
||||
|
||||
class LinuxRuntimeTests(unittest.TestCase):
|
||||
def test_only_normal_linux_vulkan_modes_use_shared_dispatch(self):
|
||||
with patch.object(build_runtime.sys, "platform", "linux"), patch.object(build_runtime.platform, "machine", return_value="x86_64"):
|
||||
for backend in ["auto", "vulkan"]:
|
||||
self.assertTrue(build_runtime.linux_dispatch(backend))
|
||||
for backend in ["cpu", "cuda", "metal"]:
|
||||
self.assertFalse(build_runtime.linux_dispatch(backend))
|
||||
with patch.object(build_runtime.sys, "platform", "darwin"):
|
||||
self.assertFalse(build_runtime.linux_dispatch("auto"))
|
||||
with patch.object(build_runtime.sys, "platform", "linux"), patch.object(build_runtime.platform, "machine", return_value="aarch64"):
|
||||
self.assertFalse(build_runtime.linux_dispatch("auto"))
|
||||
self.assertFalse(build_runtime.linux_dispatch("vulkan"))
|
||||
|
||||
def test_readelf_parses_needed_soname_and_runpath_without_executing_worker(self):
|
||||
output = " 0x1 (NEEDED) Shared library: [libggml.so.0]\n 0xe (SONAME) Library soname: [libggml-cpu-x64.so]\n 0x1d (RUNPATH) Library runpath: [$ORIGIN]\n"
|
||||
with patch.object(build_runtime.subprocess, "check_output", return_value=output) as command:
|
||||
result = build_runtime.elf_dynamic(Path("worker"))
|
||||
self.assertEqual(result, {"NEEDED": ["libggml.so.0"], "SONAME": ["libggml-cpu-x64.so"], "RUNPATH": ["$ORIGIN"]})
|
||||
command.assert_called_once_with(["readelf", "--dynamic", "worker"], text=True)
|
||||
|
||||
def fixture(self, root):
|
||||
root = Path(root)
|
||||
(root / "bin").mkdir()
|
||||
(root / "searchi-gemma-worker").write_bytes(b"worker")
|
||||
for name in ["ggml", "ggml-base", "llama", "mtmd"]:
|
||||
(root / "bin" / f"lib{name}.so").write_bytes(name.encode())
|
||||
for variant in build_runtime.LINUX_CPU_VARIANTS:
|
||||
(root / f"libggml-cpu-{variant}.so").write_bytes(variant.encode())
|
||||
(root / "libggml-vulkan.so").write_bytes(b"vulkan")
|
||||
return root
|
||||
|
||||
def metadata(self, path):
|
||||
result = {"RUNPATH": ["$ORIGIN"], "NEEDED": ["libstdc++.so.6", "libc.so.6"]}
|
||||
if path.parent.name == "bin":
|
||||
result["SONAME"] = [path.name + ".0"]
|
||||
if path.name == "libggml-vulkan.so":
|
||||
result["NEEDED"].append("libvulkan.so.1")
|
||||
return result
|
||||
|
||||
def test_flattened_core_and_bounded_modules_have_hashed_inventory(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = self.fixture(directory)
|
||||
with patch.object(build_runtime, "elf_dynamic", side_effect=self.metadata):
|
||||
build_runtime.bundle_linux_libraries(root)
|
||||
manifest = json.loads((root / "runtime-libraries.json").read_text())
|
||||
self.assertEqual(manifest["platform"], "linux")
|
||||
self.assertEqual(len(manifest["libraries"]), 5 + len(build_runtime.LINUX_CPU_VARIANTS))
|
||||
self.assertTrue((root / "libllama.so.0").is_file())
|
||||
self.assertTrue(all(not (root / item["path"]).is_symlink() for item in manifest["libraries"]))
|
||||
self.assertTrue(all(len(item["sha256"]) == 64 for item in manifest["libraries"]))
|
||||
|
||||
def test_startup_gpu_dependency_and_build_tree_rpath_are_rejected(self):
|
||||
for problem in ["gpu", "rpath", "missing"]:
|
||||
with self.subTest(problem=problem), tempfile.TemporaryDirectory() as directory:
|
||||
root = self.fixture(directory)
|
||||
previous = root / "libllama.so.0"
|
||||
previous.write_bytes(b"previous flattened core")
|
||||
def metadata(path):
|
||||
result = self.metadata(path)
|
||||
if path.name == "searchi-gemma-worker":
|
||||
if problem == "gpu":
|
||||
result["NEEDED"].append("libvulkan.so.1")
|
||||
elif problem == "rpath":
|
||||
result["RUNPATH"] = ["/private/build"]
|
||||
else:
|
||||
result["NEEDED"].append("libunpackaged.so.9")
|
||||
return result
|
||||
with patch.object(build_runtime, "elf_dynamic", side_effect=metadata), self.assertRaises(RuntimeError):
|
||||
build_runtime.bundle_linux_libraries(root)
|
||||
self.assertFalse((root / "runtime-libraries.json").exists())
|
||||
self.assertEqual(previous.read_bytes(), b"previous flattened core")
|
||||
|
||||
|
||||
class CrtTests(unittest.TestCase):
|
||||
def fixture(self, directory):
|
||||
root = Path(directory)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Namespace guards must reject unsafe handoffs before any mount operation."""
|
||||
import argparse
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import linux_loader_smoke
|
||||
|
||||
|
||||
class NamespaceGuardTests(unittest.TestCase):
|
||||
def test_same_namespace_refuses_mount_before_runtime_access(self):
|
||||
args = argparse.Namespace(outer_pid=123, outer_namespace=42)
|
||||
with patch.object(linux_loader_smoke, "namespace", return_value=42), \
|
||||
patch.object(linux_loader_smoke.subprocess, "run") as run:
|
||||
with self.assertRaisesRegex(RuntimeError, "Private mount namespace"):
|
||||
linux_loader_smoke.isolated_check(args, {"mount": "mount"})
|
||||
run.assert_not_called()
|
||||
|
||||
def test_changed_outer_namespace_refuses_mount(self):
|
||||
args = argparse.Namespace(outer_pid=123, outer_namespace=42)
|
||||
with patch.object(linux_loader_smoke, "namespace", return_value=43), \
|
||||
patch.object(linux_loader_smoke.subprocess, "run") as run:
|
||||
with self.assertRaisesRegex(RuntimeError, "Private mount namespace"):
|
||||
linux_loader_smoke.isolated_check(args, {"mount": "mount"})
|
||||
run.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,6 +12,44 @@ import stage_runtime
|
||||
|
||||
|
||||
class StagingTests(unittest.TestCase):
|
||||
def test_linux_inventory_stages_core_and_modules_and_removes_previous_owned_library(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
runtime = root / "build"
|
||||
runtime.mkdir()
|
||||
worker = runtime / "searchi-gemma-worker"
|
||||
worker.write_bytes(b"ELF fixture for integrity, not executable inference")
|
||||
names = ["libggml.so.0", "libggml-base.so.0", "libllama.so.0", "libmtmd.so.0",
|
||||
"libggml-cpu-x64.so", "libggml-vulkan.so"]
|
||||
records = []
|
||||
for name in names:
|
||||
path = runtime / name
|
||||
path.write_bytes(name.encode())
|
||||
records.append({"path": name, "bytes": path.stat().st_size,
|
||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest()})
|
||||
inventory = runtime / "runtime-libraries.json"
|
||||
inventory.write_text(json.dumps({"platform": "linux", "worker_sha256": hashlib.sha256(worker.read_bytes()).hexdigest(), "libraries": records}))
|
||||
dependencies = stage_runtime.verify_runtime(worker, True)
|
||||
destination = root / "resources"
|
||||
destination.mkdir()
|
||||
(destination / "libggml-cpu-old.so").write_bytes(b"old owned backend")
|
||||
(destination / "runtime-libraries.json").write_text(json.dumps({"libraries": [{"path": "libggml-cpu-old.so"}]}))
|
||||
(destination / "note.txt").write_bytes(b"unrelated")
|
||||
stage_runtime.stage_resources(destination, [(worker, worker.name), *[(path, path.name) for path in dependencies]],
|
||||
lambda candidate: stage_runtime.verify_runtime(candidate / worker.name, True))
|
||||
self.assertFalse((destination / "libggml-cpu-old.so").exists())
|
||||
self.assertEqual((destination / "note.txt").read_bytes(), b"unrelated")
|
||||
self.assertEqual(len(stage_runtime.verify_runtime(destination / worker.name, True)), 7)
|
||||
(destination / "libggml-vulkan.so").write_bytes(b"corrupt")
|
||||
with self.assertRaises(RuntimeError):
|
||||
stage_runtime.verify_runtime(destination / worker.name, True)
|
||||
|
||||
def test_runtime_library_names_reject_paths_and_unbounded_suffixes(self):
|
||||
for name in ["../libggml.so", "libggml.so.x", "libggml.so/1", "libggml.so..1", "ggml.dylib"]:
|
||||
with self.subTest(name=name):
|
||||
self.assertFalse(stage_runtime.library_name(name))
|
||||
self.assertTrue(stage_runtime.library_name("libggml-base.so.0.13.0"))
|
||||
|
||||
def resources(self, directory):
|
||||
root = Path(directory)
|
||||
destination = root / "src-tauri/resources"
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Check a real extracted Linux headless candidate using Google CPU inference.
|
||||
|
||||
This is an installed-layout smoke test, not a broad relevance benchmark or
|
||||
clean-machine certification. No GPU provider or network setup is exercised.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MAX_FILE = 256 * 1024 * 1024
|
||||
|
||||
|
||||
def sha256(path):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
|
||||
|
||||
def extract_checked(archive_path, destination):
|
||||
from package_linux import safe_name
|
||||
with tarfile.open(archive_path, "r:gz") as archive:
|
||||
members = archive.getmembers()
|
||||
if not 1 <= len(members) <= 128 or len({item.name for item in members}) != len(members):
|
||||
raise ValueError("Invalid candidate member inventory")
|
||||
for member in members:
|
||||
safe_name(member.name)
|
||||
if not member.isfile() or not 0 < member.size <= MAX_FILE:
|
||||
raise ValueError("Unexpected candidate member")
|
||||
manifest_member = archive.getmember("PACKAGE.json")
|
||||
if manifest_member.size > 65536:
|
||||
raise ValueError("Oversized candidate manifest")
|
||||
manifest = json.load(archive.extractfile(manifest_member))
|
||||
records = manifest["files"]
|
||||
expected = {record["path"]: record for record in records}
|
||||
if (manifest.get("platform") != "linux" or manifest.get("architecture") != "x86_64"
|
||||
or len(expected) != len(records) or set(expected) != {member.name for member in members} - {"PACKAGE.json"}
|
||||
or sum(member.size for member in members) > 1024 * 1024 * 1024):
|
||||
raise ValueError("Candidate inventory does not cover archive")
|
||||
for member in members:
|
||||
target = destination / member.name
|
||||
if not target.resolve().is_relative_to(destination.resolve()):
|
||||
raise ValueError("Candidate path escapes extraction")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with archive.extractfile(member) as source, target.open("xb") as output:
|
||||
shutil.copyfileobj(source, output, length=65536)
|
||||
if member.name != "PACKAGE.json":
|
||||
record = expected[member.name]
|
||||
mode = int(record["mode"], 8)
|
||||
if (target.stat().st_size != record["bytes"] or sha256(target) != record["sha256"]
|
||||
or mode not in {0o644, 0o755} or member.mode != mode):
|
||||
raise ValueError("Candidate bytes or permissions mismatch")
|
||||
target.chmod(mode)
|
||||
return manifest
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--archive", type=Path, required=True)
|
||||
parser.add_argument("--models", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
if os.name != "posix" or not Path("/proc/self/exe").exists():
|
||||
parser.error("Run natively on Linux")
|
||||
models = args.models.resolve()
|
||||
environment = dict(os.environ, SEARCHI_FORCE_CPU="1", GGML_DISABLE_VULKAN="1")
|
||||
environment.pop("LD_LIBRARY_PATH", None)
|
||||
environment.pop("GGML_BACKEND_PATH", None)
|
||||
environment.pop("SEARCHI_FFMPEG_PATH", None)
|
||||
environment.pop("SEARCHI_FFPROBE_PATH", None)
|
||||
timings = {}
|
||||
with tempfile.TemporaryDirectory(prefix="searchi archive QA ") as directory:
|
||||
root = Path(directory)
|
||||
installed = root / "installed"
|
||||
installed.mkdir()
|
||||
manifest = extract_checked(args.archive, installed)
|
||||
cli, worker = installed / "searchi", installed / "searchi-gemma-worker"
|
||||
corpus = root / "corpus"
|
||||
corpus.mkdir()
|
||||
(corpus / "supplier.md").write_text("The supplier partnership may be ended with sixty days written notice.\n", encoding="utf-8")
|
||||
(corpus / "retry.py").write_text("import time\n\ndef fetch_with_backoff(operation):\n delay = 1\n for attempt in range(5):\n try:\n return operation()\n except Exception:\n time.sleep(delay)\n delay *= 2\n", encoding="utf-8")
|
||||
shutil.copy2(ROOT / "crates/searchi-semantic/tests/fixtures/digup-speech.wav", corpus / "speech.wav")
|
||||
db = root / "index.sqlite"
|
||||
|
||||
def run(label, arguments, json_output=False):
|
||||
started = time.perf_counter()
|
||||
output = subprocess.run([str(cli), *map(str, arguments)], cwd=installed, env=environment,
|
||||
check=True, capture_output=True, text=True, encoding="utf-8", timeout=180)
|
||||
timings[label] = (time.perf_counter() - started) * 1000
|
||||
return json.loads(output.stdout) if json_output else output.stdout
|
||||
|
||||
run("help", ["--help"])
|
||||
run("index", ["index", "--db", db, "--root", corpus])
|
||||
# Omit explicit decoder paths to exercise installed helper discovery.
|
||||
report = run("embed", ["embed", "--db", db, "--model-dir", models,
|
||||
"--embedding-worker", worker, "--cpu-threads", "1"], True)
|
||||
if report["media_files"] != 1 or report["media_failures"] != 0 or report["passages"] != 2 or report["chunks"] < 3:
|
||||
raise RuntimeError("Installed mixed document/code/audio indexing failed")
|
||||
model_arguments = ["--model-dir", models, "--embedding-worker", worker, "--cpu-threads", "1"]
|
||||
hits = run("semantic_search", ["search", "--db", db,
|
||||
"Wie beenden wir die Zusammenarbeit mit dem Lieferanten?", "--json", *model_arguments], True)
|
||||
if not hits or hits[0]["filename"] != "supplier.md" or "sixty days" not in hits[0]["snippet"] or not hits[0]["source"]:
|
||||
raise RuntimeError("Installed Google semantic search lost the supplier passage")
|
||||
code = run("code_search", ["search", "--db", db,
|
||||
"recover from transient network failures with increasing delays", "--code-only", "--json", *model_arguments], True)
|
||||
if not code or code[0]["filename"] != "retry.py" or code[0]["source"]["kind"] != "line":
|
||||
raise RuntimeError("Installed scoped code search lost its citation")
|
||||
restricted = run("strict_filter", ["search", "--db", db, "supplier cancellation",
|
||||
"--extension", "pdf", "--json", *model_arguments], True)
|
||||
if restricted:
|
||||
raise RuntimeError("Installed semantic search violated extension filter")
|
||||
evidence = {"platform": "linux-x86_64", "cpu_only": True,
|
||||
"archive_sha256": sha256(args.archive), "archive_bytes": args.archive.stat().st_size,
|
||||
"verified_files": len(manifest["files"]), "extraction_path_contains_spaces": True,
|
||||
"automatic_media_helper_discovery": "passed", "embedding_report": report,
|
||||
"semantic_top_filename": hits[0]["filename"], "code_source": code[0]["source"],
|
||||
"strict_filter": "passed", "command_elapsed_ms": timings,
|
||||
"timing_scope": "single deployment smoke samples; not retrieval/performance benchmarks",
|
||||
"limitations": ["GPU untested", "desktop untested", "clean-machine/ABI coverage untested", "broad retrieval quality untested"]}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(evidence, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps({"passed": True, "verified_files": evidence["verified_files"], "media_files": report["media_files"]}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,259 @@
|
||||
"""Package an already-built Linux x86-64 headless development candidate.
|
||||
|
||||
No artifact is executed. readelf inspects dependency closure; native execution,
|
||||
ABI compatibility, GPU hardware and production installers remain release gates.
|
||||
"""
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MAX_FILE = 256 * 1024 * 1024
|
||||
MAX_TOTAL = 1024 * 1024 * 1024
|
||||
CPU_VARIANTS = {"x64", "sse42", "sandybridge", "ivybridge", "piledriver", "haswell",
|
||||
"skylakex", "cannonlake", "cascadelake", "icelake", "cooperlake", "zen4",
|
||||
"alderlake", "sapphirerapids"}
|
||||
SYSTEM = {"libc.so.6", "libm.so.6", "libdl.so.2", "libpthread.so.0", "librt.so.1",
|
||||
"libmvec.so.1", "libstdc++.so.6", "libgcc_s.so.1", "ld-linux-x86-64.so.2"}
|
||||
|
||||
|
||||
def safe_name(name):
|
||||
if (not isinstance(name, str) or not name or "\\" in name or ":" in name
|
||||
or str(PurePosixPath(name)) != name or PurePosixPath(name).is_absolute()
|
||||
or any(part in {".", ".."} for part in name.split("/"))):
|
||||
raise ValueError("Unsafe package path")
|
||||
return name
|
||||
|
||||
|
||||
def regular(path, parent):
|
||||
path, parent = Path(path).absolute(), Path(parent).resolve()
|
||||
if not path.is_relative_to(parent) or not path.resolve().is_relative_to(parent):
|
||||
raise ValueError("Package source escapes its directory")
|
||||
candidate = path
|
||||
while candidate != parent:
|
||||
if candidate.is_symlink():
|
||||
raise ValueError("Package source contains a symlink")
|
||||
candidate = candidate.parent
|
||||
if not path.is_file() or not 0 < path.stat().st_size <= MAX_FILE:
|
||||
raise ValueError(f"Missing or oversized package source: {path.name}")
|
||||
|
||||
|
||||
def digest(path):
|
||||
with path.open("rb") as stream:
|
||||
return hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
|
||||
|
||||
def elf_x64(path):
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(64)
|
||||
if (len(header) != 64 or header[:7] != b"\x7fELF\x02\x01\x01"
|
||||
or struct.unpack_from("<H", header, 18)[0] != 62
|
||||
or struct.unpack_from("<H", header, 16)[0] not in {2, 3}):
|
||||
raise ValueError(f"Expected Linux x86-64 ELF artifact: {path.name}")
|
||||
|
||||
|
||||
def dynamic(path):
|
||||
output = subprocess.check_output(["readelf", "--dynamic", str(path)], text=True, timeout=30)
|
||||
values = {}
|
||||
for key, value in re.findall(r"\((NEEDED|SONAME|RUNPATH|RPATH)\).*?\[([^\]]+)\]", output):
|
||||
values.setdefault(key, []).append(value)
|
||||
return values
|
||||
|
||||
|
||||
def runtime_files(runtime):
|
||||
inventory = runtime / "runtime-libraries.json"
|
||||
regular(inventory, runtime)
|
||||
if inventory.stat().st_size > 65536:
|
||||
raise ValueError("Oversized runtime inventory")
|
||||
inventory_bytes = inventory.read_bytes()
|
||||
manifest = json.loads(inventory_bytes)
|
||||
records = manifest.get("libraries") if isinstance(manifest, dict) else None
|
||||
if not isinstance(manifest, dict) or manifest.get("platform") != "linux" or not isinstance(records, list) or not 1 <= len(records) <= 64:
|
||||
raise ValueError("Invalid Linux runtime inventory")
|
||||
expected = {"searchi-gemma-worker": {"sha256": manifest.get("worker_sha256")}}
|
||||
core = {}
|
||||
for record in records:
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError("Invalid library record")
|
||||
name = record.get("path")
|
||||
safe_name(name)
|
||||
match = re.fullmatch(r"lib(ggml|ggml-base|llama|mtmd)\.so(?:\.\d+)*", name)
|
||||
if match:
|
||||
if match[1] in core:
|
||||
raise ValueError("Duplicate runtime core library")
|
||||
core[match[1]] = name
|
||||
elif name != "libggml-vulkan.so" and name not in {f"libggml-cpu-{v}.so" for v in CPU_VARIANTS}:
|
||||
raise ValueError("Unapproved runtime library")
|
||||
if name in expected or type(record.get("bytes")) is not int or record["bytes"] <= 0:
|
||||
raise ValueError("Duplicate or invalid runtime library")
|
||||
expected[name] = record
|
||||
if set(core) != {"ggml", "ggml-base", "llama", "mtmd"} or not {"libggml-cpu-x64.so", "libggml-vulkan.so"}.issubset(expected):
|
||||
raise ValueError("Runtime lacks baseline CPU, Vulkan or core library")
|
||||
for name, record in expected.items():
|
||||
path = runtime / name
|
||||
regular(path, runtime)
|
||||
checksum = record.get("sha256")
|
||||
if not isinstance(checksum, str) or not re.fullmatch(r"[0-9a-f]{64}", checksum):
|
||||
raise ValueError("Invalid runtime checksum")
|
||||
if digest(path) != checksum or ("bytes" in record and path.stat().st_size != record["bytes"]):
|
||||
raise ValueError(f"Runtime integrity mismatch: {name}")
|
||||
record["bytes"] = path.stat().st_size
|
||||
elf_x64(path)
|
||||
metadata = dynamic(path)
|
||||
if metadata.get("RPATH") or metadata.get("RUNPATH") != ["$ORIGIN"]:
|
||||
raise ValueError("Runtime requires origin-only RUNPATH")
|
||||
allowed = set(expected) | SYSTEM
|
||||
if name == "libggml-vulkan.so":
|
||||
allowed.add("libvulkan.so.1")
|
||||
if set(metadata.get("NEEDED", [])) - allowed:
|
||||
raise ValueError(f"Unpackaged runtime dependency: {name}")
|
||||
if name in core.values() and metadata.get("SONAME") != [name]:
|
||||
raise ValueError("Core SONAME differs from packaged filename")
|
||||
expected[inventory.name] = {"sha256": hashlib.sha256(inventory_bytes).hexdigest(), "bytes": len(inventory_bytes)}
|
||||
return expected
|
||||
|
||||
|
||||
def media_files(root, media):
|
||||
manifest_path = root / "tools/media-runtime/manifest.json"
|
||||
regular(manifest_path, root)
|
||||
if manifest_path.stat().st_size > 65536:
|
||||
raise ValueError("Oversized media manifest")
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
artifact = manifest["platforms"]["linux-x86_64"]
|
||||
archive_name = safe_name(artifact["archive"])
|
||||
if "/" in archive_name or not archive_name.endswith(".tar.xz"):
|
||||
raise ValueError("Invalid media archive name")
|
||||
archive = media / archive_name
|
||||
regular(archive, media)
|
||||
if archive.stat().st_size != artifact["bytes"] or digest(archive) != artifact["sha256"]:
|
||||
raise ValueError("Media archive provenance mismatch")
|
||||
selected = {}
|
||||
with tarfile.open(archive, "r:xz") as package:
|
||||
members = 0
|
||||
declared_total = 0
|
||||
for member in package:
|
||||
members += 1
|
||||
declared_total += member.size
|
||||
if (members > 10000 or member.size < 0 or member.size > MAX_FILE
|
||||
or declared_total > MAX_TOTAL):
|
||||
raise ValueError("Media archive exceeds member or size ceiling")
|
||||
basename = member.name.replace("\\", "/").split("/")[-1]
|
||||
if basename in {"ffmpeg", "ffprobe"}:
|
||||
target = f"bin/{basename}"
|
||||
elif basename.lower().startswith(("license", "copying")) or basename.lower() in {"readme.txt", "readme.md"}:
|
||||
target = "licenses/" + safe_name(member.name)
|
||||
else:
|
||||
continue
|
||||
if not member.isfile() or not 0 < member.size <= MAX_FILE or target in selected:
|
||||
raise ValueError("Invalid or duplicate media member")
|
||||
path = media / target
|
||||
regular(path, media)
|
||||
with package.extractfile(member) as stream:
|
||||
checksum = hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
if path.stat().st_size != member.size or digest(path) != checksum:
|
||||
raise ValueError(f"Media helper/notice differs from pinned archive: {target}")
|
||||
selected[target] = {"sha256": checksum, "bytes": member.size}
|
||||
if not {"bin/ffmpeg", "bin/ffprobe"}.issubset(selected) or not any(n.startswith("licenses/") for n in selected):
|
||||
raise ValueError("Media archive lacks helpers or notices")
|
||||
selected = {"media-runtime/" + name: (media / name, record) for name, record in selected.items()}
|
||||
selected["media-runtime/manifest.json"] = (manifest_path, {"sha256": digest(manifest_path), "bytes": manifest_path.stat().st_size})
|
||||
return selected
|
||||
|
||||
|
||||
def package(root, executable, runtime, media, output, llama_license=None):
|
||||
root, executable, runtime, media, output = map(Path, (root, executable, runtime, media, output))
|
||||
regular(executable, executable.parent)
|
||||
elf_x64(executable)
|
||||
expected = runtime_files(runtime)
|
||||
cli_metadata = dynamic(executable)
|
||||
if (cli_metadata.get("RPATH") or cli_metadata.get("RUNPATH")
|
||||
or set(cli_metadata.get("NEEDED", [])) - SYSTEM):
|
||||
raise ValueError("CLI has unexpected native dependency or search path")
|
||||
selected = {name: (runtime / name, record) for name, record in expected.items()}
|
||||
selected.update(media_files(root, media))
|
||||
selected["searchi"] = (executable, {"sha256": digest(executable), "bytes": executable.stat().st_size})
|
||||
license_path = Path(llama_license) if llama_license else root / ".searchi/references/llama.cpp/LICENSE"
|
||||
for path, name, parent in [(root / "LICENSE", "LICENSE.txt", root), (license_path, "LLAMA-LICENSE.txt", license_path.parent)]:
|
||||
regular(path, parent)
|
||||
selected[name] = (path, {"sha256": digest(path), "bytes": path.stat().st_size})
|
||||
for name in ("media-runtime/bin/ffmpeg", "media-runtime/bin/ffprobe"):
|
||||
elf_x64(selected[name][0])
|
||||
metadata = dynamic(selected[name][0])
|
||||
if metadata.get("RPATH") or metadata.get("RUNPATH") or set(metadata.get("NEEDED", [])) - SYSTEM:
|
||||
raise ValueError("Media helper has unpackaged dependencies")
|
||||
if sum(record["bytes"] for _, record in selected.values()) > MAX_TOTAL:
|
||||
raise ValueError("Package exceeds total size ceiling")
|
||||
if output.resolve() in {path.resolve() for path, _ in selected.values()}:
|
||||
raise ValueError("Package output would overwrite an input")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile(dir=output.parent, suffix=".partial", delete=False) as temporary:
|
||||
temporary_path = Path(temporary.name)
|
||||
try:
|
||||
records = []
|
||||
with temporary_path.open("wb") as raw, gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed, tarfile.open(fileobj=compressed, mode="w|") as archive:
|
||||
for name, (path, expected_record) in sorted(selected.items()):
|
||||
safe_name(name)
|
||||
regular(path, path.parent)
|
||||
# Bounded immutable snapshot: archive and manifest describe the same bytes.
|
||||
with path.open("rb") as stream:
|
||||
data = stream.read(MAX_FILE + 1)
|
||||
checksum = hashlib.sha256(data).hexdigest()
|
||||
if len(data) != expected_record["bytes"] or checksum != expected_record["sha256"]:
|
||||
raise ValueError(f"Source changed during packaging: {name}")
|
||||
mode = 0o755 if name in {"searchi", "searchi-gemma-worker", "media-runtime/bin/ffmpeg", "media-runtime/bin/ffprobe"} or ".so" in name else 0o644
|
||||
add(archive, name, data, mode)
|
||||
records.append({"path": name, "bytes": len(data), "sha256": checksum, "mode": oct(mode)})
|
||||
instructions = ("searchi — Find what matters. Locally.\n\n"
|
||||
"Extract the entire archive into its own folder. This is a headless development candidate, not a production installer.\n"
|
||||
"Use ./searchi --help. Setup downloads about 865 MB of verified Google model assets:\n"
|
||||
"./searchi setup --model-dir ./models\n"
|
||||
"./searchi index --db ./searchi.sqlite --root /path/to/files\n"
|
||||
"./searchi embed --db ./searchi.sqlite --model-dir ./models --embedding-worker ./searchi-gemma-worker\n"
|
||||
"./searchi search --db ./searchi.sqlite 'what you remember' --model-dir ./models --embedding-worker ./searchi-gemma-worker\n"
|
||||
"Media helpers are discovered automatically beside this CLI. Ordinary search stays local.\n"
|
||||
"The normal runtime includes Vulkan and CPU fallback. Graphics drivers and a compatible system Vulkan loader are optional.\n"
|
||||
"Requires a compatible Linux libc/C++ ABI; clean-machine and distribution coverage remain open.\n").encode()
|
||||
add(archive, "START-HERE.txt", instructions, 0o644)
|
||||
records.append({"path": "START-HERE.txt", "bytes": len(instructions),
|
||||
"sha256": hashlib.sha256(instructions).hexdigest(), "mode": oct(0o644)})
|
||||
add(archive, "PACKAGE.json", json.dumps({"platform": "linux", "architecture": "x86_64",
|
||||
"status": "unsigned headless development candidate; distribution ABI and GPU hardware validation pending",
|
||||
"system_libraries": sorted(SYSTEM), "optional_gpu_loader": "libvulkan.so.1",
|
||||
"files": records}, indent=2).encode(), 0o644)
|
||||
os.replace(temporary_path, output)
|
||||
finally:
|
||||
temporary_path.unlink(missing_ok=True)
|
||||
return "x86_64"
|
||||
|
||||
|
||||
def add(archive, name, data, mode):
|
||||
info = tarfile.TarInfo(safe_name(name))
|
||||
info.size, info.mode, info.mtime = len(data), mode, 0
|
||||
info.uid = info.gid = 0
|
||||
info.uname = info.gname = ""
|
||||
archive.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--executable", type=Path, default=ROOT / "target/release/searchi")
|
||||
parser.add_argument("--runtime", type=Path, default=ROOT / ".searchi/runtime/auto")
|
||||
parser.add_argument("--media", type=Path, default=ROOT / ".searchi/media-runtime")
|
||||
parser.add_argument("--llama-license", type=Path)
|
||||
parser.add_argument("--output", type=Path, default=ROOT / ".searchi/packages/searchi-linux-x86_64.tar.gz")
|
||||
args = parser.parse_args()
|
||||
package(ROOT, args.executable, args.runtime, args.media, args.output, args.llama_license)
|
||||
print(f"Packaged Linux x86_64 development candidate: {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,232 @@
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import package_linux as pack
|
||||
|
||||
|
||||
def elf(path, machine=62):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = bytearray(64)
|
||||
data[:7] = b"\x7fELF\x02\x01\x01"
|
||||
struct.pack_into("<HH", data, 16, 3, machine)
|
||||
path.write_bytes(data)
|
||||
|
||||
|
||||
class LinuxPackageTests(unittest.TestCase):
|
||||
def fixture(self, directory):
|
||||
root = Path(directory)
|
||||
runtime, media = root / "runtime", root / "media"
|
||||
executable = root / "build/searchi"
|
||||
elf(executable)
|
||||
(root / "LICENSE").write_text("application MIT notice")
|
||||
license = root / ".searchi/references/llama.cpp/LICENSE"
|
||||
license.parent.mkdir(parents=True)
|
||||
license.write_text("llama MIT notice")
|
||||
names = ["libggml.so.0", "libggml-base.so.0", "libllama.so.0", "libmtmd.so.0", "libggml-cpu-x64.so", "libggml-vulkan.so"]
|
||||
elf(runtime / "searchi-gemma-worker")
|
||||
records = []
|
||||
for name in names:
|
||||
elf(runtime / name)
|
||||
records.append({"path": name, "bytes": 64, "sha256": pack.digest(runtime / name)})
|
||||
(runtime / "runtime-libraries.json").write_text(json.dumps({"platform": "linux", "worker_sha256": pack.digest(runtime / "searchi-gemma-worker"), "libraries": records}))
|
||||
media.mkdir()
|
||||
elf(media / "bin/ffmpeg")
|
||||
elf(media / "bin/ffprobe")
|
||||
notice = media / "licenses/upstream/LICENSE.txt"
|
||||
notice.parent.mkdir(parents=True)
|
||||
notice.write_text("LGPL notice")
|
||||
archive = media / "verified.tar.xz"
|
||||
with tarfile.open(archive, "w:xz") as output:
|
||||
for name, path in [("upstream/bin/ffmpeg", media / "bin/ffmpeg"), ("upstream/bin/ffprobe", media / "bin/ffprobe"), ("upstream/LICENSE.txt", notice)]:
|
||||
output.add(path, arcname=name)
|
||||
manifest = root / "tools/media-runtime/manifest.json"
|
||||
manifest.parent.mkdir(parents=True)
|
||||
manifest.write_text(json.dumps({"provider": "test-fixture", "release": "fixture", "platforms": {"linux-x86_64": {"archive": archive.name, "bytes": archive.stat().st_size, "sha256": pack.digest(archive)}}}))
|
||||
return root, executable, runtime, media, root / "candidate.tar.gz"
|
||||
|
||||
@staticmethod
|
||||
def metadata(path):
|
||||
if path.name == "searchi-gemma-worker" or path.name.startswith("lib"):
|
||||
metadata = {"RUNPATH": ["$ORIGIN"], "NEEDED": ["libc.so.6"]}
|
||||
if path.name in {"libggml.so.0", "libggml-base.so.0", "libllama.so.0", "libmtmd.so.0"}:
|
||||
metadata["SONAME"] = [path.name]
|
||||
if path.name == "libggml-vulkan.so":
|
||||
metadata["NEEDED"].append("libvulkan.so.1")
|
||||
return metadata
|
||||
return {"NEEDED": ["libc.so.6"]}
|
||||
|
||||
def run_package(self, fixture):
|
||||
with mock.patch.object(pack, "dynamic", side_effect=self.metadata):
|
||||
return pack.package(*fixture)
|
||||
|
||||
def test_archive_integrity_modes_no_userdata_and_reproducibility(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
root, executable, runtime, media, output = fixture
|
||||
(runtime / "settings.json").write_text("private")
|
||||
(media / "index.sqlite").write_text("private")
|
||||
self.run_package(fixture)
|
||||
first = output.read_bytes()
|
||||
self.run_package(fixture)
|
||||
self.assertEqual(first, output.read_bytes())
|
||||
with tarfile.open(output) as archive:
|
||||
self.assertNotIn("settings.json", archive.getnames())
|
||||
manifest = json.load(archive.extractfile("PACKAGE.json"))
|
||||
for record in manifest["files"]:
|
||||
member = archive.getmember(record["path"])
|
||||
data = archive.extractfile(member).read()
|
||||
self.assertEqual(record["sha256"], hashlib.sha256(data).hexdigest())
|
||||
self.assertEqual(member.uid, 0)
|
||||
self.assertEqual(member.mtime, 0)
|
||||
self.assertEqual(archive.getmember("searchi").mode, 0o755)
|
||||
self.assertEqual(archive.getmember("media-runtime/bin/ffmpeg").mode, 0o755)
|
||||
self.assertEqual(archive.getmember("LICENSE.txt").mode, 0o644)
|
||||
|
||||
def test_tampered_runtime_preserves_previous_archive(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
self.run_package(fixture)
|
||||
previous = fixture[-1].read_bytes()
|
||||
(fixture[2] / "libggml-cpu-x64.so").write_bytes(b"tampered")
|
||||
with self.assertRaisesRegex(ValueError, "integrity mismatch"):
|
||||
self.run_package(fixture)
|
||||
self.assertEqual(previous, fixture[-1].read_bytes())
|
||||
|
||||
def test_media_helper_tamper_is_rejected_against_pinned_archive(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
(fixture[3] / "bin/ffmpeg").write_bytes(b"tampered")
|
||||
with self.assertRaisesRegex(ValueError, "differs from pinned archive"):
|
||||
self.run_package(fixture)
|
||||
|
||||
def test_media_archive_provenance_tamper_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
with (fixture[3] / "verified.tar.xz").open("ab") as stream:
|
||||
stream.write(b"tampered")
|
||||
with self.assertRaisesRegex(ValueError, "provenance mismatch"):
|
||||
self.run_package(fixture)
|
||||
|
||||
def test_mixed_architecture_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
elf(fixture[1], machine=183)
|
||||
with self.assertRaisesRegex(ValueError, "x86-64 ELF"):
|
||||
self.run_package(fixture)
|
||||
|
||||
def test_inventory_traversal_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
path = fixture[2] / "runtime-libraries.json"
|
||||
manifest = json.loads(path.read_text())
|
||||
manifest["libraries"][0]["path"] = "../private.so"
|
||||
path.write_text(json.dumps(manifest))
|
||||
with self.assertRaisesRegex(ValueError, "Unsafe package path"):
|
||||
self.run_package(fixture)
|
||||
|
||||
def test_missing_baseline_cpu_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
path = fixture[2] / "runtime-libraries.json"
|
||||
manifest = json.loads(path.read_text())
|
||||
manifest["libraries"] = [r for r in manifest["libraries"] if r["path"] != "libggml-cpu-x64.so"]
|
||||
path.write_text(json.dumps(manifest))
|
||||
with self.assertRaisesRegex(ValueError, "lacks baseline"):
|
||||
self.run_package(fixture)
|
||||
|
||||
def test_worker_cannot_link_vulkan_loader(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
def metadata(path):
|
||||
result = self.metadata(path)
|
||||
if path.name == "searchi-gemma-worker":
|
||||
result["NEEDED"].append("libvulkan.so.1")
|
||||
return result
|
||||
with mock.patch.object(pack, "dynamic", side_effect=metadata), self.assertRaisesRegex(ValueError, "Unpackaged runtime"):
|
||||
pack.package(*fixture)
|
||||
|
||||
def test_external_runtime_search_path_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
with mock.patch.object(pack, "dynamic", return_value={"RUNPATH": ["/build/lib"]}), self.assertRaisesRegex(ValueError, "origin-only"):
|
||||
pack.package(*fixture)
|
||||
|
||||
def test_glibc_vector_math_is_declared_but_unknown_cli_dependency_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
def vector_math(path):
|
||||
result = self.metadata(path)
|
||||
if path.name == "searchi":
|
||||
result["NEEDED"].append("libmvec.so.1")
|
||||
return result
|
||||
with mock.patch.object(pack, "dynamic", side_effect=vector_math):
|
||||
pack.package(*fixture)
|
||||
with tarfile.open(fixture[-1]) as archive:
|
||||
manifest = json.load(archive.extractfile("PACKAGE.json"))
|
||||
self.assertIn("libmvec.so.1", manifest["system_libraries"])
|
||||
previous = fixture[-1].read_bytes()
|
||||
def unknown(path):
|
||||
result = vector_math(path)
|
||||
if path.name == "searchi":
|
||||
result["NEEDED"].append("libunpackaged.so.1")
|
||||
return result
|
||||
with mock.patch.object(pack, "dynamic", side_effect=unknown), self.assertRaisesRegex(ValueError, "unexpected native dependency"):
|
||||
pack.package(*fixture)
|
||||
self.assertEqual(previous, fixture[-1].read_bytes())
|
||||
|
||||
def test_readelf_parser_is_nonexecuting_and_preserves_dependencies(self):
|
||||
with mock.patch.object(pack.subprocess, "check_output", return_value="(NEEDED) Shared library: [libc.so.6]\n(RUNPATH) Library runpath: [$ORIGIN]\n") as process:
|
||||
self.assertEqual(pack.dynamic(Path("candidate")), {"NEEDED": ["libc.so.6"], "RUNPATH": ["$ORIGIN"]})
|
||||
self.assertEqual(process.call_args.args[0], ["readelf", "--dynamic", "candidate"])
|
||||
|
||||
def test_missing_pinned_media_notice_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
(fixture[3] / "licenses/upstream/LICENSE.txt").unlink()
|
||||
with self.assertRaisesRegex(ValueError, "Missing or oversized"):
|
||||
self.run_package(fixture)
|
||||
|
||||
def test_output_cannot_overwrite_cli(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root, executable, runtime, media, _ = self.fixture(directory)
|
||||
original = executable.read_bytes()
|
||||
with self.assertRaisesRegex(ValueError, "overwrite an input"):
|
||||
self.run_package((root, executable, runtime, media, executable))
|
||||
self.assertEqual(original, executable.read_bytes())
|
||||
|
||||
def test_changed_source_during_packaging_preserves_previous_archive(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
self.run_package(fixture)
|
||||
previous = fixture[-1].read_bytes()
|
||||
original_add = pack.add
|
||||
def mutate(archive, name, data, mode):
|
||||
original_add(archive, name, data, mode)
|
||||
if name == "LICENSE.txt":
|
||||
fixture[1].write_bytes(b"changed after preflight")
|
||||
with mock.patch.object(pack, "add", side_effect=mutate), self.assertRaisesRegex(ValueError, "Source changed"):
|
||||
self.run_package(fixture)
|
||||
self.assertEqual(previous, fixture[-1].read_bytes())
|
||||
|
||||
def test_unapproved_plugin_rejected_even_with_valid_hash(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
fixture = self.fixture(directory)
|
||||
runtime = fixture[2]
|
||||
elf(runtime / "libarbitrary.so")
|
||||
inventory = runtime / "runtime-libraries.json"
|
||||
manifest = json.loads(inventory.read_text())
|
||||
manifest["libraries"].append({"path": "libarbitrary.so", "bytes": 64, "sha256": pack.digest(runtime / "libarbitrary.so")})
|
||||
inventory.write_text(json.dumps(manifest))
|
||||
with self.assertRaisesRegex(ValueError, "Unapproved runtime library"):
|
||||
self.run_package(fixture)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user