From fcfa0e489bdeba6563002c252d2632e0a0855a04 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Fri, 21 Aug 2026 17:12:25 +0200 Subject: [PATCH] =?UTF-8?q?bin=C3=A1rka=20ke=20sta=C5=BEen=C3=AD=20a=20jas?= =?UTF-8?q?n=C3=A9=20m=C3=ADsto=20pro=20datab=C3=A1zi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stáhni a spusť: jednosouborová binárka pro Linux, uvnitř Python, Flask, šablony i styly. Staví se ./build.sh v kontejneru na AlmaLinux 8, protože binárka slinkovaná proti glibc z Fedory 44 by nešla spustit nikde se starším systémem. Databáze tím dostává dvě různá místa. Ze zdrojáků zůstává dice.db v pracovním adresáři, ať se dá mít víc sad vedle sebe. Z binárky jde do ~/.local/share/kostky/ — binárka se rozbaluje do dočasného adresáře a pouští se odkudkoli, takže relativní cesta by databázi rozsypala po disku. DICE_DB přebije obojí. Server teď cestu k databázi vypisuje při startu, ať se na ni nemusí ptát. Co-Authored-By: Claude Opus 5 --- .gitignore | 2 ++ README.md | 54 +++++++++++++++++++++++++++++++++++++++---- build.sh | 30 ++++++++++++++++++++++++ kostky.spec | 26 +++++++++++++++++++++ storage.py | 24 +++++++++++++++++-- tests/test_storage.py | 33 ++++++++++++++++++++++++++ web.py | 18 ++++++++++++++- 7 files changed, 180 insertions(+), 7 deletions(-) create mode 100755 build.sh create mode 100644 kostky.spec diff --git a/.gitignore b/.gitignore index 5351aa3..d2f10d9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__/ .venv/ *.db .pytest_cache/ +dist/ +build/ diff --git a/README.md b/README.md index 3803366..d4cdec2 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,25 @@ ten samý Adam jako z minulého měsíce. Žádné účty, žádná hesla. ## Spuštění +Stáhni binárku z [releases](https://gitea.spacilovi.eu/david-spacil/dice-counter/releases) +a spusť ji: + +```bash +chmod +x kostky +./kostky +``` + +Nic se neinstaluje — Python, Flask i šablony jsou uvnitř. Zatím jen pro Linux +na x86-64; postavené proti glibc 2.28, takže jede na všem od RHEL 8, +Debianu 10 a Ubuntu 20.04 výš. + +Ze zdrojáků to je stejně krátké: + ```bash uv run web.py ``` -To je celé. Závislosti si skript nese v hlavičce (PEP 723) a `uv` je obstará -sám — není co instalovat ani aktivovat. +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ý: @@ -25,14 +38,33 @@ Počitadlo je dostupné na: ``` Na notebooku otevři `/board`, naskenuj QR kód telefonem a hraj. Tabule -zobrazuje i zbylé adresy — když jedna nefunguje, zkus další. +zobrazuje i zbylé adresy — když jedna nefunguje, zkus další. Zastavuje se +`Ctrl+C`. | Proměnná | K čemu je | Výchozí | |---|---|---| -| `DICE_DB` | soubor s databází | `dice.db` | +| `DICE_DB` | soubor s databází | podle způsobu spuštění, viz níž | | `DICE_PORT` | port | `8000` | | `DICE_HOST` | pevná adresa; vypne hledání | hledá se za běhu | +## Kde jsou data + +Jeden soubor SQLite. Založí se sám při prvním načtení stránky, cestu k němu +server vypíše při startu. + +| Spuštěno | Databáze | +|---|---| +| binárkou | `~/.local/share/kostky/dice.db` | +| ze zdrojáků | `dice.db` v pracovním adresáři | + +Binárka se při každém spuštění rozbaluje do dočasného adresáře a pouští se +odkudkoli, takže relativní cesta by databázi rozsypala po disku — proto +napevno domovský adresář. Ze zdrojáků zůstává relativní, ať se dá mít víc +sad vedle sebe. + +Zálohovat i stěhovat jde prostým zkopírováním souboru; `DICE_DB` si ho +najde kdekoli. + Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi může zapisovat. @@ -69,6 +101,7 @@ při remíze na prvním místě se hraje dál. | `stats.py` | síň slávy a kariérní statistiky | | `web.py` | Flask aplikace | | `dice.py` | totéž v terminálu, bez ukládání | +| `build.sh`, `kostky.spec` | stavba binárky | Terminálová verze zůstává funkční jako záloha: @@ -93,6 +126,19 @@ sudo dnf install python3-flask python3-qrcode python3-tabulate python3 web.py ``` +## Vlastní binárka + +```bash +./build.sh +``` + +Staví se v kontejneru (podman nebo docker) na AlmaLinux 8, ne na hostiteli. +Binárka slinkovaná proti glibc z Fedory 44 by nešla spustit nikde se starším +systémem — glibc drží zpětnou kompatibilitu, ne dopřednou. Výsledek je +v `dist/kostky`. + +Terminálová verze součástí binárky není; ta se pouští ze zdrojáků. + ## Testy ```bash diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..7238fe5 --- /dev/null +++ b/build.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Postaví jednosouborovou binárku pro Linux do dist/kostky. +# +# Staví se v kontejneru s glibc 2.28 (AlmaLinux 8), ne na hostiteli. Binárka +# slinkovaná proti glibc z Fedory 44 by nešla spustit nikde se starším +# systémem — glibc drží zpětnou kompatibilitu, ne dopřednou. Postavené na +# staré glibc poběží i na nové. +set -euo pipefail + +IMAGE=${IMAGE:-docker.io/library/almalinux:8} +PYTHON=${PYTHON:-python3.11} +ENGINE=${ENGINE:-$(command -v podman || command -v docker)} + +# Rootless podman mapuje roota v kontejneru na tebe, docker ne — tam by +# dist/ zůstalo cizí. +FIXUP=":" +[[ "$ENGINE" == *docker ]] && FIXUP="chown -R $(id -u):$(id -g) dist" + +"$ENGINE" run --rm -v "$PWD:/src:z" -w /src "$IMAGE" bash -euc " + dnf install -y -q python3.11 binutils + $PYTHON -m venv /tmp/build + /tmp/build/bin/pip install --quiet --upgrade pip + /tmp/build/bin/pip install --quiet flask qrcode pyinstaller + /tmp/build/bin/pyinstaller --clean --noconfirm \ + --distpath dist --workpath /tmp/work kostky.spec + $FIXUP +" + +echo +ls -lh dist/kostky diff --git a/kostky.spec b/kostky.spec new file mode 100644 index 0000000..857bd78 --- /dev/null +++ b/kostky.spec @@ -0,0 +1,26 @@ +# -*- mode: python ; coding: utf-8 -*- +"""Jedna binárka se vším: Python, Flask, šablony i styly. + +Staví se přes `./build.sh`, ne ručně — ta se stará o to, aby výsledek jel +i na starších systémech, než je ten tvůj. +""" + +analysis = Analysis( + ["web.py"], + pathex=["."], + datas=[("templates", "templates"), ("static", "static")], + noarchive=False, +) + +pyz = PYZ(analysis.pure) + +exe = EXE( + pyz, + analysis.scripts, + analysis.binaries, + analysis.datas, + name="kostky", + console=True, + strip=True, + upx=False, +) diff --git a/storage.py b/storage.py index 12723da..35cce6e 100644 --- a/storage.py +++ b/storage.py @@ -10,12 +10,30 @@ Součty se nikde neukládají; stav hry se rekonstruuje načtením tahů do import os import re import sqlite3 +import sys from datetime import datetime from pathlib import Path from core import FINAL_SCORE, Game, Turn -DB_PATH = Path(os.environ.get("DICE_DB", "dice.db")) +def default_db() -> Path: + """Kam se ukládá databáze. + + Ze zdrojáků do pracovního adresáře — dá se tak mít víc rozehraných + sad vedle sebe a je vidět, s čím se pracuje. Z binárky do datového + adresáře podle XDG: binárka se spouští odkudkoli a rozbaluje se do + dočasného adresáře, takže relativní cesta by databázi rozsypala po disku. + """ + chosen = os.environ.get("DICE_DB") + if chosen: + return Path(chosen) + if getattr(sys, "frozen", False): + share = os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share" + return Path(share) / "kostky" / "dice.db" + return Path("dice.db") + + +DB_PATH = default_db() STATUS_RUNNING = "probíhá" STATUS_FINISHED = "dohráno" @@ -60,7 +78,9 @@ CREATE INDEX IF NOT EXISTS turns_game ON turns(game_id, id); def connect(path: Path | str | None = None) -> sqlite3.Connection: - conn = sqlite3.connect(path or DB_PATH) + target = Path(path or DB_PATH) + target.parent.mkdir(parents=True, exist_ok=True) + conn = sqlite3.connect(target) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") conn.executescript(SCHEMA) diff --git a/tests/test_storage.py b/tests/test_storage.py index af390b6..603d8df 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest import storage @@ -132,3 +134,34 @@ def test_hrac_nesmi_sedet_u_stolu_dvakrat(conn): def test_prazdne_jmeno(conn): with pytest.raises(ValueError): storage.get_or_create_player(conn, " ") + + +def test_umisteni_databaze_ze_zdrojaku(monkeypatch): + """Ze zdrojáků se ukládá do pracovního adresáře.""" + monkeypatch.delenv("DICE_DB", raising=False) + monkeypatch.delattr(storage.sys, "frozen", raising=False) + + assert storage.default_db() == Path("dice.db") + + +def test_umisteni_databaze_z_binarky(monkeypatch, tmp_path): + """Binárka se rozbaluje do dočasného adresáře, databáze tam nesmí.""" + monkeypatch.delenv("DICE_DB", raising=False) + monkeypatch.setattr(storage.sys, "frozen", True, raising=False) + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path)) + + assert storage.default_db() == tmp_path / "kostky" / "dice.db" + + +def test_dice_db_prebije_oboje(monkeypatch, tmp_path): + monkeypatch.setattr(storage.sys, "frozen", True, raising=False) + monkeypatch.setenv("DICE_DB", str(tmp_path / "jinde.db")) + + assert storage.default_db() == tmp_path / "jinde.db" + + +def test_connect_zalozi_chybejici_adresar(tmp_path): + conn = storage.connect(tmp_path / "novy" / "adresar" / "dice.db") + conn.close() + + assert (tmp_path / "novy" / "adresar" / "dice.db").exists() diff --git a/web.py b/web.py index 93bba45..2d66572 100644 --- a/web.py +++ b/web.py @@ -10,6 +10,8 @@ Server-rendered HTML, formuláře přes POST/redirect/GET, žádný build step. import os import socket +import sys +from pathlib import Path import qrcode from flask import (Flask, abort, g, redirect, render_template, request, @@ -21,7 +23,19 @@ import stats import storage from core import FINAL_SCORE, Game, GameOver -app = Flask(__name__) +def bundled(folder: str) -> str: + """Cesta k šablonám a stylům. + + Ze zdrojáků leží vedle tohoto souboru. V binárce je PyInstaller rozbalí + do dočasného adresáře, na který ukazuje `sys._MEIPASS`. + """ + root = Path(getattr(sys, "_MEIPASS", Path(__file__).parent)) + return str(root / folder) + + +app = Flask(__name__, + template_folder=bundled("templates"), + static_folder=bundled("static")) PORT = int(os.environ.get("DICE_PORT", 8000)) @@ -298,4 +312,6 @@ if __name__ == "__main__": else: print("Adresu se nepodařilo zjistit. Spusť server s DICE_HOST=.") + print(f"\nDatabáze: {storage.DB_PATH.resolve()}") + app.run(host="0.0.0.0", port=PORT, threaded=True)