mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
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:
parent
f55216b764
commit
f8e27e8b53
10 changed files with 711 additions and 18 deletions
125
.github/workflows/release.yml
vendored
Normal file
125
.github/workflows/release.yml
vendored
Normal 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 }}
|
||||
15
Makefile
15
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
|
||||
|
|
|
|||
67
README.md
67
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
|
||||
|
|
|
|||
4
cetba.c
4
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);
|
||||
|
|
|
|||
4
main.c
4
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();
|
||||
|
|
|
|||
26
maturita.h
26
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);
|
||||
|
|
|
|||
|
|
@ -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
116
scripts/bundle-linux.sh
Executable 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
169
scripts/bundle-macos.sh
Executable 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
201
util.c
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue