Files
nearby/sharing/linux/app/packaging/package_minimal_appimage.sh

385 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# `bazel run` executes this script from Bazel's output tree and exports the
# actual source checkout in BUILD_WORKSPACE_DIRECTORY. Falling back to the
# script-relative path keeps direct source-tree invocation working.
readonly WORKSPACE_ROOT="$(
if [[ -n "${BUILD_WORKSPACE_DIRECTORY:-}" ]]; then
realpath "$BUILD_WORKSPACE_DIRECTORY"
else
realpath "$SCRIPT_DIR/../../../.."
fi
)"
cd "$WORKSPACE_ROOT"
readonly BINARY="$WORKSPACE_ROOT/bazel-bin/sharing/linux/app/app"
readonly BAZEL_OUTPUT_BASE="$(bazel info output_base)"
readonly QT_ROOT="$BAZEL_OUTPUT_BASE/external/rules_qt++fetch+qt_linux_x86_64"
readonly QT_DISTRIBUTION="$QT_ROOT"
readonly QML_TOOL="$QT_ROOT/bin/qml"
readonly APPIMAGE_RUNFILES="$WORKSPACE_ROOT/bazel-bin/sharing/linux/app/appimage.runfiles"
readonly APPIMAGETOOL="$APPIMAGE_RUNFILES/+http_file+appimagetool_x86_64/file/appimagetool-x86_64.AppImage"
readonly RUNTIME="$APPIMAGE_RUNFILES/+http_file+appimage_runtime_x86_64/file/runtime-x86_64"
readonly OUTPUT_DIR="$WORKSPACE_ROOT/sharing/linux/dist-minimal"
readonly FINAL_APPDIR="$OUTPUT_DIR/QuickShare.AppDir"
readonly OUTPUT_APPIMAGE="$OUTPUT_DIR/QuickShare-x86_64.AppImage"
readonly STAGE="$(mktemp -d /tmp/nearby-minimal-appdir.XXXXXX)"
readonly APPDIR="$STAGE/QuickShare.AppDir"
cleanup() {
rm -rf -- "$STAGE"
}
trap cleanup EXIT
for required in \
"$BINARY" \
"$QT_ROOT/lib/libQt6Core.so.6" \
"$QML_TOOL" \
"$APPIMAGETOOL" \
"$RUNTIME"; do
if [[ ! -e "$required" ]]; then
echo "Required packaging input is missing: $required" >&2
exit 1
fi
done
if [[ -e "$FINAL_APPDIR" || -e "$OUTPUT_APPIMAGE" ]]; then
echo "Output already exists; move or remove it first: $OUTPUT_DIR" >&2
exit 1
fi
mkdir -p \
"$APPDIR/usr/bin" \
"$APPDIR/usr/lib" \
"$APPDIR/usr/lib/qt6/plugins" \
"$APPDIR/usr/lib/qt6/qml" \
"$APPDIR/usr/share/applications" \
"$APPDIR/usr/share/icons/hicolor/scalable/apps" \
"$APPDIR/usr/share/metainfo"
install -m 0755 "$BINARY" "$APPDIR/usr/bin/quickshare"
copy_qt_library() {
local name="$1"
local files=("$QT_ROOT/lib/lib${name}.so"*)
if ((${#files[@]} == 0)); then
echo "Matching Qt library is missing: lib${name}.so*" >&2
return 1
fi
cp -a -- "${files[@]}" "$APPDIR/usr/lib/"
}
readonly DIRECT_QT_LIBRARIES=(
Qt6Core
Qt6DBus
Qt6Gui
Qt6LabsPlatform
Qt6Network
Qt6OpenGL
Qt6Qml
Qt6QmlMeta
Qt6QmlModels
Qt6QmlWorkerScript
Qt6Quick
Qt6QuickControls2
Qt6QuickLayouts
Qt6QuickShapes
Qt6QuickTemplates2
Qt6Widgets
)
for name in "${DIRECT_QT_LIBRARIES[@]}"; do
copy_qt_library "$name"
done
for name in icudata icui18n icuio icutest icutu icuuc; do
soname="lib${name}.so.73"
source="$QT_ROOT/lib/$soname"
if [[ ! -e "$source" ]]; then
echo "Matching ICU library is missing: $soname" >&2
exit 1
fi
# The rules_qt ICU links point at Bazel's external cache with absolute
# targets. Materialize each SONAME so the AppDir never contains those
# developer-machine-only links.
install -m 0644 "$(readlink -f -- "$source")" "$APPDIR/usr/lib/$soname"
done
# Copy only the QML module roots used by the application. Copying all of
# QtQuick also deploys unrelated multimedia, 3D, virtual-keyboard, and style
# modules, greatly expanding the native dependency closure.
copy_module_files() {
local source="$1"
local destination="$2"
mkdir -p "$destination"
find -L "$source" -maxdepth 1 -type f \
-exec cp -aL -t "$destination" -- {} +
}
copy_module_files "$QT_ROOT/qml/QtQml" "$APPDIR/usr/lib/qt6/qml/QtQml"
cp -aL \
"$QT_ROOT/qml/QtQml/Models" \
"$QT_ROOT/qml/QtQml/WorkerScript" \
"$APPDIR/usr/lib/qt6/qml/QtQml/"
cp -aL "$QT_ROOT/qml/QtCore" "$APPDIR/usr/lib/qt6/qml/"
copy_module_files "$QT_ROOT/qml/QtQuick" "$APPDIR/usr/lib/qt6/qml/QtQuick"
for module in Window Layouts Shapes Templates; do
cp -aL \
"$QT_ROOT/qml/QtQuick/$module" \
"$APPDIR/usr/lib/qt6/qml/QtQuick/"
done
copy_module_files \
"$QT_ROOT/qml/QtQuick/Controls" \
"$APPDIR/usr/lib/qt6/qml/QtQuick/Controls"
cp -aL \
"$QT_ROOT/qml/QtQuick/Controls/Basic" \
"$QT_ROOT/qml/QtQuick/Controls/Fusion" \
"$QT_ROOT/qml/QtQuick/Controls/impl" \
"$APPDIR/usr/lib/qt6/qml/QtQuick/Controls/"
mkdir -p "$APPDIR/usr/lib/qt6/qml/Qt/labs"
cp -aL \
"$QT_ROOT/qml/Qt/labs/platform" \
"$APPDIR/usr/lib/qt6/qml/Qt/labs/"
# Deploy the XCB, Wayland, headless-test, and SVG plugins. Wayland client-side
# support is copied as a unit because the compositor selects its shell,
# decoration, and buffer integrations dynamically.
readonly PLUGIN_CATEGORIES=(
platforms
imageformats
xcbglintegrations
wayland-decoration-client
wayland-graphics-integration-client
wayland-shell-integration
)
for category in "${PLUGIN_CATEGORIES[@]}"; do
mkdir -p "$APPDIR/usr/lib/qt6/plugins/$category"
done
for plugin in \
libqoffscreen.so \
libqxcb.so \
libqwayland-egl.so \
libqwayland-generic.so; do
cp -aL \
"$QT_ROOT/plugins/platforms/$plugin" \
"$APPDIR/usr/lib/qt6/plugins/platforms/"
done
cp -aL \
"$QT_ROOT/plugins/imageformats/libqsvg.so" \
"$APPDIR/usr/lib/qt6/plugins/imageformats/"
# The XCB platform plugin delegates OpenGL context creation to one of these
# dynamically loaded integrations. Without them libqxcb still loads, but Qt
# reports that neither GLX nor EGL is enabled and Qt Quick cannot create an RHI.
cp -aL \
"$QT_ROOT/plugins/xcbglintegrations/libqxcb-egl-integration.so" \
"$QT_ROOT/plugins/xcbglintegrations/libqxcb-glx-integration.so" \
"$APPDIR/usr/lib/qt6/plugins/xcbglintegrations/"
for category in \
wayland-decoration-client \
wayland-graphics-integration-client \
wayland-shell-integration; do
cp -aL \
"$QT_ROOT/plugins/$category/." \
"$APPDIR/usr/lib/qt6/plugins/$category/"
done
# Close only the Qt/ICU portion of the ELF graph. Every non-Qt dependency is a
# target-system responsibility; in particular, never copy the build host's
# libcurl, C++ runtime, OpenSSL, D-Bus, graphics, desktop, or network stack.
while true; do
added=0
mapfile -t elf_files < <(find "$APPDIR" -type f -print | sort)
for elf in "${elf_files[@]}"; do
file -L "$elf" | grep -q ELF || continue
while IFS= read -r soname; do
[[ -n "$soname" ]] || continue
[[ -e "$APPDIR/usr/lib/$soname" ]] && continue
if [[ "$soname" == libQt6*.so* || "$soname" == libicu*.so* ]]; then
source="$QT_ROOT/lib/$soname"
else
continue
fi
if [[ -z "$source" || ! -e "$source" ]]; then
echo "Unable to resolve $soname, required by $elf" >&2
exit 1
fi
echo "Bundling $soname"
install -m 0644 "$(readlink -f -- "$source")" "$APPDIR/usr/lib/$soname"
added=1
done < <(
readelf -d "$elf" 2>/dev/null |
sed -n 's/.*Shared library: \[\(.*\)\]/\1/p'
)
done
((added == 0)) && break
done
install -m 0644 \
"$WORKSPACE_ROOT/sharing/linux/app/packaging/quickshare.desktop" \
"$APPDIR/usr/share/applications/quickshare.desktop"
install -m 0644 \
"$WORKSPACE_ROOT/sharing/linux/app/packaging/quickshare.metainfo.xml" \
"$APPDIR/usr/share/metainfo/quickshare.metainfo.xml"
install -m 0644 \
"$WORKSPACE_ROOT/sharing/linux/app/icons/quickshare.svg" \
"$APPDIR/usr/share/icons/hicolor/scalable/apps/quickshare.svg"
cp \
"$APPDIR/usr/share/applications/quickshare.desktop" \
"$APPDIR/"
cp \
"$APPDIR/usr/share/icons/hicolor/scalable/apps/quickshare.svg" \
"$APPDIR/"
printf '%s\n' \
'[Paths]' \
'Prefix=..' \
'Plugins=lib/qt6/plugins' \
'Qml2Imports=lib/qt6/qml' \
>"$APPDIR/usr/bin/qt.conf"
printf '%s\n' \
'#!/bin/sh' \
'set -eu' \
'' \
'HERE=$(CDPATH= cd -- "$(dirname -- "$(readlink -f -- "$0")")" && pwd)' \
'' \
'export LD_LIBRARY_PATH="$HERE/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"' \
'export QT_PLUGIN_PATH="$HERE/usr/lib/qt6/plugins"' \
'export QT_QPA_PLATFORM_PLUGIN_PATH="$HERE/usr/lib/qt6/plugins/platforms"' \
'export QML_IMPORT_PATH="$HERE/usr/lib/qt6/qml"' \
'export QML2_IMPORT_PATH="$HERE/usr/lib/qt6/qml"' \
'export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-wayland;xcb}"' \
'' \
'exec "$HERE/usr/bin/quickshare" "$@"' \
>"$APPDIR/AppRun"
chmod 0755 "$APPDIR/AppRun"
# Fail on an absent dependency, a symbol-version mismatch, or any Qt library
# resolved from the host.
audit_failed=0
mapfile -t elf_files < <(find "$APPDIR" -type f -print | sort)
for elf in "${elf_files[@]}"; do
file -L "$elf" | grep -q ELF || continue
output="$(env LD_LIBRARY_PATH="$APPDIR/usr/lib" ldd "$elf" 2>&1 || true)"
problems="$(grep -E 'not found|version `' <<<"$output" || true)"
host_qt="$(grep -E 'libQt6.* => /(lib|usr/lib)' <<<"$output" || true)"
if [[ -n "$problems" || -n "$host_qt" ]]; then
echo "Dependency audit failed: $elf" >&2
[[ -n "$problems" ]] && printf '%s\n' "$problems" >&2
[[ -n "$host_qt" ]] && printf '%s\n' "$host_qt" >&2
audit_failed=1
fi
done
((audit_failed == 0)) || exit 1
while IFS= read -r -d '' link; do
if [[ ! -e "$link" ]]; then
echo "Broken packaged symlink: $link -> $(readlink -- "$link")" >&2
audit_failed=1
fi
done < <(find "$APPDIR" -type l -print0)
((audit_failed == 0)) || exit 1
# Validate the deployed QML import graph independently of the Nearby backend,
# which requires access to the system D-Bus and Bluetooth services. A qt.conf
# beside this temporary copy makes the QML tool use only the staged AppDir.
readonly QML_TEST_DIR="$STAGE/qml-test"
readonly QML_SMOKE_LOG="$STAGE/qml-smoke-test.log"
mkdir -p "$QML_TEST_DIR"
install -m 0755 "$QML_TOOL" "$QML_TEST_DIR/qml"
printf '%s\n' \
'[Paths]' \
"Prefix=$APPDIR/usr" \
'Plugins=lib/qt6/plugins' \
'Qml2Imports=lib/qt6/qml' \
>"$QML_TEST_DIR/qt.conf"
printf '%s\n' \
'import QtCore' \
'import QtQuick' \
'import QtQuick.Window' \
'import QtQuick.Controls' \
'import QtQuick.Layouts' \
'import QtQuick.Shapes' \
'import Qt.labs.platform as Labs' \
'ApplicationWindow {' \
' visible: false' \
' Component.onCompleted: Qt.quit()' \
'}' \
>"$QML_TEST_DIR/smoke.qml"
set +e
env \
LD_LIBRARY_PATH="$APPDIR/usr/lib:$QT_DISTRIBUTION/lib" \
QT_QPA_PLATFORM=offscreen \
QT_DEBUG_PLUGINS=1 \
QML_IMPORT_TRACE=1 \
timeout 15s "$QML_TEST_DIR/qml" \
-I "$APPDIR/usr/lib/qt6/qml" \
"$QML_TEST_DIR/smoke.qml" \
>"$QML_SMOKE_LOG" 2>&1
qml_smoke_status=$?
set -e
if [[ "$qml_smoke_status" -ne 0 ]]; then
echo "QML import smoke test failed with status $qml_smoke_status" >&2
tail -n 100 "$QML_SMOKE_LOG" >&2
exit "$qml_smoke_status"
fi
# The complete application smoke test is opt-in because its Nearby backend
# opens the system D-Bus before loading QML. Sandboxes without system-bus
# access abort here even when the package itself is valid.
if [[ "${RUN_BACKEND_SMOKE_TEST:-0}" == 1 ]]; then
readonly BACKEND_SMOKE_LOG="$STAGE/backend-smoke-test.log"
set +e
env \
QT_QPA_PLATFORM=offscreen \
QT_DEBUG_PLUGINS=1 \
QML_IMPORT_TRACE=1 \
timeout 15s "$APPDIR/AppRun" \
>"$BACKEND_SMOKE_LOG" 2>&1
backend_smoke_status=$?
set -e
if [[ "$backend_smoke_status" -ne 0 && "$backend_smoke_status" -ne 124 ]]; then
echo "Backend smoke test failed with status $backend_smoke_status" >&2
tail -n 100 "$BACKEND_SMOKE_LOG" >&2
exit "$backend_smoke_status"
fi
fi
mkdir -p "$OUTPUT_DIR"
env \
ARCH=x86_64 \
APPIMAGE_EXTRACT_AND_RUN=1 \
"$APPIMAGETOOL" \
--runtime-file "$RUNTIME" \
"$APPDIR" \
"$OUTPUT_APPIMAGE"
cp -a "$APPDIR" "$FINAL_APPDIR"
cp "$QML_SMOKE_LOG" "$OUTPUT_DIR/qml-smoke-test.log"
if [[ -v BACKEND_SMOKE_LOG ]]; then
cp "$BACKEND_SMOKE_LOG" "$OUTPUT_DIR/backend-smoke-test.log"
fi
echo "Created $OUTPUT_APPIMAGE"
echo "Retained $FINAL_APPDIR"