Files
searchi/tools/package_windows.py
T

100 lines
5.1 KiB
Python
Raw Normal View History

"""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
from pathlib import Path
import struct
import tempfile
import zipfile
ROOT = Path(__file__).resolve().parents[1]
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("<I", header, 60)[0]
if offset > 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("<H", signature, 4)[0]
def package(root, executable, output):
resources = root / "apps/desktop/src-tauri/resources"
selected = [(executable, "searchi.exe"), (root / "LICENSE", "LICENSE.txt")]
for name in ["searchi-gemma-worker.exe", "LLAMA-LICENSE.txt", "vulkan-1.dll", "VULKAN-LICENSE.txt", "media-runtime/manifest.json",
"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))
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()})
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 and the applicable Microsoft C++ runtime.\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()