69 lines
3.0 KiB
Python
69 lines
3.0 KiB
Python
"""Package the optional Windows CLI/MCP client; reuse the installed desktop models."""
|
|||
|
|
import argparse
|
||
|
|
import hashlib
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
import tempfile
|
||
|
|
import zipfile
|
||
|
|
|
||
|
|
from package_windows import pe_machine
|
||
|
|
from stage_extraction_worker import verify
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
||
|
|
MAX_FILE = 64 * 1024 * 1024
|
||
|
|
|
||
|
|
|
||
|
|
def package(root, executable, resources, output):
|
||
|
|
root, executable, resources, output = map(Path, (root, executable, resources, output))
|
||
|
|
def read(path):
|
||
|
|
if path.is_symlink() or not path.is_file() or path.absolute() != path.resolve():
|
||
|
|
raise ValueError('Package input missing or linked')
|
||
|
|
with path.open('rb') as stream:
|
||
|
|
data = stream.read(MAX_FILE + 1)
|
||
|
|
if not data or len(data) > MAX_FILE:
|
||
|
|
raise ValueError('Package input empty or oversized')
|
||
|
|
return data
|
||
|
|
cli = read(executable)
|
||
|
|
architecture = {0x8664: 'x86_64', 0xAA64: 'aarch64'}.get(pe_machine(executable))
|
||
|
|
if architecture is None:
|
||
|
|
raise ValueError('Unsupported Windows CLI architecture')
|
||
|
|
expected = verify(resources, root, 'windows', architecture)
|
||
|
|
selected = {'searchi-cli.exe': cli, 'LICENSE.txt': read(root / 'LICENSE')}
|
||
|
|
for name, record in expected.items():
|
||
|
|
data = read(resources / name)
|
||
|
|
if len(data) != record['bytes'] or hashlib.sha256(data).hexdigest() != record['sha256']:
|
||
|
|
raise ValueError('Extraction resource changed while packaging')
|
||
|
|
selected[name] = data
|
||
|
|
# The captured CLI must still match the header used for architecture validation.
|
||
|
|
if read(executable) != cli:
|
||
|
|
raise ValueError('CLI changed while packaging')
|
||
|
|
records = [{'path': name, 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
||
|
|
for name, data in selected.items()]
|
||
|
|
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) as archive:
|
||
|
|
for name, data in selected.items():
|
||
|
|
archive.writestr(name, data)
|
||
|
|
archive.writestr('PACKAGE.json', json.dumps({'platform': 'windows', 'architecture': architecture,
|
||
|
|
'status': 'optional unsigned CLI/MCP client; install desktop for model setup', 'files': records}, indent=2))
|
||
|
|
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, required=True)
|
||
|
|
parser.add_argument('--resources', type=Path, default=ROOT / 'apps/desktop/src-tauri/resources')
|
||
|
|
parser.add_argument('--output', type=Path, default=ROOT / '.searchi/packages/searchi-cli_0.1.0_x64-013.zip')
|
||
|
|
args = parser.parse_args()
|
||
|
|
print(f"Packaged Windows {package(ROOT, args.executable, args.resources, args.output)} CLI: {args.output}")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|
||
|
|
|