Compare commits
4 Commits
8c361c14ae
...
5738920c9c
| Author | SHA1 | Date | |
|---|---|---|---|
| 5738920c9c | |||
| 452862e0f3 | |||
| 999f47092d | |||
| 6441a92062 |
@@ -76,3 +76,6 @@ docker-compose.override.yml
|
||||
|
||||
# Generato da packaging/windows/build.ps1 (TieMeasureFlow#82): moduli-strumento dagli entry point
|
||||
packaging/windows/_strumenti.py
|
||||
|
||||
# #109: scritto da packaging/windows/sviluppo.ps1 (sha del submodule), non va versionato
|
||||
/vision-engine-version.txt
|
||||
|
||||
@@ -4,14 +4,15 @@ Nuitka (che richiede ~20 minuti): stessi quattro server e stesse porte dell'exe
|
||||
8000 backend, 8100 visione, 8765 camera), stessa cartella dati %PROGRAMDATA%\Tielogic\TieMeasureFlow.
|
||||
Prima dell'avvio: ambiente Python 3.13 in build\venv313 (uv sync, solo con -Sincronizza o se manca),
|
||||
tailwind.css e traduzioni .mo ricompilati, DLL di WeasyPrint in build\gtk (scaricate con
|
||||
pdf\prendi-gtk.ps1 se mancano).
|
||||
pdf\prendi-gtk.ps1 se mancano). Scrive vision-engine-version.txt nella radice (sha del submodule
|
||||
vendor/visionsuite) se git c'e'; senza git e senza il file si ferma con un errore chiaro (#109).
|
||||
|
||||
Come si usa (dalla radice del repo):
|
||||
powershell -ExecutionPolicy Bypass -File packaging\windows\sviluppo.ps1 [-Sincronizza] [-Ascolto 0.0.0.0] [-EscludiCamere OECS]
|
||||
Dopo una modifica: Ctrl+C e rilancio (pochi secondi). Template e JS non richiedono altro;
|
||||
classi Tailwind nuove e msgid nuovi vengono ricompilati a ogni avvio.
|
||||
|
||||
Dipende da: uv, npx (Node), la licenza tiemeasureflow di QUESTA macchina in
|
||||
Dipende da: uv, npx (Node), git (opzionale: senza, serve vision-engine-version.txt gia' nella radice), la licenza tiemeasureflow di QUESTA macchina in
|
||||
%PROGRAMDATA%\Tielogic\licenze\tiemeasureflow.json (impronta: avvio.py --impronta).
|
||||
#>
|
||||
param(
|
||||
@@ -47,6 +48,24 @@ if (-not (Test-Path (Join-Path $gtk '*.dll'))) {
|
||||
}
|
||||
$env:WEASYPRINT_DLL_DIRECTORIES = $gtk
|
||||
|
||||
# #109: il worker di visione rifiuta ogni job senza VISION_ENGINE_VERSION, che avvio.py legge da
|
||||
# <radice>\vision-engine-version.txt (come nel pacchetto, build.ps1). Da sorgente lo scrive questo script.
|
||||
$versione = Join-Path $radice 'vision-engine-version.txt'
|
||||
$shaMotore = $null
|
||||
# #109: solo se il submodule e' inizializzato (ha .git): altrimenti git risale al repo padre e darebbe lo sha di TMF
|
||||
if ((Get-Command git -ErrorAction SilentlyContinue) -and (Test-Path (Join-Path $radice 'vendor\visionsuite\.git'))) {
|
||||
$shaMotore = (git -C (Join-Path $radice 'vendor\visionsuite') rev-parse HEAD 2>$null)
|
||||
if ($LASTEXITCODE -ne 0) { $shaMotore = $null }
|
||||
}
|
||||
if ($shaMotore) {
|
||||
# 40 caratteri ASCII, UTF8 senza BOM e senza a capo (avvio.py fa .strip(), che non toglie il BOM)
|
||||
[System.IO.File]::WriteAllText($versione, $shaMotore.Trim(), (New-Object System.Text.UTF8Encoding $false))
|
||||
} elseif (Test-Path $versione) {
|
||||
Write-Host "git non disponibile: uso $versione = $((Get-Content -Raw $versione).Trim())"
|
||||
} else {
|
||||
throw "Manca vision-engine-version.txt e git non puo' leggere vendor\visionsuite. Rimedio: crea l'archivio con 'uv run python scripts/archivio_sorgente.py --out tmf.zip' da un clone con git, oppure scrivi in $versione lo sha del submodule (40 caratteri, ASCII, senza BOM e senza a capo)."
|
||||
}
|
||||
|
||||
if ($EscludiCamere) { $env:VS_CAMERA_ESCLUDI = $EscludiCamere } # #339: stesso percorso camera del PC fiera
|
||||
Set-Location $radice
|
||||
& $py (Join-Path $PSScriptRoot 'avvio.py') --senza-browser --senza-finestre --host $Ascolto # errori in console, niente MessageBox
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Test di scripts/archivio_sorgente.py (TieMeasureFlow#109): repo temporaneo con un submodule finto.
|
||||
|
||||
Uso: uv run --no-project --python 3.13 --with pytest 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
|
||||
Reference in New Issue
Block a user