diff --git a/.gitea/workflows/packages.yml b/.gitea/workflows/packages.yml new file mode 100644 index 0000000..386251d --- /dev/null +++ b/.gitea/workflows/packages.yml @@ -0,0 +1,107 @@ +name: Linux package builds (test) + +on: + push: + pull_request: + +jobs: + linux: + runs-on: ubuntu-latest + container: node:22-bookworm + env: + CARGO_HOME: /root/.cargo + RUSTUP_HOME: /root/.rustup + RUST_VERSION: 1.99.1 + steps: + - name: Checkout + uses: https://github.com/actions/checkout@v4 + - name: Install packaging dependencies + run: | + apt-get update + apt-get install -y --no-install-recommends ca-certificates curl build-essential pkg-config libasound2-dev python3 rpm cpio desktop-file-utils squashfs-tools p7zip-full file flatpak ostree zstd + - name: Rust toolchain cache + uses: https://github.com/actions/cache@v4 + with: + path: | + /root/.rustup + /root/.cargo/bin + key: rust-toolchain-v1-linux-x64-${{ env.RUST_VERSION }} + - name: Install Rust + run: | + if [ ! -x "$CARGO_HOME/bin/rustup" ]; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup-init.sh + sh /tmp/rustup-init.sh -y --profile minimal --default-toolchain none --no-modify-path + fi + "$CARGO_HOME/bin/rustup" toolchain install "$RUST_VERSION" --profile minimal --component rustfmt --component clippy + "$CARGO_HOME/bin/rustup" default "$RUST_VERSION" + echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" + - name: Cargo cache + uses: https://github.com/actions/cache@v4 + with: + path: | + /root/.cargo/registry + /root/.cargo/git + target + key: packages-v1-linux-x64-${{ env.RUST_VERSION }}-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('Cargo.toml', 'src/**', 'tests/**') }} + restore-keys: | + packages-v1-linux-x64-${{ env.RUST_VERSION }}-${{ hashFiles('Cargo.lock') }}- + - name: Compile Linux binary + id: compile + run: cargo build --release --locked + - name: Build deb + if: ${{ !cancelled() && steps.compile.outcome == 'success' }} + run: bash packaging/build.sh deb + - name: Build rpm + if: ${{ !cancelled() && steps.compile.outcome == 'success' }} + run: bash packaging/build.sh rpm + - name: Build AppImage + if: ${{ !cancelled() && steps.compile.outcome == 'success' }} + run: bash packaging/build.sh appimage + - name: Build Flatpak + if: ${{ !cancelled() && steps.compile.outcome == 'success' }} + run: bash packaging/build.sh flatpak + - name: Save test packages + if: ${{ !cancelled() }} + uses: https://github.com/actions/upload-artifact@v3 + with: + name: linux-packages-x86_64 + path: dist/* + if-no-files-found: error + retention-days: 7 + + aur: + runs-on: ubuntu-latest + container: archlinux:base-devel + steps: + - name: Install Arch dependencies + run: pacman -Syu --noconfirm --needed git rust cargo pkgconf alsa-lib python zstd + - name: Checkout + uses: https://github.com/actions/checkout@v4 + - name: Build AUR recipe from this commit + run: | + version=$(python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["version"])') + recipe_version=$(bash -c 'source packaging/aur/PKGBUILD; printf "%s" "$pkgver"') + test "$version" = "$recipe_version" + mkdir -p /tmp/aur-build + cp packaging/aur/PKGBUILD /tmp/aur-build/ + tar --exclude=.git --exclude=target --exclude=dist --transform="s,^\./,war-in-tunnels-$version/," -czf "/tmp/aur-build/war-in-tunnels-$version.tar.gz" . + cd /tmp/aur-build + checksum=$(sha256sum "war-in-tunnels-$version.tar.gz" | cut -d ' ' -f 1) + sed -i "s/sha256sums=('SKIP')/sha256sums=('$checksum')/" PKGBUILD + useradd -m builder + chown -R builder:builder /tmp/aur-build + runuser -u builder -- bash -c 'cd /tmp/aur-build && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO' + pacman -U --noconfirm ./*.pkg.tar.zst + cd /tmp + war-in-tunnels --version + - name: Save Arch test package and recipe + uses: https://github.com/actions/upload-artifact@v3 + with: + name: arch-package-x86_64 + path: | + /tmp/aur-build/*.pkg.tar.zst + /tmp/aur-build/PKGBUILD + /tmp/aur-build/.SRCINFO + /tmp/aur-build/*.tar.gz + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore index 5669dab..91f77f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target /result +/dist war_in_tunnels.log diff --git a/docs/AGENTS.md b/docs/AGENTS.md index ee17506..1f46ae1 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -23,14 +23,16 @@ nix develop -c cargo run -- --help nix develop -c cargo run -- --disable-audio --view skirmish ``` -Ostatnie polecenie wymaga interaktywnego terminala. `shellHook` sam wykonuje `cargo run --locked -- -h`, więc wejście do środowiska także kompiluje program i drukuje pomoc. Przy kontrolach używaj `--locked`, aby nie aktualizować `Cargo.lock`. Formatowanie stosuj wyłącznie do uzgodnionych zmian; nie używaj automatycznego `clippy --fix` do porządkowania całego repozytorium. +Ostatnie polecenie wymaga interaktywnego terminala. `shellHook` drukuje dostępne polecenia, bez automatycznej kompilacji. Przy kontrolach używaj `--locked`, aby nie aktualizować `Cargo.lock`. Formatowanie stosuj wyłącznie do uzgodnionych zmian; nie używaj automatycznego `clippy --fix` do porządkowania całego repozytorium. -Stan bazowy po konfiguracji lintów: 45 testów i formatowanie przechodzą; Clippy zgłasza 50 ostrzeżeń dla biblioteki. `clippy::unused_unit` jest dozwolony ze względu na wymagane jawne `-> ()`. Zakres i polecenia opisano w [TESTING.md](TESTING.md). Nie traktuj tego jako potwierdzenia działania pełnej rozgrywki. Nowa nietrywialna logika wymaga małego testu regresji. Nie dodawaj zależności ani warstw abstrakcji bez potrzeby. +Aktualny zestaw obejmuje 46 testów. Stan bazowy Clippy to 50 ostrzeżeń dla biblioteki. `clippy::unused_unit` jest dozwolony ze względu na wymagane jawne `-> ()`. Zakres i polecenia opisano w [TESTING.md](TESTING.md). Nie traktuj tego jako potwierdzenia działania pełnej rozgrywki. Nowa nietrywialna logika wymaga małego testu regresji. Nie dodawaj zależności ani warstw abstrakcji bez potrzeby. `Cargo.toml` wymaga Rust 1.88 lub nowszego, zabrania `unsafe` we własnym kodzie i ignorowania wyników `must_use`, a w release włącza kontrolę przepełnień. CI Gitea w `.gitea/workflows/rust.yml` sprawdza formatowanie, kompilację, testy debug/release i Clippy bez Nix, przez Rust 1.99.1 instalowany rustup; lokalnie nadal używaj Nix. Oddzielny `.gitea/workflows/nix.yml` buduje `.#default` z `default.nix`, z testami pakietu i kontrolą instalacji. Nie dodawaj `-D warnings` przed uporządkowaniem istniejących ostrzeżeń. Audio wymaga ALSA i urządzenia odtwarzania; do pracy nad TUI używaj `--disable-audio`. `--mute` nadal uruchamia wątek audio. Ścieżki zasobów i pakowanie sprawdzaj osobno przy zmianach audio/Nix. Rozróżniaj lokalny build Cargo, pakiet `.#develop` i domyślny pakiet Nix. +`.gitea/workflows/packages.yml` testuje paczki Linux x86_64 bez Nix: deb, rpm, AppImage, Flatpak oraz przepis AUR budowany przez makepkg. Nie publikuje wydań ani tagów. Przepisy, layout i ograniczenia opisano w [PACKAGING.md](PACKAGING.md); zmiany pakowania muszą zachować muzykę obok binarki i działanie poza katalogiem repozytorium. + ## Konwencje kodu - Formatowanie określa `rustfmt.toml`: styl Rust 2024, wcięcia po 2 spacje, szerokość docelowa 100 znaków i zakończenia LF. Stosuj `nix develop -c cargo fmt --all`; kontroluj przez `nix develop -c cargo fmt --all -- --check`. diff --git a/docs/CODE_MAP.md b/docs/CODE_MAP.md index c5d9dcb..3eefec7 100644 --- a/docs/CODE_MAP.md +++ b/docs/CODE_MAP.md @@ -24,6 +24,7 @@ Wszystkie ścieżki poniżej są względem katalogu głównego repozytorium. | Formatowanie | `rustfmt.toml` | Rustfmt, styl 2024, wcięcia po 2 spacje, docelowo 100 znaków i zakończenia LF | | Kontrole CI | `.gitea/workflows/rust.yml` | Gitea Actions w `node:22-bookworm`: Rust 1.99.1 przez rustup, cache toolchaina i Cargo, formatowanie, kompilacja, testy debug/release i Clippy | | Budowa paczki Nix | `.gitea/workflows/nix.yml` | Oddzielny workflow buduje `.#default`, czyli `default.nix` przez nixpkgs przypięte w `flake.lock`, z testami i kontrolą instalacji pakietu | +| Paczki Linux | `.gitea/workflows/packages.yml`, `packaging/` | Testowe deb, rpm, AppImage, Flatpak oraz makepkg dla AUR; bez wydań i tagów; szczegóły w `docs/PACKAGING.md` | | Licencja | `LICENSE`, `Cargo.toml`, `default.nix` | MIT, Copyright (c) 2026 GarandPLG | | Zasoby | `soundtracks/default/`, `ui_draft/` | Dwa pliki OGG i tekstowe szkice ekranów | @@ -104,7 +105,7 @@ Postęp bieżącego kroku jest udostępniany przez `MovingTask::get_movement_pro Główny wątek wysyła `AudioCmd`; osobny wątek obsługuje głośność, mute, część i soundtrack. Cache wykorzystuje `Lazy`, `RwLock`, `Arc` i bajty OGG. Calm ładuje `calm.ogg`; Buildup, Assault i Outro korzystają obecnie z `test.ogg`. Błędy urządzenia, odczytu i dekodowania mogą zakończyć wątek przez `expect`. -Rodio zapętla źródło przez `Decoder::new_looped`. Wątek sterujący blokuje się na odbiorniku (`for cmd in rx`), zamiast aktywnie sprawdzać komendy i koniec utworu. Zmiana części lub soundtracku dopisuje zapętlone źródło i pomija bieżące przez `skip_one`. Zamknięcie kanału kończy wątek. Test `audio_repeats_after_end_of_file` sprawdza przekroczenie końca rzeczywistego pliku OGG bez urządzenia audio. +Rodio zapętla źródło przez `Decoder::new_looped`. Wątek sterujący blokuje się na odbiorniku (`for cmd in rx`), zamiast aktywnie sprawdzać komendy i koniec utworu. Zmiana części lub soundtracku dopisuje zapętlone źródło i pomija bieżące przez `skip_one`. Zamknięcie kanału kończy wątek. Soundtracki są szukane obok rzeczywistej binarki, z fallbackiem do bieżącego katalogu dla Cargo. Testy audio obejmują zapętlanie OGG i ładowanie zasobów poza repozytorium, bez urządzenia audio. `flake.nix` dostarcza devShell dla `x86_64-linux`, domyślny pakiet z `default.nix` i pakiet `.#develop` budowany przez Naersk. Obie ścieżki pakowania kopiują soundtracki do `$out/bin` i zmieniają nazwę binarki na `war-in-tunnels`. `default.nix` ma stały `cargoHash`; po zmianie zależności wymaga osobnej weryfikacji. Samo `cargo test` nie sprawdza pakowania. @@ -116,7 +117,7 @@ To obserwacje statyczne, nie lista napraw wykonanych w tej sesji: - **Zaznaczenie:** wybrany górnik czeka podczas rysowania trasy; inne jednostki wykonują zadania normalnie. - **Audio:** usunięto aktywne odpytywanie; pomiar CPU na docelowym urządzeniu i odsłuch pozostają do wykonania. Test dekodera nie weryfikuje urządzenia audio. - **Granice UI:** małe terminale, resize i trzy poziomy zoomu wymagają interaktywnego sprawdzenia. Indeksowanie planszy zakłada poprawne współrzędne. -- **Pokrycie:** 45 testów CLI, sterowania, renderowania, planszy, wykonywania zadań i dekodowania audio; szczegóły w [TESTING.md](TESTING.md). Workflow Gitea sprawdza też profil release; nie obejmuje pełnej rozgrywki. Clippy zgłasza 50 ostrzeżeń biblioteki, `unused_unit` jest jawnie dozwolony. Nie uruchamiać automatycznych poprawek poza zakresem zadania. +- **Pokrycie:** 46 testów CLI, sterowania, renderowania, planszy, wykonywania zadań i dekodowania audio; szczegóły w [TESTING.md](TESTING.md). Workflow Gitea sprawdza też profil release; nie obejmuje pełnej rozgrywki. Clippy zgłasza 50 ostrzeżeń biblioteki, `unused_unit` jest jawnie dozwolony. Nie uruchamiać automatycznych poprawek poza zakresem zadania. ## Gdzie zacząć konkretną zmianę diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md new file mode 100644 index 0000000..af4803b --- /dev/null +++ b/docs/PACKAGING.md @@ -0,0 +1,35 @@ +# Testowe paczki Linux + +Workflow `.gitea/workflows/packages.yml` uruchamia się po pushu i dla pull requestów. Nie tworzy tagów, wydań ani wpisów w AUR/Flathub. Paczki są niesygnowanymi artefaktami CI przechowywanymi przez 7 dni. Używa `upload-artifact@v3` dla zgodności z Gitea. + +Obsługiwana architektura to x86_64. Są to formaty dystrybucji Linux, nie kompilacje Windows/macOS. + +| Format | Budowanie i kontrola | +|---|---| +| `.deb` | Rust 1.99.1 w Debian Bookworm, `dpkg-deb`, odczyt metadanych i uruchomienie rozpakowanej binarki z `--version` | +| `.rpm` | Ta sama binarka, `rpmbuild` i `packaging/war-in-tunnels.spec`, automatyczne zależności ELF, ekstrakcja i `--version` | +| AppImage | AppDir z binarką, muzyką i libasound.so.2; appimagetool 1.9.1; ekstrakcja narzędzia i paczki przez 7z, uruchomienie AppRun z `--version` bez FUSE | +| AUR | `packaging/aur/PKGBUILD`, lokalne archiwum bieżącego checkoutu z SHA256, `makepkg` jako zwykły użytkownik w Arch Linux; kompilacja, testy, instalacja paczki i `--version`; eksport .SRCINFO | +| Flatpak | Binarka z Debiana pakowana poleceniami `flatpak build-init`, `build-finish`, `build-export`, `build-bundle`; runtime org.freedesktop.Platform 25.08; instalacja paczki w kontenerze, kontrola soundtracku i `--version` zainstalowanej binarki bez sandboxa | + +`packaging/stage.sh` instaluje binarkę i soundtracki w `PREFIX/lib/war-in-tunnels`, launcher w `PREFIX/bin`, licencję, plik desktop z `Terminal=true` i ikonę SVG. Nie zmienia katalogu roboczego, więc opcja `--log` nadal zapisuje log w katalogu wybranym przez użytkownika. Audio najpierw szuka soundtracków obok rzeczywistej binarki, a następnie w bieżącym katalogu dla lokalnego Cargo. Test audio sprawdza działanie poza repozytorium. + +W kontenerze Debian z zależnościami workflow można wykonać: + +```sh +cargo build --release --locked +bash packaging/build.sh deb +bash packaging/build.sh rpm +bash packaging/build.sh appimage +bash packaging/build.sh flatpak +``` + +Lokalnie na hoście projektu polecenia Cargo uruchamiaj przez `nix develop -c`; binarka zbudowana przez Nix może zależeć od `/nix/store`, dlatego do paczek dystrybucyjnych używaj kompilacji kontenerowej. Skrypty oczekują bieżącego katalogu repozytorium i zapisują wyniki do `dist/`. + +Przepis AUR jest testowy: nie wskazuje publicznego tarballa wydania. Workflow dostarcza lokalny tarball oraz zastępuje `SKIP` rzeczywistym SHA256. Wersja `pkgver` musi odpowiadać Cargo.toml; CI to sprawdza. Artefakt AUR zawiera paczkę, przepis, .SRCINFO i pasujące źródła. + +Flatpak jest testem pakowania istniejącej binarki, nie kompilacją wewnątrz SDK Flatpak. Bookworm dostarcza glibc 2.36, runtime 25.08 jest nowszy. Pakowana jest biblioteka ALSA; uprawnienia obejmują PulseAudio i urządzenia dla bezpośredniego ALSA. `build-export --disable-sandbox` wyłącza wyłącznie sandbox walidatora własnej ikony z repozytorium, nie walidację ani sandbox gotowej aplikacji. Tworzenie paczki nie potrzebuje privileged ani zmiany polityki Dockera. Instalacja w Dockerze może zgłaszać bwrap dla hooków integracji pulpitu mimo poprawnego zainstalowania paczki; sprawdzamy kod wyjścia, pliki i binarkę. Pełnego uruchomienia TUI i audio w sandboxie te kontrole nie zastępują. Runtime nie jest zawarty w pliku .flatpak; użytkownik musi go zainstalować z Flathub. CI używa --no-related, aby nie pobierać opcjonalnych dodatków GPU i tłumaczeń. + +Paczki Debian, RPM i AppImage mają bazę ABI Bookworm (glibc >= 2.36); nie gwarantują działania na starszych dystrybucjach. AppImage zawiera ALSA, ale korzysta z konfiguracji audio hosta. Testy nie potwierdzają odsłuchu ani otwierania terminala z menu desktop. Appimagetool jest przypięty do wersji; jego domyślny runtime jest pobierany z upstream podczas pakowania. Rust i Cargo mają cache oddzielny od zwykłego workflow Rust; dla cache wymagany jest działający serwer runnera. Arch i Flatpak pobierają własne zależności bez trwałego cache. + +Dokumentacja narzędzi: [AppImage](https://github.com/AppImage/appimagetool/tree/1.9.1), [Flatpak](https://docs.flatpak.org/en/latest/flatpak-command-reference.html), [makepkg](https://man.archlinux.org/man/makepkg.8). diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 5f127df..bfa7dbf 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,5 +1,17 @@ # Dziennik prac +## 2026-10-19 — testowe paczki Linux poza Nix + +Dodano `.gitea/workflows/packages.yml` dla push i pull request: job Debian kompiluje Rust 1.99.1 i tworzy deb, rpm, AppImage oraz Flatpak; job Arch kompiluje i testuje lokalny przepis AUR przez makepkg jako użytkownik builder. Workflow zapisuje artefakty przez 7 dni, nie tworzy wydań, tagów ani publikacji AUR/Flathub. Zależności i toolchain Cargo mają cache; Flatpak i Arch nie mają jeszcze trwałego cache. Obsługiwana architektura: x86_64. + +Przepisy i wspólna instalacja są w `packaging/`, a zakres i ograniczenia w `docs/PACKAGING.md`. Pakowane są muzyka, licencja, launcher zachowujący katalog roboczy, plik desktop Terminal=true i ikona SVG. Flatpak pakuje binarkę z Debiana do runtime 25.08, bez kompilacji w SDK i bez zagnieżdżonego sandboxa. Przepis AUR dostaje archiwum checkoutu z rzeczywistym SHA256; nie jest jeszcze przepisem publikowanego wydania. Dodano `dist/` do .gitignore. + +Audio szuka zasobów obok rzeczywistej binarki z fallbackiem do katalogu roboczego, aby instalowane paczki działały poza repozytorium bez zmiany miejsca zapisu logów. Dodano regresję uruchamianą w osobnym procesie z przeniesioną binarką testu i soundtrackami. Łącznie 46 testów integracyjnych. + +Weryfikacja: cargo check --all-targets --locked, cargo test --all-targets --locked (46 testów), cargo fmt --all -- --check i cargo clippy --all-targets --locked przez nix develop przeszły; Clippy nadal ma 50 ostrzeżeń biblioteki. Przeszły git diff --check i bash -n. W jednorazowym Debianie rzeczywiście skompilowano grę, utworzono deb i rpm, rozpakowano i sprawdzono --version; zbudowano też AppImage, rozpakowano je i uruchomiono AppRun --version. Narzędzie i wynik AppImage rozpakowywane są przez 7z, bez FUSE i kolizji z binfmt hosta. Osobny Arch Linux wykonał makepkg, testy release i doctest, instalację paczki oraz --version. Flatpak został wyeksportowany, zapisany do pliku i zainstalowany w kontenerze; zweryfikowano soundtrack i --version zainstalowanej binarki. Wyłączono sandbox wyłącznie walidatora własnej ikony przy build-export; instalacja może zgłaszać ostrzeżenia bwrap dla hooków integracji pulpitu w standardowym Dockerze. Nie wyłączono sandboxa gotowej aplikacji. Instalacja runtime i aplikacji używa --no-related, aby nie pobierać opcjonalnych sterowników GPU ani tłumaczeń dla testu TUI. + +Nie sprawdzono pełnej rozgrywki, odsłuchu, uruchamiania z menu pulpitu ani działania na starszych dystrybucjach; pełny workflow i transport artefaktów wymagają przebiegu na Gitea. Testowe kontenery miały repozytorium tylko do odczytu; nie instalowano paczek na hoście. Usunięto tymczasowy kontener war-packages-check i obraz war-in-tunnels-packaging-test:local. + ## 2026-10-19 — oddzielny workflow budowania default.nix Dodano `.gitea/workflows/nix.yml` dla push i pull request. Workflow w `node:22-bookworm` instaluje sudo i Nix (KVM wyłączone), następnie buduje `.#default` z przypiętym `flake.lock`, bez aktualizacji lockfile i bez devShell. Obejmuje testy buildRustPackage oraz versionCheckHook. Nie zmieniono workflow Rust ani konfiguracji runnera; nie dodano trwałego cache Nix. diff --git a/docs/TESTING.md b/docs/TESTING.md index 667f371..84c2f9f 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -16,10 +16,10 @@ Testy korzystają z wbudowanego mechanizmu Rust, Clap i `ratatui::backend::TestB | `tests/app.rs` | 7 | Nawigacja menu, wybór ekranów, wyjście, ignorowanie zwolnienia klawisza, komendy audio i zamknięty kanał, panel, zoom, ruch, renderowanie wszystkich widoków bez mutacji stanu, wejście przed inicjalizacją | | `tests/board.rs` | 7 | Rozmieszczenie baz/złóż/jednostek, granice kursora i kamery, zatwierdzanie/anulowanie ścieżki, cofanie z powtórzonym polem, duży viewport, resize i zoom | | `tests/tasks.rs` | 7 | Przyrost i limit kopania, reset etapu, pusta ścieżka, kolejność zadań, timer MovingTask, całkowity postęp, zachowanie liczby etapów przy konwersji, formatowanie postępu aktywnych i oczekujących zadań | -| `tests/audio.rs` | 1 | Zapętlanie rzeczywistego pliku OGG po jego końcu | +| `tests/audio.rs` | 2 | Zapętlanie rzeczywistego pliku OGG po jego końcu i ładowanie soundtracków obok binarki poza katalogiem repozytorium | | `tests/digging.rs` | 18 | Wykonywanie kopania i ruchu, kolejki, 3 sekundy na krok, wejście po wykopaniu, blokady i walidacja, kolory i procenty w TUI, podział trasy Moving/Digging, ochrona struktur, zmiana rodzaju zadania, panel Tab z postępem etapu i całego zadania | -Łącznie: 45 testów. Wszystkie testy znajdują się w `tests/` i sprawdzają publiczne interfejsy biblioteki lub binarkę. Funkcja `load_audio` jest eksportowana przez `app::threads`, dzięki czemu test korzysta z tego samego dekodera co odtwarzanie. +Łącznie: 46 testów. Wszystkie testy znajdują się w `tests/` i sprawdzają publiczne interfejsy biblioteki lub binarkę. Funkcja `load_audio` jest eksportowana przez `app::threads`, dzięki czemu test korzysta z tego samego dekodera co odtwarzanie. Wybrane zestawy i pojedynczy przypadek: @@ -41,10 +41,12 @@ nix develop -c cargo fmt --all -- --check nix develop -c cargo clippy --all-targets --locked ``` -`shellHook` środowiska Nix dodatkowo wykonuje `cargo run --locked -- -h`. Wynik pomocy nie jest wynikiem testów; właściwy raport zawiera podsumowania `test result`. +`shellHook` środowiska Nix wyświetla dostępne polecenia; nie kompiluje automatycznie programu. ## CI i profile kompilacji +Testowe paczki deb, rpm, AppImage, AUR i Flatpak sprawdza niezależny `.gitea/workflows/packages.yml`, bez publikowania wydań. Zasady budowania, artefakty i ograniczenia opisano w [PACKAGING.md](PACKAGING.md). + Osobny workflow `.gitea/workflows/nix.yml` (push i pull request) instaluje Nix w `node:22-bookworm` i wykonuje `nix build .#default --no-update-lock-file --no-link --print-out-paths --print-build-logs`. Atrybut `.#default` wywołuje `pkgs.callPackage ./default.nix {}` z nixpkgs przypiętym w `flake.lock`; nie buduje wariantu `.#develop` opartego na Fenix. Budowa obejmuje standardową fazę testów `buildRustPackage` i włączony w pakiecie `versionCheckHook`. Nie wymaga devShell ani uruchamiania gry. Instalator potrzebuje sudo; KVM jest wyłączone. Workflow nie konfiguruje trwałego cache magazynu Nix, więc nowy job może ponownie pobierać zależności. Kontrole Rust opisane poniżej pozostają niezależne. Workflow `.gitea/workflows/rust.yml` uruchamia kontrole po pushu i dla pull requestów. Wymaga włączonych Actions w repozytorium Gitea i runnera Linux x86_64 o etykiecie `ubuntu-latest`, obsługującego akcje Node. Nazwy akcji zawierają pełne adresy GitHub. Konfiguracja nie rejestruje runnera ani nie włącza Actions na serwerze; zobacz [instrukcję Gitea](https://docs.gitea.com/usage/actions/quickstart/). @@ -53,7 +55,7 @@ Workflow określa obraz zadania jawnie jako `node:22-bookworm`. Kontener działa Wersję Rust w CI określa `RUST_VERSION` w workflow, niezależnie od lokalnego `flake.lock`. Formatowanie, kompilacja, testy i Clippy są oddzielnymi krokami Cargo. Polecenia kompilujące projekt mają `--locked`; Clippy nie używa `-D warnings` z powodu istniejących ostrzeżeń. `actions/cache@v4` zachowuje osobno toolchain oraz rejestr Cargo, repozytoria git i `target`. Klucze uwzględniają wersję Rust, a dla Cargo także manifest, lockfile i kod; klucze częściowe pozwalają ponownie wykorzystać wcześniejsze kompilacje. Działanie cache wymaga dostępnego serwera cache runnera. Pierwszy przebieg wypełnia cache; błędy pobierania cache mogą spowodować ponowną instalację i kompilację. -Release ma `overflow-checks = true`. Ten sam zestaw 45 testów można wykonać z kontrolą przepełnień w profilu produkcyjnym: +Release ma `overflow-checks = true`. Ten sam zestaw 46 testów można wykonać z kontrolą przepełnień w profilu produkcyjnym: ```sh nix develop -c cargo test --all-targets --release --locked diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD new file mode 100644 index 0000000..e6c0273 --- /dev/null +++ b/packaging/aur/PKGBUILD @@ -0,0 +1,28 @@ +# Test recipe: CI supplies the source archive from the checked-out commit. +pkgname=war-in-tunnels +pkgver=0.1.0 +pkgrel=1 +pkgdesc='Underground strategy game in your terminal' +arch=('x86_64') +url='https://gitea.garandplg.com/GarandPLG/war-in-tunnels' +license=('MIT') +depends=('alsa-lib' 'gcc-libs' 'glibc') +makedepends=('cargo' 'pkgconf') +options=('!debug') +source=("$pkgname-$pkgver.tar.gz") +sha256sums=('SKIP') + +build() { + cd "$srcdir/$pkgname-$pkgver" + cargo build --release --locked +} + +check() { + cd "$srcdir/$pkgname-$pkgver" + cargo test --release --locked +} + +package() { + cd "$srcdir/$pkgname-$pkgver" + sh packaging/stage.sh "$pkgdir/usr" +} diff --git a/packaging/build.sh b/packaging/build.sh new file mode 100644 index 0000000..462fce9 --- /dev/null +++ b/packaging/build.sh @@ -0,0 +1,91 @@ +#!/bin/bash +set -euo pipefail + +format=${1:?expected deb, rpm, appimage or flatpak} +version=$(python3 -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["version"])') +mkdir -p dist +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +sh packaging/stage.sh "$work/root/usr" +desktop-file-validate "$work/root/usr/share/applications/com.garandplg.WarInTunnels.desktop" +"$work/root/usr/bin/war-in-tunnels" --version | grep -Fx "war_in_tunnels $version" +test -s "$work/root/usr/lib/war-in-tunnels/soundtracks/default/calm.ogg" + +case "$format" in + deb) + mkdir -p "$work/root/DEBIAN" + cat > "$work/root/DEBIAN/control" <= 2.36), libgcc-s1 +Section: games +Priority: optional +Description: Underground strategy game in your terminal +EOF + output="$PWD/dist/war-in-tunnels_${version}_amd64.deb" + dpkg-deb --root-owner-group --build "$work/root" "$output" + dpkg-deb --info "$output" + dpkg-deb --extract "$output" "$work/extracted" + "$work/extracted/usr/bin/war-in-tunnels" --version + ;; + rpm) + rpmbuild -bb packaging/war-in-tunnels.spec \ + --define "_topdir $work/rpmbuild" \ + --define "package_version $version" \ + --define "staging $work/root" \ + --define '_build_id_links none' + cp "$work"/rpmbuild/RPMS/x86_64/*.rpm dist/ + rpm -qp --requires dist/*.rpm + mkdir "$work/extracted" + rpm2cpio dist/*.rpm | (cd "$work/extracted" && cpio -idm --quiet) + "$work/extracted/usr/bin/war-in-tunnels" --version + ;; + appimage) + appdir="$work/WarInTunnels.AppDir" + mv "$work/root" "$appdir" + cp packaging/com.garandplg.WarInTunnels.desktop "$appdir/" + cp packaging/com.garandplg.WarInTunnels.svg "$appdir/" + mkdir -p "$appdir/usr/lib/bundled" + # Bundle ALSA; glibc and the ELF loader come from the host. + cp -L /usr/lib/x86_64-linux-gnu/libasound.so.2 "$appdir/usr/lib/bundled/" + cat > "$appdir/AppRun" <<'EOF' +#!/bin/sh +set -eu +appdir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +export LD_LIBRARY_PATH="$appdir/usr/lib/bundled${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +exec "$appdir/usr/bin/war-in-tunnels" "$@" +EOF + chmod +x "$appdir/AppRun" + curl -fL --retry 3 https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage -o "$work/appimagetool" + # Extract without executing the runtime: works without FUSE and with host binfmt handlers. + 7z x "$work/appimagetool" "-o$work/appimagetool-dir" > /dev/null + output="$PWD/dist/war-in-tunnels-${version}-x86_64.AppImage" + ARCH=x86_64 "$work/appimagetool-dir/AppRun" --no-appstream "$appdir" "$output" + 7z x "$output" "-o$work/appimage-check" > /dev/null + "$work/appimage-check/AppRun" --version + ;; + flatpak) + appid=com.garandplg.WarInTunnels + # Package the Debian-built binary against a newer glibc runtime; no nested sandbox is needed. + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --user install -y --noninteractive --no-related flathub org.freedesktop.Platform//25.08 + flatpak build-init "$work/flatpak" "$appid" org.freedesktop.Platform org.freedesktop.Platform 25.08 + cp -a "$work/root/usr/." "$work/flatpak/files/" + install -Dm755 /usr/lib/x86_64-linux-gnu/libasound.so.2 "$work/flatpak/files/lib/libasound.so.2" + flatpak build-finish --command=war-in-tunnels --socket=pulseaudio --device=all "$work/flatpak" + # The icon is repository-owned; its validator cannot create a sandbox in standard Docker. + flatpak build-export --disable-sandbox "$work/repo" "$work/flatpak" test + output="$PWD/dist/war-in-tunnels-${version}-x86_64.flatpak" + flatpak build-bundle --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo "$work/repo" "$output" "$appid" test + flatpak --user install -y --noninteractive --no-related "$output" + location=$(flatpak --user info --show-location "$appid") + test -s "$location/files/lib/war-in-tunnels/soundtracks/default/calm.ogg" + "$location/files/bin/war-in-tunnels" --version + ;; + *) + echo "Unknown package format: $format" >&2 + exit 1 + ;; +esac diff --git a/packaging/com.garandplg.WarInTunnels.desktop b/packaging/com.garandplg.WarInTunnels.desktop new file mode 100644 index 0000000..0c082f8 --- /dev/null +++ b/packaging/com.garandplg.WarInTunnels.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=War in Tunnels +Comment=Underground strategy game in your terminal +Exec=war-in-tunnels +Icon=com.garandplg.WarInTunnels +Terminal=true +Categories=Game;StrategyGame; diff --git a/packaging/com.garandplg.WarInTunnels.svg b/packaging/com.garandplg.WarInTunnels.svg new file mode 100644 index 0000000..7ee6baa --- /dev/null +++ b/packaging/com.garandplg.WarInTunnels.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packaging/stage.sh b/packaging/stage.sh new file mode 100644 index 0000000..3c1e6e4 --- /dev/null +++ b/packaging/stage.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +prefix=${1:?installation prefix required} +binary=${2:-target/release/war_in_tunnels} +install -Dm755 "$binary" "$prefix/lib/war-in-tunnels/war-in-tunnels" +mkdir -p "$prefix/bin" +cat > "$prefix/bin/war-in-tunnels" <<'EOF' +#!/bin/sh +set -eu +bindir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +exec "$bindir/../lib/war-in-tunnels/war-in-tunnels" "$@" +EOF +chmod +x "$prefix/bin/war-in-tunnels" +cp -r soundtracks "$prefix/lib/war-in-tunnels/" +install -Dm644 LICENSE "$prefix/share/licenses/war-in-tunnels/LICENSE" +install -Dm644 packaging/com.garandplg.WarInTunnels.desktop "$prefix/share/applications/com.garandplg.WarInTunnels.desktop" +install -Dm644 packaging/com.garandplg.WarInTunnels.svg "$prefix/share/icons/hicolor/scalable/apps/com.garandplg.WarInTunnels.svg" diff --git a/packaging/war-in-tunnels.spec b/packaging/war-in-tunnels.spec new file mode 100644 index 0000000..62039b1 --- /dev/null +++ b/packaging/war-in-tunnels.spec @@ -0,0 +1,21 @@ +Name: war-in-tunnels +Version: %{package_version} +Release: 1 +Summary: Underground strategy game in your terminal +License: MIT +URL: https://gitea.garandplg.com/GarandPLG/war-in-tunnels +Requires: alsa-lib + +%description +A terminal-based underground strategy game. + +%install +mkdir -p %{buildroot} +cp -a %{staging}/. %{buildroot}/ + +%files +/usr/bin/war-in-tunnels +/usr/lib/war-in-tunnels +/usr/share/applications/com.garandplg.WarInTunnels.desktop +/usr/share/icons/hicolor/scalable/apps/com.garandplg.WarInTunnels.svg +%license /usr/share/licenses/war-in-tunnels/LICENSE diff --git a/src/app/threads/audio.rs b/src/app/threads/audio.rs index ffa2147..8db4907 100644 --- a/src/app/threads/audio.rs +++ b/src/app/threads/audio.rs @@ -8,6 +8,7 @@ use std::{ collections::HashMap, fs::read, io::{BufReader, Cursor}, + path::{Path, PathBuf}, process::exit, sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver}, }; @@ -49,15 +50,24 @@ static PRELOADED_SOUNDTRACKS: Lazy SoundtrackCache { - let base: String = format!("soundtracks/{}", folder); + let soundtrack_root: PathBuf = std::env::current_exe() + .ok() + .and_then(|executable: PathBuf| -> Option { + executable + .parent() + .map(|directory: &Path| -> PathBuf { directory.join("soundtracks") }) + }) + .filter(|directory: &PathBuf| -> bool { directory.is_dir() }) + .unwrap_or_else(|| -> PathBuf { PathBuf::from("soundtracks") }); + let base: PathBuf = soundtrack_root.join(folder); let mut cache: HashMap> = HashMap::new(); macro_rules! load { ($part:ident, $file:expr) => { - let path = format!("{}/{}", base, $file); + let path: PathBuf = base.join($file); cache.insert( SoundrackParts::$part, - read(&path).expect(&format!("cannot read {}", path)), + read(&path).expect(&format!("cannot read {}", path.display())), ); }; } diff --git a/tests/audio.rs b/tests/audio.rs index 3be0db7..0b32d0d 100644 --- a/tests/audio.rs +++ b/tests/audio.rs @@ -1,7 +1,9 @@ use rodio::Decoder; use std::{ - fs::read, + fs::{self, read}, io::{BufReader, Cursor}, + path::PathBuf, + process::{Command, Output}, }; use war_in_tunnels::app::threads::{SoundrackParts, Soundtrack, load_audio}; @@ -17,3 +19,47 @@ fn audio_repeats_after_end_of_file() { assert_eq!(source.by_ref().take(sample_count).count(), sample_count); assert_eq!(source.take(1024).count(), 1024); } + +#[test] +fn packaged_audio_loads_without_project_working_directory() -> () { + if std::env::var_os("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST").is_some() { + let mut source: rodio::source::Amplify< + rodio::decoder::LoopedDecoder>>>, + > = load_audio(&Soundtrack::Default, &SoundrackParts::Calm); + assert_eq!(source.by_ref().take(1024).count(), 1024); + return; + } + + let directory: PathBuf = std::env::temp_dir().join(format!( + "war-in-tunnels-packaged-audio-{}", + std::process::id() + )); + fs::create_dir_all(directory.join("soundtracks/default")).unwrap(); + fs::create_dir_all(directory.join("working-directory")).unwrap(); + let executable: PathBuf = directory.join("audio-test"); + fs::copy(std::env::current_exe().unwrap(), &executable).unwrap(); + for filename in ["calm.ogg", "test.ogg"] { + fs::copy( + PathBuf::from("soundtracks/default").join(filename), + directory.join("soundtracks/default").join(filename), + ) + .unwrap(); + } + let output: Output = Command::new(executable) + .args([ + "--exact", + "packaged_audio_loads_without_project_working_directory", + "--nocapture", + ]) + .env("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST", "1") + .current_dir(directory.join("working-directory")) + .output() + .unwrap(); + fs::remove_dir_all(directory).unwrap(); + assert!( + output.status.success(), + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +}