Add installers that fetch the newest release when run.

Each installer carries no application of its own and resolves
/releases/latest/download/ at the moment it runs, so the file never goes
stale and the same download keeps working across future releases.

macOS installs into /Applications, falling back to the user's own folder
when that is not writable, and clears the quarantine flag the unsigned
build would otherwise trip over. Windows extracts over any existing
install rather than replacing the folder, so the progress files kept
inside it survive, and adds a Start menu entry. Linux drops the AppImage
in ~/Applications with a desktop entry and its icon.

Publish the macOS one zipped, because a release asset loses its
executable bit and a .command needs one to be double-clicked.

The .cmd also had to be excluded from the *.cmd ignore rule, which comes
from the kernel section of the template and targets build artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-17 15:16:46 +02:00
commit 7d503d6c5b
6 changed files with 286 additions and 0 deletions

View file

@ -111,6 +111,17 @@ jobs:
path: release-assets path: release-assets
merge-multiple: true merge-multiple: true
# The installers carry no application: each downloads the newest release
# when the user runs it. The macOS one is zipped because a release asset
# loses its executable bit, which a .command needs to be double-clicked.
- name: Collect installers
run: |
cp installers/maturita-installer-linux.sh release-assets/
cp installers/maturita-installer-windows.cmd release-assets/
chmod +x installers/maturita-installer-macos.command
(cd installers && zip -q ../release-assets/maturita-installer-macos.zip \
maturita-installer-macos.command)
- name: List assets - name: List assets
run: find release-assets -type f -print run: find release-assets -type f -print
@ -121,6 +132,9 @@ jobs:
release-assets/maturita-linux-*.AppImage release-assets/maturita-linux-*.AppImage
release-assets/maturita-windows-x64.zip release-assets/maturita-windows-x64.zip
release-assets/maturita-macos-*.zip release-assets/maturita-macos-*.zip
release-assets/maturita-installer-macos.zip
release-assets/maturita-installer-windows.cmd
release-assets/maturita-installer-linux.sh
generate_release_notes: true generate_release_notes: true
fail_on_unmatched_files: true fail_on_unmatched_files: true
env: env:

3
.gitignore vendored
View file

