Publish builds to a branch instead of to releases.

Releases and version numbers are gone. CI now rebuilds all three platforms
on every change to main and commits the packages to the `builds` branch,
so what anyone downloads is the current state of the repository. The
branch is rewritten from scratch each time rather than added to, so the
repository keeps one copy of the packages instead of a ~70 MB set per
commit.

Without tags there is no version to compare, so a build carries the
commit it was made from and the branch carries a VERSION file naming the
same commit. The updater fetches those few bytes and treats any
difference as being behind; commits are not ordered, and the branch only
ever holds the current build.

The installers fetch from the branch over raw.githubusercontent.com for
the same reason.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-17 15:40:47 +02:00
commit 737e27deb0
10 changed files with 190 additions and 198 deletions

View file

@ -1,9 +1,18 @@
name: Release name: Build
# Packages are published to the `builds` branch instead of to a GitHub
# Release, so everything the installers and the in-app updater fetch comes
# straight from the repository and always matches what is on main.
on: on:
push: push:
tags: branches:
- "v*" - main
# Nothing to rebuild when only documentation changed.
paths-ignore:
- "README.md"
- "LICENSE"
- ".gitignore"
workflow_dispatch:
permissions: permissions:
contents: write contents: write
@ -26,7 +35,7 @@ jobs:
- name: Build AppImage - name: Build AppImage
env: env:
VERSION: ${{ github.ref_name }} COMMIT: ${{ github.sha }}
run: | run: |
chmod +x scripts/bundle-linux.sh chmod +x scripts/bundle-linux.sh
./scripts/bundle-linux.sh ./scripts/bundle-linux.sh
@ -60,7 +69,7 @@ jobs:
- name: Build and bundle - name: Build and bundle
env: env:
VERSION: ${{ github.ref_name }} COMMIT: ${{ github.sha }}
run: | run: |
make bundle make bundle
cd dist cd dist
@ -87,7 +96,7 @@ jobs:
- name: Build .app zip - name: Build .app zip
env: env:
VERSION: ${{ github.ref_name }} COMMIT: ${{ github.sha }}
run: | run: |
chmod +x scripts/bundle-macos.sh chmod +x scripts/bundle-macos.sh
./scripts/bundle-macos.sh ./scripts/bundle-macos.sh
@ -99,7 +108,7 @@ jobs:
path: dist/maturita-macos-*.zip path: dist/maturita-macos-*.zip
publish: publish:
name: Publish GitHub Release name: Publish to builds branch
needs: [linux, windows, macos] needs: [linux, windows, macos]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -108,34 +117,34 @@ jobs:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
path: release-assets path: staged
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 - name: Collect installers
run: | run: |
cp installers/maturita-installer-linux.sh release-assets/ cp installers/maturita-installer-linux.sh staged/
cp installers/maturita-installer-windows.cmd release-assets/ cp installers/maturita-installer-windows.cmd staged/
chmod +x installers/maturita-installer-macos.command chmod +x installers/maturita-installer-macos.command
(cd installers && zip -q ../release-assets/maturita-installer-macos.zip \ (cd installers && zip -q ../staged/maturita-installer-macos.zip \
maturita-installer-macos.command) maturita-installer-macos.command)
# The updater downloads this to see whether it is behind.
echo "${{ github.sha }}" > staged/VERSION
- name: List assets # The branch is rewritten from scratch every time rather than added to,
run: find release-assets -type f -print # so the repository keeps one copy of the packages instead of a new
# ~70 MB set per commit.
- name: Create release - name: Push to builds branch
uses: softprops/action-gh-release@v2 run: |
with: git config user.name "github-actions[bot]"
files: | git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
release-assets/maturita-linux-*.AppImage rm -rf .git/index.lock
release-assets/maturita-windows-x64.zip git checkout --orphan builds
release-assets/maturita-macos-*.zip git rm -rf --cached . >/dev/null
release-assets/maturita-installer-macos.zip find . -maxdepth 1 ! -name . ! -name .git ! -name staged \
release-assets/maturita-installer-windows.cmd -exec rm -rf {} +
release-assets/maturita-installer-linux.sh mv staged/* .
generate_release_notes: true rmdir staged
fail_on_unmatched_files: true ls -la
env: git add -A
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} git commit -q -m "Build from ${{ github.sha }}"
git push -f origin builds

View file

@ -52,9 +52,9 @@ else()
add_executable(maturita ${MATURITA_SOURCES}) add_executable(maturita ${MATURITA_SOURCES})
endif() endif()
# Mirrors VERSION in the Makefile: bakes the release tag in for the updater. # Mirrors COMMIT in the Makefile: bakes the source commit in for the updater.
if(DEFINED APP_VERSION AND NOT APP_VERSION STREQUAL "") if(DEFINED APP_COMMIT AND NOT APP_COMMIT STREQUAL "")
target_compile_definitions(maturita PRIVATE APP_VERSION="${APP_VERSION}") target_compile_definitions(maturita PRIVATE APP_COMMIT="${APP_COMMIT}")
endif() endif()
if(MSVC) if(MSVC)

View file

@ -17,12 +17,12 @@ GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4)
CFLAGS = -Wall -Wextra -Wno-deprecated-declarations $(GTK_CFLAGS) CFLAGS = -Wall -Wextra -Wno-deprecated-declarations $(GTK_CFLAGS)
LIBS = $(GTK_LIBS) -lm LIBS = $(GTK_LIBS) -lm
# Release builds pass the git tag (the CI workflow exports it), which bakes the # Published builds pass the commit they were made from (the CI workflow
# version into the binary for the updater to compare against. Without it the # exports it), which the updater compares against the build in the repository.
# build keeps the "dev" default from maturita.h and never offers an update. # Without it the build keeps the "dev" default and never offers an update.
VERSION ?= COMMIT ?=
ifneq ($(VERSION),) ifneq ($(COMMIT),)
CFLAGS += -DAPP_VERSION='"$(VERSION)"' CFLAGS += -DAPP_COMMIT='"$(COMMIT)"'
endif endif
# Windows (MSYS2 / MinGW): .exe suffix, no console window, embed .ico. # Windows (MSYS2 / MinGW): .exe suffix, no console window, embed .ico.

View file

@ -125,44 +125,35 @@ GTK 4 interface.
### Downloads (prebuilt) ### Downloads (prebuilt)
Precompiled, self-contained builds are published on the There are no releases and no version numbers. CI rebuilds all three platforms
**GitHub Releases** page of this project (enable Actions on a GitHub remote) whenever `main` changes and commits the packages to the
when a version tag (`v*`) is pushed. No compiler or GTK development packages [`builds`](../../tree/builds) branch, so what you download is always the
are required: current state of the repository. No compiler or GTK development packages are
required.
| Platform | Asset | How to run | Start with the installer for your platform. It contains no application of its
| -------- | ----- | ---------- | own — it fetches the current build when you run it, so the file never goes out
| Linux (x86_64) | `maturita-linux-x86_64.AppImage` | `chmod +x` the file, then double-click or run it | of date and you only ever need this one download:
| macOS (Apple Silicon) | `maturita-macos-arm64.zip` | Unzip and open `Maturita.app` |
| Windows (x64) | `maturita-windows-x64.zip` | Unzip and run `maturita.exe` |
Progress and settings are saved next to the AppImage / `.app` / unzipped | Platform | File | How to run |
folder in a `progress/` directory. | -------- | ---- | ---------- |
| macOS (Apple Silicon) | [`maturita-installer-macos.zip`](../../raw/builds/maturita-installer-macos.zip) | Unzip, then double-click the `.command` file |
| Windows (x64) | [`maturita-installer-windows.cmd`](../../raw/builds/maturita-installer-windows.cmd) | Double-click the file |
| Linux (x86_64) | [`maturita-installer-linux.sh`](../../raw/builds/maturita-installer-linux.sh) | `sh maturita-installer-linux.sh` |
#### Installers Once installed, the app keeps itself up to date: it compares the commit it was
built from against `VERSION` on the `builds` branch shortly after startup and
Each release also ships a small installer that contains no application of its offers the update in a banner. There is a manual check under
own: it downloads the newest build when you run it, so the file never goes out **Settings → Updates**.
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: 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 on macOS right-click the installer and pick **Open**, and on Windows choose
**More info → Run anyway** if SmartScreen appears. **More info → Run anyway** if SmartScreen appears.
To cut a release yourself (needs a GitHub remote with Actions enabled): The packages themselves are on the same branch if you would rather skip the
installer: `maturita-linux-x86_64.AppImage`, `maturita-macos-arm64.zip` and
```bash `maturita-windows-x64.zip`. Progress and settings are saved next to the
git tag v1.0.0 AppImage / `.app` / unzipped folder in a `progress/` directory.
git push origin v1.0.0
```
Local packaging (same scripts the CI uses): Local packaging (same scripts the CI uses):
@ -605,43 +596,37 @@ moderním GTK 4 rozhraním stylovaným přes CSS.
### Stažení (předkompilované) ### Stažení (předkompilované)
Hotové self-contained buildy vycházejí na stránce Žádné releasy ani čísla verzí nejsou. CI přestaví všechny tři platformy
**GitHub Releases** tohoto projektu (Actions na GitHub remote) pokaždé, když se změní `main`, a balíčky commitne do větve
při pushi tagu `v*`. Kompilátor ani vývojové balíčky GTK nejsou potřeba: [`builds`](../../tree/builds) — stahujete tedy vždycky aktuální stav repa.
Kompilátor ani vývojové balíčky GTK nejsou potřeba.
Začněte instalátorem pro svou platformu. Samotnou aplikaci neobsahuje, stáhne
si aktuální build až při spuštění, takže soubor nikdy nezestárne a stačí vám
tohle jedno stažení:
| Platforma | Soubor | Spuštění | | Platforma | Soubor | Spuštění |
| --------- | ------ | -------- | | --------- | ------ | -------- |
| Linux (x86_64) | `maturita-linux-x86_64.AppImage` | `chmod +x` a spustit / dvojklik | | macOS (Apple Silicon) | [`maturita-installer-macos.zip`](../../raw/builds/maturita-installer-macos.zip) | Rozbalit a dvojklik na soubor `.command` |
| macOS (Apple Silicon) | `maturita-macos-arm64.zip` | Rozbalit a otevřít `Maturita.app` | | Windows (x64) | [`maturita-installer-windows.cmd`](../../raw/builds/maturita-installer-windows.cmd) | Dvojklik na soubor |
| Windows (x64) | `maturita-windows-x64.zip` | Rozbalit a spustit `maturita.exe` | | Linux (x86_64) | [`maturita-installer-linux.sh`](../../raw/builds/maturita-installer-linux.sh) | `sh maturita-installer-linux.sh` |
Postup a nastavení se ukládají vedle AppImage / `.app` / rozbalené složky Nainstalovaná aplikace se pak udržuje aktuální sama: chvíli po startu porovná
do adresáře `progress/`. commit, ze kterého je postavená, se souborem `VERSION` ve větvi `builds`
a případnou aktualizaci nabídne v pruhu nahoře. Ruční kontrola je
#### 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**. 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 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 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**. hlášce SmartScreenu **Další informace → Přesto spustit**.
Vytvoření releasu (GitHub remote s Actions): Kdo chce instalátor přeskočit, najde na stejné větvi i samotné balíčky:
`maturita-linux-x86_64.AppImage`, `maturita-macos-arm64.zip` a
`maturita-windows-x64.zip`. Postup a nastavení se ukládají vedle AppImage /
`.app` / rozbalené složky do adresáře `progress/`.
```bash Vydání nové verze je prostý push na `main` — build i publikování do větve
git tag v1.0.0 `builds` obstará CI.
git push origin v1.0.0
```
Lokální balíček (stejné skripty jako CI): Lokální balíček (stejné skripty jako CI):

15
i18n.c
View file

@ -20,11 +20,12 @@ const TrEntry tr_ui[] = {
{"theme", "TÉMA", "THEME"}, {"theme", "TÉMA", "THEME"},
{"language", "JAZYK", "LANGUAGE"}, {"language", "JAZYK", "LANGUAGE"},
{"updates", "AKTUALIZACE", "UPDATES"}, {"updates", "AKTUALIZACE", "UPDATES"},
{"update_current", "Verze %s", "Version %s"}, {"update_current", "Build %s", "Build %s"},
{"update_checking", "Hledám aktualizace…", "Checking for updates…"}, {"update_checking", "Hledám aktualizace…", "Checking for updates…"},
{"update_uptodate", "Máte nejnovější verzi (%s).", {"update_uptodate", "Máte nejnovější build (%s).",
"You are up to date (%s)."}, "You are up to date (%s)."},
{"update_available", "Je dostupná verze %s.", "Version %s is available."}, {"update_available", "Je dostupný novější build (%s).",
"A newer build is available (%s)."},
{"update_downloading", "Stahuji aktualizaci…", "Downloading the update…"}, {"update_downloading", "Stahuji aktualizaci…", "Downloading the update…"},
{"update_staged", "Aktualizace je připravená — restartujte aplikaci.", {"update_staged", "Aktualizace je připravená — restartujte aplikaci.",
"The update is ready — restart the app to finish."}, "The update is ready — restart the app to finish."},
@ -38,16 +39,16 @@ const TrEntry tr_ui[] = {
{"update_err_download", "Stahování se nepovedlo.", "The download failed."}, {"update_err_download", "Stahování se nepovedlo.", "The download failed."},
{"update_err_curl", "V systému chybí curl, aktualizace nejsou dostupné.", {"update_err_curl", "V systému chybí curl, aktualizace nejsou dostupné.",
"curl is missing, so updates are unavailable."}, "curl is missing, so updates are unavailable."},
{"update_err_unsupported", "Pro tuto platformu se aktualizace nevydává.", {"update_err_unsupported", "Pro tuto platformu se build nepublikuje.",
"No update is published for this platform."}, "No build is published for this platform."},
{"update_err_readonly", {"update_err_readonly",
"Do umístění aplikace nelze zapisovat. Přesuňte ji třeba do Aplikací.", "Do umístění aplikace nelze zapisovat. Přesuňte ji třeba do Aplikací.",
"The app's location is not writable. Move it somewhere else, for " "The app's location is not writable. Move it somewhere else, for "
"example to Applications."}, "example to Applications."},
{"update_err_stage", "Aktualizaci nešlo připravit.", {"update_err_stage", "Aktualizaci nešlo připravit.",
"Could not prepare the update."}, "Could not prepare the update."},
{"update_err_devbuild", "Tento build nemá číslo verze (dev).", {"update_err_devbuild", "Tento build nepochází z repozitáře (dev).",
"This build carries no version number (dev)."}, "This build did not come from the repository (dev)."},
{"welcome_body", {"welcome_body",
"maturita.C je vzdělávací program pro studenty středních škol a " "maturita.C je vzdělávací program pro studenty středních škol a "
"gymnázií na přípravu k maturitě.\n\n" "gymnázií na přípravu k maturitě.\n\n"

View file

@ -1,9 +1,9 @@
#!/bin/sh #!/bin/sh
# maturita.c installer for Linux. # maturita.c installer for Linux.
# #
# Carries no application of its own: it downloads whatever the newest release # Carries no application of its own: it downloads the current build straight
# is at the moment you run it, so this file never goes stale. GitHub resolves # from the repository's `builds` branch, which CI rewrites whenever main
# the /releases/latest/download/ path to the current tag on every request. # changes. This file therefore never goes stale.
# #
# Run it with: sh maturita-installer-linux.sh # Run it with: sh maturita-installer-linux.sh
# The app updates itself from then on. # The app updates itself from then on.
@ -11,8 +11,9 @@
set -eu set -eu
REPO="pepaskopec-blip/maturita.c" REPO="pepaskopec-blip/maturita.c"
BRANCH="builds"
ASSET="maturita-linux-x86_64.AppImage" ASSET="maturita-linux-x86_64.AppImage"
URL="https://github.com/$REPO/releases/latest/download/$ASSET" URL="https://raw.githubusercontent.com/$REPO/$BRANCH/$ASSET"
# ~/Applications is where desktop environments look for AppImages, and the app # ~/Applications is where desktop environments look for AppImages, and the app
# keeps its progress/ directory next to its own file. # keeps its progress/ directory next to its own file.

View file

@ -1,17 +1,18 @@
#!/bin/sh #!/bin/sh
# maturita.c installer for macOS. # maturita.c installer for macOS.
# #
# Carries no application of its own: it downloads whatever the newest release # Carries no application of its own: it downloads the current build straight
# is at the moment you run it, so this file never goes stale. GitHub resolves # from the repository's `builds` branch, which CI rewrites whenever main
# the /releases/latest/download/ path to the current tag on every request. # changes. This file therefore never goes stale.
# #
# Double-click the file to run it. The app updates itself from then on. # Double-click the file to run it. The app updates itself from then on.
set -eu set -eu
REPO="pepaskopec-blip/maturita.c" REPO="pepaskopec-blip/maturita.c"
BRANCH="builds"
ASSET="maturita-macos-arm64.zip" ASSET="maturita-macos-arm64.zip"
URL="https://github.com/$REPO/releases/latest/download/$ASSET" URL="https://raw.githubusercontent.com/$REPO/$BRANCH/$ASSET"
say() { printf '%s\n' "$*"; } say() { printf '%s\n' "$*"; }
die() { printf '\nError: %s\n' "$*" >&2; printf 'Press Return to close. '; read -r _; exit 1; } die() { printf '\nError: %s\n' "$*" >&2; printf 'Press Return to close. '; read -r _; exit 1; }

View file

@ -1,16 +1,17 @@
@echo off @echo off
rem maturita.c installer for Windows. rem maturita.c installer for Windows.
rem rem
rem Carries no application of its own: it downloads whatever the newest release rem Carries no application of its own: it downloads the current build straight
rem is at the moment you run it, so this file never goes stale. GitHub resolves rem from the repository's `builds` branch, which CI rewrites whenever main
rem the /releases/latest/download/ path to the current tag on every request. rem changes. This file therefore never goes stale.
rem rem
rem Double-click the file to run it. The app updates itself from then on. rem Double-click the file to run it. The app updates itself from then on.
setlocal setlocal
set "REPO=pepaskopec-blip/maturita.c" set "REPO=pepaskopec-blip/maturita.c"
set "BRANCH=builds"
set "ASSET=maturita-windows-x64.zip" set "ASSET=maturita-windows-x64.zip"
set "URL=https://github.com/%REPO%/releases/latest/download/%ASSET%" set "URL=https://raw.githubusercontent.com/%REPO%/%BRANCH%/%ASSET%"
set "DEST=%LOCALAPPDATA%\Programs\Maturita" set "DEST=%LOCALAPPDATA%\Programs\Maturita"
echo Installing maturita.C echo Installing maturita.C
@ -51,7 +52,7 @@ exit /b 0
echo. echo.
echo Error: this installer needs curl and tar, which come with Windows 10 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 version 1803 and later. Please update Windows, or download the zip
echo manually from https://github.com/%REPO%/releases/latest echo manually from https://github.com/%REPO%/tree/%BRANCH%
pause pause
exit /b 1 exit /b 1

View file

@ -21,13 +21,16 @@
#define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */
#define CSS_FILE "style.css" #define CSS_FILE "style.css"
#define APP_ID "org.maturita.Maturita" #define APP_ID "org.maturita.Maturita"
/* Release builds bake the git tag in (see VERSION in the Makefile), so the /* Published builds bake in the commit they were made from (see COMMIT in the
* updater can tell what it is running. Plain builds stay at "dev" and never * Makefile), which is what the updater compares against the build published
* report an update as available. */ * in the repository. Local builds stay at "dev" and never offer an update. */
#ifndef APP_VERSION #ifndef APP_COMMIT
#define APP_VERSION "dev" #define APP_COMMIT "dev"
#endif #endif
#define UPDATE_REPO "pepaskopec-blip/maturita.c" #define UPDATE_REPO "pepaskopec-blip/maturita.c"
/* Packages live on their own branch rather than in releases, so the update
* always matches whatever is on main. */
#define UPDATE_BRANCH "builds"
#define ICON_NAME "maturita" #define ICON_NAME "maturita"
#define ICON_FILE "assets/app-icon.png" #define ICON_FILE "assets/app-icon.png"
#define ICON_THEME_DIR "assets/icons" /* cwd when run from source tree */ #define ICON_THEME_DIR "assets/icons" /* cwd when run from source tree */

