mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-15 07:06:11 -04:00
385 lines
12 KiB
Bash
Executable File
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"
|