@ -63,6 +63,9 @@ assets/icons/
# Kernel Module Compile Results # Kernel Module Compile Results
*.mod* *.mod*
*.cmd *.cmd
# The rule above targets kernel build artifacts such as .foo.o.cmd; the
# Windows installer is a source file that has to be tracked.
!installers/*.cmd
.tmp_versions/ .tmp_versions/
modules.order modules.order
Module.symvers Module.symvers

View file

@ -139,6 +139,24 @@ are required:
Progress and settings are saved next to the AppImage / `.app` / unzipped Progress and settings are saved next to the AppImage / `.app` / unzipped
folder in a `progress/` directory. folder in a `progress/` directory.
#### Installers
Each release also ships a small installer that contains no application of its
own: it downloads the newest build when you run it, so the file never goes out
of date. Once installed, the app keeps itself up to date on its own — it checks
shortly after startup and offers the update in a banner, and there is a manual
check under **Settings → Updates**.
| Platform | Asset | How to run |
| -------- | ----- | ---------- |
| macOS (Apple Silicon) | `maturita-installer-macos.zip` | Unzip, then double-click the `.command` file |
| Windows (x64) | `maturita-installer-windows.cmd` | Double-click the file |
| Linux (x86_64) | `maturita-installer-linux.sh` | `sh maturita-installer-linux.sh` |
The builds are not code-signed, so the first launch needs one extra step:
on macOS right-click the installer and pick **Open**, and on Windows choose
**More info → Run anyway** if SmartScreen appears.
To cut a release yourself (needs a GitHub remote with Actions enabled): To cut a release yourself (needs a GitHub remote with Actions enabled):
```bash ```bash
@ -600,6 +618,24 @@ při pushi tagu `v*`. Kompilátor ani vývojové balíčky GTK nejsou potřeba:
Postup a nastavení se ukládají vedle AppImage / `.app` / rozbalené složky Postup a nastavení se ukládají vedle AppImage / `.app` / rozbalené složky
do adresáře `progress/`. do adresáře `progress/`.
#### Instalátory
Ke každému releasu vychází i malý instalátor, který samotnou aplikaci
neobsahuje: nejnovější build si stáhne až při spuštění, takže soubor nikdy
nezestárne. Nainstalovaná aplikace se pak udržuje aktuální sama — chvíli po
startu zkontroluje nové verze a nabídne je v pruhu nahoře, ruční kontrola je
v **Nastavení → Aktualizace**.
| Platforma | Soubor | Spuštění |
| --------- | ------ | -------- |
| macOS (Apple Silicon) | `maturita-installer-macos.zip` | Rozbalit a dvojklik na soubor `.command` |
| Windows (x64) | `maturita-installer-windows.cmd` | Dvojklik na soubor |
| Linux (x86_64) | `maturita-installer-linux.sh` | `sh maturita-installer-linux.sh` |
Buildy nejsou podepsané, takže první spuštění chce jeden krok navíc: na macOS
klikněte na instalátor pravým tlačítkem a zvolte **Otevřít**, na Windows při
hlášce SmartScreenu **Další informace → Přesto spustit**.
Vytvoření releasu (GitHub remote s Actions): Vytvoření releasu (GitHub remote s Actions):
```bash ```bash

View file

@ -0,0 +1,79 @@
#!/bin/sh
# maturita.c installer for Linux.
#
# Carries no application of its own: it downloads whatever the newest release
# is at the moment you run it, so this file never goes stale. GitHub resolves
# the /releases/latest/download/ path to the current tag on every request.
#
# Run it with: sh maturita-installer-linux.sh
# The app updates itself from then on.
set -eu
REPO="pepaskopec-blip/maturita.c"
ASSET="maturita-linux-x86_64.AppImage"
URL="https://github.com/$REPO/releases/latest/download/$ASSET"
# ~/Applications is where desktop environments look for AppImages, and the app
# keeps its progress/ directory next to its own file.
DEST_DIR="$HOME/Applications"
DEST="$DEST_DIR/maturita.AppImage"
DESKTOP_DIR="$HOME/.local/share/applications"
ICON_DIR="$HOME/.local/share/icons/hicolor/512x512/apps"
say() { printf '%s\n' "$*"; }
die() { printf '\nError: %s\n' "$*" >&2; exit 1; }
say "Installing maturita.C"
say "---------------------"
arch=$(uname -m)
if [ "$arch" != "x86_64" ]; then
die "only x86_64 builds are published, but this machine reports '$arch'.
Build from source instead: https://github.com/$REPO"
fi
command -v curl >/dev/null 2>&1 || die "curl was not found. Install it first."
mkdir -p "$DEST_DIR"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT INT TERM
say "Downloading the latest release..."
curl -fL --progress-bar -o "$tmp/$ASSET" "$URL" ||
die "the download failed. Check your internet connection."
say "Installing to $DEST"
chmod +x "$tmp/$ASSET"
mv -f "$tmp/$ASSET" "$DEST" || die "could not write to $DEST_DIR."
# A menu entry needs the icon as a separate file, which the AppImage can
# unpack without FUSE. Losing the icon is not worth failing the install over.
if (cd "$tmp" && "$DEST" --appimage-extract \
'usr/share/icons/hicolor/512x512/apps/maturita.png' >/dev/null 2>&1); then
icon="$tmp/squashfs-root/usr/share/icons/hicolor/512x512/apps/maturita.png"
if [ -f "$icon" ]; then
mkdir -p "$ICON_DIR"
cp -f "$icon" "$ICON_DIR/maturita.png"
fi
fi
mkdir -p "$DESKTOP_DIR"
cat > "$DESKTOP_DIR/maturita.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=maturita.C
Comment=Practice app for the Czech maturita exam
Exec=$DEST
Icon=maturita
Terminal=false
Categories=Education;
EOF
command -v update-desktop-database >/dev/null 2>&1 &&
update-desktop-database "$DESKTOP_DIR" >/dev/null 2>&1 || true
say ""
say "Done. Starting the app..."
"$DEST" >/dev/null 2>&1 &

View file

@ -0,0 +1,74 @@
#!/bin/sh
# maturita.c installer for macOS.
#
# Carries no application of its own: it downloads whatever the newest release
# is at the moment you run it, so this file never goes stale. GitHub resolves
# the /releases/latest/download/ path to the current tag on every request.
#
# Double-click the file to run it. The app updates itself from then on.
set -eu
REPO="pepaskopec-blip/maturita.c"
ASSET="maturita-macos-arm64.zip"
URL="https://github.com/$REPO/releases/latest/download/$ASSET"
say() { printf '%s\n' "$*"; }
die() { printf '\nError: %s\n' "$*" >&2; printf 'Press Return to close. '; read -r _; exit 1; }
say "Installing maturita.C"
say "---------------------"
# Only Apple Silicon builds are published; Rosetta cannot run an arm64 app.
arch=$(uname -m)
if [ "$arch" != "arm64" ]; then
die "this build is for Apple Silicon Macs, but this one reports '$arch'.
Build from source instead: https://github.com/$REPO"
fi
command -v curl >/dev/null 2>&1 || die "curl was not found."
# Prefer the shared folder, fall back to the user's own when it is locked down.
if [ -w /Applications ]; then
dest_dir="/Applications"
else
dest_dir="$HOME/Applications"
mkdir -p "$dest_dir"
fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT INT TERM
say "Downloading the latest release..."
curl -fL --progress-bar -o "$tmp/$ASSET" "$URL" ||
die "the download failed. Check your internet connection."
say "Unpacking..."
mkdir -p "$tmp/new"
# ditto keeps bundle symlinks and permissions intact, unlike plain unzip.
ditto -x -k "$tmp/$ASSET" "$tmp/new" || die "the archive could not be unpacked."
app=$(find "$tmp/new" -maxdepth 1 -name '*.app' -print -quit)
[ -n "$app" ] || die "the archive did not contain an application."
name=$(basename "$app")
say "Installing to $dest_dir/$name"
rm -rf "$dest_dir/$name.old"
if [ -e "$dest_dir/$name" ]; then
mv "$dest_dir/$name" "$dest_dir/$name.old"
fi
if mv "$app" "$dest_dir/$name"; then
rm -rf "$dest_dir/$name.old"
else
# Put the previous copy back rather than leaving nothing installed.
[ -e "$dest_dir/$name.old" ] && mv "$dest_dir/$name.old" "$dest_dir/$name"
die "could not write to $dest_dir."
fi
# The download carries a quarantine flag, and the app is not signed, so
# without this macOS refuses to open it.
xattr -cr "$dest_dir/$name" 2>/dev/null || true
say ""
say "Done. Starting the app..."
open "$dest_dir/$name"

View file

@ -0,0 +1,80 @@
@echo off
rem maturita.c installer for Windows.
rem
rem Carries no application of its own: it downloads whatever the newest release
rem is at the moment you run it, so this file never goes stale. GitHub resolves
rem the /releases/latest/download/ path to the current tag on every request.
rem
rem Double-click the file to run it. The app updates itself from then on.
setlocal
set "REPO=pepaskopec-blip/maturita.c"
set "ASSET=maturita-windows-x64.zip"
set "URL=https://github.com/%REPO%/releases/latest/download/%ASSET%"
set "DEST=%LOCALAPPDATA%\Programs\Maturita"
echo Installing maturita.C
echo ---------------------
rem curl and tar both ship with Windows 10 1803 and later.
where curl >nul 2>&1 || goto :no_tools
where tar >nul 2>&1 || goto :no_tools
set "TMP_DIR=%TEMP%\maturita-installer"
rmdir /s /q "%TMP_DIR%" 2>nul
mkdir "%TMP_DIR%" || goto :fail_tmp
echo Downloading the latest release...
curl -fL --progress-bar -o "%TMP_DIR%\%ASSET%" "%URL%" || goto :fail_download
echo Installing to %DEST%
if not exist "%DEST%" mkdir "%DEST%" || goto :fail_dest
rem Extracted over any existing install rather than replacing the folder, so
rem the progress files kept inside it survive an upgrade.
tar -xf "%TMP_DIR%\%ASSET%" -C "%DEST%" || goto :fail_extract
rem Start menu entry, so the app can be found without digging for the folder.
set "LNK=%APPDATA%\Microsoft\Windows\Start Menu\Programs\maturita.C.lnk"
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$s=(New-Object -ComObject WScript.Shell).CreateShortcut('%LNK%');" ^
"$s.TargetPath='%DEST%\maturita.exe';$s.WorkingDirectory='%DEST%';$s.Save()" >nul 2>&1
rmdir /s /q "%TMP_DIR%" 2>nul
echo.
echo Done. Starting the app...
start "" "%DEST%\maturita.exe"
exit /b 0
:no_tools
echo.
echo Error: this installer needs curl and tar, which come with Windows 10
echo version 1803 and later. Please update Windows, or download the zip
echo manually from https://github.com/%REPO%/releases/latest
pause
exit /b 1
:fail_tmp
echo.
echo Error: could not create a temporary folder in %TEMP%.
pause
exit /b 1
:fail_download
echo.
echo Error: the download failed. Check your internet connection.
pause
exit /b 1
:fail_dest
echo.
echo Error: could not create %DEST%.
pause
exit /b 1
:fail_extract
echo.
echo Error: the archive could not be unpacked.
pause
exit /b 1