"""Build a Windows desktop archive from already compiled, staged resources. This packages an unsigned development build; it does not publish or certify an installer. No model cache, user settings or local index enters the archive. """ import argparse import hashlib import json import os import re from pathlib import Path import struct import tempfile import zipfile ROOT = Path(__file__).resolve().parents[1] CRT_LIBRARIES = {"concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll", "msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vccorlib140.dll", "vcruntime140.dll", "vcruntime140_1.dll", "vcruntime140_threads.dll"} def pe_machine(path): with path.open("rb") as stream: header = stream.read(64) if len(header) != 64 or header[:2] != b"MZ": raise ValueError(f"Not a Windows executable: {path.name}") offset = struct.unpack_from(" path.stat().st_size - 6: raise ValueError(f"Invalid executable header: {path.name}") stream.seek(offset) signature = stream.read(6) if signature[:4] != b"PE\0\0": raise ValueError(f"Invalid PE signature: {path.name}") return struct.unpack_from(" 65536: raise ValueError("Required runtime dependency inventory is missing or oversized") inventory_bytes = inventory.read_bytes() manifest = json.loads(inventory_bytes) if not isinstance(manifest, dict): raise ValueError("Invalid runtime dependency inventory") libraries = manifest.get("libraries") if not isinstance(libraries, list) or not 1 <= len(libraries) <= 64: raise ValueError("Invalid runtime dependency inventory") expected = {"searchi-gemma-worker.exe": {"sha256": manifest.get("worker_sha256")}} for record in libraries: if not isinstance(record, dict): raise ValueError("Invalid runtime library metadata") name = record.get("path", "") if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z0-9_-]+\.dll", name) or name.lower() in {key.lower() for key in expected}: raise ValueError("Invalid or duplicate runtime library path") if type(record.get("bytes")) is not int or record["bytes"] <= 0: raise ValueError("Invalid runtime library size") expected[name] = record if not ({"ggml-cpu-x64.dll", "ggml-vulkan.dll", "vulkan-1.dll", "ggml.dll", "ggml-base.dll", "llama.dll", "mtmd.dll"} | CRT_LIBRARIES).issubset(expected): raise ValueError("Runtime inventory lacks required CPU/GPU libraries") expected[inventory.name] = {"bytes": len(inventory_bytes), "sha256": hashlib.sha256(inventory_bytes).hexdigest()} for name, record in expected.items(): path = resources / name if path.is_symlink() or not path.resolve().is_relative_to(resources.resolve()) or not path.is_file(): raise ValueError(f"Required runtime library is missing or outside staging: {name}") checksum = record.get("sha256", "") if not isinstance(checksum, str) or not re.fullmatch(r"[0-9a-f]{64}", checksum): raise ValueError("Invalid runtime checksum") with path.open("rb") as stream: if hashlib.file_digest(stream, "sha256").hexdigest() != checksum: raise ValueError(f"Runtime checksum mismatch: {name}") if "bytes" in record and path.stat().st_size != record["bytes"]: raise ValueError(f"Runtime size mismatch: {name}") selected = [(executable, "searchi.exe"), (root / "LICENSE", "LICENSE.txt")] for name in ["searchi-gemma-worker.exe", "LLAMA-LICENSE.txt", "MSVC-REDISTRIBUTION.txt", "vulkan-1.dll", "VULKAN-LICENSE.txt", "media-runtime/manifest.json", "media-runtime/bin/ffmpeg.exe", "media-runtime/bin/ffprobe.exe", "licenses/Inter-OFL.txt", "licenses/Fluent-MIT.txt", "media-runtime/licenses/LICENSE.txt"]: selected.append((resources / name, name)) selected.append((inventory, inventory.name)) selected_names = {name for _, name in selected} selected.extend((resources / name, name) for name in expected if name not in selected_names) for directory in ["licenses", "media-runtime/licenses"]: notices = list((resources / directory).rglob("*.txt")) if not notices: raise ValueError(f"Required notices are missing: {directory}") selected_names = {name for _, name in selected} selected.extend((path, path.relative_to(resources).as_posix()) for path in notices if path.relative_to(resources).as_posix() not in selected_names) machine = pe_machine(executable) architecture = {0x8664: "x86_64", 0xAA64: "aarch64"}.get(machine) if architecture is None: raise ValueError("Unsupported Windows architecture") for path, name in selected: if not path.is_file(): raise ValueError(f"Required package file is missing: {name}") if path != executable and path != root / "LICENSE" and not path.resolve().is_relative_to(resources.resolve()): raise ValueError("Resource resolves outside the staging directory") if name.endswith((".exe", ".dll")) and pe_machine(path) != machine: raise ValueError(f"Executable architecture differs: {name}") records = [] 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: with zipfile.ZipFile(temporary_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive: for path, name in selected: # Hash and count the same blocks written to the archive: a # second source read or subsequent stat can describe different # bytes when a staged file changes during release preparation. checksum = hashlib.sha256() count = 0 with path.open("rb") as stream, archive.open(name, "w", force_zip64=True) as entry: while block := stream.read(1024 * 1024): entry.write(block) checksum.update(block) count += len(block) records.append({"path": name, "bytes": count, "sha256": checksum.hexdigest()}) if name in expected and (checksum.hexdigest() != expected[name]["sha256"] or ("bytes" in expected[name] and count != expected[name]["bytes"])): raise ValueError(f"Runtime changed while packaging: {name}") archive.writestr("PACKAGE.json", json.dumps({"platform": "windows", "architecture": architecture, "status": "unsigned development package; clean-machine verification pending", "files": records}, indent=2)) archive.writestr("START-HERE.txt", "searchi — Find what matters. Locally.\n\n" "Extract the entire folder, then open searchi.exe. Keep its helpers alongside it.\n" "Windows needs WebView2. The Microsoft C++ runtime ships with these helpers; Visual Studio is not required.\n" "First launch downloads approximately 865 MB of verified Google model files.\n" "Choose Add folder to index your own files; ordinary search stays local.\n" "This is an unsigned development build, not a certified production release.\n") os.replace(temporary_path, output) finally: temporary_path.unlink(missing_ok=True) return architecture def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--executable", type=Path, default=ROOT / "apps/desktop/src-tauri/target/release/searchi-desktop.exe") parser.add_argument("--output", type=Path, default=ROOT / ".searchi/packages/searchi-windows.zip") args = parser.parse_args() architecture = package(ROOT, args.executable, args.output) print(f"Packaged Windows {architecture}: {args.output}") if __name__ == "__main__": main()