diff --git a/.github/scripts/check_appimage_elf.py b/.github/scripts/check_appimage_elf.py new file mode 100644 index 00000000..4f131dcf --- /dev/null +++ b/.github/scripts/check_appimage_elf.py @@ -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', 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') diff --git a/.github/scripts/smoke_native_apprun.sh b/.github/scripts/smoke_native_apprun.sh new file mode 100644 index 00000000..cb84203c --- /dev/null +++ b/.github/scripts/smoke_native_apprun.sh @@ -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" diff --git a/.github/workflows/test-appimage-native.yml b/.github/workflows/test-appimage-native.yml new file mode 100644 index 00000000..6bbdeba9 --- /dev/null +++ b/.github/workflows/test-appimage-native.yml @@ -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 diff --git a/AppImage/scripts/AppRun b/AppImage/scripts/AppRun index 74ad4d22..db6e2b6b 100644 --- a/AppImage/scripts/AppRun +++ b/AppImage/scripts/AppRun @@ -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 diff --git a/AppImage/scripts/appimage_arch.sh b/AppImage/scripts/appimage_arch.sh new file mode 100644 index 00000000..ef7b9f84 --- /dev/null +++ b/AppImage/scripts/appimage_arch.sh @@ -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 +} diff --git a/AppImage/scripts/borg_native_arm.sh b/AppImage/scripts/borg_native_arm.sh new file mode 100644 index 00000000..ba49b8ba --- /dev/null +++ b/AppImage/scripts/borg_native_arm.sh @@ -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 diff --git a/AppImage/scripts/build_appimage.sh b/AppImage/scripts/build_appimage.sh index 3ccf3d66..be49cf1c 100755 --- a/AppImage/scripts/build_appimage.sh +++ b/AppImage/scripts/build_appimage.sh @@ -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/" diff --git a/docs/monitor-native-builds.md b/docs/monitor-native-builds.md new file mode 100644 index 00000000..b578bcc2 --- /dev/null +++ b/docs/monitor-native-builds.md @@ -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. diff --git a/lang/de.json b/lang/de.json index 4af4b676..5de23f68 100644 --- a/lang/de.json +++ b/lang/de.json @@ -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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ": "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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ", + "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.", diff --git a/lang/es.json b/lang/es.json index 9d052db2..5948fc98 100644 --- a/lang/es.json +++ b/lang/es.json @@ -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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ": "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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ", + "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.", diff --git a/lang/fr.json b/lang/fr.json index 5a4bf47e..4f2e47ad 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ": "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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge ajouter ", + "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.", diff --git a/lang/it.json b/lang/it.json index 3d3811ad..7e831a0a 100644 --- a/lang/it.json +++ b/lang/it.json @@ -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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ": "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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ", + "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.", diff --git a/lang/pt.json b/lang/pt.json index f7a05c14..56c4130a 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ": "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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ", + "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.", diff --git a/lang/sk.json b/lang/sk.json index 6235032e..ba1208a9 100644 --- a/lang/sk.json +++ b/lang/sk.json @@ -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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ": "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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ", + "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.", diff --git a/lang/sv.json b/lang/sv.json index d4345ae9..9e6f9555 100644 --- a/lang/sv.json +++ b/lang/sv.json @@ -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 -- env THELOUNGE_HOME=/config s6-setuidgid abc thelounge add ": "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 - env THELOUNGE_HOME=/config s6-setuidgid abc thelounge addera ", + "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.", diff --git a/scripts/backup_restore/lib_host_backup_common.sh b/scripts/backup_restore/lib_host_backup_common.sh index 1c465b19..b7938053 100755 --- a/scripts/backup_restore/lib_host_backup_common.sh +++ b/scripts/backup_restore/lib_host_backup_common.sh @@ -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; } diff --git a/tests/test_appimage_arch.py b/tests/test_appimage_arch.py new file mode 100644 index 00000000..5fef1cf1 --- /dev/null +++ b/tests/test_appimage_arch.py @@ -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('&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()