Add self-contained release packaging for Linux, macOS, and Windows.

Resolve asset/progress paths for AppImage and .app bundles, and ship GitHub Actions builds on version tags.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-16 08:35:29 +02:00
commit f8e27e8b53
10 changed files with 711 additions and 18 deletions

125
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,125 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
linux:
name: Linux AppImage
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential pkg-config curl \
libgtk-4-dev librsvg2-dev \
desktop-file-utils \
appstream
- name: Build AppImage
env:
VERSION: ${{ github.ref_name }}
run: |
chmod +x scripts/bundle-linux.sh
./scripts/bundle-linux.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: maturita-linux
path: dist/maturita-linux-*.AppImage
windows:
name: Windows zip
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- uses: actions/checkout@v4
- uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
make
zip
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-gtk4
mingw-w64-ucrt-x86_64-pkg-config
mingw-w64-ucrt-x86_64-make
- name: Build and bundle
run: |
make bundle
cd dist
zip -qry ../maturita-windows-x64.zip .
cd ..
mkdir -p dist-out
mv maturita-windows-x64.zip dist-out/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: maturita-windows
path: dist-out/maturita-windows-x64.zip
macos:
name: macOS app
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
brew install gtk4 pkg-config dylibbundler
- name: Build .app zip
env:
VERSION: ${{ github.ref_name }}
run: |
chmod +x scripts/bundle-macos.sh
./scripts/bundle-macos.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: maturita-macos
path: dist/maturita-macos-*.zip
publish:
name: Publish GitHub Release
needs: [linux, windows, macos]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- name: List assets
run: find release-assets -type f -print
- name: Create release
uses: softprops/action-gh-release@v2
with:
files: |
release-assets/maturita-linux-*.AppImage
release-assets/maturita-windows-x64.zip
release-assets/maturita-macos-*.zip
generate_release_notes: true
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -23,11 +23,13 @@ TARGET = maturita.exe
LIBS += -mwindows
WINDRES ?= windres
RC_OBJ = maturita_rc.o
UNAME_S :=
else
TARGET = maturita
RC_OBJ =
UNAME_S := $(shell uname -s 2>/dev/null)
# macOS Dock icon is set via AppKit (see macos_dock.c).
ifeq ($(shell uname -s 2>/dev/null),Darwin)
ifeq ($(UNAME_S),Darwin)
LIBS += -framework AppKit -framework Foundation
endif
endif
@ -62,9 +64,7 @@ clean:
run: $(TARGET)
./$(TARGET)
# Windows only: copy the executable and every non-system (MinGW/GTK) DLL it
# needs into dist/ so the app can be started by double-clicking, without an
# MSYS2 shell on PATH.
# Self-contained package for the current platform into dist/.
ifeq ($(OS),Windows_NT)
bundle: $(TARGET)
@rm -rf dist
@ -77,9 +77,14 @@ bundle: $(TARGET)
@ldd $(TARGET) | grep -Ei '/(ucrt64|mingw64)/bin/' | awk '{print $$3}' \
| xargs -r -I{} cp -f {} dist/
@echo "Bundled into dist/ - run dist/$(TARGET)"
else ifeq ($(UNAME_S),Darwin)
bundle:
@chmod +x scripts/bundle-macos.sh
@./scripts/bundle-macos.sh
else
bundle:
@echo "make bundle is only needed on Windows (MSYS2/MinGW)."
@chmod +x scripts/bundle-linux.sh
@./scripts/bundle-linux.sh
endif
.PHONY: all clean run bundle

View file

