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 LIBS += -mwindows
WINDRES ?= windres WINDRES ?= windres
RC_OBJ = maturita_rc.o RC_OBJ = maturita_rc.o
UNAME_S :=
else else
TARGET = maturita TARGET = maturita
RC_OBJ = RC_OBJ =
UNAME_S := $(shell uname -s 2>/dev/null)
# macOS Dock icon is set via AppKit (see macos_dock.c). # 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 LIBS += -framework AppKit -framework Foundation
endif endif
endif endif
@ -62,9 +64,7 @@ clean:
run: $(TARGET) run: $(TARGET)
./$(TARGET) ./$(TARGET)
# Windows only: copy the executable and every non-system (MinGW/GTK) DLL it # Self-contained package for the current platform into dist/.
# needs into dist/ so the app can be started by double-clicking, without an
# MSYS2 shell on PATH.
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
bundle: $(TARGET) bundle: $(TARGET)
@rm -rf dist @rm -rf dist
@ -77,9 +77,14 @@ bundle: $(TARGET)
@ldd $(TARGET) | grep -Ei '/(ucrt64|mingw64)/bin/' | awk '{print $$3}' \ @ldd $(TARGET) | grep -Ei '/(ucrt64|mingw64)/bin/' | awk '{print $$3}' \
| xargs -r -I{} cp -f {} dist/ | xargs -r -I{} cp -f {} dist/
@echo "Bundled into dist/ - run dist/$(TARGET)" @echo "Bundled into dist/ - run dist/$(TARGET)"
else ifeq ($(UNAME_S),Darwin)
bundle:
@chmod +x scripts/bundle-macos.sh
@./scripts/bundle-macos.sh
else else
bundle: bundle:
@echo "make bundle is only needed on Windows (MSYS2/MinGW)." @chmod +x scripts/bundle-linux.sh
@./scripts/bundle-linux.sh
endif endif
.PHONY: all clean run bundle .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 - **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) - 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 ### Installation
#### Prerequisites #### Prerequisites
@ -373,7 +402,10 @@ hw.c computer-hardware path (10 nodes), units 1–3 slides
and quizzes (architecture, history, bit/byte) and quizzes (architecture, history, bit/byte)
main.c application entry point and page wiring main.c application entry point and page wiring
style.css GTK stylesheet (palette colors are injected at runtime) 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) CMakeLists.txt optional CMake build (Linux / macOS)
build-windows.bat legacy MSVC build (requires Visual Studio + gvsbuild GTK) 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 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` - **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) - 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 ### Instalace
#### Požadavky #### Požadavky
@ -800,7 +860,10 @@ hw.c cesta technického vybavení (10 uzlů), jednotky 1–3
(architektura, historie, bit/byte + kvízy) (architektura, historie, bit/byte + kvízy)
main.c vstupní bod aplikace a zapojení stránek main.c vstupní bod aplikace a zapojení stránek
style.css GTK styl (barvy palety se vkládají za běhu) 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) CMakeLists.txt volitelný CMake build (Linux / macOS)
build-windows.bat legacy MSVC build (vyžaduje Visual Studio + gvsbuild GTK) 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 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) { GtkWidget *build_cetba1984_plot_page(void) {
lit_plot_unit.page = "cetba1984"; 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"), return build_assembly(&lit_plot_unit, tr("lit_plot_title"),
"lit_plot_sub", 1, lit_plot_items, "lit_plot_sub", 1, lit_plot_items,
lit_plot_meaning, 1); lit_plot_meaning, 1);
@ -1119,7 +1119,7 @@ static const char *fuks_plot_meaning[] = {
GtkWidget *build_cetba_fuks_plot_page(void) { GtkWidget *build_cetba_fuks_plot_page(void) {
lit_plot_unit.page = "cetbaFuks"; 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"), return build_assembly(&lit_plot_unit, tr("lit_fuks_plot_title"),
"lit_fuks_plot_sub", 1, fuks_plot_items, "lit_fuks_plot_sub", 1, fuks_plot_items,
fuks_plot_meaning, 1); fuks_plot_meaning, 1);

4
main.c
View file

@ -476,6 +476,10 @@ int main(int argc, char **argv) {
GtkApplication *app; GtkApplication *app;
int status; 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 /* Must run before GtkApplication so Wayland can resolve our .desktop
* file / icons via the application id. */ * file / icons via the application id. */
setup_portable_share_dir(); setup_portable_share_dir();

View file

@ -22,13 +22,24 @@
#define ICON_THEME_DIR_ALT "icons" /* next to the binary after install */ #define ICON_THEME_DIR_ALT "icons" /* next to the binary after install */
#define ICON_FILE_ALT "icons/hicolor/512x512/apps/maturita.png" #define ICON_FILE_ALT "icons/hicolor/512x512/apps/maturita.png"
#define SHARE_DIR "share" /* portable Freedesktop data root */ #define SHARE_DIR "share" /* portable Freedesktop data root */
#define PROGRESS_DIR "progress" /* Relative leaf names; absolute paths are filled by setup_portable_paths(). */
#define PROGRESS_U1 "progress/unit1.conf" #define PROGRESS_DIR_NAME "progress"
#define PROGRESS_U2 "progress/unit2.conf" extern char *app_progress_dir;
#define PROGRESS_U3 "progress/unit3.conf" extern char *app_progress_u1;
#define SETTINGS_FILE "progress/settings.conf" extern char *app_progress_u2;
#define PROGRESS_NET "progress/net.conf" extern char *app_progress_u3;
#define PROGRESS_HW "progress/hw.conf" 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 NODE_SIZE 88.0
#define PATH_SPAC 240.0 #define PATH_SPAC 240.0
#define ROAD_MX 150.0 /* horizontal canvas margin */ #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); int width, int height, gpointer data);
GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn, GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
double r, double g, double b, int px); double r, double g, double b, int px);
void setup_portable_paths(void);
char *normalize_answer(const char *input); char *normalize_answer(const char *input);
void shuffle_indices(int *arr, int n); void shuffle_indices(int *arr, int n);
void flow_clear(GtkFlowBox *fb); 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 / " mluv_unit.sub_key = "Cvičení z mluvnice – styl maturita / "
"přijímačky z ČJL"; "přijímačky z ČJL";
mluv_unit.back_target = "czechmap"; 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; mluv_unit.n_ex = MLUV_N_EX;
for (int i = 0; i < MLUV_N_EX; i++) for (int i = 0; i < MLUV_N_EX; i++)
mluv_unit.ex_names[i + 1] = mluv_names[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" #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) { char *normalize_answer(const char *input) {
GString *pre = g_string_new(NULL); GString *pre = g_string_new(NULL);
GString *out = g_string_new(NULL); GString *out = g_string_new(NULL);