* feat(ui): About page for this app, VocaHQ, and talk to us Settings About now has three groups. Vocalinux keeps the existing icon, version, AGPL-3.0, updater, and What's New. The website button opens vocalinux.com. Part of VocaHQ lists the family sites with PRODUCT.md status. Talk to us opens GitHub issues, Discord, X, and email. Official Design marks from VocaHQ/.github are vendored as-is and painted with iron-white ink. * fix(ui): unique About Open names and pin Design SVG hashes Open buttons on About still say Open. Each one now uses its tooltip as the accessible name so a screen reader can tell vocahq.com from vocalinux.com from Latest Release. The Talk-to-us SVG test pins sha256 of the four official marks from VocaHQ/.github 61c8eee. * fix(ui): circular app icon and About source code link The launcher and About header used the square teal fill. Switch both to the circular family logo (voca-logo.svg). Website stays vocalinux.com; Source code next to it opens the GitHub repo. * fix(appimage): pin pywhispercpp to the version install.sh declares * fix(appimage): pin pywhispercpp at both install sites * test(docs): match AGENTS.md uv.lock wording #727 dropped "unused until those phases land". The test still looked for that line, so any PR that runs Python tests against current main failed. Pin the current contract instead: uv.lock is authoritative, and requirements/*.txt are not hand-edited. --------- Co-authored-by: Sergey Silaev <hello@8am.dev>
403 lines
16 KiB
Bash
Executable File
403 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build a relocatable AppImage for Vocalinux, natively for whichever
|
|
# architecture this script runs on (x86_64 or aarch64) - no cross-compiling.
|
|
#
|
|
# Usage: build.sh <path-to-wheel> <version> [output-dir]
|
|
#
|
|
# Bundles a full copy of the active Python interpreter (relocated via
|
|
# PYTHONHOME, not a venv, since venvs hard-code absolute paths) plus
|
|
# PyGObject/GTK3/AppIndicator/IBus GObject-Introspection typelibs, since
|
|
# those are needed by `gi.repository` at runtime and linuxdeploy-plugin-gtk
|
|
# does not bundle them (it targets native C GTK apps, which don't need
|
|
# introspection data).
|
|
#
|
|
# ponytail: text-injection CLI tools (xdotool/wtype/ydotool) are not
|
|
# bundled, same runtime prerequisite as the PyPI install path documented
|
|
# in docs/INSTALL.md. Add bundling if users hit missing-binary complaints.
|
|
#
|
|
# GPU: pip wheels of pywhispercpp are CPU-only. This script rebuilds
|
|
# pywhispercpp from source with GGML_VULKAN=1 when Vulkan headers and a
|
|
# shader compiler are on the build host. The AppImage uses the host Vulkan
|
|
# loader/ICDs at runtime (do not bundle NVIDIA/AMD driver ICDs).
|
|
# Set VOCALINUX_APPIMAGE_REQUIRE_VULKAN=1 in CI so a failed rebuild fails
|
|
# the job instead of shipping a CPU-only image. Set
|
|
# VOCALINUX_APPIMAGE_SKIP_VULKAN=1 to force the CPU wheel.
|
|
set -euo pipefail
|
|
|
|
WHEEL="$1"
|
|
VERSION="$2"
|
|
OUTDIR="${3:-dist}"
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
|
# Same pywhispercpp release install.sh pins. Read from there rather than repeated
|
|
# here: unpinned, this build picks up whatever PyPI published today, which is how
|
|
# a 1.5.1 released mid-PR broke the Vulkan rebuild.
|
|
PYWHISPERCPP_VERSION="$(sed -n 's/^PYWHISPERCPP_VERSION="\([^"]*\)"/\1/p' "$REPO_ROOT/install.sh" | head -n1)"
|
|
if [ -z "$PYWHISPERCPP_VERSION" ]; then
|
|
echo "Could not read PYWHISPERCPP_VERSION from install.sh" >&2
|
|
exit 1
|
|
fi
|
|
ARCH="$(uname -m)"
|
|
PYTHON="${PYTHON:-python3}"
|
|
|
|
case "$ARCH" in
|
|
x86_64|aarch64) ;;
|
|
*) echo "Unsupported architecture: $ARCH (need x86_64 or aarch64)" >&2; exit 1 ;;
|
|
esac
|
|
|
|
# Seed typelibs Vocalinux imports directly, plus transitive GIR deps that Gtk
|
|
# and AppIndicator require (xlib, Dbusmenu, …). Without those, GI falls back to
|
|
# the builder's baked-in path (/usr/lib/x86_64-linux-gnu/girepository-1.0), which
|
|
# does not exist on Fedora/openSUSE/Arch — startup then reports "missing GTK3".
|
|
TYPELIBS=(
|
|
Gtk-3.0 Gdk-3.0 GdkX11-3.0 GdkPixbuf-2.0 GLib-2.0 GObject-2.0 Gio-2.0
|
|
GModule-2.0 Pango-1.0 PangoCairo-1.0 cairo-1.0 HarfBuzz-0.0 Atk-1.0
|
|
freetype2-2.0 fontconfig-2.0 xlib-2.0
|
|
AyatanaAppIndicator3-0.1 AyatanaAppindicator3-0.1 AppIndicator3-0.1
|
|
Dbusmenu-0.4 Notify-0.7 IBus-1.0 Rsvg-2.0
|
|
)
|
|
|
|
# Runtime only needs one tray stack (same order as tray_indicator.py). Prefer
|
|
# Ayatana; accept the rare lowercase typelib; legacy AppIndicator3 last.
|
|
INDICATOR_TYPELIBS=(
|
|
AyatanaAppIndicator3-0.1 AyatanaAppindicator3-0.1 AppIndicator3-0.1
|
|
)
|
|
|
|
# Shared libs loaded via GI at runtime (not linked into python3), so
|
|
# linuxdeploy will not discover them from -e python3 alone.
|
|
GI_RUNTIME_LIBS=(
|
|
libappindicator3.so.1
|
|
libayatana-appindicator3.so.1
|
|
libdbusmenu-glib.so.4
|
|
libdbusmenu-gtk3.so.4
|
|
libnotify.so.4
|
|
)
|
|
|
|
WORKDIR="$(mktemp -d)"
|
|
trap 'rm -rf "$WORKDIR"' EXIT
|
|
APPDIR="$WORKDIR/AppDir"
|
|
TOOLDIR="$WORKDIR/tools"
|
|
mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/lib" "$TOOLDIR" "$OUTDIR"
|
|
|
|
# Nested AppImages need extract-and-run on hosts without usable FUSE.
|
|
export APPIMAGE_EXTRACT_AND_RUN="${APPIMAGE_EXTRACT_AND_RUN:-1}"
|
|
|
|
echo "== Fetching AppImage tooling ($ARCH) =="
|
|
curl -fsSL -o "$TOOLDIR/linuxdeploy" \
|
|
"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
|
|
curl -fsSL -o "$TOOLDIR/linuxdeploy-plugin-gtk.sh" \
|
|
https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh
|
|
curl -fsSL -o "$TOOLDIR/appimagetool" \
|
|
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${ARCH}.AppImage"
|
|
chmod +x "$TOOLDIR/linuxdeploy" "$TOOLDIR/linuxdeploy-plugin-gtk.sh" "$TOOLDIR/appimagetool"
|
|
|
|
echo "== Bundling Python runtime ($PYTHON) =="
|
|
# Use base_prefix so a venv builder still ships a full stdlib (encodings, etc.).
|
|
PY_PREFIX="$("$PYTHON" -c 'import sys; print(sys.base_prefix)')"
|
|
PY_VER="$("$PYTHON" -c 'import sys; print("%d.%d" % sys.version_info[:2])')"
|
|
PY_BIN="$("$PYTHON" -c 'import sys; print(sys.base_prefix)')/bin/python${PY_VER}"
|
|
if [ ! -x "$PY_BIN" ]; then
|
|
PY_BIN="$("$PYTHON" -c 'import sys; print(sys.base_prefix)')/bin/python3"
|
|
fi
|
|
if [ ! -x "$PY_BIN" ]; then
|
|
PY_BIN="$("$PYTHON" -c 'import sys; print(sys.executable)')"
|
|
fi
|
|
cp -L "$PY_BIN" "$APPDIR/usr/bin/python3"
|
|
cp -r "$PY_PREFIX/lib/python${PY_VER}" "$APPDIR/usr/lib/python${PY_VER}"
|
|
rm -rf "$APPDIR/usr/lib/python${PY_VER}/site-packages"
|
|
|
|
echo "== Installing Vocalinux + runtime deps into the bundle =="
|
|
# --ignore-installed: pip otherwise treats the builder env's packages as
|
|
# satisfying deps and skips copying vosk/pywhispercpp/etc. into AppDir.
|
|
# pywhispercpp is named explicitly so this resolves to the pinned release rather
|
|
# than the newest inside the wheel's >=1.5.0,<2 range: the Vulkan rebuild below
|
|
# installs the pinned one, and two versions' libggml*.so in one AppDir leave
|
|
# linuxdeploy chasing a dependency that is not there.
|
|
"$PYTHON" -m pip install --no-cache-dir --ignore-installed --prefix "$APPDIR/usr" \
|
|
"$WHEEL" "pywhispercpp==$PYWHISPERCPP_VERSION" PyGObject pycairo onnxruntime
|
|
|
|
echo "== Adding desktop entry + icon =="
|
|
install -Dm644 "$REPO_ROOT/vocalinux.desktop" "$APPDIR/usr/share/applications/vocalinux.desktop"
|
|
# AppImage desktop integration expects Exec=AppRun; set WM class for the tray app.
|
|
sed -i \
|
|
-e 's|^Exec=.*|Exec=AppRun|' \
|
|
-e '/^StartupWMClass=/d' \
|
|
"$APPDIR/usr/share/applications/vocalinux.desktop"
|
|
printf 'StartupWMClass=vocalinux\n' >> "$APPDIR/usr/share/applications/vocalinux.desktop"
|
|
install -Dm644 "$REPO_ROOT/resources/icons/scalable/vocalinux.svg" \
|
|
"$APPDIR/usr/share/icons/hicolor/scalable/apps/vocalinux.svg"
|
|
|
|
copy_typelibs() {
|
|
local dest="$1"
|
|
local require_all="${2:-0}"
|
|
mkdir -p "$dest"
|
|
local typelib found missing=() indicator_found=0
|
|
for typelib in "${TYPELIBS[@]}"; do
|
|
found="$(find /usr/lib /usr/lib64 -name "${typelib}.typelib" 2>/dev/null | head -1 || true)"
|
|
if [ -n "$found" ]; then
|
|
cp "$found" "$dest/"
|
|
case " ${INDICATOR_TYPELIBS[*]} " in
|
|
*" ${typelib} "*) indicator_found=1 ;;
|
|
esac
|
|
else
|
|
missing+=("$typelib")
|
|
fi
|
|
done
|
|
|
|
# Tray indicator typelibs are alternates; drop them from the hard-fail list
|
|
# when at least one copied successfully.
|
|
local hard_missing=()
|
|
for typelib in "${missing[@]}"; do
|
|
case " ${INDICATOR_TYPELIBS[*]} " in
|
|
*" ${typelib} "*)
|
|
if [ "$indicator_found" -eq 0 ]; then
|
|
hard_missing+=("$typelib")
|
|
fi
|
|
;;
|
|
*)
|
|
hard_missing+=("$typelib")
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ "$require_all" = "1" ] && [ "${#hard_missing[@]}" -gt 0 ]; then
|
|
echo "Missing required typelibs on the build host:" >&2
|
|
printf ' - %s\n' "${hard_missing[@]}" >&2
|
|
if [ "$indicator_found" -eq 0 ]; then
|
|
echo "Need at least one of: ${INDICATOR_TYPELIBS[*]}" >&2
|
|
fi
|
|
echo "Install the matching gir1.2-* packages and retry." >&2
|
|
exit 1
|
|
fi
|
|
if [ "${#missing[@]}" -gt 0 ]; then
|
|
echo "Warning: typelibs not found on build host (optional/alternate): ${missing[*]}" >&2
|
|
fi
|
|
}
|
|
|
|
copy_gi_runtime_libs() {
|
|
local dest="$1"
|
|
mkdir -p "$dest"
|
|
local lib found
|
|
for lib in "${GI_RUNTIME_LIBS[@]}"; do
|
|
found="$(find /usr/lib /usr/lib64 -name "$lib" 2>/dev/null | head -1 || true)"
|
|
if [ -n "$found" ]; then
|
|
cp -aL "$found" "$dest/"
|
|
# Prefer versioned real files so linuxdeploy can resolve the SONAME.
|
|
if [ -L "$found" ]; then
|
|
real="$(readlink -f "$found")"
|
|
cp -aL "$real" "$dest/" 2>/dev/null || true
|
|
fi
|
|
echo " bundled $lib from $found"
|
|
else
|
|
echo "Warning: $lib not found on build host (tray/notify may need host libs)" >&2
|
|
fi
|
|
done
|
|
}
|
|
|
|
has_vulkan_build_deps() {
|
|
if [ -f /usr/include/vulkan/vulkan.h ] || pkg-config --exists vulkan 2>/dev/null; then
|
|
:
|
|
else
|
|
return 1
|
|
fi
|
|
command -v cmake >/dev/null 2>&1 || return 1
|
|
command -v g++ >/dev/null 2>&1 || command -v clang++ >/dev/null 2>&1 || return 1
|
|
command -v glslc >/dev/null 2>&1 || command -v glslangValidator >/dev/null 2>&1 || return 1
|
|
return 0
|
|
}
|
|
|
|
remove_appdir_pywhispercpp() {
|
|
find "$APPDIR/usr" -depth \( \
|
|
-name 'pywhispercpp' -o \
|
|
-name 'pywhispercpp.libs' -o \
|
|
-name 'pywhispercpp-*.dist-info' -o \
|
|
-name '_pywhispercpp*' \
|
|
\) -exec rm -rf {} + 2>/dev/null || true
|
|
}
|
|
|
|
copy_whisper_native_libs_to_usr_lib() {
|
|
mkdir -p "$APPDIR/usr/lib"
|
|
local lib
|
|
while IFS= read -r lib; do
|
|
[ -n "$lib" ] || continue
|
|
cp -aL "$lib" "$APPDIR/usr/lib/" 2>/dev/null || cp -a "$lib" "$APPDIR/usr/lib/"
|
|
echo " staged $(basename "$lib") -> usr/lib"
|
|
done < <(find "$APPDIR/usr" \( -name 'libggml*.so*' -o -name 'libwhisper.so*' \) ! -path '*/usr/lib/*' 2>/dev/null || true)
|
|
}
|
|
|
|
rebuild_pywhispercpp_vulkan() {
|
|
local require_vulkan="${VOCALINUX_APPIMAGE_REQUIRE_VULKAN:-0}"
|
|
if [ "${VOCALINUX_APPIMAGE_SKIP_VULKAN:-0}" = "1" ]; then
|
|
echo "== Skipping pywhispercpp Vulkan rebuild (VOCALINUX_APPIMAGE_SKIP_VULKAN=1) =="
|
|
return 0
|
|
fi
|
|
|
|
if ! has_vulkan_build_deps; then
|
|
echo "Vulkan build deps missing (libvulkan-dev, cmake, g++, glslc/glslangValidator)." >&2
|
|
if [ "$require_vulkan" = "1" ]; then
|
|
echo "VOCALINUX_APPIMAGE_REQUIRE_VULKAN=1: refusing to ship a CPU-only AppImage." >&2
|
|
exit 1
|
|
fi
|
|
echo "Warning: AppImage will use the CPU-only pywhispercpp wheel." >&2
|
|
return 0
|
|
fi
|
|
|
|
echo "== Rebuilding pywhispercpp with Vulkan =="
|
|
remove_appdir_pywhispercpp
|
|
local pip_log="$WORKDIR/pywhispercpp-vulkan.log"
|
|
export CMAKE_BUILD_PARALLEL_LEVEL="${CMAKE_BUILD_PARALLEL_LEVEL:-$(nproc)}"
|
|
# Hosts that default cc/c++ to clang (this Cloud Agent image) fail the
|
|
# CMAKE C++ compiler test: clang does not search gcc's libstdc++ path.
|
|
if ! CC="${CC:-gcc}" CXX="${CXX:-g++}" \
|
|
GGML_VULKAN=1 \
|
|
CMAKE_ARGS='-DCMAKE_INSTALL_RPATH=$ORIGIN -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON' \
|
|
"$PYTHON" -m pip install --verbose --no-cache-dir --force-reinstall \
|
|
--no-binary pywhispercpp --ignore-installed --prefix "$APPDIR/usr" \
|
|
--log "$pip_log" "pywhispercpp==$PYWHISPERCPP_VERSION"; then
|
|
echo "pywhispercpp Vulkan rebuild failed. Last 80 log lines:" >&2
|
|
tail -n 80 "$pip_log" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
if [ "$require_vulkan" = "1" ]; then
|
|
exit 1
|
|
fi
|
|
echo "Warning: continuing with the CPU-only pywhispercpp wheel." >&2
|
|
return 0
|
|
fi
|
|
|
|
local vk_lib
|
|
vk_lib="$(find "$APPDIR/usr" -name 'libggml-vulkan.so*' 2>/dev/null | head -1 || true)"
|
|
if [ -z "$vk_lib" ]; then
|
|
echo "Vulkan rebuild did not produce libggml-vulkan.so." >&2
|
|
if [ "$require_vulkan" = "1" ]; then
|
|
tail -n 80 "$pip_log" 2>/dev/null | sed 's/^/ /' >&2 || true
|
|
exit 1
|
|
fi
|
|
echo "Warning: AppImage will use CPU-only pywhispercpp." >&2
|
|
return 0
|
|
fi
|
|
echo " found $vk_lib"
|
|
copy_whisper_native_libs_to_usr_lib
|
|
}
|
|
|
|
echo "== Copying GObject-Introspection typelibs (not handled by linuxdeploy-plugin-gtk) =="
|
|
copy_typelibs "$APPDIR/usr/lib/girepository-1.0" 0
|
|
|
|
echo "== Copying GI runtime shared libraries (AppIndicator/Notify) =="
|
|
copy_gi_runtime_libs "$APPDIR/usr/lib"
|
|
|
|
echo "== Writing AppRun =="
|
|
cat > "$APPDIR/AppRun" << 'APPRUN'
|
|
#!/usr/bin/env bash
|
|
HERE="$(dirname "$(readlink -f "${0}")")"
|
|
export PYTHONHOME="$HERE/usr"
|
|
export PYTHONPATH="$HERE/usr/lib/python3:$HERE/usr/lib/python3/site-packages"
|
|
export GI_TYPELIB_PATH="$HERE/usr/lib/girepository-1.0"
|
|
export LD_LIBRARY_PATH="$HERE/usr/lib:${LD_LIBRARY_PATH:-}"
|
|
export XDG_DATA_DIRS="$HERE/usr/share:${XDG_DATA_DIRS:-/usr/share}"
|
|
exec "$HERE/usr/bin/python3" -m vocalinux.main "$@"
|
|
APPRUN
|
|
# PYTHONPATH above uses a version-agnostic symlink so AppRun doesn't need
|
|
# to know the exact interpreter minor version at runtime.
|
|
ln -sfn "python${PY_VER}" "$APPDIR/usr/lib/python3"
|
|
chmod +x "$APPDIR/AppRun"
|
|
|
|
rebuild_pywhispercpp_vulkan
|
|
|
|
echo "== Running linuxdeploy (resolves the shared-library closure + GTK theming) =="
|
|
export DEPLOY_GTK_VERSION=3
|
|
"$TOOLDIR/linuxdeploy" --appdir "$APPDIR" \
|
|
--plugin gtk \
|
|
-e "$APPDIR/usr/bin/python3" \
|
|
-d "$APPDIR/usr/share/applications/vocalinux.desktop" \
|
|
-i "$APPDIR/usr/share/icons/hicolor/scalable/apps/vocalinux.svg"
|
|
|
|
# linuxdeploy copies the host's full typelib set; keep those extras (transitive
|
|
# GIR deps vary by distro) and re-assert our required seed on top.
|
|
echo "== Ensuring required typelibs are present (keeping linuxdeploy extras) =="
|
|
copy_typelibs "$APPDIR/usr/lib/girepository-1.0" 1
|
|
|
|
echo "== Patching linuxdeploy GTK AppRun hook (Wayland-friendly GDK backend) =="
|
|
GTK_HOOK="$APPDIR/apprun-hooks/linuxdeploy-plugin-gtk.sh"
|
|
if [ -f "$GTK_HOOK" ]; then
|
|
# Upstream forces GDK_BACKEND=x11 even on Wayland, which breaks hover/focus
|
|
# for GTK widgets under Plasma (XWayland). Prefer native Wayland unless the
|
|
# user opts into X11 via VOCALINUX_FORCE_X11=1.
|
|
python3 - "$GTK_HOOK" <<'PY'
|
|
import pathlib, re, sys
|
|
path = pathlib.Path(sys.argv[1])
|
|
text = path.read_text()
|
|
replacement = (
|
|
'# Prefer Wayland when available; force X11 only when requested.\n'
|
|
'if [ "${VOCALINUX_FORCE_X11:-0}" = "1" ]; then\n'
|
|
' export GDK_BACKEND=x11\n'
|
|
'elif [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then\n'
|
|
' export GDK_BACKEND=wayland\n'
|
|
'else\n'
|
|
' export GDK_BACKEND=x11\n'
|
|
'fi'
|
|
)
|
|
new, n = re.subn(
|
|
r'^[ \t]*export GDK_BACKEND=x11[^\n]*$',
|
|
replacement,
|
|
text,
|
|
count=1,
|
|
flags=re.M,
|
|
)
|
|
if n != 1:
|
|
raise SystemExit(f"failed to patch GDK_BACKEND in {path} (matches={n})")
|
|
path.write_text(new)
|
|
print(f"Patched {path}")
|
|
PY
|
|
else
|
|
echo "Warning: GTK AppRun hook not found at $GTK_HOOK" >&2
|
|
fi
|
|
|
|
echo "== Smoke-testing GI imports without host typelibs =="
|
|
# Catch the openSUSE/Fedora class of failure before packaging: Gtk needs xlib
|
|
# (and friends) from the bundle when the host GI search path is not Debian's.
|
|
smoke_gi_imports() {
|
|
local appdir="$1"
|
|
unshare --user --mount --map-root-user bash -c "
|
|
set -euo pipefail
|
|
mkdir -p /tmp/vocalinux-empty-gi
|
|
for d in /usr/lib/x86_64-linux-gnu/girepository-1.0 \
|
|
/usr/lib/aarch64-linux-gnu/girepository-1.0 \
|
|
/usr/lib/girepository-1.0 \
|
|
/usr/lib64/girepository-1.0; do
|
|
if [ -d \"\$d\" ]; then
|
|
mount --bind /tmp/vocalinux-empty-gi \"\$d\"
|
|
fi
|
|
done
|
|
export PYTHONHOME='$appdir/usr'
|
|
export PYTHONPATH='$appdir/usr/lib/python3:$appdir/usr/lib/python3/site-packages'
|
|
export GI_TYPELIB_PATH='$appdir/usr/lib/girepository-1.0'
|
|
export LD_LIBRARY_PATH='$appdir/usr/lib'
|
|
'$appdir/usr/bin/python3' - <<'PY'
|
|
import gi
|
|
gi.require_version('Gtk', '3.0')
|
|
from gi.repository import Gtk # noqa: F401
|
|
try:
|
|
gi.require_version('AyatanaAppIndicator3', '0.1')
|
|
from gi.repository import AyatanaAppIndicator3 # noqa: F401
|
|
except (ImportError, ValueError):
|
|
try:
|
|
gi.require_version('AyatanaAppindicator3', '0.1')
|
|
from gi.repository import AyatanaAppindicator3 # noqa: F401
|
|
except (ImportError, ValueError):
|
|
gi.require_version('AppIndicator3', '0.1')
|
|
from gi.repository import AppIndicator3 # noqa: F401
|
|
print('GI smoke OK')
|
|
PY
|
|
"
|
|
}
|
|
|
|
if command -v unshare >/dev/null 2>&1 && unshare --user --mount --map-root-user true 2>/dev/null; then
|
|
smoke_gi_imports "$APPDIR"
|
|
else
|
|
echo "Warning: unshare unavailable; skipping isolated GI smoke test" >&2
|
|
fi
|
|
|
|
echo "== Packaging AppImage =="
|
|
OUTPUT="$OUTDIR/Vocalinux-${VERSION}-${ARCH}.AppImage"
|
|
ARCH="$ARCH" "$TOOLDIR/appimagetool" "$APPDIR" "$OUTPUT"
|
|
echo "Built $OUTPUT"
|