mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-05 20:32:15 +00:00
feat(monitor): validate native ARM64 and AMD64 AppImage builds
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
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/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
|
||||||
|
run: sudo apt-get update
|
||||||
|
- name: Build native AppImage without publishing it
|
||||||
|
run: bash AppImage/scripts/build_appimage.sh
|
||||||
|
- 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__)'
|
||||||
|
- 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__)'
|
||||||
@@ -6,8 +6,11 @@
|
|||||||
# Get the directory where this AppImage is mounted
|
# Get the directory where this AppImage is mounted
|
||||||
APPDIR="$(dirname "$(readlink -f "${0}")")"
|
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 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
|
# 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
|
# uses CPython 3.11 and PVE 9 uses CPython 3.13; both runtimes are bundled in
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ set -e
|
|||||||
WORK_DIR="/tmp/proxmenux_build"
|
WORK_DIR="/tmp/proxmenux_build"
|
||||||
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
|
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
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"
|
DIST_DIR="$SCRIPT_DIR/../dist"
|
||||||
APPIMAGE_ROOT="$SCRIPT_DIR/.."
|
APPIMAGE_ROOT="$SCRIPT_DIR/.."
|
||||||
|
|
||||||
@@ -24,7 +26,7 @@ ZOPE_EVENT_VERSION="6.2"
|
|||||||
GEVENT_WEBSOCKET_VERSION="0.10.1"
|
GEVENT_WEBSOCKET_VERSION="0.10.1"
|
||||||
|
|
||||||
VERSION=$(node -p "require('$APPIMAGE_ROOT/package.json').version")
|
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..."
|
echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with hardware monitoring tools..."
|
||||||
|
|
||||||
@@ -33,16 +35,9 @@ echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with hardware monitor
|
|||||||
# under the non-root GitHub Actions runner — previously hardcoded to
|
# under the non-root GitHub Actions runner — previously hardcoded to
|
||||||
# /var/cache/, which the CI user can't write to (mkdir Permission
|
# /var/cache/, which the CI user can't write to (mkdir Permission
|
||||||
# denied → build aborted before doing any actual work).
|
# 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
|
# Architecture-specific caches never reuse an executable from a different CPU.
|
||||||
# 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"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean and create work directory
|
# Clean and create work directory
|
||||||
rm -rf "$WORK_DIR"
|
rm -rf "$WORK_DIR"
|
||||||
@@ -57,7 +52,7 @@ if [ -s "$APPIMAGETOOL_CACHE" ]; then
|
|||||||
fi
|
fi
|
||||||
if [ ! -s "$WORK_DIR/appimagetool" ]; then
|
if [ ! -s "$WORK_DIR/appimagetool" ]; then
|
||||||
echo "📥 Downloading appimagetool..."
|
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 "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${PMX_APPIMAGE_ARCH}.AppImage" -O "$WORK_DIR/appimagetool" || true
|
||||||
if [ ! -s "$WORK_DIR/appimagetool" ]; then
|
if [ ! -s "$WORK_DIR/appimagetool" ]; then
|
||||||
echo "❌ Failed to download appimagetool" >&2
|
echo "❌ Failed to download appimagetool" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -310,6 +305,7 @@ fi
|
|||||||
echo "📋 Copying AppRun script..."
|
echo "📋 Copying AppRun script..."
|
||||||
if [ -f "$SCRIPT_DIR/AppRun" ]; then
|
if [ -f "$SCRIPT_DIR/AppRun" ]; then
|
||||||
cp "$SCRIPT_DIR/AppRun" "$APP_DIR/AppRun"
|
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"
|
chmod +x "$APP_DIR/AppRun"
|
||||||
echo "✅ AppRun script copied successfully"
|
echo "✅ AppRun script copied successfully"
|
||||||
else
|
else
|
||||||
@@ -379,7 +375,7 @@ pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" --upgrade \
|
|||||||
# Phase 3b: Install the complete gevent/WSS runtime separately for every
|
# Phase 3b: Install the complete gevent/WSS runtime separately for every
|
||||||
# supported host ABI. Keeping each dependency tree isolated avoids mixing
|
# supported host ABI. Keeping each dependency tree isolated avoids mixing
|
||||||
# CPython-specific modules from the build runner with those loaded by PVE.
|
# 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
|
# backwards-compatible manylinux2014 zope.interface wheels to be resolved in
|
||||||
# one deterministic installation.
|
# one deterministic installation.
|
||||||
install_gevent_runtime() {
|
install_gevent_runtime() {
|
||||||
@@ -394,9 +390,9 @@ install_gevent_runtime() {
|
|||||||
--target "$target" \
|
--target "$target" \
|
||||||
--upgrade \
|
--upgrade \
|
||||||
--only-binary=:all: \
|
--only-binary=:all: \
|
||||||
--platform manylinux_2_28_x86_64 \
|
--platform "manylinux_2_28_${PMX_APPIMAGE_ARCH}" \
|
||||||
--platform manylinux_2_27_x86_64 \
|
--platform "manylinux_2_27_${PMX_APPIMAGE_ARCH}" \
|
||||||
--platform manylinux2014_x86_64 \
|
--platform "manylinux2014_${PMX_APPIMAGE_ARCH}" \
|
||||||
--implementation cp \
|
--implementation cp \
|
||||||
--python-version "$python_version" \
|
--python-version "$python_version" \
|
||||||
--abi "$abi" \
|
--abi "$abi" \
|
||||||
@@ -547,7 +543,7 @@ if [ -d "$APP_DIR/bin" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🔍 Sanity check (ldd + presence of libfreeipmi)"
|
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
|
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)"
|
echo "❌ libfreeipmi.so.17 not found inside AppDir (ipmitool will fail)"
|
||||||
@@ -608,7 +604,7 @@ echo "🔨 Building unified AppImage v${VERSION}..."
|
|||||||
cd "$WORK_DIR"
|
cd "$WORK_DIR"
|
||||||
export NO_CLEANUP=1
|
export NO_CLEANUP=1
|
||||||
export APPIMAGE_EXTRACT_AND_RUN=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 "$APP_DIR" "$APPIMAGE_NAME"
|
||||||
|
|
||||||
# Move to dist directory
|
# Move to dist directory
|
||||||
mv "$APPIMAGE_NAME" "$DIST_DIR/"
|
mv "$APPIMAGE_NAME" "$DIST_DIR/"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 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. 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.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""Exercise native architecture wiring without building or changing the host."""
|
||||||
|
import json
|
||||||
|
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_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)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user