139
update.c
View file

@ -19,9 +19,13 @@
* Every call is asynchronous, so a slow or unreachable network never blocks * Every call is asynchronous, so a slow or unreachable network never blocks
* the interface. * the interface.
* *
* The newest version is read from the redirect GitHub serves for * Packages are not published as releases. They live on the `builds` branch of
* /releases/latest, which points at /releases/tag/<tag>. That avoids the REST * the repository, which CI rewrites on every change to main, so an update is
* API together with its unauthenticated rate limit, and needs no JSON parser. * simply whatever that branch currently holds. The branch also carries a
* VERSION file naming the commit its packages were built from; comparing that
* against the commit baked into this binary is the whole check. Serving it
* from raw.githubusercontent.com avoids the REST API and its unauthenticated
* rate limit, and needs no JSON parser.
* *
* An application cannot reliably replace its own files while it is running, so * An application cannot reliably replace its own files while it is running, so
* all three platforms follow the same shape: unpack the new version into a * all three platforms follow the same shape: unpack the new version into a
@ -30,9 +34,9 @@
* script detached and quit. Staging beside the install keeps the final move on * script detached and quit. Staging beside the install keeps the final move on
* one filesystem and proves up front that the location is writable. */ * one filesystem and proves up front that the location is writable. */
#define UPDATE_LATEST_URL "https://github.com/" UPDATE_REPO "/releases/latest" #define UPDATE_BASE_URL "https://raw.githubusercontent.com/" UPDATE_REPO \
#define UPDATE_ASSET_URL "https://github.com/" UPDATE_REPO \ "/" UPDATE_BRANCH "/"
"/releases/latest/download/" #define UPDATE_VERSION_URL UPDATE_BASE_URL "VERSION"
#define UPDATE_STAGING_DIR ".maturita-update" #define UPDATE_STAGING_DIR ".maturita-update"
#ifdef _WIN32 #ifdef _WIN32
@ -64,7 +68,7 @@ typedef enum {
static struct { static struct {
UpdateState state; UpdateState state;
gboolean interactive; /* user asked, so also report "up to date" */ gboolean interactive; /* user asked, so also report "up to date" */
char *latest; /* newest release version, without any "v" */ char *latest; /* commit the published packages were built from */
char *staging; /* scratch directory beside the install */ char *staging; /* scratch directory beside the install */
char *archive; /* asset downloaded into the staging directory */ char *archive; /* asset downloaded into the staging directory */
char *source; /* unpacked version the swap script moves in */ char *source; /* unpacked version the swap script moves in */
@ -81,39 +85,33 @@ static struct {
} up; } up;
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Versions */ /* Build identity */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
static const char *strip_v(const char *v) { /* Commits are not ordered, so there is nothing to compare: the branch only
return (v && (v[0] == 'v' || v[0] == 'V')) ? v + 1 : v; * ever holds the current build, and anything else means this copy is behind.
* Short and full forms count as the same commit, so the interface can show a
* short one without confusing the check. */
static gboolean same_commit(const char *a, const char *b) {
size_t la, lb, n;
if (!a || !b || !*a || !*b)
return FALSE;
la = strlen(a);
lb = strlen(b);
n = MIN(la, lb);
return n >= 7 && g_ascii_strncasecmp(a, b, n) == 0;
} }
/* Component-wise numeric comparison, so 1.0.10 sorts above 1.0.9. Missing /* Only builds published by CI carry a commit; see APP_COMMIT in maturita.h. */
* components count as zero, which makes 1.1 and 1.1.0 equal. */ static gboolean is_published_build(void) {
static int version_cmp(const char *a, const char *b) { return g_ascii_isxdigit(APP_COMMIT[0]) && strlen(APP_COMMIT) >= 7;
char **pa = g_strsplit(strip_v(a), ".", -1);
char **pb = g_strsplit(strip_v(b), ".", -1);
guint na = g_strv_length(pa);
guint nb = g_strv_length(pb);
guint n = MAX(na, nb);
int result = 0;
for (guint i = 0; i < n && result == 0; i++) {
gint64 va = i < na ? g_ascii_strtoll(pa[i], NULL, 10) : 0;
gint64 vb = i < nb ? g_ascii_strtoll(pb[i], NULL, 10) : 0;
if (va != vb)
result = va < vb ? -1 : 1;
}
g_strfreev(pa);
g_strfreev(pb);
return result;
} }
/* Only tagged builds know their version; see APP_VERSION in maturita.h. */ /* Commit hashes are unreadable at full length; seven characters is what git
static gboolean is_release_build(void) { * itself shows. */
return g_ascii_isdigit(strip_v(APP_VERSION)[0]); static char *short_commit(const char *sha) {
return g_strndup(sha ? sha : "", 7);
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@ -272,22 +270,28 @@ static void update_refresh_ui(void) {
switch (up.state) { switch (up.state) {
case UPDATE_IDLE: case UPDATE_IDLE:
owned = g_strdup_printf(tr("update_current"), APP_VERSION); case UPDATE_UP_TO_DATE: {
char *mine = short_commit(APP_COMMIT);
owned = g_strdup_printf(tr(up.state == UPDATE_IDLE
? "update_current"
: "update_uptodate"), mine);
status = owned; status = owned;
g_free(mine);
break; break;
}
case UPDATE_CHECKING: case UPDATE_CHECKING:
status = tr("update_checking"); status = tr("update_checking");
break; break;
case UPDATE_UP_TO_DATE: case UPDATE_AVAILABLE: {
owned = g_strdup_printf(tr("update_uptodate"), APP_VERSION); char *newest = short_commit(up.latest);
status = owned;
break; owned = g_strdup_printf(tr("update_available"), newest);
case UPDATE_AVAILABLE:
owned = g_strdup_printf(tr("update_available"),
up.latest ? up.latest : "");
status = owned; status = owned;
action = "update_install"; action = "update_install";
g_free(newest);
break; break;
}
case UPDATE_DOWNLOADING: case UPDATE_DOWNLOADING:
status = tr("update_downloading"); status = tr("update_downloading");
action = "update_install"; action = "update_install";
@ -581,7 +585,7 @@ static void size_probe_done(GObject *src, GAsyncResult *res, gpointer data) {
if (up.state != UPDATE_DOWNLOADING) if (up.state != UPDATE_DOWNLOADING)
return; return;
url = g_strconcat(UPDATE_ASSET_URL, asset_name(), NULL); url = g_strconcat(UPDATE_BASE_URL, asset_name(), NULL);
argv[0] = "curl"; argv[0] = "curl";
argv[1] = "-fsSL"; argv[1] = "-fsSL";
argv[2] = "--max-time"; argv[2] = "--max-time";
@ -630,7 +634,7 @@ static void update_start_download(void) {
update_set_state(UPDATE_DOWNLOADING); update_set_state(UPDATE_DOWNLOADING);
/* HEAD the asset with its headers on stdout, to size the progress bar. */ /* HEAD the asset with its headers on stdout, to size the progress bar. */
url = g_strconcat(UPDATE_ASSET_URL, asset_name(), NULL); url = g_strconcat(UPDATE_BASE_URL, asset_name(), NULL);
argv[0] = "curl"; argv[0] = "curl";
argv[1] = "-fsSLI"; argv[1] = "-fsSLI";
argv[2] = "--max-time"; argv[2] = "--max-time";
@ -651,18 +655,6 @@ static void update_start_download(void) {
/* Check */ /* Check */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* The effective URL ends in /releases/tag/<tag>. */
static char *tag_from_url(const char *url) {
const char *slash;
if (!url || !*url)
return NULL;
slash = g_strrstr(url, "/");
if (!slash || !slash[1])
return NULL;
return g_strdup(slash + 1);
}
/* A failed background check stays silent; only an explicit one reports. */ /* A failed background check stays silent; only an explicit one reports. */
static void check_giveup(void) { static void check_giveup(void) {
if (up.interactive) if (up.interactive)
@ -674,7 +666,6 @@ static void check_giveup(void) {
static void check_done(GObject *src, GAsyncResult *res, gpointer data) { static void check_done(GObject *src, GAsyncResult *res, gpointer data) {
GSubprocess *proc = G_SUBPROCESS(src); GSubprocess *proc = G_SUBPROCESS(src);
char *out = NULL; char *out = NULL;
char *tag;
gboolean ok; gboolean ok;
(void)data; (void)data;
@ -682,30 +673,30 @@ static void check_done(GObject *src, GAsyncResult *res, gpointer data) {
ok = g_subprocess_get_successful(proc); ok = g_subprocess_get_successful(proc);
g_object_unref(proc); g_object_unref(proc);
tag = ok ? tag_from_url(g_strstrip(out ? out : "")) : NULL; if (out)
g_free(out); g_strstrip(out);
if (!tag) { if (!ok || !out || !*out) {
g_free(out);
check_giveup(); check_giveup();
return; return;
} }
g_free(up.latest); g_free(up.latest);
up.latest = g_strdup(strip_v(tag)); up.latest = out;
g_free(tag);
if (version_cmp(APP_VERSION, up.latest) >= 0) if (same_commit(APP_COMMIT, up.latest))
update_set_state(up.interactive ? UPDATE_UP_TO_DATE : UPDATE_IDLE); update_set_state(up.interactive ? UPDATE_UP_TO_DATE : UPDATE_IDLE);
else if (update_supported()) else if (update_supported())
update_set_state(UPDATE_AVAILABLE); update_set_state(UPDATE_AVAILABLE);
else if (up.interactive) else if (up.interactive)
/* Newer release exists, but this copy cannot swap itself out. */ /* A newer build exists, but this copy cannot swap itself out. */
update_fail(tr("update_err_unsupported")); update_fail(tr("update_err_unsupported"));
else else
update_set_state(UPDATE_IDLE); update_set_state(UPDATE_IDLE);
} }
void update_check_async(gboolean interactive) { void update_check_async(gboolean interactive) {
const char *argv[10]; const char *argv[8];
if (up.state == UPDATE_CHECKING || up.state == UPDATE_DOWNLOADING || if (up.state == UPDATE_CHECKING || up.state == UPDATE_DOWNLOADING ||
up.state == UPDATE_STAGED) up.state == UPDATE_STAGED)
@ -713,8 +704,8 @@ void update_check_async(gboolean interactive) {
up.interactive = interactive; up.interactive = interactive;
/* Nothing to compare against without a baked-in version. */ /* Nothing to compare against without a baked-in commit. */
if (!is_release_build()) { if (!is_published_build()) {
if (interactive) if (interactive)
update_fail(tr("update_err_devbuild")); update_fail(tr("update_err_devbuild"));
return; return;
@ -722,16 +713,16 @@ void update_check_async(gboolean interactive) {
update_set_state(UPDATE_CHECKING); update_set_state(UPDATE_CHECKING);
/* The CDN in front of raw.githubusercontent.com caches for a few minutes,
* so ask it not to hand back a stale commit. */
argv[0] = "curl"; argv[0] = "curl";
argv[1] = "-fsSLI"; argv[1] = "-fsSL";
argv[2] = "--max-time"; argv[2] = "--max-time";
argv[3] = "20"; argv[3] = "20";
argv[4] = "-o"; argv[4] = "-H";
argv[5] = NULL_DEVICE; argv[5] = "Cache-Control: no-cache";
argv[6] = "-w"; argv[6] = UPDATE_VERSION_URL;
argv[7] = "%{url_effective}"; argv[7] = NULL;
argv[8] = UPDATE_LATEST_URL;
argv[9] = NULL;
if (!curl_async(argv, check_done)) { if (!curl_async(argv, check_done)) {
if (interactive) if (interactive)