mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Fix broken macOS release bundle missing GTK libraries.
Replace fragile dylibbundler with a recursive otool-based bundler, verify every dependency, smoke-test launch, and ship GTK schemas with the Windows zip. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b49c456098
commit
9f27724751
3 changed files with 169 additions and 68 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -81,7 +81,7 @@ jobs:
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
brew install gtk4 pkg-config dylibbundler
|
brew install gtk4 pkg-config
|
||||||
|
|
||||||
- name: Build .app zip
|
- name: Build .app zip
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
8
Makefile
8
Makefile
|
|
@ -79,6 +79,14 @@ bundle: $(TARGET)
|
||||||
@cp -R share dist/
|
@cp -R share dist/
|
||||||
@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/
|
||||||
|
@# GTK runtime data (schemas / pixbuf loaders) next to the exe.
|
||||||
|
@GTK_PREFIX="$$($(PKG_CONFIG) --variable=prefix gtk4)"; \
|
||||||
|
if [ -d "$$GTK_PREFIX/share/glib-2.0" ]; then \
|
||||||
|
mkdir -p dist/share && cp -R "$$GTK_PREFIX/share/glib-2.0" dist/share/; \
|
||||||
|
fi; \
|
||||||
|
if [ -d "$$GTK_PREFIX/lib/gdk-pixbuf-2.0" ]; then \
|
||||||
|
mkdir -p dist/lib && cp -R "$$GTK_PREFIX/lib/gdk-pixbuf-2.0" dist/lib/; \
|
||||||
|
fi
|
||||||
@echo "Bundled into dist/ - run dist/$(TARGET)"
|
@echo "Bundled into dist/ - run dist/$(TARGET)"
|
||||||
else ifeq ($(UNAME_S),Darwin)
|
else ifeq ($(UNAME_S),Darwin)
|
||||||
bundle:
|
bundle:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Build a self-contained macOS .app with GTK dylibs relocated into the bundle.
|
# Build a self-contained macOS .app with GTK dylibs relocated into the bundle.
|
||||||
# Requires: make, pkg-config, GTK4 (Homebrew), dylibbundler (brew install dylibbundler).
|
# Requires: make, pkg-config, GTK4 (Homebrew).
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
|
@ -21,13 +21,9 @@ APP="$ROOT/dist/$APP_NAME"
|
||||||
MACOS="$APP/Contents/MacOS"
|
MACOS="$APP/Contents/MacOS"
|
||||||
RES="$APP/Contents/Resources"
|
RES="$APP/Contents/Resources"
|
||||||
FW="$APP/Contents/Frameworks"
|
FW="$APP/Contents/Frameworks"
|
||||||
ICON_DST="assets/icons/hicolor/512x512/apps/maturita.png"
|
ICON_512="$ROOT/share/icons/hicolor/512x512/apps/maturita.png"
|
||||||
OUT_ZIP="maturita-macos-${ARCH_TAG}.zip"
|
OUT_ZIP="maturita-macos-${ARCH_TAG}.zip"
|
||||||
|
BREW_PREFIX="$(brew --prefix 2>/dev/null || echo /opt/homebrew)"
|
||||||
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"
|
echo "==> Building maturita"
|
||||||
make -C "$ROOT" clean
|
make -C "$ROOT" clean
|
||||||
|
|
@ -39,34 +35,144 @@ mkdir -p "$MACOS" "$RES/icons/hicolor/512x512/apps" "$RES/share" "$FW"
|
||||||
|
|
||||||
cp -f "$ROOT/maturita" "$MACOS/maturita-bin"
|
cp -f "$ROOT/maturita" "$MACOS/maturita-bin"
|
||||||
cp -f "$ROOT/style.css" "$RES/style.css"
|
cp -f "$ROOT/style.css" "$RES/style.css"
|
||||||
cp -f "$ROOT/$ICON_DST" "$RES/icons/hicolor/512x512/apps/maturita.png"
|
cp -f "$ICON_512" "$RES/icons/hicolor/512x512/apps/maturita.png"
|
||||||
cp -R "$ROOT/share/." "$RES/share/"
|
cp -R "$ROOT/share/." "$RES/share/"
|
||||||
|
|
||||||
# Bundle Homebrew / MacPorts GTK stack next to the binary.
|
# Resolve an absolute Homebrew-style dependency to a real file on disk.
|
||||||
echo "==> Relocating dylibs with dylibbundler"
|
resolve_lib() {
|
||||||
BREW_PREFIX="$(brew --prefix 2>/dev/null || true)"
|
local dep="$1"
|
||||||
DYLIB_SEARCH=()
|
local candidate
|
||||||
if [[ -n "$BREW_PREFIX" ]]; then
|
|
||||||
DYLIB_SEARCH+=(-s "$BREW_PREFIX/lib")
|
if [[ -f "$dep" ]]; then
|
||||||
# Common keg-only / versioned lib dirs dylibbundler otherwise prompts for.
|
realpath "$dep"
|
||||||
for sub in librsvg glib gettext; do
|
return 0
|
||||||
[[ -d "$BREW_PREFIX/opt/$sub/lib" ]] && DYLIB_SEARCH+=(-s "$BREW_PREFIX/opt/$sub/lib")
|
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
|
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
|
fi
|
||||||
|
echo " Frameworks: $(ls "$FW" | wc -l | tr -d ' ') dylibs"
|
||||||
|
|
||||||
dylibbundler -od -b \
|
# Copy GLib / GTK runtime data (schemas, modules, icon themes).
|
||||||
-x "$MACOS/maturita-bin" \
|
|
||||||
-d "$FW" \
|
|
||||||
-p "@executable_path/../Frameworks" \
|
|
||||||
"${DYLIB_SEARCH[@]+"${DYLIB_SEARCH[@]}"}" </dev/null
|
|
||||||
|
|
||||||
# Copy GLib / GTK runtime data (schemas, modules, icons themes used by GTK).
|
|
||||||
copy_pkg_share() {
|
copy_pkg_share() {
|
||||||
local prefix
|
local prefix
|
||||||
prefix="$(pkg-config --variable=prefix "$1" 2>/dev/null || true)"
|
prefix="$(pkg-config --variable=prefix "$1" 2>/dev/null || true)"
|
||||||
[[ -n "$prefix" && -d "$prefix/share" ]] || return 0
|
[[ -n "$prefix" && -d "$prefix/share" ]] || return 0
|
||||||
mkdir -p "$RES/share"
|
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
|
for sub in glib-2.0 gtk-4.0 icons themes locale; do
|
||||||
if [[ -d "$prefix/share/$sub" ]]; then
|
if [[ -d "$prefix/share/$sub" ]]; then
|
||||||
rsync -a "$prefix/share/$sub" "$RES/share/" 2>/dev/null \
|
rsync -a "$prefix/share/$sub" "$RES/share/" 2>/dev/null \
|
||||||
|
|
@ -78,39 +184,6 @@ copy_pkg_share() {
|
||||||
copy_pkg_share gtk4
|
copy_pkg_share gtk4
|
||||||
copy_pkg_share glib-2.0
|
copy_pkg_share glib-2.0
|
||||||
|
|
||||||
# gdk-pixbuf loaders (PNG may be built-in; copy others for GIF/ICO/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
|
|
||||||
# SVG loader pulls librsvg; not needed (app assets are PNG). Drop it so
|
|
||||||
# dylibbundler never blocks on an interactive "locate this library" prompt.
|
|
||||||
rm -f "$LOADERS_DST"/libpixbufloader_svg.* "$LOADERS_DST"/libpixbufloader-svg.*
|
|
||||||
# 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
|
|
||||||
# stdin closed → if a dep is still missing, fail instead of hanging CI.
|
|
||||||
dylibbundler -od -b -x "$loader" -d "$FW" \
|
|
||||||
-p "@executable_path/../Frameworks" \
|
|
||||||
"${DYLIB_SEARCH[@]+"${DYLIB_SEARCH[@]}"}" </dev/null \
|
|
||||||
|| echo "warning: could not fully relocate $(basename "$loader")" >&2
|
|
||||||
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
|
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
|
glib-compile-schemas "$RES/share/glib-2.0/schemas" || true
|
||||||
fi
|
fi
|
||||||
|
|
@ -122,19 +195,13 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
export DYLD_LIBRARY_PATH="$ROOT/Frameworks${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
|
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 XDG_DATA_DIRS="$ROOT/Resources/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
|
||||||
export GSETTINGS_SCHEMA_DIR="$ROOT/Resources/share/glib-2.0/schemas"
|
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_DATA_PREFIX="$ROOT/Resources"
|
||||||
export GTK_EXE_PREFIX="$ROOT"
|
export GTK_EXE_PREFIX="$ROOT"
|
||||||
# Adwaita / hicolor icons for GTK widgets.
|
|
||||||
export XDG_DATA_HOME="$ROOT/Resources"
|
export XDG_DATA_HOME="$ROOT/Resources"
|
||||||
exec "$ROOT/MacOS/maturita-bin" "$@"
|
exec "$ROOT/MacOS/maturita-bin" "$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$MACOS/maturita"
|
chmod +x "$MACOS/maturita"
|
||||||
|
|
||||||
# Info.plist
|
|
||||||
cat > "$APP/Contents/Info.plist" <<EOF
|
cat > "$APP/Contents/Info.plist" <<EOF
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
|
|
@ -165,18 +232,44 @@ cat > "$APP/Contents/Info.plist" <<EOF
|
||||||
</plist>
|
</plist>
|
||||||
EOF
|
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
|
if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then
|
||||||
ICONSET="$RES/AppIcon.iconset"
|
ICONSET="$RES/AppIcon.iconset"
|
||||||
mkdir -p "$ICONSET"
|
mkdir -p "$ICONSET"
|
||||||
for sz in 16 32 64 128 256 512; do
|
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" "$sz" "$ICON_512" --out "$ICONSET/icon_${sz}x${sz}.png" >/dev/null
|
||||||
sips -z $((sz * 2)) $((sz * 2)) "$ROOT/$ICON_DST" \
|
sips -z $((sz * 2)) $((sz * 2)) "$ICON_512" \
|
||||||
--out "$ICONSET/icon_${sz}x${sz}@2x.png" >/dev/null
|
--out "$ICONSET/icon_${sz}x${sz}@2x.png" >/dev/null
|
||||||
done
|
done
|
||||||
iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true
|
iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true
|
||||||
fi
|
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"
|
echo "==> Zipping"
|
||||||
rm -f "$ROOT/dist/$OUT_ZIP"
|
rm -f "$ROOT/dist/$OUT_ZIP"
|
||||||
(
|
(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue