diff --git a/.gitea/workflows/binarka.yml b/.gitea/workflows/binarka.yml index 77b53eb..32fa422 100644 --- a/.gitea/workflows/binarka.yml +++ b/.gitea/workflows/binarka.yml @@ -56,7 +56,13 @@ jobs: run: uv run --with-requirements requirements-dev.txt --no-project pytest -q - name: Binárka - run: ./build.sh + run: | + # Runner klonuje jeden commit bez značek, takže si git describe + # nemá o co opřít — na tagu mu verzi řekneme rovnou. + if [ "$GITHUB_REF_TYPE" = tag ]; then + export VERSION="$GITHUB_REF_NAME" + fi + ./build.sh - name: Zkouška run: | @@ -74,6 +80,7 @@ jobs: curl -sf -o /dev/null http://127.0.0.1:8321/static/style.css curl -sf -o /dev/null http://127.0.0.1:8321/board test -f /tmp/zkouska/kostky/dice.db + ./dist/kostky --version cat /tmp/server.log - name: Na release diff --git a/.github/workflows/binarky.yml b/.github/workflows/binarky.yml index 33f4b15..9d8d0ef 100644 --- a/.github/workflows/binarky.yml +++ b/.github/workflows/binarky.yml @@ -55,7 +55,13 @@ jobs: run: uv run --with-requirements requirements-dev.txt --no-project pytest -q - name: Binárka - run: ./build.sh + run: | + # Na tagu je verze jistá. Jinde je repozitář naklonovaný mělce + # a build.sh si vystačí s tím, co z gitu vyčte. + if [ "$GITHUB_REF_TYPE" = tag ]; then + export VERSION="$GITHUB_REF_NAME" + fi + ./build.sh - name: Zkouška run: | @@ -75,6 +81,10 @@ jobs: curl -sf "http://127.0.0.1:8123/board" | grep -q "127.0.0.1:8123" test -f zkouska/dice.db + # Verze se musí do binárky opravdu dostat, ne se hlásit jako neznámá. + "${{ matrix.built }}" --version + ! "${{ matrix.built }}" --version | grep -q "neznámá" + echo "Binárka jede." - name: Na release diff --git a/.gitignore b/.gitignore index a9551e5..4e2bc71 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ dist/ build/ .build/ +verze.txt diff --git a/README.md b/README.md index 6073eab..ec6eacd 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,17 @@ Závislosti si skript nese v hlavičce (PEP 723) a `uv` je obstará sám. Server vypíše všechny adresy, na kterých je dostupný: ``` +Kostky v1.1.0 + Počitadlo je dostupné na: → http://10.186.234.182:8000 místní síť http://100.91.0.24:8000 přes Tailscale http://172.17.0.1:8000 virtuální síť ``` +Kterou verzi máš, řekne i `kostky --version`; visí taky v patičce každé +stránky. Když něco nefunguje, je to první věc, na kterou se zeptám. + Na notebooku otevři `/board`, naskenuj QR kód telefonem a hraj. Tabule nabízí i zbylé adresy — když jedna nefunguje, klikni na jinou a QR kód se přepne na ni. Zastavuje se `Ctrl+C`. @@ -138,6 +143,7 @@ při remíze na prvním místě se hraje dál. | `web.py` | Flask aplikace | | `dice.py` | totéž v terminálu, bez ukládání | | `console.py` | aby čeština prošla i windowsovou konzolí | +| `version.py` | která verze to je — z gitu, nebo z binárky | | `build.sh`, `kostky.spec` | stavba binárky | | `.gitea/workflows/` | testy a linuxová binárka doma | | `.github/workflows/` | binárky pro Windows a macOS | @@ -195,6 +201,11 @@ srovnat. ./build.sh ``` +Verzi si `build.sh` vezme z `git describe`, nebo se dá vnutit přes +`VERSION=v1.2.3 ./build.sh`. Zapíše ji do `verze.txt`, `kostky.spec` ji +přibalí a binárka ji pak umí ohlásit i na cizím počítači, kde žádný git není. +Verzi Pythonu, proti kterému se staví, přebíjí `PYTHON_VERSION`. + Staví se proti samostatnému CPythonu od `uv`, ne proti systémovému. Ten je slinkovaný s glibc 2.17; postavené proti Pythonu z Fedory 44 by to chtělo glibc 2.43 a nešlo by spustit skoro nikde — glibc drží zpětnou kompatibilitu, diff --git a/build.sh b/build.sh index 03b120a..1d7606d 100755 --- a/build.sh +++ b/build.sh @@ -12,11 +12,18 @@ # se do ní nedostane, takže kontejner k tomu není potřeba. set -euo pipefail -VERSION=${VERSION:-3.11} +PYTHON_VERSION=${PYTHON_VERSION:-3.11} BUILD=${BUILD:-.build} -uv python install "$VERSION" -uv venv --quiet --managed-python --python "$VERSION" "$BUILD" +# Verze, kterou binárka ohlásí přes --version. V CI ji na tagu vnutíme +# proměnnou, protože tam je repozitář naklonovaný na jeden commit a git +# describe nemá o co se opřít. +VERSION=${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo neznámá)} +printf '%s\n' "$VERSION" > verze.txt +echo "Verze: $VERSION" + +uv python install "$PYTHON_VERSION" +uv venv --quiet --managed-python --python "$PYTHON_VERSION" "$BUILD" # Windows dává spustitelné soubory venvu do Scripts/, zbytek světa do bin/. python="$BUILD/bin/python" diff --git a/dice.py b/dice.py index 8135999..433eb89 100644 --- a/dice.py +++ b/dice.py @@ -8,11 +8,13 @@ Pravidla žijí v `core.Game`, tenhle modul je jen vstup a výstup. Hraje se v paměti, nic se neukládá — na historii je webová aplikace (`web.py`). """ +import sys from os import name, system from tabulate import tabulate import console +import version from core import FINAL_SCORE, Game table_header = ["Hráč", "Skóre"] @@ -130,4 +132,9 @@ def main() -> None: if __name__ == "__main__": console.utf8() + + if {"--version", "-V"} & set(sys.argv[1:]): + print(f"kostky {version.current()}") + sys.exit(0) + main() diff --git a/kostky.spec b/kostky.spec index 68164e8..cffe88e 100644 --- a/kostky.spec +++ b/kostky.spec @@ -5,12 +5,19 @@ Staví se přes `./build.sh`, ne ručně — ta se stará o to, aby výsledek je i na starších systémech, než je ten tvůj. """ +import os import sys +# Verzi zapisuje build.sh těsně před stavbou. Když se pyinstaller pustí +# ručně bez ní, binárka se postaví taky — jen se pak hlásí jako neznámá. +datas = [("templates", "templates"), ("static", "static")] +if os.path.exists("verze.txt"): + datas.append(("verze.txt", ".")) + analysis = Analysis( ["web.py"], pathex=["."], - datas=[("templates", "templates"), ("static", "static")], + datas=datas, noarchive=False, ) diff --git a/static/style.css b/static/style.css index 9284f3e..4331551 100644 --- a/static/style.css +++ b/static/style.css @@ -513,3 +513,11 @@ button { font-size: 0.85em; color: var(--ink); } + +/* Verze v patičce. Má být k nalezení, když ji někdo hledá, a jinak neplést. */ +.verze { + margin: 2rem 0 0.75rem; + text-align: center; + font-size: 0.75rem; + opacity: 0.35; +} diff --git a/templates/base.html b/templates/base.html index 5d8cdb6..a2ef8c5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -8,5 +8,6 @@ {% block body %}{% endblock %} + diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..06f33a0 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,48 @@ +"""Verze se musí poznat i z binárky, kde žádný git není.""" + +import pytest + +import version + + +@pytest.fixture(autouse=True) +def bez_pameti(): + """`current()` si výsledek pamatuje; mezi testy je potřeba čistý stůl.""" + version.current.cache_clear() + yield + version.current.cache_clear() + + +def test_z_binarky_se_cte_pribaleny_soubor(monkeypatch, tmp_path): + """V binárce leží verze.txt v dočasném adresáři vedle šablon.""" + (tmp_path / version.NAME).write_text("v9.9.9\n", encoding="utf-8") + monkeypatch.setattr(version.sys, "frozen", True, raising=False) + monkeypatch.setattr(version.sys, "_MEIPASS", str(tmp_path), raising=False) + + assert version.current() == "v9.9.9" + + +def test_binarka_se_gitu_nepta(monkeypatch, tmp_path): + """Na cizím počítači žádný repozitář není a ptát se nemá koho.""" + monkeypatch.setattr(version.sys, "frozen", True, raising=False) + monkeypatch.setattr(version.sys, "_MEIPASS", str(tmp_path), raising=False) + monkeypatch.setattr(version, "described", + lambda: pytest.fail("binárka se ptala gitu")) + + assert version.current() == version.UNKNOWN + + +def test_ze_zdrojaku_ma_prednost_git(monkeypatch): + monkeypatch.delattr(version.sys, "frozen", raising=False) + monkeypatch.setattr(version, "described", lambda: "v1.2.3-4-gabcdef") + monkeypatch.setattr(version, "bundled", lambda: "v1.0.0") + + assert version.current() == "v1.2.3-4-gabcdef" + + +def test_bez_gitu_a_bez_souboru_zbyde_neznama(monkeypatch): + monkeypatch.delattr(version.sys, "frozen", raising=False) + monkeypatch.setattr(version, "described", lambda: "") + monkeypatch.setattr(version, "bundled", lambda: "") + + assert version.current() == version.UNKNOWN diff --git a/version.py b/version.py new file mode 100644 index 0000000..4e9715f --- /dev/null +++ b/version.py @@ -0,0 +1,49 @@ +"""Která verze počitadla to vlastně je. + +Venku jsou čtyři binárky pro tři systémy a k tomu zdrojáky. Když někdo +napíše, že mu něco nefunguje, je první otázka vždycky stejná — a bez +`--version` na ni nemá jak odpovědět. +""" + +import subprocess +import sys +from functools import lru_cache +from pathlib import Path + +NAME = "verze.txt" +UNKNOWN = "neznámá" + + +def bundled() -> str: + """Verze vypálená do binárky při stavbě. + + `build.sh` ji zapíše do souboru, `kostky.spec` ji přibalí. V binárce + leží vedle šablon v dočasném adresáři, na který ukazuje `sys._MEIPASS`. + """ + root = Path(getattr(sys, "_MEIPASS", Path(__file__).parent)) + try: + return (root / NAME).read_text(encoding="utf-8").strip() + except OSError: + return "" + + +def described() -> str: + """Verze podle gitu. Ze zdrojáků je to přesnější než cokoli zapsaného.""" + try: + done = subprocess.run( + ["git", "describe", "--tags", "--always", "--dirty"], + cwd=Path(__file__).parent, capture_output=True, text=True, + timeout=5, check=True) + except (OSError, subprocess.SubprocessError): + return "" + + return done.stdout.strip() + + +@lru_cache(maxsize=1) +def current() -> str: + """Verze pro výpis. Ptát se gitu má smysl jen mimo binárku.""" + if getattr(sys, "frozen", False): + return bundled() or UNKNOWN + + return described() or bundled() or UNKNOWN diff --git a/web.py b/web.py index 1dbc687..53767e5 100644 --- a/web.py +++ b/web.py @@ -22,6 +22,7 @@ import console import net import stats import storage +import version from core import FINAL_SCORE, Game, GameOver def bundled(folder: str) -> str: @@ -125,6 +126,12 @@ def qr_svg(data: str) -> Markup: ) +@app.context_processor +def verze(): + """Verze do patičky. Když někdo hlásí chybu, tohle je první otázka.""" + return {"verze": version.current()} + + @app.template_filter("datum") def datum(value: str) -> str: """Z ISO textu udělá 12. 8. 2026.""" @@ -320,8 +327,9 @@ def hall(): games=storage.recent_games(conn)) -if __name__ == "__main__": - console.utf8() +def announce() -> None: + """Vypíše, kde všude je počitadlo k mání, a čím je zrovna spuštěné.""" + print(f"Kostky {version.current()}\n") found = net.addresses() @@ -336,4 +344,13 @@ if __name__ == "__main__": print(f"\nDatabáze: {storage.DB_PATH.resolve()}") + +if __name__ == "__main__": + console.utf8() + + if {"--version", "-V"} & set(sys.argv[1:]): + print(f"kostky {version.current()}") + sys.exit(0) + + announce() app.run(host="0.0.0.0", port=PORT, threaded=True)