diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-01 21:35:30 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-01 21:38:36 +0100 |
| commit | 7830b3c5e89f0339949940b8986bf4849ad7f223 (patch) | |
| tree | d4ebb4f10583cef1da3b181e093dd5d65ac8a0bb /scripts | |
| parent | v4.2.1: plain-language game copy and a balance fetch on login (diff) | |
| download | zagan-bet-7830b3c5e89f0339949940b8986bf4849ad7f223.tar.gz zagan-bet-7830b3c5e89f0339949940b8986bf4849ad7f223.zip | |
v4.3.0: package the desktop app with verifiable installers
publish.sh builds the GUI with the gui-release profile, names it zagan-bet-gui-<version>-<platform>-<arch>, and renders a bash and a PowerShell installer next to each app. Both embed the app's SHA-256, verify the file (or fetch it), install per-user and add a launcher: a desktop entry plus an SVG icon on Linux, a Start-menu shortcut on Windows.
The download page gains a Desktop app section with the apps and installers, keeps the terminal client and server, links the public git repository for the GPL source, and lists every file in the sha256sum block. deploy.sh verifies all four artifact kinds. Publishing refuses a dirty tree so the linked history matches the binaries.
Diffstat (limited to '')
| -rwxr-xr-x | scripts/deploy.sh | 9 | ||||
| -rw-r--r-- | scripts/installers/linux.sh.in | 78 | ||||
| -rw-r--r-- | scripts/installers/windows.ps1.in | 75 | ||||
| -rwxr-xr-x | scripts/publish.sh | 66 | ||||
| -rw-r--r-- | scripts/render-installer.py | 49 |
5 files changed, 271 insertions, 6 deletions
diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 99129ad..b18d462 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | #!/usr/bin/env bash | 1 | #!/usr/bin/env bash |
| 2 | # Upload web/ and verify every published artifact over the public URL. | 2 | # Upload web/ and verify every published artifact (GUI app and installers, |
| 3 | # terminal client and server) over the public URL. | ||
| 3 | # | 4 | # |
| 4 | # Usage: | 5 | # Usage: |
| 5 | # ./scripts/deploy.sh user@host:/srv/zagan.bet https://zagan.bet/ | 6 | # ./scripts/deploy.sh user@host:/srv/zagan.bet https://zagan.bet/ |
| @@ -50,7 +51,11 @@ done | |||
| 50 | [ -f web/index.php ] || die "web/index.php is missing" | 51 | [ -f web/index.php ] || die "web/index.php is missing" |
| 51 | 52 | ||
| 52 | shopt -s nullglob | 53 | shopt -s nullglob |
| 53 | artifacts=(web/bin/client/zagan-bet-* web/bin/server/zagan-bet-*) | 54 | artifacts=( |
| 55 | web/bin/gui/zagan-bet-* | ||
| 56 | web/bin/client/zagan-bet-* | ||
| 57 | web/bin/server/zagan-bet-* | ||
| 58 | ) | ||
| 54 | shopt -u nullglob | 59 | shopt -u nullglob |
| 55 | [ ${#artifacts[@]} -gt 0 ] || die "no artifacts in web/bin; run scripts/publish.sh first" | 60 | [ ${#artifacts[@]} -gt 0 ] || die "no artifacts in web/bin; run scripts/publish.sh first" |
| 56 | 61 | ||
diff --git a/scripts/installers/linux.sh.in b/scripts/installers/linux.sh.in new file mode 100644 index 0000000..5b53c02 --- /dev/null +++ b/scripts/installers/linux.sh.in | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | # Zagan-Bet desktop installer for Linux. | ||
| 3 | # | ||
| 4 | # Verifies the app against its published SHA-256, installs it for the current | ||
| 5 | # user and adds a launcher. Put the downloaded app next to this script, or run | ||
| 6 | # the script on its own and it will fetch the matching binary. | ||
| 7 | set -euo pipefail | ||
| 8 | |||
| 9 | VERSION="__VERSION__" | ||
| 10 | BINARY="__BINARY__" | ||
| 11 | SHA256="__SHA256__" | ||
| 12 | URL="__URL__" | ||
| 13 | |||
| 14 | say() { printf '%s\n' "$*"; } | ||
| 15 | die() { printf 'install: %s\n' "$*" >&2; exit 1; } | ||
| 16 | |||
| 17 | command -v sha256sum >/dev/null 2>&1 || die "sha256sum is required" | ||
| 18 | |||
| 19 | here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) | ||
| 20 | source_file="" | ||
| 21 | for candidate in "$here/$BINARY" "./$BINARY" "$HOME/Downloads/$BINARY"; do | ||
| 22 | if [ -f "$candidate" ]; then | ||
| 23 | source_file=$candidate | ||
| 24 | break | ||
| 25 | fi | ||
| 26 | done | ||
| 27 | |||
| 28 | cleanup="" | ||
| 29 | if [ -z "$source_file" ]; then | ||
| 30 | command -v curl >/dev/null 2>&1 || die "put $BINARY next to this installer, or install curl" | ||
| 31 | cleanup=$(mktemp) || die "cannot create a temporary file" | ||
| 32 | say "Downloading $BINARY..." | ||
| 33 | curl -fsSL --max-time 300 "$URL" -o "$cleanup" || die "download failed" | ||
| 34 | source_file=$cleanup | ||
| 35 | fi | ||
| 36 | trap 'if [ -n "$cleanup" ]; then rm -f "$cleanup"; fi' EXIT | ||
| 37 | |||
| 38 | actual=$(sha256sum "$source_file" | awk '{print $1}') | ||
| 39 | [ "$actual" = "$SHA256" ] || die "checksum mismatch: expected $SHA256, got $actual" | ||
| 40 | |||
| 41 | bin_dir="${XDG_BIN_HOME:-$HOME/.local/bin}" | ||
| 42 | data_dir="${XDG_DATA_HOME:-$HOME/.local/share}" | ||
| 43 | app="$bin_dir/zagan-bet-gui" | ||
| 44 | desktop_dir="$data_dir/applications" | ||
| 45 | icon_dir="$data_dir/icons/hicolor/scalable/apps" | ||
| 46 | |||
| 47 | mkdir -p "$bin_dir" "$desktop_dir" "$icon_dir" | ||
| 48 | install -m 0755 "$source_file" "$app" | ||
| 49 | |||
| 50 | cat > "$icon_dir/zagan-bet.svg" <<'ICON' | ||
| 51 | __ICON__ | ||
| 52 | ICON | ||
| 53 | |||
| 54 | cat > "$desktop_dir/zagan-bet.desktop" <<DESKTOP | ||
| 55 | [Desktop Entry] | ||
| 56 | Type=Application | ||
| 57 | Name=Zagan-Bet | ||
| 58 | Comment=Slots, roulette, blackjack and crash | ||
| 59 | Exec=$app | ||
| 60 | Icon=zagan-bet | ||
| 61 | Terminal=false | ||
| 62 | Categories=Game; | ||
| 63 | DESKTOP | ||
| 64 | chmod 0644 "$desktop_dir/zagan-bet.desktop" "$icon_dir/zagan-bet.svg" | ||
| 65 | |||
| 66 | if command -v update-desktop-database >/dev/null 2>&1; then | ||
| 67 | update-desktop-database "$desktop_dir" >/dev/null 2>&1 || true | ||
| 68 | fi | ||
| 69 | if command -v gtk-update-icon-cache >/dev/null 2>&1; then | ||
| 70 | gtk-update-icon-cache -q "$data_dir/icons/hicolor" >/dev/null 2>&1 || true | ||
| 71 | fi | ||
| 72 | |||
| 73 | say "Installed Zagan-Bet $VERSION to $app" | ||
| 74 | say "A launcher was added to your applications menu." | ||
| 75 | case ":$PATH:" in | ||
| 76 | *":$bin_dir:"*) ;; | ||
| 77 | *) say "Add $bin_dir to PATH to start it as 'zagan-bet-gui'." ;; | ||
| 78 | esac | ||
diff --git a/scripts/installers/windows.ps1.in b/scripts/installers/windows.ps1.in new file mode 100644 index 0000000..fd2b8b3 --- /dev/null +++ b/scripts/installers/windows.ps1.in | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | # Zagan-Bet desktop installer for Windows. | ||
| 2 | # | ||
| 3 | # Verifies the app against its published SHA-256, installs it for the current | ||
| 4 | # user and adds a Start-menu shortcut. Put the downloaded app next to this | ||
| 5 | # script, or run the script on its own and it will fetch the matching binary. | ||
| 6 | # | ||
| 7 | # Run it with: | ||
| 8 | # powershell -ExecutionPolicy Bypass -File .\zagan-bet-gui-*-install.ps1 | ||
| 9 | $ErrorActionPreference = "Stop" | ||
| 10 | |||
| 11 | $Version = "__VERSION__" | ||
| 12 | $Binary = "__BINARY__" | ||
| 13 | $Sha256 = "__SHA256__" | ||
| 14 | $Url = "__URL__" | ||
| 15 | |||
| 16 | function Stop-Install([string]$Message) { | ||
| 17 | Write-Host "install: $Message" -ForegroundColor Red | ||
| 18 | exit 1 | ||
| 19 | } | ||
| 20 | |||
| 21 | $here = Split-Path -Parent $MyInvocation.MyCommand.Path | ||
| 22 | $candidates = @( | ||
| 23 | (Join-Path $here $Binary), | ||
| 24 | (Join-Path (Get-Location) $Binary), | ||
| 25 | (Join-Path $env:USERPROFILE "Downloads\$Binary") | ||
| 26 | ) | ||
| 27 | $sourceFile = $null | ||
| 28 | foreach ($candidate in $candidates) { | ||
| 29 | if (Test-Path -LiteralPath $candidate) { | ||
| 30 | $sourceFile = $candidate | ||
| 31 | break | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | $temporary = $null | ||
| 36 | if (-not $sourceFile) { | ||
| 37 | $temporary = Join-Path ([System.IO.Path]::GetTempPath()) $Binary | ||
| 38 | Write-Host "Downloading $Binary..." | ||
| 39 | try { | ||
| 40 | Invoke-WebRequest -Uri $Url -OutFile $temporary -UseBasicParsing | ||
| 41 | } catch { | ||
| 42 | Stop-Install "download failed: $_" | ||
| 43 | } | ||
| 44 | $sourceFile = $temporary | ||
| 45 | } | ||
| 46 | |||
| 47 | try { | ||
| 48 | $actual = (Get-FileHash -LiteralPath $sourceFile -Algorithm SHA256).Hash.ToLowerInvariant() | ||
| 49 | if ($actual -ne $Sha256.ToLowerInvariant()) { | ||
| 50 | Stop-Install "checksum mismatch: expected $Sha256, got $actual" | ||
| 51 | } | ||
| 52 | |||
| 53 | $installDir = Join-Path $env:LOCALAPPDATA "Programs\Zagan-Bet" | ||
| 54 | New-Item -ItemType Directory -Force -Path $installDir | Out-Null | ||
| 55 | $target = Join-Path $installDir "zagan-bet-gui.exe" | ||
| 56 | Copy-Item -LiteralPath $sourceFile -Destination $target -Force | ||
| 57 | |||
| 58 | $startMenu = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs" | ||
| 59 | New-Item -ItemType Directory -Force -Path $startMenu | Out-Null | ||
| 60 | $shortcutPath = Join-Path $startMenu "Zagan-Bet.lnk" | ||
| 61 | $shell = New-Object -ComObject WScript.Shell | ||
| 62 | $shortcut = $shell.CreateShortcut($shortcutPath) | ||
| 63 | $shortcut.TargetPath = $target | ||
| 64 | $shortcut.WorkingDirectory = $installDir | ||
| 65 | $shortcut.IconLocation = "$target,0" | ||
| 66 | $shortcut.Description = "Slots, roulette, blackjack and crash" | ||
| 67 | $shortcut.Save() | ||
| 68 | |||
| 69 | Write-Host "Installed Zagan-Bet $Version to $target" | ||
| 70 | Write-Host "A Start-menu shortcut was created." | ||
| 71 | } finally { | ||
| 72 | if ($temporary -and (Test-Path -LiteralPath $temporary)) { | ||
| 73 | Remove-Item -LiteralPath $temporary -Force | ||
| 74 | } | ||
| 75 | } | ||
diff --git a/scripts/publish.sh b/scripts/publish.sh index 8240859..3a04631 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh | |||
| @@ -3,14 +3,22 @@ | |||
| 3 | # download page expects: | 3 | # download page expects: |
| 4 | # | 4 | # |
| 5 | # zagan-bet-client-<version>-<platform>-<arch>[.exe] | 5 | # zagan-bet-client-<version>-<platform>-<arch>[.exe] |
| 6 | # zagan-bet-server-<version>-<platform>-<arch> | 6 | # zagan-bet-server-<version>-<platform>-<arch>[.exe] |
| 7 | # zagan-bet-gui-<version>-<platform>-<arch>[.exe] | ||
| 8 | # zagan-bet-gui-<version>-<platform>-<arch>-install.sh | ||
| 9 | # zagan-bet-gui-<version>-<platform>-<arch>-install.ps1 | ||
| 7 | # | 10 | # |
| 8 | # Cargo always writes target/<triple>/release/<bin>, so the rename happens here | 11 | # Cargo always writes target/<triple>/<profile>/<bin>, so the rename happens |
| 9 | # after each build. The version comes from Cargo.toml. | 12 | # here after each build. The version comes from Cargo.toml. |
| 13 | # | ||
| 14 | # The GUI uses the workspace's `gui-release` profile (small, stripped, LTO). | ||
| 15 | # The public git repository is the GPL source for these binaries, so publish | ||
| 16 | # from a clean tree (or set ALLOW_DIRTY=1 for a preview). | ||
| 10 | # | 17 | # |
| 11 | # Usage: | 18 | # Usage: |
| 12 | # ./scripts/publish.sh | 19 | # ./scripts/publish.sh |
| 13 | # DEST=/srv/www/bin ./scripts/publish.sh | 20 | # DEST=/srv/www/bin ./scripts/publish.sh |
| 21 | # URL_BASE=https://staging.example/ ./scripts/publish.sh | ||
| 14 | set -euo pipefail | 22 | set -euo pipefail |
| 15 | 23 | ||
| 16 | ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) | 24 | ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) |
| @@ -23,6 +31,21 @@ if [ -z "${VERSION:-}" ]; then | |||
| 23 | fi | 31 | fi |
| 24 | 32 | ||
| 25 | DEST=${DEST:-web/bin} | 33 | DEST=${DEST:-web/bin} |
| 34 | URL_BASE=${URL_BASE:-https://zagan.2137697.xyz} | ||
| 35 | |||
| 36 | if [ "${ALLOW_DIRTY:-0}" != "1" ] && [ -n "$(git status --porcelain)" ]; then | ||
| 37 | echo "the working tree is dirty; commit the release first (or ALLOW_DIRTY=1)" >&2 | ||
| 38 | exit 1 | ||
| 39 | fi | ||
| 40 | |||
| 41 | command -v python3 >/dev/null || { | ||
| 42 | echo "python3 is required to render the installers" >&2 | ||
| 43 | exit 1 | ||
| 44 | } | ||
| 45 | |||
| 46 | hash_of() { | ||
| 47 | sha256sum "$1" | awk '{print $1}' | ||
| 48 | } | ||
| 26 | 49 | ||
| 27 | publish() { | 50 | publish() { |
| 28 | local triple=$1 kind=$2 platform=$3 arch=$4 | 51 | local triple=$1 kind=$2 platform=$3 arch=$4 |
| @@ -47,11 +70,46 @@ publish() { | |||
| 47 | echo " $DEST/$kind/$artifact ($(du -h "$DEST/$kind/$artifact" | cut -f1))" | 70 | echo " $DEST/$kind/$artifact ($(du -h "$DEST/$kind/$artifact" | cut -f1))" |
| 48 | } | 71 | } |
| 49 | 72 | ||
| 73 | publish_gui() { | ||
| 74 | local triple=$1 platform=$2 arch=$3 | ||
| 75 | local ext="" | ||
| 76 | if [ "$platform" = windows ]; then | ||
| 77 | ext=".exe" | ||
| 78 | fi | ||
| 79 | |||
| 80 | local artifact="zagan-bet-gui-$VERSION-$platform-$arch$ext" | ||
| 81 | local source="target/$triple/gui-release/zagan-bet-gui$ext" | ||
| 82 | |||
| 83 | echo "==> $artifact" | ||
| 84 | cargo build -p zagan-bet-gui --profile gui-release --quiet --target "$triple" | ||
| 85 | |||
| 86 | mkdir -p "$DEST/gui" | ||
| 87 | install -m 0755 "$source" "$DEST/gui/$artifact" | ||
| 88 | local sha | ||
| 89 | sha=$(hash_of "$DEST/gui/$artifact") | ||
| 90 | echo " $DEST/gui/$artifact ($(du -h "$DEST/gui/$artifact" | cut -f1))" | ||
| 91 | |||
| 92 | if [ "$platform" = linux ]; then | ||
| 93 | python3 scripts/render-installer.py \ | ||
| 94 | scripts/installers/linux.sh.in \ | ||
| 95 | "$DEST/gui/zagan-bet-gui-$VERSION-linux-$arch-install.sh" \ | ||
| 96 | "$VERSION" "$artifact" "$sha" "$URL_BASE/bin/gui/$artifact" | ||
| 97 | elif [ "$platform" = windows ]; then | ||
| 98 | python3 scripts/render-installer.py \ | ||
| 99 | scripts/installers/windows.ps1.in \ | ||
| 100 | "$DEST/gui/zagan-bet-gui-$VERSION-windows-$arch-install.ps1" \ | ||
| 101 | "$VERSION" "$artifact" "$sha" "$URL_BASE/bin/gui/$artifact" | ||
| 102 | fi | ||
| 103 | } | ||
| 104 | |||
| 105 | publish_gui x86_64-unknown-linux-gnu linux x86_64 | ||
| 106 | publish_gui x86_64-pc-windows-gnu windows x86_64 | ||
| 107 | |||
| 50 | publish x86_64-unknown-linux-gnu client linux x86_64 | 108 | publish x86_64-unknown-linux-gnu client linux x86_64 |
| 51 | publish i686-unknown-linux-gnu client linux i686 | 109 | publish i686-unknown-linux-gnu client linux i686 |
| 52 | publish x86_64-pc-windows-gnu client windows x86_64 | 110 | publish x86_64-pc-windows-gnu client windows x86_64 |
| 53 | publish x86_64-unknown-linux-gnu server linux x86_64 | 111 | publish x86_64-unknown-linux-gnu server linux x86_64 |
| 54 | publish x86_64-pc-windows-gnu server windows x86_64 | 112 | publish x86_64-pc-windows-gnu server windows x86_64 |
| 55 | 113 | ||
| 56 | echo | 114 | echo |
| 57 | echo "published $VERSION to $DEST" | 115 | echo "published $VERSION to $DEST" |
diff --git a/scripts/render-installer.py b/scripts/render-installer.py new file mode 100644 index 0000000..1a00551 --- /dev/null +++ b/scripts/render-installer.py | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Fill an installer template with one published artifact's details. | ||
| 3 | |||
| 4 | Usage: render-installer.py <template> <output> <version> <binary> <sha256> <url> | ||
| 5 | |||
| 6 | The Linux template embeds the application icon so the installer works offline. | ||
| 7 | """ | ||
| 8 | |||
| 9 | from __future__ import annotations | ||
| 10 | |||
| 11 | import pathlib | ||
| 12 | import sys | ||
| 13 | |||
| 14 | |||
| 15 | def main(argv: list[str]) -> int: | ||
| 16 | if len(argv) != 7: | ||
| 17 | print( | ||
| 18 | "usage: render-installer.py <template> <output> <version> " | ||
| 19 | "<binary> <sha256> <url>", | ||
| 20 | file=sys.stderr, | ||
| 21 | ) | ||
| 22 | return 2 | ||
| 23 | |||
| 24 | template, output, version, binary, sha256, url = argv[1:] | ||
| 25 | root = pathlib.Path(__file__).resolve().parent.parent | ||
| 26 | icon = (root / "gui" / "assets" / "zagan-bet.svg").read_text() | ||
| 27 | |||
| 28 | text = pathlib.Path(template).read_text() | ||
| 29 | replacements = { | ||
| 30 | "__VERSION__": version, | ||
| 31 | "__BINARY__": binary, | ||
| 32 | "__SHA256__": sha256, | ||
| 33 | "__URL__": url, | ||
| 34 | "__ICON__": icon.rstrip("\n"), | ||
| 35 | } | ||
| 36 | for placeholder, value in replacements.items(): | ||
| 37 | text = text.replace(placeholder, value) | ||
| 38 | |||
| 39 | destination = pathlib.Path(output) | ||
| 40 | destination.write_text(text) | ||
| 41 | if destination.suffix == ".sh": | ||
| 42 | destination.chmod(0o755) | ||
| 43 | |||
| 44 | print(f" {destination} ({destination.stat().st_size} bytes)") | ||
| 45 | return 0 | ||
| 46 | |||
| 47 | |||
| 48 | if __name__ == "__main__": | ||
| 49 | raise SystemExit(main(sys.argv)) | ||
