binárka ke stažení a jasné místo pro databázi

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 <noreply@anthropic.com>
This commit is contained in:
gitea-actionsandClaude Opus 5 committed 2026-08-21 17:17:09 +02:00
1 parent f33104b5c2
commit fcfa0e489b
7 files changed
+180 -7

No files matched your search

+2
View File
@@ -2,3 +2,5 @@ __pycache__/
.venv/ .venv/
*.db *.db
.pytest_cache/ .pytest_cache/
dist/
build/
+50 -4
View File
@@ -8,12 +8,25 @@ ten samý Adam jako z minulého měsíce. Žádné účty, žádná hesla.
## Spuštění ## 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 ```bash
uv run web.py uv run web.py
``` ```
To je celé. Závislosti si skript nese v hlavičce (PEP 723) a `uv` je obstará Závislosti si skript nese v hlavičce (PEP 723) a `uv` je obstará sám.
sám — není co instalovat ani aktivovat.
Server vypíše všechny adresy, na kterých je dostupný: 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 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í | | 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_PORT` | port | `8000` |
| `DICE_HOST` | pevná adresa; vypne hledání | hledá se za běhu | | `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 Počítá se s domácí sítí — appka nemá přihlašování a kdokoli na stejné WiFi
může zapisovat. 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 | | `stats.py` | síň slávy a kariérní statistiky |
| `web.py` | Flask aplikace | | `web.py` | Flask aplikace |
| `dice.py` | totéž v terminálu, bez ukládání | | `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: 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 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 ## Testy
```bash ```bash
Executable
+30
View File
@@ -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
+26
View File
@@ -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,
)
+22 -2
View File
@@ -10,12 +10,30 @@ Součty se nikde neukládají; stav hry se rekonstruuje načtením tahů do
import os import os
import re import re
import sqlite3 import sqlite3
import sys
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from core import FINAL_SCORE, Game, Turn 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_RUNNING = "probíhá"
STATUS_FINISHED = "dohráno" 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: 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.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA foreign_keys = ON")
conn.executescript(SCHEMA) conn.executescript(SCHEMA)
+33
View File
@@ -1,3 +1,5 @@
from pathlib import Path
import pytest import pytest
import storage import storage
@@ -132,3 +134,34 @@ def test_hrac_nesmi_sedet_u_stolu_dvakrat(conn):
def test_prazdne_jmeno(conn): def test_prazdne_jmeno(conn):
with pytest.raises(ValueError): with pytest.raises(ValueError):
storage.get_or_create_player(conn, " ") 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()
+17 -1
View File
@@ -10,6 +10,8 @@ Server-rendered HTML, formuláře přes POST/redirect/GET, žádný build step.
import os import os
import socket import socket
import sys
from pathlib import Path
import qrcode import qrcode
from flask import (Flask, abort, g, redirect, render_template, request, from flask import (Flask, abort, g, redirect, render_template, request,
@@ -21,7 +23,19 @@ import stats
import storage import storage
from core import FINAL_SCORE, Game, GameOver 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)) PORT = int(os.environ.get("DICE_PORT", 8000))
@@ -298,4 +312,6 @@ if __name__ == "__main__":
else: else:
print("Adresu se nepodařilo zjistit. Spusť server s DICE_HOST=<adresa>.") print("Adresu se nepodařilo zjistit. Spusť server s DICE_HOST=<adresa>.")
print(f"\nDatabáze: {storage.DB_PATH.resolve()}")
app.run(host="0.0.0.0", port=PORT, threaded=True) app.run(host="0.0.0.0", port=PORT, threaded=True)