@ -123,6 +123,35 @@ GTK 4 interface.
- **Keyboard quit shortcuts** – `Super/Cmd+Q` or `Alt+F4` closes the app
- Modular C codebase split across several files, cross-platform (Linux, macOS and Windows)
### Downloads (prebuilt)
Precompiled, self-contained builds are published on the
**GitHub Releases** page of this project (enable Actions on a GitHub remote)
when a version tag (`v*`) is pushed. No compiler or GTK development packages
are required:
| Platform | Asset | How to run |
| -------- | ----- | ---------- |
| Linux (x86_64) | `maturita-linux-x86_64.AppImage` | `chmod +x` the file, then double-click or run it |
| 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
folder in a `progress/` directory.
To cut a release yourself (needs a GitHub remote with Actions enabled):
```bash
git tag v1.0.0
git push origin v1.0.0
```
Local packaging (same scripts the CI uses):
```bash
make bundle # Linux AppImage, macOS .app zip, or Windows DLL folder
```
### Installation
#### Prerequisites
@ -373,7 +402,10 @@ hw.c computer-hardware path (10 nodes), units 1–3 slides
and quizzes (architecture, history, bit/byte)
main.c application entry point and page wiring
style.css GTK stylesheet (palette colors are injected at runtime)
Makefile build & run targets (Linux, macOS, MSYS2); `make bundle` on Windows
Makefile build & run targets (Linux, macOS, MSYS2); `make bundle` packs a release
scripts/bundle-linux.sh Linux AppImage (linuxdeploy + GTK plugin)
scripts/bundle-macos.sh macOS Maturita.app with relocated GTK dylibs
.github/workflows/ GitHub Actions release on tag `v*`
CMakeLists.txt optional CMake build (Linux / macOS)
build-windows.bat legacy MSVC build (requires Visual Studio + gvsbuild GTK)
run-windows.bat legacy launcher for the MSVC build with GTK DLLs on PATH
@ -553,6 +585,34 @@ moderním GTK 4 rozhraním stylovaným přes CSS.
- **Klávesové zkratky pro ukončení** – `Super/Cmd+Q` nebo `Alt+F4`
- Modulární C kód v několika souborech, multiplatformní (Linux, macOS i Windows)
### Stažení (předkompilované)
Hotové self-contained buildy vycházejí na stránce
**GitHub Releases** tohoto projektu (Actions na GitHub remote)
při pushi tagu `v*`. Kompilátor ani vývojové balíčky GTK nejsou potřeba:
| Platforma | Soubor | Spuštění |
| --------- | ------ | -------- |
| Linux (x86_64) | `maturita-linux-x86_64.AppImage` | `chmod +x` a spustit / dvojklik |
| macOS (Apple Silicon) | `maturita-macos-arm64.zip` | Rozbalit a otevřít `Maturita.app` |
| Windows (x64) | `maturita-windows-x64.zip` | Rozbalit a spustit `maturita.exe` |
Postup a nastavení se ukládají vedle AppImage / `.app` / rozbalené složky
do adresáře `progress/`.
Vytvoření releasu (GitHub remote s Actions):
```bash
git tag v1.0.0
git push origin v1.0.0
```
Lokální balíček (stejné skripty jako CI):
```bash
make bundle # Linux AppImage, macOS .app zip, nebo Windows složka s DLL
```
### Instalace
#### Požadavky
@ -800,7 +860,10 @@ hw.c cesta technického vybavení (10 uzlů), jednotky 1–3
(architektura, historie, bit/byte + kvízy)
main.c vstupní bod aplikace a zapojení stránek
style.css GTK styl (barvy palety se vkládají za běhu)
Makefile build & run (Linux, macOS, MSYS2); `make bundle` na Windows
Makefile build & run (Linux, macOS, MSYS2); `make bundle` vytvoří release balíček
scripts/bundle-linux.sh Linux AppImage (linuxdeploy + GTK plugin)
scripts/bundle-macos.sh macOS Maturita.app s přbalenými GTK dylibs
.github/workflows/ GitHub Actions release při tagu `v*`
CMakeLists.txt volitelný CMake build (Linux / macOS)
build-windows.bat legacy MSVC build (vyžaduje Visual Studio + gvsbuild GTK)
run-windows.bat legacy spouštění MSVC buildu s GTK DLL na PATH

View file

