Move the GTK application sources into desktop/.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
FBukovina 2026-09-17 15:44:16 +02:00
commit 4708c69d9c
46 changed files with 0 additions and 0 deletions

123
desktop/scripts/bundle-linux.sh Executable file
View file

@ -0,0 +1,123 @@
#!/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"
# Real 512×512 PNG (assets/app-icon.png is 1024×1024 and linuxdeploy rejects it).
ICON_512="$ROOT/share/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 "$ICON_512" "$APPDIR/icons/hicolor/512x512/apps/maturita.png"
cp -f "$ICON_512" "$APPDIR/usr/share/icons/hicolor/512x512/apps/maturita.png"
cp -f "$ICON_512" "$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"
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 under another name;
# only rename when it is not already at the expected path (mv same→same
# fails under `set -e` and was aborting an otherwise successful build).
DEST="$ROOT/dist/$OUT_NAME"
if [[ ! -f "$DEST" ]]; then
shopt -s nullglob
for f in "$ROOT"/*.AppImage "$ROOT/dist"/*.AppImage; do
case "$(basename "$f")" in
linuxdeploy*|appimagetool*) continue ;;
esac
mv -f "$f" "$DEST"
break
done
shopt -u nullglob
fi
if [[ ! -f "$DEST" ]]; then
echo "AppImage was not created" >&2
find "$ROOT" "$ROOT/dist" -maxdepth 2 -name '*.AppImage' -print >&2 || true
exit 1
fi
chmod +x "$DEST"
echo "Created dist/$OUT_NAME"

280
desktop/scripts/bundle-macos.sh Executable file
View file

@ -0,0 +1,280 @@
#!/usr/bin/env bash
# Build a self-contained macOS .app with GTK dylibs relocated into the bundle.
# Requires: make, pkg-config, GTK4 (Homebrew).
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_512="$ROOT/share/icons/hicolor/512x512/apps/maturita.png"
OUT_ZIP="maturita-macos-${ARCH_TAG}.zip"
BREW_PREFIX="$(brew --prefix 2>/dev/null || echo /opt/homebrew)"
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 "$ICON_512" "$RES/icons/hicolor/512x512/apps/maturita.png"
cp -R "$ROOT/share/." "$RES/share/"
# Resolve an absolute Homebrew-style dependency to a real file on disk.
resolve_lib() {
local dep="$1"
local candidate
if [[ -f "$dep" ]]; then
realpath "$dep"
return 0
fi
# Try common Homebrew layouts for the basename.
local base
base="$(basename "$dep")"
for candidate in \
"$BREW_PREFIX/lib/$base" \
"$BREW_PREFIX/opt"/*/lib/"$base" \
"$BREW_PREFIX/Cellar"/*/*/lib/"$base"; do
if [[ -f "$candidate" ]]; then
realpath "$candidate"
return 0
fi
done
return 1
}
# Recursively copy non-system dylibs into Frameworks and rewrite load paths.
# Unlike dylibbundler, this never prompts interactively and fails loudly.
bundle_deps() {
local target="$1"
local line dep name dest real
while IFS= read -r line; do
dep="$(echo "$line" | awk '{print $1}')"
case "$dep" in
/System/*|/usr/lib/*|@executable_path/*|@loader_path/*)
continue
;;
@rpath/*)
name="$(basename "$dep")"
if ! real="$(resolve_lib "$BREW_PREFIX/lib/$name")"; then
echo "error: cannot resolve @rpath dependency '$dep' needed by $target" >&2
exit 1
fi
;;
/*)
name="$(basename "$dep")"
case "$name" in
libjpeg*.dylib) name="libjpeg.8.dylib" ;;
esac
if [[ -f "$dep" ]]; then
real="$(realpath "$dep")"
elif ! real="$(resolve_lib "$dep")"; then
echo "error: cannot resolve dependency '$dep' needed by $target" >&2
exit 1
fi
;;
*)
continue
;;
esac
dest="$FW/$name"
if [[ ! -f "$dest" ]]; then
echo " + $name (from $real)"
cp -f "$real" "$dest"
chmod u+w "$dest"
install_name_tool -id "@executable_path/../Frameworks/$name" "$dest" 2>/dev/null || true
codesign --force --sign - "$dest" >/dev/null 2>&1 || true
bundle_deps "$dest"
fi
# Rewrite matching absolute / @rpath references on the target.
while IFS= read -r changed; do
changed="$(echo "$changed" | awk '{print $1}')"
case "$changed" in
/*|@rpath/*)
if [[ "$(basename "$changed")" == "$name" ]] || \
[[ "$(basename "$changed")" == "$(basename "$real")" ]] || \
{ [[ "$name" == "libjpeg.8.dylib" ]] && [[ "$(basename "$changed")" == libjpeg* ]]; }; then
install_name_tool -change "$changed" \
"@executable_path/../Frameworks/$name" "$target" 2>/dev/null || true
fi
;;
esac
done < <(otool -L "$target" | tail -n +2)
done < <(otool -L "$target" | tail -n +2)
}
echo "==> Bundling GTK / GLib dylibs into Frameworks"
bundle_deps "$MACOS/maturita-bin"
codesign --force --sign - "$MACOS/maturita-bin" >/dev/null 2>&1 || true
echo "==> Verifying Frameworks completeness"
MISSING=0
check_file_deps() {
local file="$1"
local line dep name
while IFS= read -r line; do
dep="$(echo "$line" | awk '{print $1}')"
case "$dep" in
@executable_path/../Frameworks/*)
name="$(basename "$dep")"
if [[ ! -f "$FW/$name" ]]; then
echo "MISSING: $name (required by $(basename "$file"))" >&2
MISSING=1
fi
;;
@rpath/*)
echo "UNRESOLVED @rpath: $dep (in $(basename "$file"))" >&2
MISSING=1
;;
/opt/homebrew/*|/usr/local/*)
echo "UNBUNDLED absolute dep still present: $dep (in $(basename "$file"))" >&2
MISSING=1
;;
esac
done < <(otool -L "$file" | tail -n +2)
}
check_file_deps "$MACOS/maturita-bin"
for f in "$FW"/*.dylib; do
check_file_deps "$f"
done
if [[ "$MISSING" -ne 0 ]]; then
echo "error: macOS bundle is incomplete" >&2
exit 1
fi
echo " Frameworks: $(ls "$FW" | wc -l | tr -d ' ') dylibs"
# Copy GLib / GTK runtime data (schemas, modules, icon themes).
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"
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
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 GTK_DATA_PREFIX="$ROOT/Resources"
export GTK_EXE_PREFIX="$ROOT"
export XDG_DATA_HOME="$ROOT/Resources"
exec "$ROOT/MacOS/maturita-bin" "$@"
EOF
chmod +x "$MACOS/maturita"
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
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" "$ICON_512" --out "$ICONSET/icon_${sz}x${sz}.png" >/dev/null
sips -z $((sz * 2)) $((sz * 2)) "$ICON_512" \
--out "$ICONSET/icon_${sz}x${sz}@2x.png" >/dev/null
done
iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true
fi
# Ad-hoc sign the .app so Gatekeeper is less likely to kill dyld loads.
codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || true
echo "==> Smoke-testing launch"
if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then
echo " (CI: skipping GUI smoke test; Frameworks verification already passed)"
else
SMOKE_LOG="$(mktemp)"
"$MACOS/maturita" >"$SMOKE_LOG" 2>&1 &
SMOKE_PID=$!
sleep 3
if ! kill -0 "$SMOKE_PID" 2>/dev/null; then
if grep -q "Library not loaded" "$SMOKE_LOG"; then
echo "error: Maturita.app missing bundled libraries" >&2
cat "$SMOKE_LOG" >&2 || true
exit 1
fi
echo "error: Maturita.app exited during smoke test" >&2
cat "$SMOKE_LOG" >&2 || true
exit 1
fi
kill "$SMOKE_PID" 2>/dev/null || true
wait "$SMOKE_PID" 2>/dev/null || true
rm -f "$SMOKE_LOG"
echo " smoke test OK (process stayed up)"
fi
echo "==> Zipping"
rm -f "$ROOT/dist/$OUT_ZIP"
(
cd "$ROOT/dist"
zip -qry "$OUT_ZIP" "$APP_NAME"
)
echo "Created dist/$OUT_ZIP"

85
desktop/scripts/gen-icons.py Executable file
View file

@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Regenerate assets/app-icon.ico and share/icons from assets/app-icon.png.
Uses macOS `sips` when available; otherwise requires Pillow.
Run from the repository root.
"""
from __future__ import annotations
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "assets" / "app-icon.png"
ICO = ROOT / "assets" / "app-icon.ico"
ICO_SIZES = [16, 32, 48, 64, 128, 256]
SHARE_SIZES = [16, 32, 48, 64, 128, 256, 512]
def resize_png(src: Path, dest: Path, size: int) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
if subprocess.call(["which", "sips"], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL) == 0:
subprocess.check_call(
["sips", "-z", str(size), str(size), str(src), "--out", str(dest)],
stdout=subprocess.DEVNULL,
)
return
try:
from PIL import Image
except ImportError as exc:
raise SystemExit(
"Need macOS sips or Pillow to resize icons"
) from exc
img = Image.open(src).convert("RGBA")
img = img.resize((size, size), Image.Resampling.LANCZOS)
img.save(dest, format="PNG")
def write_ico(entries: list[tuple[int, bytes]], dest: Path) -> None:
count = len(entries)
header = struct.pack("<HHH", 0, 1, count)
offset = 6 + 16 * count
direntries = []
blobs = []
for size, data in entries:
w = 0 if size >= 256 else size
h = 0 if size >= 256 else size
direntries.append(
struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, len(data), offset)
)
blobs.append(data)
offset += len(data)
dest.write_bytes(header + b"".join(direntries) + b"".join(blobs))
def main() -> int:
if not SRC.is_file():
print(f"missing {SRC}", file=sys.stderr)
return 1
with tempfile.TemporaryDirectory(prefix="maturita-ico-") as tmp:
entries: list[tuple[int, bytes]] = []
for size in ICO_SIZES:
png = Path(tmp) / f"icon_{size}.png"
resize_png(SRC, png, size)
entries.append((size, png.read_bytes()))
write_ico(entries, ICO)
print(f"wrote {ICO.relative_to(ROOT)}")
for size in SHARE_SIZES:
dest = ROOT / "share" / "icons" / "hicolor" / f"{size}x{size}" / "apps" / "maturita.png"
resize_png(SRC, dest, size)
print(f"wrote {dest.relative_to(ROOT)}")
assets_icon = ROOT / "assets" / "icons" / "hicolor" / "512x512" / "apps" / "maturita.png"
resize_png(SRC, assets_icon, 512)
print(f"wrote {assets_icon.relative_to(ROOT)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())