build: add checked Windows desktop archive packaging
This commit is contained in:
1 parent
99c423c0fd
commit
d9336db1d5
5 files changed
+187
No files matched your search
@@ -0,0 +1,90 @@
|
||||
"""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", "media-runtime/manifest.json",
|
||||
"media-runtime/bin/ffmpeg.exe", "media-runtime/bin/ffprobe.exe"]:
|
||||
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.extend((path, path.relative_to(resources).as_posix()) for path in notices)
|
||||
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") 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:
|
||||
with path.open("rb") as stream:
|
||||
checksum = hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
archive.write(path, name)
|
||||
records.append({"path": name, "bytes": path.stat().st_size, "sha256": checksum})
|
||||
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()
|
||||
Reference in new issue
Block a user