TARGET_SPACE / scripts /build_release.py
RDS777's picture
Upload 134 files
8f4b341 verified
Raw History Blame Contribute Delete
6.12 kB
"""Construit une version livrable du Space : manifest.json à jour, puis un zip des fichiers de la version.
python scripts/build_release.py # manifest + dist/unique-v3.9.zip
python scripts/build_release.py --check # vérifie seulement que l'arbre correspond au manifest
Le manifest liste les fichiers d'exécution (ceux que le panneau du Space vérifie au démarrage, et que
`unique_ext.updater` contrôle avant d'installer un zip) : ceux de la v3.8 plus tout `unique_ext/`.
Le zip contient le manifest, ces fichiers, et les tests et scripts associés. Il s'envoie tel quel dans le Space,
à la main (dossiers dans Files → Upload files) ou, dès la v3.9 en place, en un seul fichier avec
UNIQUE_EXT_INSTALL=<chemin du zip>.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = "mouh/learning/manifest.json"
CORE = ["app.py", "requirements.txt", "mouh/service.py", "mouh/verification/loop.py", "mouh/learning/__init__.py",
"mouh/learning/prompts.py", "mouh/learning/records.py", "mouh/learning/recorder.py", "mouh/learning/memory.py",
"mouh/learning/unique_generator.py", "jobs/train_unique.py",
"mouh/learning/local_store.py", "mouh/learning/local_ingest.py", "mouh/learning/local_trainer.py",
"mouh/learning/dashboard.py",
"mouh/geometry/__init__.py", "mouh/geometry/mesh.py", "mouh/geometry/api.py", "mouh/geometry/sync.py"]
# Frontend VORTEX / SYMBIOZA 3D : tout l'arbre web/symbioza en fichiers texte (page, CSS, modules, three.js vendorisé).
# Seuls les suffixes texte sont hachés : l'empreinte est calculée en UTF-8, un binaire n'y entrerait pas.
WEB_DIR = "web/symbioza"
WEB_SUFFIXES = (".html", ".css", ".js", ".svg", ".json", ".txt")
# v4.1 : fichiers retirés (mode Jobs, maître, alimentation, export GGUF, installation à la volée, v3.14) ; l'updater
# les supprime du Space au moment de l'installation, et les rétablit en cas de retour arrière.
REMOVED = ["mouh/learning/autopilot.py", "jobs/teacher_tasks.py", "jobs/ingest_tasks.py", "unique_ext/autopilot.py",
"unique_ext/install.py", "unique_ext/backends/gguf.py", "unique_ext/jobs/__init__.py",
"unique_ext/jobs/export_gguf.py", "unique_ext/jobs/eval_generators.py",
"unique_ext/remote/__init__.py", "unique_ext/remote/vllm.py", "unique_ext/remote/factory.py",
"unique_ext/remote/replay.py", "unique_ext/control/__init__.py", "unique_ext/control/loop.py",
"unique_ext/control/sources.py", "unique_ext/control/app.py", "tests/test_trispace.py",
"docs/ADR-003-TRI-SPACE.md"]
EXTRA = ["tests/test_learning.py", "tests/test_ext.py", "tests/test_local.py", "scripts/build_release.py",
"docs/ADR-004-LOCAL.md", "docs/SYMBIOZA.md", "tests/test_geometry.py", "tests/test_symbioza.py",
"tests/test_selfint.py", "tests/test_sky_sync.py", "tests/test_frontend.py"]
def normalized_hash(text: str) -> str: # identique à unique_ext.updater.normalized_hash
text = text.replace("\r\n", "\n").lstrip(" \t")
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]
def _tree(rel_dir: str, suffixes: tuple[str, ...]) -> list[str]:
return sorted(str(p.relative_to(ROOT)) for p in (ROOT / rel_dir).rglob("*")
if p.is_file() and "__pycache__" not in p.parts and p.suffix in suffixes)
def runtime_files() -> list[str]:
ext = _tree("unique_ext", (".py", ".md"))
web = _tree(WEB_DIR, WEB_SUFFIXES)
return CORE + ext + web
def build_manifest(version: str) -> dict:
return {"version": version, "files": {rel: normalized_hash((ROOT / rel).read_text(encoding="utf-8"))
for rel in runtime_files()},
"removed": [r for r in REMOVED if not (ROOT / r).exists()]}
def check() -> list[str]:
man = json.loads((ROOT / MANIFEST).read_text(encoding="utf-8"))
problems = [f"{rel} absent du manifest" for rel in runtime_files() if rel not in man["files"]]
for rel, h in sorted(man["files"].items()):
p = ROOT / rel
if not p.exists():
problems.append(f"{rel} manquant")
elif normalized_hash(p.read_text(encoding="utf-8")) != h:
problems.append(f"{rel} différent du manifest {man['version']}")
return problems
def main(argv: list[str] | None = None) -> int:
sys.path.insert(0, str(ROOT))
import unique_ext
major_minor = ".".join(unique_ext.__version__.split(".")[:2])
ap = argparse.ArgumentParser()
ap.add_argument("--version", default=f"v{major_minor}")
ap.add_argument("--out", default=None, help="défaut : dist/unique-<version>.zip")
ap.add_argument("--check", action="store_true")
ap.add_argument("--since", default=None, help="manifest.json de la version en place : zip des seuls fichiers changés")
a = ap.parse_args(argv)
if a.check:
problems = check()
print("\n".join(problems) or "arbre conforme au manifest")
return 1 if problems else 0
man = build_manifest(a.version)
(ROOT / MANIFEST).write_text(json.dumps(man, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
out = Path(a.out) if a.out else ROOT / "dist" / f"unique-{a.version}.zip"
out.parent.mkdir(parents=True, exist_ok=True)
names = sorted(set(runtime_files() + [MANIFEST] + [e for e in EXTRA if (ROOT / e).exists()]))
if a.since: # mise à jour : seulement ce qui a changé (l'updater vérifie que le reste est déjà en place)
before = json.loads(Path(a.since).read_text(encoding="utf-8")).get("files", {})
names = [n for n in names if n == MANIFEST or n in EXTRA or before.get(n) != man["files"].get(n)]
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
for rel in names:
z.write(ROOT / rel, rel)
print(f"{out} : {len(names)} fichiers, version {a.version}")
for rel in names:
print(f" {rel}")
return 0
if __name__ == "__main__":
raise SystemExit(main())