mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
369 lines
11 KiB
Bash
Executable File
369 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Validate the final, repacked AppImage bytes before release signing.
|
|
#
|
|
# This file is sourceable for fixture tests. When executed directly it extracts
|
|
# exactly one AppImage from a foreign working directory, validates the released
|
|
# sharun layout, and optionally performs a bounded Xvfb startup smoke.
|
|
#
|
|
# Usage: validate-appimage-runtime.sh <final.AppImage>
|
|
#
|
|
# Env:
|
|
# APPIMAGE_RUNTIME_SMOKE — set to 1 to require the bounded Xvfb startup smoke;
|
|
# otherwise only static final-artifact checks run
|
|
# APPIMAGE_RUNTIME_APPARMOR_USERNS — set to 1 on Ubuntu 24.04 CI to load a
|
|
# temporary, per-executable AppArmor profile granting
|
|
# Chromium's sandbox access to user namespaces
|
|
|
|
set -euo pipefail
|
|
|
|
RUNTIME_VALIDATOR_SCRIPT_DIR="$(
|
|
cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
|
|
)"
|
|
# shellcheck source=strip-appimage-graphics-libs.sh
|
|
source "$RUNTIME_VALIDATOR_SCRIPT_DIR/strip-appimage-graphics-libs.sh"
|
|
|
|
runtime_validation_error() {
|
|
echo "[appimage-runtime] ERROR: $*" >&2
|
|
return 1
|
|
}
|
|
|
|
validate_extracted_appdir() {
|
|
local appdir="$1"
|
|
local lib_path="$appdir/shared/lib/lib.path"
|
|
|
|
echo "[appimage-runtime] Validating extracted AppDir: $appdir"
|
|
if [ -f "$lib_path" ]; then
|
|
echo "[appimage-runtime] shared/lib/lib.path:"
|
|
sed 's/^/[appimage-runtime] /' "$lib_path"
|
|
else
|
|
echo "[appimage-runtime] shared/lib/lib.path: <missing>"
|
|
fi
|
|
|
|
if ! uses_sharun_launcher "$appdir"; then
|
|
runtime_validation_error "released AppDir does not use a detectable sharun launcher"
|
|
return 1
|
|
fi
|
|
|
|
local executable
|
|
for executable in \
|
|
"$appdir/AppRun" \
|
|
"$appdir/sharun" \
|
|
"$appdir/bin/OpenHuman" \
|
|
"$appdir/shared/bin/OpenHuman"; do
|
|
if ! is_executable_elf "$executable"; then
|
|
runtime_validation_error "${executable#"$appdir"/} is not an executable ELF"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
local alias
|
|
for alias in "$appdir/AppRun" "$appdir/bin/OpenHuman"; do
|
|
if [ "$alias" -ef "$appdir/sharun" ] || cmp -s "$alias" "$appdir/sharun"; then
|
|
continue
|
|
fi
|
|
runtime_validation_error "${alias#"$appdir"/} does not match sharun"
|
|
return 1
|
|
done
|
|
|
|
validate_sharun_lib_path "$appdir" || return 1
|
|
validate_appimage_required_libs "$appdir" || return 1
|
|
|
|
local real_app="$appdir/shared/bin/OpenHuman"
|
|
local needed
|
|
if ! needed="$(patchelf --print-needed "$real_app")"; then
|
|
runtime_validation_error \
|
|
"could not read NEEDED entries from shared/bin/OpenHuman"
|
|
return 1
|
|
fi
|
|
echo "[appimage-runtime] shared/bin/OpenHuman NEEDED entries:"
|
|
if [ -n "$needed" ]; then
|
|
printf '%s\n' "$needed" | sed 's/^/[appimage-runtime] /'
|
|
else
|
|
echo "[appimage-runtime] <none>"
|
|
fi
|
|
|
|
local expected_needed="${APPIMAGE_EXPECTED_NEEDED-libxdo.so.3 libcef.so}"
|
|
local needed_name
|
|
for needed_name in $expected_needed; do
|
|
if ! printf '%s\n' "$needed" | grep -Fx "$needed_name" >/dev/null; then
|
|
runtime_validation_error "shared/bin/OpenHuman is missing NEEDED entry '$needed_name'"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
local root elf rpath
|
|
for root in \
|
|
"$appdir/shared/bin" \
|
|
"$appdir/shared/lib" \
|
|
"$appdir/bin" \
|
|
"$appdir/lib" \
|
|
"$appdir/usr/bin" \
|
|
"$appdir/usr/lib"; do
|
|
[ -d "$root" ] || continue
|
|
while IFS= read -r -d '' elf; do
|
|
is_elf "$elf" || continue
|
|
rpath="$(patchelf --print-rpath "$elf" 2>/dev/null || true)"
|
|
case "$rpath" in
|
|
*"/home/runner/"*|*"/__w/"*)
|
|
runtime_validation_error \
|
|
"${elf#"$appdir"/} retains forbidden build-runner RPATH '$rpath'"
|
|
return 1
|
|
;;
|
|
esac
|
|
done < <(find "$root" -type f -print0)
|
|
done
|
|
}
|
|
|
|
smoke_extracted_apprun() {
|
|
local appdir="$1"
|
|
local foreign_cwd="$2"
|
|
local log_file="$3"
|
|
|
|
command -v timeout >/dev/null 2>&1 \
|
|
|| { runtime_validation_error "timeout is required for the AppImage smoke"; return 1; }
|
|
command -v xvfb-run >/dev/null 2>&1 \
|
|
|| { runtime_validation_error "xvfb-run is required for the AppImage smoke"; return 1; }
|
|
|
|
echo "[appimage-runtime] Smoking AppRun from AppDir: $appdir"
|
|
echo "[appimage-runtime] Smoke caller CWD: $foreign_cwd"
|
|
|
|
local temp_root
|
|
temp_root="$(dirname "$foreign_cwd")"
|
|
local smoke_home="$temp_root/home"
|
|
local smoke_config="$temp_root/config"
|
|
local smoke_data="$temp_root/data"
|
|
local smoke_cache="$temp_root/cache"
|
|
mkdir -p \
|
|
"$foreign_cwd" \
|
|
"$smoke_home" \
|
|
"$smoke_config" \
|
|
"$smoke_data" \
|
|
"$smoke_cache"
|
|
|
|
local -a unset_args=()
|
|
local secret_name
|
|
for secret_name in \
|
|
GITHUB_TOKEN \
|
|
GH_TOKEN \
|
|
OPENHUMAN_CEF_NO_SANDBOX \
|
|
TAURI_SIGNING_PRIVATE_KEY \
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD \
|
|
SENTRY_AUTH_TOKEN; do
|
|
unset_args+=(-u "$secret_name")
|
|
done
|
|
while IFS= read -r secret_name; do
|
|
[ -n "$secret_name" ] && unset_args+=(-u "$secret_name")
|
|
done < <(compgen -v OPENAI_ || true)
|
|
|
|
local status
|
|
if (
|
|
cd "$foreign_cwd"
|
|
timeout --signal=TERM --kill-after=5s 15s \
|
|
xvfb-run -a --server-args="-screen 0 1280x960x24" \
|
|
env "${unset_args[@]}" \
|
|
HOME="$smoke_home" \
|
|
XDG_CONFIG_HOME="$smoke_config" \
|
|
XDG_DATA_HOME="$smoke_data" \
|
|
XDG_CACHE_HOME="$smoke_cache" \
|
|
OPENHUMAN_CEF_PREWARM=0 \
|
|
OPENHUMAN_DISABLE_GPU=1 \
|
|
"$appdir/AppRun"
|
|
) >"$log_file" 2>&1; then
|
|
status=0
|
|
else
|
|
status=$?
|
|
fi
|
|
|
|
local forbidden=0
|
|
if grep -Eiq \
|
|
'anylinux\.so.*cannot be preloaded|cannot be preloaded.*anylinux\.so' \
|
|
"$log_file"; then
|
|
forbidden=1
|
|
fi
|
|
if grep -Eiq \
|
|
'libxdo\.so\.3.*cannot open shared object file|cannot open shared object file.*libxdo\.so\.3' \
|
|
"$log_file"; then
|
|
forbidden=1
|
|
fi
|
|
if grep -Eiq \
|
|
'libcef\.so.*cannot open shared object file|cannot open shared object file.*libcef\.so' \
|
|
"$log_file"; then
|
|
forbidden=1
|
|
fi
|
|
if grep -Eiq 'error while loading shared libraries' "$log_file"; then
|
|
forbidden=1
|
|
fi
|
|
|
|
# The desktop process is expected to remain alive until timeout ends the
|
|
# startup window. An earlier clean exit is a failure as well as loader output.
|
|
if [ "$forbidden" -ne 0 ] || [ "$status" -ne 124 ]; then
|
|
echo "[appimage-runtime] AppImage startup smoke failed (status $status):" >&2
|
|
sed 's/^/[appimage-runtime] /' "$log_file" >&2
|
|
return 1
|
|
fi
|
|
|
|
grep -Ei 'loader|loading|cef|startup|started|ready' "$log_file" \
|
|
| sed 's/^/[appimage-runtime] /' \
|
|
|| true
|
|
echo "[appimage-runtime] Application remained alive for the 15-second startup window"
|
|
}
|
|
|
|
install_smoke_userns_profile() {
|
|
local appdir="$1"
|
|
local profile_file="$2"
|
|
local executable="$appdir/shared/bin/OpenHuman"
|
|
|
|
[ -x "$executable" ] \
|
|
|| { runtime_validation_error "AppArmor target is not executable: $executable"; return 1; }
|
|
command -v sudo >/dev/null 2>&1 \
|
|
|| { runtime_validation_error "sudo is required for the AppImage smoke AppArmor profile"; return 1; }
|
|
command -v apparmor_parser >/dev/null 2>&1 \
|
|
|| { runtime_validation_error "apparmor_parser is required for the AppImage smoke AppArmor profile"; return 1; }
|
|
|
|
local profile_name="openhuman_appimage_runtime_smoke_$$"
|
|
printf '%s\n' \
|
|
'abi <abi/4.0>,' \
|
|
'include <tunables/global>' \
|
|
'' \
|
|
"profile $profile_name \"$executable\" flags=(unconfined) {" \
|
|
' userns,' \
|
|
'}' \
|
|
>"$profile_file"
|
|
|
|
sudo --non-interactive apparmor_parser --replace "$profile_file" >/dev/null \
|
|
|| { runtime_validation_error "could not load the AppImage smoke AppArmor profile"; return 1; }
|
|
echo "[appimage-runtime] Loaded temporary AppArmor userns profile for: $executable"
|
|
}
|
|
|
|
remove_smoke_userns_profile() {
|
|
local profile_file="$1"
|
|
sudo --non-interactive apparmor_parser --remove "$profile_file" >/dev/null \
|
|
|| { runtime_validation_error "could not remove the AppImage smoke AppArmor profile"; return 1; }
|
|
echo "[appimage-runtime] Removed temporary AppArmor userns profile: $profile_file"
|
|
}
|
|
|
|
smoke_extracted_apprun_with_userns() {
|
|
local appdir="$1"
|
|
local foreign_cwd="$2"
|
|
local log_file="$3"
|
|
local profile_file="$4"
|
|
local profile_loaded=0
|
|
local previous_hup_trap previous_int_trap previous_term_trap
|
|
previous_hup_trap="$(trap -p HUP)"
|
|
previous_int_trap="$(trap -p INT)"
|
|
previous_term_trap="$(trap -p TERM)"
|
|
|
|
restore_smoke_signal_traps() {
|
|
trap - HUP INT TERM
|
|
[ -z "$previous_hup_trap" ] || eval "$previous_hup_trap"
|
|
[ -z "$previous_int_trap" ] || eval "$previous_int_trap"
|
|
[ -z "$previous_term_trap" ] || eval "$previous_term_trap"
|
|
}
|
|
|
|
cleanup_smoke_userns_profile() {
|
|
if [ "$profile_loaded" -eq 1 ]; then
|
|
profile_loaded=0
|
|
remove_smoke_userns_profile "$profile_file" \
|
|
|| echo "[appimage-runtime] ERROR: AppArmor cleanup failed after smoke interruption" >&2
|
|
fi
|
|
}
|
|
|
|
interrupt_smoke_with_userns() {
|
|
local exit_status="$1"
|
|
trap - HUP INT TERM
|
|
cleanup_smoke_userns_profile
|
|
exit "$exit_status"
|
|
}
|
|
trap 'interrupt_smoke_with_userns 129' HUP
|
|
trap 'interrupt_smoke_with_userns 130' INT
|
|
trap 'interrupt_smoke_with_userns 143' TERM
|
|
|
|
if ! install_smoke_userns_profile "$appdir" "$profile_file"; then
|
|
restore_smoke_signal_traps
|
|
return 1
|
|
fi
|
|
profile_loaded=1
|
|
|
|
local smoke_status
|
|
if smoke_extracted_apprun "$appdir" "$foreign_cwd" "$log_file"; then
|
|
smoke_status=0
|
|
else
|
|
smoke_status=$?
|
|
fi
|
|
|
|
profile_loaded=0
|
|
local remove_status
|
|
if remove_smoke_userns_profile "$profile_file"; then
|
|
remove_status=0
|
|
else
|
|
remove_status=$?
|
|
fi
|
|
|
|
restore_smoke_signal_traps
|
|
# Preserve the original smoke failure even if profile cleanup also fails.
|
|
[ "$smoke_status" -eq 0 ] || return "$smoke_status"
|
|
return "$remove_status"
|
|
}
|
|
|
|
validate_final_appimage() (
|
|
local image="$1"
|
|
if ! image="$(realpath "$image")"; then
|
|
runtime_validation_error "could not resolve AppImage path: $1"
|
|
return 1
|
|
fi
|
|
if [ ! -f "$image" ] || [ ! -x "$image" ]; then
|
|
runtime_validation_error "AppImage must be an executable regular file: $image"
|
|
return 1
|
|
fi
|
|
command -v patchelf >/dev/null 2>&1 \
|
|
|| { runtime_validation_error "patchelf is required for final AppImage validation"; return 1; }
|
|
|
|
local temp_root
|
|
temp_root="$(mktemp -d)"
|
|
trap 'rm -rf -- "$temp_root"' EXIT
|
|
local foreign_cwd="$temp_root/foreign-cwd"
|
|
local extraction_log="$temp_root/extraction.log"
|
|
local smoke_log="$temp_root/smoke.log"
|
|
mkdir -p "$foreign_cwd"
|
|
|
|
if ! (
|
|
cd "$foreign_cwd"
|
|
"$image" --appimage-extract
|
|
) >"$extraction_log" 2>&1; then
|
|
echo "[appimage-runtime] AppImage extraction failed:" >&2
|
|
sed 's/^/[appimage-runtime] /' "$extraction_log" >&2
|
|
return 1
|
|
fi
|
|
|
|
local appdir="$foreign_cwd/squashfs-root"
|
|
if [ ! -d "$appdir" ]; then
|
|
runtime_validation_error "extraction did not create $appdir"
|
|
return 1
|
|
fi
|
|
validate_extracted_appdir "$appdir" || return 1
|
|
|
|
if [ "${APPIMAGE_RUNTIME_SMOKE:-0}" = "1" ]; then
|
|
if [ "${APPIMAGE_RUNTIME_APPARMOR_USERNS:-0}" = "1" ]; then
|
|
smoke_extracted_apprun_with_userns \
|
|
"$appdir" \
|
|
"$foreign_cwd" \
|
|
"$smoke_log" \
|
|
"$temp_root/openhuman-appimage-smoke.profile" \
|
|
|| return 1
|
|
else
|
|
smoke_extracted_apprun "$appdir" "$foreign_cwd" "$smoke_log" || return 1
|
|
fi
|
|
else
|
|
echo "[appimage-runtime] Static validation complete; executable smoke disabled for this architecture"
|
|
fi
|
|
)
|
|
|
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
[ "$#" -eq 1 ] || {
|
|
echo "Usage: $0 <final.AppImage>" >&2
|
|
exit 2
|
|
}
|
|
APPIMAGE_EXPECTED_NEEDED="libxdo.so.3 libcef.so" \
|
|
validate_final_appimage "$1"
|
|
fi
|