diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..93705b1 --- /dev/null +++ b/.github/workflows/release.yml @@ -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 }} diff --git a/Makefile b/Makefile index 65c418f..d9ec1ba 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 31be609..3ebafd2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cetba.c b/cetba.c index 95cf48e..96fdeb0 100644 --- a/cetba.c +++ b/cetba.c @@ -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); diff --git a/main.c b/main.c index 086418e..f30150f 100644 --- a/main.c +++ b/main.c @@ -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(); diff --git a/maturita.h b/maturita.h index 46fe7d5..0ff3136 100644 --- a/maturita.h +++ b/maturita.h @@ -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); diff --git a/mluvnice.c b/mluvnice.c index 17f8b8b..ab77ed9 100644 --- a/mluvnice.c +++ b/mluvnice.c @@ -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]; diff --git a/scripts/bundle-linux.sh b/scripts/bundle-linux.sh new file mode 100755 index 0000000..a05e8c2 --- /dev/null +++ b/scripts/bundle-linux.sh @@ -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" diff --git a/scripts/bundle-macos.sh b/scripts/bundle-macos.sh new file mode 100755 index 0000000..84d9daf --- /dev/null +++ b/scripts/bundle-macos.sh @@ -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" < + + + + CFBundleName + maturita.c + CFBundleDisplayName + maturita.c + CFBundleIdentifier + org.maturita.Maturita + CFBundleVersion + ${VERSION:-1.0} + CFBundleShortVersionString + ${VERSION:-1.0} + CFBundleExecutable + maturita + CFBundlePackageType + APPL + CFBundleIconFile + AppIcon + LSMinimumSystemVersion + 12.0 + NSHighResolutionCapable + + + +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" diff --git a/util.c b/util.c index cb64ddc..bd68ccc 100644 --- a/util.c +++ b/util.c @@ -1,9 +1,208 @@ #include "maturita.h" +#ifdef _WIN32 +#include +#elif defined(__APPLE__) +#include +#include +#else +#include +#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);