From 6441a92062b72015351d3c57b7bc46103bc6303d Mon Sep 17 00:00:00 2001 From: "Adriano (daitarn)" Date: Fri, 6 Nov 2026 11:54:15 +0100 Subject: [PATCH] Archivio sorgente con submodule e vision-engine-version.txt (#109) Co-Authored-By: Claude Sonnet 5.5 --- packaging/windows/tests/test_avvio.py | 10 +++ scripts/archivio_sorgente.py | 108 ++++++++++++++++++++++++ scripts/tests/test_archivio_sorgente.py | 92 ++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 scripts/archivio_sorgente.py create mode 100644 scripts/tests/test_archivio_sorgente.py diff --git a/packaging/windows/tests/test_avvio.py b/packaging/windows/tests/test_avvio.py index 26ec6ed..7caf5ab 100644 --- a/packaging/windows/tests/test_avvio.py +++ b/packaging/windows/tests/test_avvio.py @@ -124,6 +124,16 @@ def test_variabili_del_contratto(avvio, tmp_path): assert len(e[k]) >= 32 +def test_versione_motore_visione_letta_dalla_radice_sorgente(avvio, tmp_path, monkeypatch): + # #109: da sorgente il file vision-engine-version.txt sta nella radice del repo e arriva al worker + prog = tmp_path / "prog" + prog.mkdir() + (prog / "vision-engine-version.txt").write_bytes(b"a" * 40) + monkeypatch.setattr(avvio, "cartella_programma", lambda: prog) + avvio.imposta_ambiente(tmp_path / "dati", "127.0.0.1", {k: "x" * 32 for k in avvio.SEGRETI}) + assert os.environ["VISION_ENGINE_VERSION"] == "a" * 40 + + def test_config_env_generato_una_volta_e_riletto(avvio, tmp_path): dati = tmp_path / "Tielogic" / "TieMeasureFlow" primo = avvio.carica_segreti(dati) diff --git a/scripts/archivio_sorgente.py b/scripts/archivio_sorgente.py new file mode 100644 index 0000000..c644158 --- /dev/null +++ b/scripts/archivio_sorgente.py @@ -0,0 +1,108 @@ +"""Archivio sorgente di TieMeasureFlow per un PC senza git (TieMeasureFlow#109). + +Cosa fa + Crea uno zip del repo a un commit, con dentro anche il submodule vendor/visionsuite (che + `git archive` da solo NON include) e il file vision-engine-version.txt nella radice dello zip. + Quel file contiene lo sha del submodule REGISTRATO nel commit (non la HEAD del checkout del + submodule): packaging/windows/avvio.py lo legge e lo passa al worker di visione come + VISION_ENGINE_VERSION; senza, il worker rifiuta ogni job. Formato: 40 caratteri ASCII, + senza BOM e senza a capo (avvio.py usa .strip(), che non toglie un BOM). + +Come si usa (dalla radice del repo, con il submodule inizializzato) + uv run python scripts/archivio_sorgente.py --out C:/tmp/tmf.zip [--ref HEAD] + Esce 0 e stampa lo sha; esce 1 con un messaggio e senza lasciare lo zip se il submodule + non e' inizializzato o non ha il commit registrato (git submodule update --init vendor/visionsuite). + +Dipende da: git nel PATH, solo libreria standard. +""" +from __future__ import annotations + +import argparse +import io +import os +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +SUBMODULE = "vendor/visionsuite" +FILE_VERSIONE = "vision-engine-version.txt" + + +class ErroreArchivio(RuntimeError): + """Errore chiaro da mostrare all'operatore (niente traceback).""" + + +def _git(cartella: Path, *args: str, binario: bool = False): + """Esegue git in `cartella`; solleva ErroreArchivio con lo stderr se fallisce.""" + r = subprocess.run(["git", "-C", str(cartella), *args], capture_output=True) + if r.returncode != 0: + raise ErroreArchivio(f"git {' '.join(args)} fallito in {cartella}: {r.stderr.decode(errors='replace').strip()}") + return r.stdout if binario else r.stdout.decode().strip() + + +def sha_registrato(repo: Path, ref: str) -> str: + """Sha del submodule registrato nel commit `ref` (gitlink, modo 160000), non la HEAD del checkout.""" + campi = _git(repo, "ls-tree", ref, SUBMODULE).split() + if len(campi) < 3 or campi[0] != "160000": + raise ErroreArchivio(f"{SUBMODULE} non e' un submodule registrato in {ref}") + return campi[2] + + +def crea_archivio(repo: Path, out: Path, ref: str = "HEAD") -> str: + """Scrive lo zip in `out` e ritorna lo sha del submodule; in caso di errore non lascia nulla.""" + repo, out = Path(repo), Path(out) + sha = sha_registrato(repo, ref) + sub = repo / SUBMODULE + # submodule non inizializzato o senza il commit registrato: errore prima di scrivere lo zip + if not (sub / ".git").exists(): + raise ErroreArchivio(f"{SUBMODULE} non e' inizializzato: git submodule update --init {SUBMODULE}") + if subprocess.run(["git", "-C", str(sub), "cat-file", "-e", f"{sha}^{{commit}}"], capture_output=True).returncode != 0: + raise ErroreArchivio(f"{SUBMODULE} non ha il commit {sha}: git submodule update --init {SUBMODULE}") + + zip_repo = _git(repo, "archive", "--format=zip", ref, binario=True) + zip_sub = _git(sub, "archive", "--format=zip", f"--prefix={SUBMODULE}/", sha, binario=True) + + out.parent.mkdir(parents=True, exist_ok=True) + fd, tmp = tempfile.mkstemp(suffix=".zip.tmp", dir=out.parent) # scrittura atomica: o c'e' tutto o niente + os.close(fd) + try: + viste: set[str] = set() + with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as z: + for blob in (zip_repo, zip_sub): + with zipfile.ZipFile(io.BytesIO(blob)) as sorgente: + for info in sorgente.infolist(): + if info.filename in viste: + continue # es. la cartella vendor/visionsuite/ del gitlink, gia' nel primo zip + viste.add(info.filename) + z.writestr(info, sorgente.read(info.filename), compress_type=zipfile.ZIP_DEFLATED) + if FILE_VERSIONE in viste: + raise ErroreArchivio(f"{FILE_VERSIONE} e' gia' nel repo a {ref}: va tolto dal versionamento") + # 40 caratteri ASCII, senza BOM e senza a capo (avvio.py fa .strip(), che non toglie un BOM) + z.writestr(FILE_VERSIONE, sha.encode("ascii")) + os.replace(tmp, out) + except BaseException: + if os.path.exists(tmp): + os.unlink(tmp) + raise + return sha + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Zip del sorgente con submodule e vision-engine-version.txt (#109)") + p.add_argument("--out", required=True, type=Path, help="file .zip da creare") + p.add_argument("--ref", default="HEAD", help="commit da archiviare (default HEAD)") + a = p.parse_args(argv) + radice = Path(__file__).resolve().parents[1] + try: + sha = crea_archivio(radice, a.out, a.ref) + except ErroreArchivio as e: + print(f"ERRORE: {e}", file=sys.stderr) + return 1 + print(f"{a.out} creato; {FILE_VERSIONE} = {sha}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/tests/test_archivio_sorgente.py b/scripts/tests/test_archivio_sorgente.py new file mode 100644 index 0000000..6925781 --- /dev/null +++ b/scripts/tests/test_archivio_sorgente.py @@ -0,0 +1,92 @@ +"""Test di scripts/archivio_sorgente.py (TieMeasureFlow#109): repo temporaneo con un submodule finto. + +Uso: uv run --python 3.13 --extra dev pytest -q scripts/tests/test_archivio_sorgente.py +""" +import importlib.util +import subprocess +import zipfile +from pathlib import Path + +import pytest + +_spec = importlib.util.spec_from_file_location( + "archivio_sorgente", Path(__file__).resolve().parents[1] / "archivio_sorgente.py") +archivio = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(archivio) + +GIT = ["git", "-c", "core.autocrlf=false", "-c", "protocol.file.allow=always", + "-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false"] + + +def _g(cartella, *args): + r = subprocess.run([*GIT, "-C", str(cartella), *args], capture_output=True, text=True) + assert r.returncode == 0, r.stderr + return r.stdout.strip() + + +def _repo(tmp_path, nome): + d = tmp_path / nome + d.mkdir() + _g(d, "init", "-q", "-b", "main") + return d + + +@pytest.fixture +def padre(tmp_path): + """Repo padre con vendor/visionsuite registrato (submodule finto da un repo locale).""" + origine = _repo(tmp_path, "origine") + (origine / "motore.py").write_text("x = 1\n") + _g(origine, "add", ".") + _g(origine, "commit", "-q", "-m", "motore") + p = _repo(tmp_path, "padre") + (p / "app.py").write_text("print('tmf')\n") + _g(p, "add", ".") + _g(p, "commit", "-q", "-m", "app") + _g(p, "submodule", "add", "-q", str(origine), "vendor/visionsuite") + _g(p, "commit", "-q", "-m", "submodule") + return p + + +def test_zip_con_repo_submodule_e_versione(padre, tmp_path): + atteso = _g(padre, "ls-tree", "HEAD", "vendor/visionsuite").split()[2] + out = tmp_path / "o" / "tmf.zip" + assert archivio.crea_archivio(padre, out) == atteso + with zipfile.ZipFile(out) as z: + nomi = z.namelist() + assert "app.py" in nomi and "vendor/visionsuite/motore.py" in nomi + dati = z.read("vision-engine-version.txt") + assert dati == atteso.encode("ascii") and len(dati) == 40 + assert not dati.startswith(b"\xef\xbb\xbf") and b"\n" not in dati # niente BOM, niente a capo + + +def test_sha_resta_quello_registrato_se_il_submodule_avanza(padre, tmp_path): + registrato = _g(padre, "ls-tree", "HEAD", "vendor/visionsuite").split()[2] + sub = padre / "vendor" / "visionsuite" + (sub / "nuovo.py").write_text("y = 2\n") + _g(sub, "add", ".") + _g(sub, "commit", "-q", "-m", "avanti non registrato") + assert _g(sub, "rev-parse", "HEAD") != registrato + out = tmp_path / "tmf.zip" + assert archivio.crea_archivio(padre, out) == registrato + with zipfile.ZipFile(out) as z: + assert z.read("vision-engine-version.txt").decode() == registrato + assert "vendor/visionsuite/nuovo.py" not in z.namelist() # contenuto del commit registrato + + +def test_submodule_non_inizializzato_errore_e_nessuno_zip(padre, tmp_path): + clone = tmp_path / "clone" + _g(tmp_path, "clone", "-q", str(padre), str(clone)) # senza --recurse-submodules + out = tmp_path / "tmf.zip" + with pytest.raises(archivio.ErroreArchivio, match="inizializzato"): + archivio.crea_archivio(clone, out) + assert not out.exists() and not list(tmp_path.glob("*.tmp")) # nessuno zip ne' temporaneo + + +def test_cli_esce_1_con_messaggio(padre, tmp_path, capsys, monkeypatch): + clone = tmp_path / "clone" + _g(tmp_path, "clone", "-q", str(padre), str(clone)) + originale = archivio.crea_archivio + # la CLI lavora sul repo che contiene lo script: la si dirotta sul clone senza submodule + monkeypatch.setattr(archivio, "crea_archivio", lambda r, o, ref="HEAD": originale(clone, o, ref)) + assert archivio.main(["--out", str(tmp_path / "x.zip")]) == 1 + assert "ERRORE" in capsys.readouterr().err