Merge pull request #596 from jcastro/feature/monitor-native-arm64-build

feat(monitor): add native ARM64 AppImage build validation
This commit is contained in:
MacRimi
2026-11-05 18:21:10 +01:00
committed by GitHub
17 changed files with 386 additions and 17 deletions
+33
View File
@@ -0,0 +1,33 @@
"""Reject foreign ELF files in a native AppDir without executing any binary."""
import argparse
from pathlib import Path
import struct
def check(directory, architecture):
expected = {'x86_64': 62, 'aarch64': 183}[architecture]
checked = 0
for path in Path(directory).rglob('*'):
if path.is_symlink() or not path.is_file():
continue
with path.open('rb') as stream:
header = stream.read(20)
if not header.startswith(b'\x7fELF'):
continue
if len(header) != 20 or header[5] not in (1, 2):
raise ValueError(f'Invalid ELF header: {path}')
machine = struct.unpack('<H' if header[5] == 1 else '>H', header[18:20])[0]
if machine != expected:
raise ValueError(f'Foreign ELF machine {machine} in {architecture} AppDir: {path}')
checked += 1
if not checked:
raise ValueError('No native ELF files found in AppDir')
return checked
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('directory', type=Path)
parser.add_argument('architecture', choices=('x86_64', 'aarch64'))
args = parser.parse_args()
print(f'PASS: {check(args.directory, args.architecture)} native ELF files')
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Exercise the extracted launcher and its real native Python environment,
# intercepting only the final server exec so no host-monitoring server starts.
set -eu
appdir="$1"
export PMX_SMOKE_PYTHON
PMX_SMOKE_PYTHON="$(command -v python)"
fixture_bin="$(mktemp -d)"
trap 'rm -rf "$fixture_bin"' EXIT
cat > "$fixture_bin/python3" <<'WRAPPER'
#!/bin/sh
case "$1" in
*/flask_server.py)
exec "$PMX_SMOKE_PYTHON" -c 'import gevent, greenlet, geventwebsocket, zope.interface; from PIL import Image; from gevent import pywsgi; print("PASS: extracted AppRun native runtime")'
;;
*) exec "$PMX_SMOKE_PYTHON" "$@" ;;
esac
WRAPPER
chmod +x "$fixture_bin/python3"
PATH="$fixture_bin:$PATH" bash "$appdir/AppRun"
@@ -0,0 +1,79 @@
name: Native Monitor AppImage validation
on:
pull_request:
paths:
- '.github/workflows/test-appimage-native.yml'
- 'AppImage/scripts/build_appimage.sh'
- 'AppImage/scripts/appimage_arch.sh'
- 'AppImage/scripts/borg_native_arm.sh'
- '.github/scripts/smoke_native_apprun.sh'
- '.github/scripts/check_appimage_elf.py'
- 'AppImage/scripts/AppRun'
- 'tests/test_appimage_arch.py'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: native-appimage-${{ github.ref }}
cancel-in-progress: true
jobs:
native-build:
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-22.04
arch: x86_64
- runner: ubuntu-22.04-arm
arch: aarch64
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Validate native architecture selection
run: |
test "$(uname -m)" = '${{ matrix.arch }}'
python -B -m unittest discover -s tests -p test_appimage_arch.py -v
- name: Refresh package indexes for bundled monitoring tools
# Third-party runner repositories occasionally fail; the build reports missing tools.
run: sudo apt-get update || true
- name: Build native AppImage without publishing it
run: bash AppImage/scripts/build_appimage.sh
- name: Reject foreign ELF payloads
run: python .github/scripts/check_appimage_elf.py /tmp/proxmenux_build/ProxMenux.AppDir '${{ matrix.arch }}'
- name: Extract native AppImage without FUSE
run: |
mkdir /tmp/proxmenux-native-extract
cd /tmp/proxmenux-native-extract
image=$(find "$GITHUB_WORKSPACE/AppImage/dist" -name '*.AppImage' -type f -print -quit)
test -n "$image"
chmod +x "$image"
"$image" --appimage-extract
test -f squashfs-root/usr/bin/flask_server.py
test -f squashfs-root/usr/bin/appimage_arch.sh
test -f squashfs-root/web/index.html
- name: Import bundled Python 3.11 native WebSocket runtime
env:
PYTHONPATH: /tmp/proxmenux-native-extract/squashfs-root/usr/lib/python3/vendor/cp311:/tmp/proxmenux-native-extract/squashfs-root/usr/lib/python3/dist-packages
run: |
python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from PIL import Image; from gevent import pywsgi; print(gevent.__version__)'
bash .github/scripts/smoke_native_apprun.sh /tmp/proxmenux-native-extract/squashfs-root
- uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Import bundled Python 3.13 native WebSocket runtime
env:
PYTHONPATH: /tmp/proxmenux-native-extract/squashfs-root/usr/lib/python3/vendor/cp313:/tmp/proxmenux-native-extract/squashfs-root/usr/lib/python3/dist-packages
run: |
python -c 'import gevent, greenlet, geventwebsocket, zope.interface; from PIL import Image; from gevent import pywsgi; print(gevent.__version__)'
bash .github/scripts/smoke_native_apprun.sh /tmp/proxmenux-native-extract/squashfs-root
+4 -1
View File
@@ -6,8 +6,11 @@
# Get the directory where this AppImage is mounted
APPDIR="$(dirname "$(readlink -f "${0}")")"
source "${APPDIR}/usr/bin/appimage_arch.sh" || exit 1
pmx_appimage_arch "$(uname -m)" || exit 1
export PATH="${APPDIR}/usr/bin:${PATH}"
export LD_LIBRARY_PATH="${APPDIR}/usr/lib/x86_64-linux-gnu:${APPDIR}/usr/lib:${APPDIR}/lib/x86_64-linux-gnu:${APPDIR}/lib:${LD_LIBRARY_PATH}"
export LD_LIBRARY_PATH="${APPDIR}/usr/lib/${PMX_APPIMAGE_TRIPLET}:${APPDIR}/usr/lib:${APPDIR}/lib/${PMX_APPIMAGE_TRIPLET}:${APPDIR}/lib:${LD_LIBRARY_PATH}"
# Select the native dependency tree matching the host interpreter. PVE 8
# uses CPython 3.11 and PVE 9 uses CPython 3.13; both runtimes are bundled in
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Shared native build/runtime architecture names. No emulated cross-builds.
pmx_appimage_arch() {
case "$1" in
x86_64|amd64)
PMX_APPIMAGE_ARCH=x86_64
PMX_APPIMAGE_TRIPLET=x86_64-linux-gnu
PMX_APPIMAGE_SUFFIX=""
;;
aarch64|arm64)
PMX_APPIMAGE_ARCH=aarch64
PMX_APPIMAGE_TRIPLET=aarch64-linux-gnu
PMX_APPIMAGE_SUFFIX=-aarch64
;;
*)
echo "Unsupported Monitor AppImage architecture: $1" >&2
return 1
;;
esac
}
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# Borg 1.2.8 has no official ARM64 standalone release. Never ship or run
# its AMD64 executable on ARM; use the host's native distro package instead.
if [ -x /usr/bin/borg ]; then
# The distro package must use its own Python modules and shared libraries.
unset PYTHONPATH PYTHONHOME LD_LIBRARY_PATH
exec /usr/bin/borg "$@"
fi
echo 'Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.' >&2
exit 127
+38 -16
View File
@@ -8,6 +8,8 @@ set -e
WORK_DIR="/tmp/proxmenux_build"
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/appimage_arch.sh"
pmx_appimage_arch "$(uname -m)" || exit 1
DIST_DIR="$SCRIPT_DIR/../dist"
APPIMAGE_ROOT="$SCRIPT_DIR/.."
@@ -24,7 +26,7 @@ ZOPE_EVENT_VERSION="6.2"
GEVENT_WEBSOCKET_VERSION="0.10.1"
VERSION=$(node -p "require('$APPIMAGE_ROOT/package.json').version")
APPIMAGE_NAME="ProxMenux-${VERSION}.AppImage"
APPIMAGE_NAME="ProxMenux-${VERSION}${PMX_APPIMAGE_SUFFIX}.AppImage"
echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with hardware monitoring tools..."
@@ -33,17 +35,18 @@ echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with hardware monitor
# under the non-root GitHub Actions runner — previously hardcoded to
# /var/cache/, which the CI user can't write to (mkdir Permission
# denied → build aborted before doing any actual work).
APPIMAGETOOL_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/proxmenux-build/appimagetool"
APPIMAGETOOL_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/proxmenux-build/appimagetool-${PMX_APPIMAGE_ARCH}"
# Preserve a cached copy of appimagetool across builds. wget -q has bitten
# us repeatedly when GitHub momentarily rate-limits or the runner has no
# network — the result is a 0-byte file that passes the `[ -f ]` check on
# the next run and breaks the build silently.
if [ -f "$WORK_DIR/appimagetool" ] && [ -s "$WORK_DIR/appimagetool" ]; then
mkdir -p "$(dirname "$APPIMAGETOOL_CACHE")"
cp -f "$WORK_DIR/appimagetool" "$APPIMAGETOOL_CACHE"
APPIMAGETOOL_URL="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${PMX_APPIMAGE_ARCH}.AppImage"
if [ "$PMX_APPIMAGE_ARCH" = aarch64 ]; then
# The old AppImageKit ARM build does not accept ARCH=aarch64 reliably.
# Use a versioned tool; leave the existing AMD64 tool unchanged.
APPIMAGETOOL_URL="https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-aarch64.AppImage"
APPIMAGETOOL_CACHE="${APPIMAGETOOL_CACHE}-1.9.1"
fi
# Architecture-specific caches never reuse an executable from a different CPU.
# Clean and create work directory
rm -rf "$WORK_DIR"
mkdir -p "$APP_DIR"
@@ -57,7 +60,7 @@ if [ -s "$APPIMAGETOOL_CACHE" ]; then
fi
if [ ! -s "$WORK_DIR/appimagetool" ]; then
echo "📥 Downloading appimagetool..."
wget --tries=3 --timeout=60 "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O "$WORK_DIR/appimagetool" || true
wget --tries=3 --timeout=60 "$APPIMAGETOOL_URL" -O "$WORK_DIR/appimagetool" || true
if [ ! -s "$WORK_DIR/appimagetool" ]; then
echo "❌ Failed to download appimagetool" >&2
exit 1
@@ -67,6 +70,17 @@ if [ ! -s "$WORK_DIR/appimagetool" ]; then
cp -f "$WORK_DIR/appimagetool" "$APPIMAGETOOL_CACHE"
fi
APPIMAGETOOL_ARGS=()
if [ "$PMX_APPIMAGE_ARCH" = aarch64 ]; then
echo "f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158 $WORK_DIR/appimagetool" | sha256sum -c -
# Without --runtime-file, appimagetool 1.9.1 embeds the unpinned
# `continuous` runtime. Use a released runtime checked by its digest.
RUNTIME_URL="https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-aarch64"
wget --tries=3 --timeout=60 "$RUNTIME_URL" -O "$WORK_DIR/runtime-aarch64"
echo "00cbdfcf917cc6c0ff6d3347d59e0ca1f7f45a6df1a428a0d6d8a78664d87444 $WORK_DIR/runtime-aarch64" | sha256sum -c -
APPIMAGETOOL_ARGS=(--runtime-file "$WORK_DIR/runtime-aarch64")
fi
# Create directory structure
mkdir -p "$APP_DIR/usr/bin"
mkdir -p "$APP_DIR/usr/lib/python3/dist-packages"
@@ -245,6 +259,7 @@ fi
# hb_ensure_borg used to download on demand — kept in lockstep so both
# code paths see the same version semantics. SHA256 is the upstream
# release checksum; bump both together.
if [ "$PMX_APPIMAGE_ARCH" = x86_64 ]; then
BORG_VERSION="1.2.8"
BORG_URL="https://github.com/borgbackup/borg/releases/download/${BORG_VERSION}/borg-linux64"
BORG_SHA256="cfa50fb704a93d3a4fa258120966345fddb394f960dca7c47fcb774d0172f40b"
@@ -280,6 +295,12 @@ else
exit 1
fi
else
echo "⚠️ No official Borg 1.2.8 ARM64 standalone binary; using native-host adapter"
cp "$SCRIPT_DIR/borg_native_arm.sh" "$APP_DIR/usr/bin/borg"
chmod +x "$APP_DIR/usr/bin/borg"
fi
# Copy Next.js build
echo "📋 Copying web dashboard..."
if [ -d "$APPIMAGE_ROOT/out" ]; then
@@ -310,6 +331,7 @@ fi
echo "📋 Copying AppRun script..."
if [ -f "$SCRIPT_DIR/AppRun" ]; then
cp "$SCRIPT_DIR/AppRun" "$APP_DIR/AppRun"
cp "$SCRIPT_DIR/appimage_arch.sh" "$APP_DIR/usr/bin/appimage_arch.sh"
chmod +x "$APP_DIR/AppRun"
echo "✅ AppRun script copied successfully"
else
@@ -379,7 +401,7 @@ pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" --upgrade \
# Phase 3b: Install the complete gevent/WSS runtime separately for every
# supported host ABI. Keeping each dependency tree isolated avoids mixing
# CPython-specific modules from the build runner with those loaded by PVE.
# The two --platform values allow gevent's manylinux_2_28 wheels and the
# The three --platform values allow gevent's manylinux_2_28 wheels and the
# backwards-compatible manylinux2014 zope.interface wheels to be resolved in
# one deterministic installation.
install_gevent_runtime() {
@@ -394,9 +416,9 @@ install_gevent_runtime() {
--target "$target" \
--upgrade \
--only-binary=:all: \
--platform manylinux_2_28_x86_64 \
--platform manylinux_2_27_x86_64 \
--platform manylinux2014_x86_64 \
--platform "manylinux_2_28_${PMX_APPIMAGE_ARCH}" \
--platform "manylinux_2_27_${PMX_APPIMAGE_ARCH}" \
--platform "manylinux2014_${PMX_APPIMAGE_ARCH}" \
--implementation cp \
--python-version "$python_version" \
--abi "$abi" \
@@ -547,7 +569,7 @@ if [ -d "$APP_DIR/bin" ]; then
fi
echo "🔍 Sanity check (ldd + presence of libfreeipmi)"
export LD_LIBRARY_PATH="$APP_DIR/lib:$APP_DIR/lib/x86_64-linux-gnu:$APP_DIR/usr/lib:$APP_DIR/usr/lib/x86_64-linux-gnu"
export LD_LIBRARY_PATH="$APP_DIR/lib:$APP_DIR/lib/${PMX_APPIMAGE_TRIPLET}:$APP_DIR/usr/lib:$APP_DIR/usr/lib/${PMX_APPIMAGE_TRIPLET}"
if ! find "$APP_DIR/usr/lib" "$APP_DIR/lib" -maxdepth 3 -name 'libfreeipmi.so.17*' | grep -q .; then
echo "❌ libfreeipmi.so.17 not found inside AppDir (ipmitool will fail)"
@@ -608,7 +630,7 @@ echo "🔨 Building unified AppImage v${VERSION}..."
cd "$WORK_DIR"
export NO_CLEANUP=1
export APPIMAGE_EXTRACT_AND_RUN=1
ARCH=x86_64 ./appimagetool --no-appstream --verbose "$APP_DIR" "$APPIMAGE_NAME"
ARCH="$PMX_APPIMAGE_ARCH" ./appimagetool --no-appstream --verbose "${APPIMAGETOOL_ARGS[@]}" "$APP_DIR" "$APPIMAGE_NAME"
# Move to dist directory
mv "$APPIMAGE_NAME" "$DIST_DIR/"
+37
View File
@@ -0,0 +1,37 @@
# Native Monitor AppImage builds
The builder selects its native architecture using `uname -m`. Supported targets
are x86_64 and aarch64 (ARM64). It selects matching appimagetool executables,
separate download caches, manylinux wheels and GNU library paths. Cross-building
by overriding a target variable is intentionally unsupported: APT packages and
other native Python dependencies must also match the build machine.
Run `bash AppImage/scripts/build_appimage.sh` on a Linux builder with Node.js,
Python/pip, APT and the usual AppImage build dependencies. AMD64 keeps the existing
`ProxMenux-VERSION.AppImage` name; ARM64 produces
`ProxMenux-VERSION-aarch64.AppImage`.
The native validation workflow builds both architectures on GitHub's native
Ubuntu 22.04 runners, extracts each AppImage without FUSE, and imports its bundled
CPython 3.11 and 3.13 WebSocket/image runtimes, including the extracted launcher
with only its final server execution intercepted. It does not upload artifacts,
change release assets, or modify an installed Monitor.
This build lane is a prerequisite for ARM distribution, not a declaration of
PXVIRT or Raspberry Pi hardware compatibility. ARM release/checksum publication,
installer selection and validation on the maintainer's PXVIRT lab remain separate
steps before ARM installations can use a published image. The current release
and installer distribution remain unchanged.
## Borg host backups on ARM64
Borg 1.2.8 publishes an AMD64 standalone executable but no ARM64 equivalent.
ARM64 builds therefore include a small adapter that uses `/usr/bin/borg` from
the host's native `borgbackup` package. Without that package it exits with a
clear error; it never runs the AMD64 binary. The host-backup helper likewise
refuses AMD64 cached/downloaded binaries on non-AMD64 hosts. The existing AMD64
standalone binary and checksum remain unchanged. Operators must install their
native distro package separately; this build does not install it on the host.
ARM64 uses the official versioned appimagetool 1.9.1 with SHA256 verification;
the existing AMD64 packaging tool remains unchanged.
+1
View File
@@ -4398,6 +4398,7 @@
"Name or part of the option or of the application": "Name oder Teil der Option oder der Anwendung",
"Name:": "Name:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Benannte Konten benötigen einen privaten Modus. Stoppen Sie den Container, setzen Sie public: false in /config/config.js, starten Sie ihn erneut und erstellen Sie jeden Benutzer mit: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "Natives Borg ist nicht verfügbar. Installieren Sie das native Paket borgbackup auf diesem Host, um Borg-Backups zu verwenden.",
"Needed:": "Erforderlich:",
"Needs": "Benötigt",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Weder /etc/kernel/cmdline noch /etc/default/grub gefunden.",
+1
View File
@@ -4393,6 +4393,7 @@
"Name or part of the option or of the application": "Nombre o parte de la opción o de la aplicación",
"Name:": "Nombre:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Las cuentas con nombre necesitan el modo privado. Detén el contenedor, pon public: false en /config/config.js, vuelve a arrancarlo y crea cada usuario con: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "Borg nativo no está disponible. Instala el paquete borgbackup nativo en este host para usar los backups de Borg.",
"Needed:": "Necesario:",
"Needs": "Necesita",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "No se encontraron /etc/kernel/cmdline ni /etc/default/grub.",
+1
View File
@@ -4398,6 +4398,7 @@
"Name or part of the option or of the application": "Nom ou partie de l'option ou de l'application",
"Name:": "Nom:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Les comptes nommés nécessitent un mode privé. Arrêter le conteneur, définir public: false in /config/config.js, recommencer et créer chaque utilisateur avec: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge ajouter <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "La version native de Borg n’est pas disponible. Installez le paquet natif borgbackup sur cet hôte pour utiliser les sauvegardes Borg.",
"Needed:": "Nécessaire :",
"Needs": "Nécessite",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Ni /etc/kernel/cmdline ni /etc/default/grub n'ont été trouvés.",
+1
View File
@@ -4398,6 +4398,7 @@
"Name or part of the option or of the application": "Nome dell'opzione o dell'applicazione, anche parziale",
"Name:": "Nome:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Gli account nominativi richiedono la modalità privata. Arresta il container, imposta public: false in /config/config.js, riavvialo e crea ogni utente con: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "La versione nativa di Borg non è disponibile. Installa il pacchetto borgbackup nativo su questo host per utilizzare i backup Borg.",
"Needed:": "Spazio richiesto:",
"Needs": "Richiede",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Non sono stati trovati né /etc/kernel/cmdline né /etc/default/grub.",
+1
View File
@@ -4398,6 +4398,7 @@
"Name or part of the option or of the application": "Nome ou parte da opção ou do aplicativo",
"Name:": "Nome:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Contas nomeadas precisam de modo privado. Pare o contêiner, defina público: false in /config/config.js, inicie-o novamente e crie cada usuário com: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "A versão nativa do Borg não está disponível. Instale o pacote borgbackup nativo neste host para utilizar as cópias de segurança do Borg.",
"Needed:": "Necessário:",
"Needs": "Requer",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Nem /etc/kernel/cmdline nem /etc/default/grub foram encontrados.",
+1
View File
@@ -4398,6 +4398,7 @@
"Name or part of the option or of the application": "Názov alebo časť možnosti alebo aplikácie",
"Name:": "Názov:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Používateľské účty vyžadujú súkromný režim. Zastavte kontajner, v /config/config.js nastavte public: false, znova ho spustite a každého používateľa vytvorte príkazom: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "Natívna verzia Borg nie je dostupná. Ak chcete používať zálohy Borg, nainštalujte na tento hostiteľ natívny balík borgbackup.",
"Needed:": "Potrebné:",
"Needs": "Vyžaduje",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Nenašiel sa /etc/kernel/cmdline ani /etc/default/grub.",
+1
View File
@@ -4398,6 +4398,7 @@
"Name or part of the option or of the application": "Namn eller del av alternativet eller applikationen",
"Name:": "Namn:",
"Named accounts need private mode. Stop the container, set public: false in /config/config.js, start it again and create each user with: pct exec <CTID> -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add <user>": "Namngivna konton behöver privat läge. Stoppa behållaren, ställ in offentligt: falskt i /config/config.js, starta den igen och skapa varje användare med: pct exec <CTID> - env THELOUNGE_HOME=/config s6-setuidgid abc thelounge addera <user>",
"Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.": "En inbyggd version av Borg är inte tillgänglig. Installera paketet borgbackup för värdens arkitektur för att använda Borg-säkerhetskopior.",
"Needed:": "Behövs:",
"Needs": "Kräver",
"Neither /etc/kernel/cmdline nor /etc/default/grub found.": "Varken /etc/kernel/cmdline eller /etc/default/grub hittades.",
@@ -2021,6 +2021,13 @@ hb_ensure_borg() {
# without the new AppImage)
command -v borg >/dev/null 2>&1 && { echo "borg"; return 0; }
# Cached/bundled/downloaded borg-linux64 below is AMD64 only. A native
# distro Borg may be used above, but never try a foreign cached executable.
if [[ "$(uname -m)" != x86_64 ]]; then
msg_error "$(hb_translate "Native Borg is unavailable. Install the native borgbackup package on this host to use Borg backups.")"
return 1
fi
local appimage_cache="$HB_STATE_DIR/borg"
[[ -x "$appimage_cache" ]] && { echo "$appimage_cache"; return 0; }
+131
View File
@@ -0,0 +1,131 @@
"""Exercise native architecture wiring without building or changing the host."""
import importlib.util
import struct
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / 'AppImage/scripts'
class AppImageArchitectureTests(unittest.TestCase):
def configure(self, machine):
return subprocess.run(['bash', '-c',
'source "$1"; pmx_appimage_arch "$2" || exit; '
'printf "%s|%s|%s" "$PMX_APPIMAGE_ARCH" "$PMX_APPIMAGE_TRIPLET" "$PMX_APPIMAGE_SUFFIX"',
'fixture', str(SCRIPTS / 'appimage_arch.sh'), machine], capture_output=True, text=True)
def test_elf_audit_rejects_foreign_binary_without_executing_it(self):
spec = importlib.util.spec_from_file_location('appimage_elf_check', ROOT / '.github/scripts/check_appimage_elf.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
with tempfile.TemporaryDirectory() as folder:
binary = Path(folder) / 'fixture-elf'
header = bytearray(20)
header[:6] = b'\x7fELF\x02\x01'
header[18:20] = struct.pack('<H', 183)
binary.write_bytes(header)
self.assertEqual(module.check(folder, 'aarch64'), 1)
with self.assertRaisesRegex(ValueError, 'Foreign ELF machine 183'):
module.check(folder, 'x86_64')
binary.write_text('#!/bin/sh\nexit 127\n')
with self.assertRaisesRegex(ValueError, 'No native ELF files'):
module.check(folder, 'aarch64')
def test_supported_native_architectures_and_aliases(self):
for machine in ['x86_64', 'amd64']:
self.assertEqual(self.configure(machine).stdout, 'x86_64|x86_64-linux-gnu|')
for machine in ['aarch64', 'arm64']:
self.assertEqual(self.configure(machine).stdout, 'aarch64|aarch64-linux-gnu|-aarch64')
def test_unsupported_architecture_fails_before_build_side_effects(self):
with tempfile.TemporaryDirectory() as folder:
binary = Path(folder) / 'uname'
binary.write_text('#!/bin/sh\necho loongarch64\n')
binary.chmod(0o755)
result = subprocess.run(['bash', str(SCRIPTS / 'build_appimage.sh')],
env={**os.environ, 'PATH': folder + ':' + os.environ['PATH']}, capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn('Unsupported Monitor AppImage architecture: loongarch64', result.stderr)
self.assertNotIn('Building ProxMenux', result.stdout)
def test_gevent_wheel_installer_uses_native_platform_for_both_abis(self):
source = (SCRIPTS / 'build_appimage.sh').read_text()
function = source[source.index('install_gevent_runtime() {'):source.index('\ninstall_gevent_runtime "3.11"')]
for machine in ['x86_64', 'aarch64']:
with tempfile.TemporaryDirectory() as folder:
command = ('source "$1"; pmx_appimage_arch "$2"; APP_DIR="$3"; '
'python3() { printf "%s\\n" "$*"; }; ' + function +
'\ninstall_gevent_runtime 3.11 cp311\ninstall_gevent_runtime 3.13 cp313')
result = subprocess.run(['bash', '-c', command, 'fixture', str(SCRIPTS / 'appimage_arch.sh'), machine, folder],
capture_output=True, text=True, check=True)
for platform in ['manylinux_2_28_', 'manylinux_2_27_', 'manylinux2014_']:
self.assertEqual(result.stdout.count('--platform ' + platform + machine), 2)
for abi in ['cp311', 'cp313']:
self.assertIn('--abi ' + abi, result.stdout)
def test_apprun_uses_native_library_paths_and_matching_host_python_tree(self):
for machine in ['x86_64', 'aarch64']:
with tempfile.TemporaryDirectory() as folder:
app = Path(folder)
(app / 'usr/bin').mkdir(parents=True)
(app / 'usr/lib/python3/vendor/cp311').mkdir(parents=True)
shutil.copy(SCRIPTS / 'appimage_arch.sh', app / 'usr/bin')
shutil.copy(SCRIPTS / 'AppRun', app / 'AppRun')
for name, body in {
'uname': 'echo ' + machine,
'readlink': 'printf "%s\\n" "$2"',
'python3': 'if [ "$1" = -c ]; then echo cp311; else printf "LIB=%s\\nPY=%s\\n" "$LD_LIBRARY_PATH" "$PYTHONPATH"; fi',
}.items():
file = app / 'usr/bin' / name
file.write_text('#!/bin/sh\n' + body + '\n')
file.chmod(0o755)
result = subprocess.run(['bash', str(app / 'AppRun'), '--translate'],
env={**os.environ, 'PATH': str(app / 'usr/bin') + ':' + os.environ['PATH']},
capture_output=True, text=True, check=True)
self.assertIn(str(app / ('usr/lib/' + machine + '-linux-gnu')), result.stdout)
self.assertIn(str(app / 'usr/lib/python3/vendor/cp311'), result.stdout)
def test_arm_borg_adapter_reports_unavailable_or_uses_native_host_package(self):
source = (SCRIPTS / 'borg_native_arm.sh').read_text()
with tempfile.TemporaryDirectory() as folder:
native = Path(folder) / 'borg'
adapter = Path(folder) / 'adapter'
adapter.write_text(source.replace('/usr/bin/borg', str(native)))
missing = subprocess.run(['sh', str(adapter), '--version'], capture_output=True, text=True)
self.assertEqual(missing.returncode, 127)
self.assertIn('Install the native borgbackup package', missing.stderr)
native.write_text('#!/bin/sh\nprintf "native %s|%s|%s|%s" "$*" "${PYTHONPATH-unset}" "${PYTHONHOME-unset}" "${LD_LIBRARY_PATH-unset}"\n')
native.chmod(0o755)
installed = subprocess.run(['sh', str(adapter), 'list', 'fixture-repo'],
env={**os.environ, 'PYTHONPATH': '/fixture/appimage/python', 'PYTHONHOME': '/fixture/appimage/home',
'LD_LIBRARY_PATH': '/fixture/appimage/lib'}, capture_output=True, text=True, check=True)
self.assertEqual(installed.stdout, 'native list fixture-repo|unset|unset|unset')
def test_arm_host_backup_never_selects_cached_amd64_borg_or_downloads_it(self):
source = (ROOT / 'scripts/backup_restore/lib_host_backup_common.sh').read_text()
function = source[source.index('hb_ensure_borg() {'):source.index('\nhb_borg_init_if_needed()')]
with tempfile.TemporaryDirectory() as folder:
cached = Path(folder) / 'borg'
cached.write_text('#!/bin/sh\nexit 0\n')
cached.chmod(0o755)
script = (function + '\n'
'command() { if [ "$*" = "-v borg" ]; then return 1; fi; builtin command "$@"; }; '
'uname() { echo aarch64; }; hb_translate() { printf "%s" "$1"; }; '
'msg_error() { printf "%s" "$1" >&2; }; '
'wget() { echo UNEXPECTED_DOWNLOAD >&2; return 99; }; '
'HB_STATE_DIR="$1"; hb_ensure_borg')
result = subprocess.run(['bash', '-c', script, 'fixture', folder], capture_output=True, text=True)
self.assertEqual(result.returncode, 1)
self.assertIn('Native Borg is unavailable.', result.stderr)
self.assertNotIn('UNEXPECTED_DOWNLOAD', result.stderr)
self.assertEqual(result.stdout, '')
if __name__ == '__main__':
unittest.main()