2 Commits
Author SHA1 Message Date
gitea-actionsandClaude Opus 5 cb39036724 release se staví sám z tagu
binárka / linux (pull_request) Failing after 0s
Otagovaný commit postaví binárku na runneru, projede testy, zkusí ji
nastartovat a pověsí ji na release. Nad pull requesty běží totéž bez
posledního kroku, ať se rozbitá stavba pozná dřív než na tagu.

Staví se v kontejneru na AlmaLinux 8 ze stejného důvodu jako build.sh —
kvůli glibc. Záměrně bez hotových akcí z marketplace: na vlastním runneru
tím odpadá starost, jestli je v kontejneru node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:18:39 +02:00
gitea-actionsandClaude Opus 5 fcfa0e489b 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>
2026-08-21 17:17:09 +02:00
8 changed files with 283 additions and 7 deletions

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
# Postaví binárku a pověsí ji na release.
#
# Běží v kontejneru na AlmaLinux 8, ne na runneru: binárka slinkovaná proti
# novější glibc by nešla spustit na starších systémech. Záměrně bez hotových
# akcí z marketplace — na vlastním runneru odpadá starost, jestli v kontejneru
# je node.
name: binárka
on:
push:
tags: ["v*"]
pull_request:
workflow_dispatch:
jobs:
linux:
runs-on: homelab
container: almalinux:8
steps:
- name: Nástroje
run: dnf install -y -q git python3.11 binutils
- name: Zdrojáky
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git init -q .
git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
git config http.extraheader "Authorization: token ${TOKEN}"
git fetch -q --depth 1 origin "${GITHUB_REF}"
git checkout -q FETCH_HEAD
git config --unset http.extraheader
- name: Prostředí
run: |
python3.11 -m venv /tmp/build
/tmp/build/bin/pip install --quiet --upgrade pip
/tmp/build/bin/pip install --quiet -r requirements.txt pyinstaller
- name: Testy
run: /tmp/build/bin/pytest -q
- name: Binárka
run: |
/tmp/build/bin/pyinstaller --clean --noconfirm \
--distpath dist --workpath /tmp/work kostky.spec
ls -lh dist/kostky
- name: Zkouška
run: |
XDG_DATA_HOME=/tmp/data DICE_PORT=8321 DICE_HOST=127.0.0.1 \
./dist/kostky > /tmp/server.log 2>&1 &
for _ in $(seq 30); do
curl -sf -o /dev/null http://127.0.0.1:8321/ && break
sleep 1
done
curl -sf -o /dev/null http://127.0.0.1:8321/
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/data/kostky/dice.db
cat /tmp/server.log
- name: Na release
if: startsWith(github.ref, 'refs/tags/')
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
ASSET: kostky-linux-x86_64
run: |
api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
auth="Authorization: token ${TOKEN}"
# Release může, ale nemusí existovat — na tag se dá kliknout ručně.
release=$(curl -s -H "$auth" "$api/releases/tags/${GITHUB_REF_NAME}")
id=$(printf '%s' "$release" | python3.11 -c \
'import json,sys; print(json.load(sys.stdin).get("id",""))' || true)
if [ -z "$id" ]; then
id=$(curl -sf -X POST -H "$auth" -H "Content-Type: application/json" \
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\"}" \
"$api/releases" | python3.11 -c \
'import json,sys; print(json.load(sys.stdin)["id"])')
fi
# Aby se dal build zopakovat, stará příloha stejného jména jde pryč.
curl -s -H "$auth" "$api/releases/$id/assets" | python3.11 -c \
'import json,os,sys; [print(a["id"]) for a in json.load(sys.stdin) if a["name"] == os.environ["ASSET"]]' \
| while read -r old; do
curl -sf -X DELETE -H "$auth" "$api/releases/$id/assets/$old"
done
curl -sf -X POST -H "$auth" -F "attachment=@dist/kostky" \
"$api/releases/$id/assets?name=${ASSET}"
+2
View File
@@ -2,3 +2,5 @@ __pycache__/
.venv/ .venv/
*.db *.db
.pytest_cache/ .pytest_cache/
dist/
build/
+60 -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,29 @@ 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ů.
Releasy si tohle dělají samy. Otagovaný commit spustí
`.gitea/workflows/binarka.yml`, ta binárku postaví, projede testy, zkusí
ji nastartovat a pověsí ji na release jako `kostky-linux-x86_64`:
```bash
git tag v1.0.0 && git push origin v1.0.0
```
Stejná stavba běží i nad každým pull requestem, jen se nikam nevěší.
## 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)