@ -1090,7 +1090,7 @@ static UnitCtx lit_plot_unit;
GtkWidget *build_cetba1984_plot_page(void) {
lit_plot_unit.page = "cetba1984";
lit_plot_unit.progress_file = "progress/cetba.conf";
lit_plot_unit.progress_file = app_progress_cetba;
return build_assembly(&lit_plot_unit, tr("lit_plot_title"),
"lit_plot_sub", 1, lit_plot_items,
lit_plot_meaning, 1);
@ -1119,7 +1119,7 @@ static const char *fuks_plot_meaning[] = {
GtkWidget *build_cetba_fuks_plot_page(void) {
lit_plot_unit.page = "cetbaFuks";
lit_plot_unit.progress_file = "progress/cetba.conf";
lit_plot_unit.progress_file = app_progress_cetba;
return build_assembly(&lit_plot_unit, tr("lit_fuks_plot_title"),
"lit_fuks_plot_sub", 1, fuks_plot_items,
fuks_plot_meaning, 1);

4
main.c
View file

@ -476,6 +476,10 @@ int main(int argc, char **argv) {
GtkApplication *app;
int status;
/* Resolve style.css / icons next to the binary (or AppImage / .app) and
* point progress files at a writable location before any I/O. */
setup_portable_paths();
/* Must run before GtkApplication so Wayland can resolve our .desktop
* file / icons via the application id. */
setup_portable_share_dir();

View file

@ -22,13 +22,24 @@
#define ICON_THEME_DIR_ALT "icons" /* next to the binary after install */
#define ICON_FILE_ALT "icons/hicolor/512x512/apps/maturita.png"
#define SHARE_DIR "share" /* portable Freedesktop data root */
#define PROGRESS_DIR "progress"
#define PROGRESS_U1 "progress/unit1.conf"
#define PROGRESS_U2 "progress/unit2.conf"
#define PROGRESS_U3 "progress/unit3.conf"
#define SETTINGS_FILE "progress/settings.conf"
#define PROGRESS_NET "progress/net.conf"
#define PROGRESS_HW "progress/hw.conf"
/* Relative leaf names; absolute paths are filled by setup_portable_paths(). */
#define PROGRESS_DIR_NAME "progress"
extern char *app_progress_dir;
extern char *app_progress_u1;
extern char *app_progress_u2;
extern char *app_progress_u3;
extern char *app_settings_file;
extern char *app_progress_net;
extern char *app_progress_hw;
extern char *app_progress_mluvnice;
extern char *app_progress_cetba;
#define PROGRESS_DIR app_progress_dir
#define PROGRESS_U1 app_progress_u1
#define PROGRESS_U2 app_progress_u2
#define PROGRESS_U3 app_progress_u3
#define SETTINGS_FILE app_settings_file
#define PROGRESS_NET app_progress_net
#define PROGRESS_HW app_progress_hw
#define NODE_SIZE 88.0
#define PATH_SPAC 240.0
#define ROAD_MX 150.0 /* horizontal canvas margin */
@ -779,6 +790,7 @@ void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data);
GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
double r, double g, double b, int px);
void setup_portable_paths(void);
char *normalize_answer(const char *input);
void shuffle_indices(int *arr, int n);
void flow_clear(GtkFlowBox *fb);

View file

@ -64,7 +64,7 @@ void mluvnice_init(void) {
mluv_unit.sub_key = "Cvičení z mluvnice – styl maturita / "
"přijímačky z ČJL";
mluv_unit.back_target = "czechmap";
mluv_unit.progress_file = "progress/mluvnice.conf";
mluv_unit.progress_file = app_progress_mluvnice;
mluv_unit.n_ex = MLUV_N_EX;
for (int i = 0; i < MLUV_N_EX; i++)
mluv_unit.ex_names[i + 1] = mluv_names[i];

116
scripts/bundle-linux.sh Executable file
View file

@ -0,0 +1,116 @@
#!/usr/bin/env bash
# Build a self-contained Linux AppImage (x86_64).
# Requires: make, pkg-config, GTK4, curl, and fuse/appimagetool (pulled as needed).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64) ARCH=x86_64 ;;
aarch64|arm64) ARCH=aarch64 ;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac
OUT_NAME="maturita-linux-${ARCH}.AppImage"
APPDIR="$ROOT/dist/AppDir"
TOOLS="$ROOT/dist/tools"
ICON_DST="assets/icons/hicolor/512x512/apps/maturita.png"
echo "==> Building maturita"
make -C "$ROOT" clean
make -C "$ROOT"
echo "==> Preparing AppDir"
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin" \
"$APPDIR/usr/share/applications" \
"$APPDIR/usr/share/icons/hicolor/512x512/apps" \
"$APPDIR/icons/hicolor/512x512/apps" \
"$APPDIR/share"
cp -f "$ROOT/maturita" "$APPDIR/usr/bin/maturita"
cp -f "$ROOT/style.css" "$APPDIR/style.css"
cp -f "$ROOT/$ICON_DST" "$APPDIR/icons/hicolor/512x512/apps/maturita.png"
cp -f "$ROOT/$ICON_DST" "$APPDIR/usr/share/icons/hicolor/512x512/apps/maturita.png"
cp -f "$ROOT/$ICON_DST" "$APPDIR/maturita.png"
cp -R "$ROOT/share/." "$APPDIR/share/"
cp -R "$ROOT/share/." "$APPDIR/usr/share/"
cp -f "$ROOT/share/applications/org.maturita.Maturita.desktop" \
"$APPDIR/usr/share/applications/org.maturita.Maturita.desktop"
# AppImage desktop entry must live at AppDir root and Exec= must be AppRun-compatible.
cp -f "$ROOT/share/applications/org.maturita.Maturita.desktop" \
"$APPDIR/org.maturita.Maturita.desktop"
# linuxdeploy expects Icon= without path and a matching PNG at AppDir root.
sed -i.bak 's|^Exec=.*|Exec=maturita|' "$APPDIR/org.maturita.Maturita.desktop"
sed -i.bak 's|^Icon=.*|Icon=maturita|' "$APPDIR/org.maturita.Maturita.desktop"
rm -f "$APPDIR/org.maturita.Maturita.desktop.bak"
mkdir -p "$TOOLS"
LINUXDEPLOY="$TOOLS/linuxdeploy-${ARCH}.AppImage"
GTK_PLUGIN="$TOOLS/linuxdeploy-plugin-gtk.sh"
if [[ ! -x "$LINUXDEPLOY" ]]; then
echo "==> Downloading linuxdeploy"
curl -fsSL -o "$LINUXDEPLOY" \
"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
chmod +x "$LINUXDEPLOY"
fi
if [[ ! -f "$GTK_PLUGIN" ]]; then
echo "==> Downloading linuxdeploy-plugin-gtk"
curl -fsSL -o "$GTK_PLUGIN" \
"https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh"
chmod +x "$GTK_PLUGIN"
fi
# Extract linuxdeploy if FUSE is unavailable (common on CI).
run_linuxdeploy() {
if "$LINUXDEPLOY" --appimage-help >/dev/null 2>&1; then
"$LINUXDEPLOY" "$@"
else
local extract_dir="$TOOLS/linuxdeploy-extracted"
rm -rf "$extract_dir"
cd "$TOOLS"
"$LINUXDEPLOY" --appimage-extract >/dev/null
mv squashfs-root "$extract_dir"
cd "$ROOT"
"$extract_dir/AppRun" "$@"
fi
}
echo "==> Bundling GTK libraries into AppDir"
export LDAI_OUTPUT="$ROOT/dist/$OUT_NAME"
export LINUXDEPLOY_OUTPUT_VERSION="${VERSION:-continuous}"
# Place plugin on PATH so linuxdeploy can find it.
export PATH="$TOOLS:$PATH"
cp -f "$GTK_PLUGIN" "$TOOLS/linuxdeploy-plugin-gtk.sh"
run_linuxdeploy \
--appdir "$APPDIR" \
--executable "$APPDIR/usr/bin/maturita" \
--desktop-file "$APPDIR/org.maturita.Maturita.desktop" \
--icon-file "$APPDIR/maturita.png" \
--plugin gtk \
--output appimage
# linuxdeploy may write the AppImage into the cwd; normalise the name/location.
shopt -s nullglob
for f in "$ROOT"/*.AppImage "$ROOT/dist"/*.AppImage; do
case "$(basename "$f")" in
linuxdeploy*|appimagetool*) continue ;;
esac
mv -f "$f" "$ROOT/dist/$OUT_NAME"
done
shopt -u nullglob
if [[ ! -f "$ROOT/dist/$OUT_NAME" ]]; then
echo "AppImage was not created" >&2
exit 1
fi
chmod +x "$ROOT/dist/$OUT_NAME"
echo "Created dist/$OUT_NAME"

169
scripts/bundle-macos.sh Executable file
View file

@ -0,0 +1,169 @@
#!/usr/bin/env bash
# Build a self-contained macOS .app with GTK dylibs relocated into the bundle.
# Requires: make, pkg-config, GTK4 (Homebrew), dylibbundler (brew install dylibbundler).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
ARCH="$(uname -m)"
case "$ARCH" in
arm64|aarch64) ARCH_TAG=arm64 ;;
x86_64) ARCH_TAG=x86_64 ;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac
APP_NAME="Maturita.app"
APP="$ROOT/dist/$APP_NAME"
MACOS="$APP/Contents/MacOS"
RES="$APP/Contents/Resources"
FW="$APP/Contents/Frameworks"
ICON_DST="assets/icons/hicolor/512x512/apps/maturita.png"
OUT_ZIP="maturita-macos-${ARCH_TAG}.zip"
if ! command -v dylibbundler >/dev/null 2>&1; then
echo "dylibbundler not found. Install with: brew install dylibbundler" >&2
exit 1
fi
echo "==> Building maturita"
make -C "$ROOT" clean
make -C "$ROOT"
echo "==> Creating $APP_NAME"
rm -rf "$APP"
mkdir -p "$MACOS" "$RES/icons/hicolor/512x512/apps" "$RES/share" "$FW"
cp -f "$ROOT/maturita" "$MACOS/maturita-bin"
cp -f "$ROOT/style.css" "$RES/style.css"
cp -f "$ROOT/$ICON_DST" "$RES/icons/hicolor/512x512/apps/maturita.png"
cp -R "$ROOT/share/." "$RES/share/"
# Bundle Homebrew / MacPorts GTK stack next to the binary.
echo "==> Relocating dylibs with dylibbundler"
dylibbundler -od -b \
-x "$MACOS/maturita-bin" \
-d "$FW" \
-p "@executable_path/../Frameworks"
# Copy GLib / GTK runtime data (schemas, modules, icons themes used by GTK).
copy_pkg_share() {
local prefix
prefix="$(pkg-config --variable=prefix "$1" 2>/dev/null || true)"
[[ -n "$prefix" && -d "$prefix/share" ]] || return 0
mkdir -p "$RES/share"
# Copy only the subtrees GTK needs at runtime.
for sub in glib-2.0 gtk-4.0 icons themes locale; do
if [[ -d "$prefix/share/$sub" ]]; then
rsync -a "$prefix/share/$sub" "$RES/share/" 2>/dev/null \
|| cp -R "$prefix/share/$sub" "$RES/share/"
fi
done
}
copy_pkg_share gtk4
copy_pkg_share glib-2.0
# gdk-pixbuf loaders (PNG may be built-in; copy others for GIF/SVG/etc.).
PIXBUF_DIR="$(pkg-config --variable=gdk_pixbuf_moduledir gdk-pixbuf-2.0 2>/dev/null || true)"
LOADERS_DST="$RES/lib/gdk-pixbuf-2.0/2.10.0/loaders"
if [[ -n "$PIXBUF_DIR" && -d "$PIXBUF_DIR" ]]; then
mkdir -p "$LOADERS_DST"
# Homebrew keeps loaders as relative symlinks into Cellar — dereference.
if command -v rsync >/dev/null 2>&1; then
rsync -aL "$PIXBUF_DIR"/ "$LOADERS_DST"/
else
cp -R -L "$PIXBUF_DIR"/. "$LOADERS_DST"/
fi
# Drop broken leftovers and relocate real loader modules.
find "$LOADERS_DST" -type l ! -exec test -e {} \; -delete 2>/dev/null || true
shopt -s nullglob
for loader in "$LOADERS_DST"/*; do
[[ -f "$loader" ]] || continue
dylibbundler -od -b -x "$loader" -d "$FW" -p "@executable_path/../Frameworks" || true
done
shopt -u nullglob
if command -v gdk-pixbuf-query-loaders >/dev/null 2>&1; then
export GDK_PIXBUF_MODULEDIR="$LOADERS_DST"
gdk-pixbuf-query-loaders > "$RES/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" || true
fi
fi
# Compile GSettings schemas if glib-compile-schemas is available.
if [[ -d "$RES/share/glib-2.0/schemas" ]] && command -v glib-compile-schemas >/dev/null 2>&1; then
glib-compile-schemas "$RES/share/glib-2.0/schemas" || true
fi
# Launcher sets GTK search paths then execs the real binary.
cat > "$MACOS/maturita" <<'EOF'
#!/bin/bash
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
export DYLD_LIBRARY_PATH="$ROOT/Frameworks${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
export XDG_DATA_DIRS="$ROOT/Resources/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export GSETTINGS_SCHEMA_DIR="$ROOT/Resources/share/glib-2.0/schemas"
export GDK_PIXBUF_MODULEDIR="$ROOT/Resources/lib/gdk-pixbuf-2.0/2.10.0/loaders"
if [[ -f "$ROOT/Resources/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" ]]; then
export GDK_PIXBUF_MODULE_FILE="$ROOT/Resources/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"
fi
export GTK_DATA_PREFIX="$ROOT/Resources"
export GTK_EXE_PREFIX="$ROOT"
# Adwaita / hicolor icons for GTK widgets.
export XDG_DATA_HOME="$ROOT/Resources"
exec "$ROOT/MacOS/maturita-bin" "$@"
EOF
chmod +x "$MACOS/maturita"
# Info.plist
cat > "$APP/Contents/Info.plist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>maturita.c</string>
<key>CFBundleDisplayName</key>
<string>maturita.c</string>
<key>CFBundleIdentifier</key>
<string>org.maturita.Maturita</string>
<key>CFBundleVersion</key>
<string>${VERSION:-1.0}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION:-1.0}</string>
<key>CFBundleExecutable</key>
<string>maturita</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
EOF
# Optional .icns from PNG if iconutil / sips available.
if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then
ICONSET="$RES/AppIcon.iconset"
mkdir -p "$ICONSET"
for sz in 16 32 64 128 256 512; do
sips -z "$sz" "$sz" "$ROOT/$ICON_DST" --out "$ICONSET/icon_${sz}x${sz}.png" >/dev/null
sips -z $((sz * 2)) $((sz * 2)) "$ROOT/$ICON_DST" \
--out "$ICONSET/icon_${sz}x${sz}@2x.png" >/dev/null
done
iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true
fi
echo "==> Zipping"
rm -f "$ROOT/dist/$OUT_ZIP"
(
cd "$ROOT/dist"
zip -qry "$OUT_ZIP" "$APP_NAME"
)
echo "Created dist/$OUT_ZIP"

201
util.c
View file

@ -1,9 +1,208 @@
#include "maturita.h"
#ifdef _WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <mach-o/dyld.h>
#include <unistd.h>
#else
#include <unistd.h>
#endif
/* ------------------------------------------------------------------ */
/* Utilities */
/* Portable data / progress paths */
/* ------------------------------------------------------------------ */
char *app_progress_dir;
char *app_progress_u1;
char *app_progress_u2;
char *app_progress_u3;
char *app_settings_file;
char *app_progress_net;
char *app_progress_hw;
char *app_progress_mluvnice;
char *app_progress_cetba;
static char *dup_dirname(const char *path) {
char *tmp = g_strdup(path);
char *dir;
if (!tmp)
return NULL;
dir = g_path_get_dirname(tmp);
g_free(tmp);
return dir;
}
/* Absolute path of this process's executable, or NULL. */
static char *executable_path(void) {
#ifdef _WIN32
wchar_t wbuf[MAX_PATH];
DWORD n = GetModuleFileNameW(NULL, wbuf, MAX_PATH);
char *out;
if (n == 0 || n >= MAX_PATH)
return NULL;
out = g_utf16_to_utf8((const gunichar2 *)wbuf, -1, NULL, NULL, NULL);
return out;
#elif defined(__APPLE__)
char *buf;
uint32_t size = 0;
_NSGetExecutablePath(NULL, &size);
if (size == 0)
return NULL;
buf = g_malloc(size);
if (_NSGetExecutablePath(buf, &size) != 0) {
g_free(buf);
return NULL;
}
{
char *real = g_canonicalize_filename(buf, NULL);
g_free(buf);
return real ? real : NULL;
}
#else
return g_file_read_link("/proc/self/exe", NULL);
#endif
}
static gboolean dir_has_stylesheet(const char *dir) {
char *css;
gboolean ok;
if (!dir || !dir[0])
return FALSE;
css = g_build_filename(dir, CSS_FILE, NULL);
ok = g_file_test(css, G_FILE_TEST_IS_REGULAR);
g_free(css);
return ok;
}
static char *macos_resources_dir(const char *exe) {
char *macos_dir;
char *contents;
char *resources;
if (!exe)
return NULL;
macos_dir = dup_dirname(exe);
if (!macos_dir)
return NULL;
if (!g_str_has_suffix(macos_dir, "MacOS")) {
g_free(macos_dir);
return NULL;
}
contents = dup_dirname(macos_dir);
g_free(macos_dir);
if (!contents)
return NULL;
resources = g_build_filename(contents, "Resources", NULL);
g_free(contents);
if (!dir_has_stylesheet(resources)) {
g_free(resources);
return NULL;
}
return resources;
}
/* Directory next to Foo.app when exe is .../Foo.app/Contents/MacOS/maturita. */
static char *macos_app_parent_dir(const char *exe) {
char *macos_dir;
char *contents;
char *app_bundle;
char *parent;
if (!exe)
return NULL;
macos_dir = dup_dirname(exe);
if (!macos_dir || !g_str_has_suffix(macos_dir, "MacOS")) {
g_free(macos_dir);
return NULL;
}
contents = dup_dirname(macos_dir);
g_free(macos_dir);
if (!contents)
return NULL;
app_bundle = dup_dirname(contents);
g_free(contents);
if (!app_bundle)
return NULL;
parent = dup_dirname(app_bundle);
g_free(app_bundle);
return parent;
}
static void set_progress_paths(const char *write_root) {
g_free(app_progress_dir);
g_free(app_progress_u1);
g_free(app_progress_u2);
g_free(app_progress_u3);
g_free(app_settings_file);
g_free(app_progress_net);
g_free(app_progress_hw);
g_free(app_progress_mluvnice);
g_free(app_progress_cetba);
app_progress_dir = g_build_filename(write_root, PROGRESS_DIR_NAME, NULL);
app_progress_u1 = g_build_filename(app_progress_dir, "unit1.conf", NULL);
app_progress_u2 = g_build_filename(app_progress_dir, "unit2.conf", NULL);
app_progress_u3 = g_build_filename(app_progress_dir, "unit3.conf", NULL);
app_settings_file = g_build_filename(app_progress_dir, "settings.conf", NULL);
app_progress_net = g_build_filename(app_progress_dir, "net.conf", NULL);
app_progress_hw = g_build_filename(app_progress_dir, "hw.conf", NULL);
app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL);
app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL);
}
/* Resolve read-only asset root, chdir there, and point progress at a writable
* location (next to the AppImage / .app, or beside the binary / source tree). */
void setup_portable_paths(void) {
const char *appdir = g_getenv("APPDIR");
const char *appimage = g_getenv("APPIMAGE");
char *exe = executable_path();
char *exe_dir = exe ? dup_dirname(exe) : NULL;
char *data_dir = NULL;
char *write_root = NULL;
char *cwd;
if (appdir && dir_has_stylesheet(appdir))
data_dir = g_strdup(appdir);
if (!data_dir)
data_dir = macos_resources_dir(exe);
if (!data_dir && exe_dir && dir_has_stylesheet(exe_dir))
data_dir = g_strdup(exe_dir);
cwd = g_get_current_dir();
if (!data_dir && dir_has_stylesheet(cwd))
data_dir = g_strdup(cwd);
if (!data_dir && cwd)
data_dir = g_strdup(cwd);
if (appimage && appimage[0])
write_root = dup_dirname(appimage);
if (!write_root)
write_root = macos_app_parent_dir(exe);
if (!write_root && exe_dir)
write_root = g_strdup(exe_dir);
if (!write_root && data_dir)
write_root = g_strdup(data_dir);
if (!write_root)
write_root = g_strdup(".");
set_progress_paths(write_root);
if (data_dir && g_chdir(data_dir) != 0)
g_warning("Could not chdir to data directory '%s'", data_dir);
g_free(cwd);
g_free(write_root);
g_free(data_dir);
g_free(exe_dir);
g_free(exe);
}
char *normalize_answer(const char *input) {
GString *pre = g_string_new(NULL);
GString *out = g_string_new(NULL);